aboutsummaryrefslogtreecommitdiffstats
path: root/tiddlers/current-tiddler-toc-widget.tid
diff options
context:
space:
mode:
Diffstat (limited to 'tiddlers/current-tiddler-toc-widget.tid')
-rw-r--r--tiddlers/current-tiddler-toc-widget.tid284
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;
+
+})();