diff options
| author | shu <nvcsadjkojf@outlook.com> | 2026-07-26 18:33:43 +0800 |
|---|---|---|
| committer | shu <nvcsadjkojf@outlook.com> | 2026-07-26 18:33:43 +0800 |
| commit | c1414f940c1fca93df0c266a3e90c9ff1d7e88c7 (patch) | |
| tree | 4d613a365905d12699d71b3bfb2f68d2a7089c1c /tiddlers/current-tiddler-toc-widget.tid | |
Initial TiddlyWiki setup
Diffstat (limited to 'tiddlers/current-tiddler-toc-widget.tid')
| -rw-r--r-- | tiddlers/current-tiddler-toc-widget.tid | 284 |
1 files changed, 284 insertions, 0 deletions
diff --git a/tiddlers/current-tiddler-toc-widget.tid b/tiddlers/current-tiddler-toc-widget.tid new file mode 100644 index 0000000..a68e449 --- /dev/null +++ b/tiddlers/current-tiddler-toc-widget.tid @@ -0,0 +1,284 @@ +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; + +})(); |
