title: $:/plugins/local/current-tiddler-toc/widget.js type: application/javascript module-type: widget /*\ title: $:/plugins/local/current-tiddler-toc/widget.js type: application/javascript module-type: widget Displays a table of contents for the current story tiddler. \*/ (function() { "use strict"; var Widget = require("$:/core/modules/widgets/widget.js").widget; var CurrentTiddlerTocWidget = function(parseTreeNode,options) { this.initialise(parseTreeNode,options); }; CurrentTiddlerTocWidget.prototype = new Widget(); CurrentTiddlerTocWidget.prototype.constructor = CurrentTiddlerTocWidget; CurrentTiddlerTocWidget.prototype.render = function(parent,nextSibling) { this.parentDomNode = parent; this.computeAttributes(); this.execute(); var container = this.document.createElement("div"); container.className = "tc-current-tiddler-toc"; parent.insertBefore(container,nextSibling); this.domNodes.push(container); this.containerNode = container; if($tw.browser) { var self = this, windowObject = this.document.defaultView || window; this.windowObject = windowObject; this.resizeHandler = function() { self.updateMaxHeight(); }; windowObject.addEventListener("resize",this.resizeHandler); if(windowObject.visualViewport) { windowObject.visualViewport.addEventListener("resize",this.resizeHandler); } } this.scheduleBuild(); }; CurrentTiddlerTocWidget.prototype.execute = function() { }; CurrentTiddlerTocWidget.prototype.getStoryTitles = function() { return this.wiki.getTiddlerList("$:/StoryList") || []; }; CurrentTiddlerTocWidget.prototype.findTiddlerFrame = function(title) { var frames = this.document.querySelectorAll(".tc-tiddler-frame[data-tiddler-title]"), index; for(index = 0; index < frames.length; index++) { var frame = frames[index], frameTitle = frame.getAttribute("data-tiddler-title"), frameTiddler; if(frameTitle === title) { return frame; } frameTiddler = this.wiki.getTiddler(frameTitle); if(frameTiddler && (frameTiddler.fields["draft.of"] === title || frameTiddler.fields["draft.title"] === title)) { return frame; } } return null; }; CurrentTiddlerTocWidget.prototype.clearContainer = function() { while(this.containerNode && this.containerNode.firstChild) { this.containerNode.removeChild(this.containerNode.firstChild); } }; CurrentTiddlerTocWidget.prototype.showMessage = function(message) { var messageNode = this.document.createElement("div"); messageNode.className = "tc-current-tiddler-toc-empty"; messageNode.textContent = message; this.containerNode.appendChild(messageNode); }; CurrentTiddlerTocWidget.prototype.updateMaxHeight = function() { if(!this.containerNode || !this.containerNode.parentNode || !this.windowObject) { return; } var viewport = this.windowObject.visualViewport, viewportHeight = viewport ? viewport.height : this.windowObject.innerHeight, top = this.containerNode.getBoundingClientRect().top, availableHeight = Math.max(160,Math.floor(viewportHeight - top - 16)); this.containerNode.style.maxHeight = availableHeight + "px"; }; CurrentTiddlerTocWidget.prototype.getHeadings = function(frame) { var body = frame.querySelector(".tc-tiddler-body"); if(!body) { body = frame.querySelector(".tc-tiddler-preview-preview"); } if(!body) { return []; } var headingNodes = body.querySelectorAll("h1, h2, h3, h4, h5, h6"), headings = [], index; for(index = 0; index < headingNodes.length; index++) { var heading = headingNodes[index]; // Ignore headings that belong to nested, independent tiddler frames. if(heading.closest && heading.closest(".tc-tiddler-frame") !== frame) { continue; } headings.push({ node: heading, level: parseInt(heading.tagName.substring(1),10), text: heading.textContent.trim() || "未命名标题" }); } return headings; }; CurrentTiddlerTocWidget.prototype.addTiddlerGroup = function(title,frame) { var self = this, groupNode = this.document.createElement("section"), titleButton = this.document.createElement("button"); groupNode.className = "tc-current-tiddler-toc-group"; titleButton.setAttribute("type","button"); titleButton.className = "tc-btn-invisible tc-current-tiddler-toc-title"; titleButton.textContent = title; titleButton.title = "滚动到条目:" + title; titleButton.addEventListener("click",function(event) { event.preventDefault(); frame.scrollIntoView({ behavior: "smooth", block: "start" }); }); groupNode.appendChild(titleButton); var headings = this.getHeadings(frame); if(!headings.length) { var emptyNode = this.document.createElement("div"); emptyNode.className = "tc-current-tiddler-toc-group-empty"; emptyNode.textContent = "没有标题"; groupNode.appendChild(emptyNode); this.containerNode.appendChild(groupNode); return; } var minimumLevel = headings.reduce(function(result,heading) { return Math.min(result,heading.level); },6), listNode = this.document.createElement("ol"); listNode.className = "tc-current-tiddler-toc-list"; groupNode.appendChild(listNode); headings.forEach(function(heading) { var itemNode = self.document.createElement("li"), buttonNode = self.document.createElement("button"); itemNode.className = "tc-current-tiddler-toc-item"; itemNode.style.paddingLeft = ((heading.level - minimumLevel) * 1.1) + "em"; buttonNode.setAttribute("type","button"); buttonNode.className = "tc-btn-invisible tc-current-tiddler-toc-link"; buttonNode.textContent = heading.text; buttonNode.title = heading.text; buttonNode.addEventListener("click",function(event) { event.preventDefault(); heading.node.scrollIntoView({ behavior: "smooth", block: "start" }); }); itemNode.appendChild(buttonNode); listNode.appendChild(itemNode); }); this.containerNode.appendChild(groupNode); }; CurrentTiddlerTocWidget.prototype.build = function() { if(!this.containerNode || !this.containerNode.parentNode) { return; } this.clearContainer(); var titles = this.getStoryTitles(), index, visibleCount = 0; if(!titles.length) { this.showMessage("当前没有打开的条目"); this.updateMaxHeight(); return; } for(index = 0; index < titles.length; index++) { var title = titles[index], frame = this.findTiddlerFrame(title); if(frame) { this.addTiddlerGroup(title,frame); visibleCount += 1; } } if(!visibleCount) { this.showMessage("打开的条目尚未显示"); } this.updateMaxHeight(); }; CurrentTiddlerTocWidget.prototype.scheduleBuild = function() { var self = this; if(!$tw.browser) { return; } if(this.buildTimer) { clearTimeout(this.buildTimer); } // Wait until the story river has completed the current DOM update. this.buildTimer = setTimeout(function() { self.buildTimer = null; self.build(); },0); }; CurrentTiddlerTocWidget.prototype.refresh = function() { this.computeAttributes(); this.scheduleBuild(); return false; }; CurrentTiddlerTocWidget.prototype.removeChildDomNodes = function() { if(this.buildTimer) { clearTimeout(this.buildTimer); this.buildTimer = null; } if(this.windowObject && this.resizeHandler) { this.windowObject.removeEventListener("resize",this.resizeHandler); if(this.windowObject.visualViewport) { this.windowObject.visualViewport.removeEventListener("resize",this.resizeHandler); } this.resizeHandler = null; this.windowObject = null; } Widget.prototype.removeChildDomNodes.call(this); }; exports["current-tiddler-toc"] = CurrentTiddlerTocWidget; })();