From a4eb139f99d7aff5fcece4509d7cdaa3210dbdb5 Mon Sep 17 00:00:00 2001 From: Jermolene Date: Fri, 1 Feb 2019 10:43:42 +0000 Subject: [PATCH] Innerwiki: Add support for draggable anchors --- .../tiddlers/screenshot-7-anchor-1-x.tid | 2 + .../tiddlers/screenshot-7-anchor-1-y.tid | 2 + .../tiddlywiki/innerwiki/crosshairs.svg.tid | 21 ++ plugins/tiddlywiki/innerwiki/data.js | 2 + plugins/tiddlywiki/innerwiki/doc/docs.tid | 16 ++ plugins/tiddlywiki/innerwiki/doc/examples.tid | 23 +- .../innerwiki/doc/inner-example.tid | 2 + plugins/tiddlywiki/innerwiki/doc/readme.tid | 2 +- plugins/tiddlywiki/innerwiki/innerwiki.js | 255 ++++++++++++++---- 9 files changed, 264 insertions(+), 61 deletions(-) create mode 100644 editions/innerwikidemo/tiddlers/screenshot-7-anchor-1-x.tid create mode 100644 editions/innerwikidemo/tiddlers/screenshot-7-anchor-1-y.tid create mode 100644 plugins/tiddlywiki/innerwiki/crosshairs.svg.tid diff --git a/editions/innerwikidemo/tiddlers/screenshot-7-anchor-1-x.tid b/editions/innerwikidemo/tiddlers/screenshot-7-anchor-1-x.tid new file mode 100644 index 000000000..0c1d9500e --- /dev/null +++ b/editions/innerwikidemo/tiddlers/screenshot-7-anchor-1-x.tid @@ -0,0 +1,2 @@ +title: screenshot-7-anchor-1-x +text: 100 diff --git a/editions/innerwikidemo/tiddlers/screenshot-7-anchor-1-y.tid b/editions/innerwikidemo/tiddlers/screenshot-7-anchor-1-y.tid new file mode 100644 index 000000000..c9421c6f1 --- /dev/null +++ b/editions/innerwikidemo/tiddlers/screenshot-7-anchor-1-y.tid @@ -0,0 +1,2 @@ +title: screenshot-7-anchor-1-y +text: 70 diff --git a/plugins/tiddlywiki/innerwiki/crosshairs.svg.tid b/plugins/tiddlywiki/innerwiki/crosshairs.svg.tid new file mode 100644 index 000000000..3325966ef --- /dev/null +++ b/plugins/tiddlywiki/innerwiki/crosshairs.svg.tid @@ -0,0 +1,21 @@ +title: $:/plugins/tiddlywiki/innerwiki/crosshairs.svg +type: image/svg+xml + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/tiddlywiki/innerwiki/data.js b/plugins/tiddlywiki/innerwiki/data.js index 4f2067934..c325a3cf8 100644 --- a/plugins/tiddlywiki/innerwiki/data.js +++ b/plugins/tiddlywiki/innerwiki/data.js @@ -15,6 +15,7 @@ Widget to represent a single item of data var Widget = require("$:/core/modules/widgets/widget.js").widget; var DataWidget = function(parseTreeNode,options) { + this.dataWidgetTag = parseTreeNode.type; this.initialise(parseTreeNode,options); }; @@ -52,5 +53,6 @@ DataWidget.prototype.refresh = function(changedTiddlers) { }; exports.data = DataWidget; +exports.anchor = DataWidget; })(); diff --git a/plugins/tiddlywiki/innerwiki/doc/docs.tid b/plugins/tiddlywiki/innerwiki/doc/docs.tid index 6b840adc3..e965c673d 100644 --- a/plugins/tiddlywiki/innerwiki/doc/docs.tid +++ b/plugins/tiddlywiki/innerwiki/doc/docs.tid @@ -47,6 +47,22 @@ This example injects all image tiddlers with the addition of the field "custom" <$data $filter="[is[image]]" custom="Beta"/> ``` +! `<$anchor>` widget + +The `<$anchor>` widget is used within the `<$innerwiki>` widget to specify draggable anchors to be overlaid on the innerwiki. + +It supports the following attributes: + +|!Attribute |!Description | +|x |The title of the tiddler containing the X coordinate of the anchor | +|y |The title of the tiddler containing the Y coordinate of the anchor | + +This example declares an anchor whose coordinates are contained in the tiddlers [[my-anchor-x]] and [[my-anchor-y]]: + +``` +<$anchor x="my-anchor-x" y="my-anchor-y"/> +``` + ! `screenshot` command Saves PNG screenshots of the `<$innerwiki>` widgets rendered by a set of tiddlers identified by a filter. diff --git a/plugins/tiddlywiki/innerwiki/doc/examples.tid b/plugins/tiddlywiki/innerwiki/doc/examples.tid index 2fc5892a8..82f2a582a 100644 --- a/plugins/tiddlywiki/innerwiki/doc/examples.tid +++ b/plugins/tiddlywiki/innerwiki/doc/examples.tid @@ -92,11 +92,32 @@ By injecting the right payload tiddlers, the innerwiki can be initialised to any <$data title="$:/state/showeditpreview" text="yes"/> """/> +!! Draggable anchors + +This example shows how the `<$anchor>` widget is used to display draggable anchors overlaid on the innerwiki. The `<$anchor>` widget is used to declare the tiddlers containing the coordinates of each anchor. These tiddlers can then be transcluded by SVG graphic primitives to position them according to the anchor locations. + +<$macrocall $name="example" text="""screenshot-7-anchor-1-x: <$edit-text tag="input" tiddler="screenshot-7-anchor-1-x"/> + +screenshot-7-anchor-1-y: <$edit-text tag="input" tiddler="screenshot-7-anchor-1-y"/> + +screenshot-7-anchor-2-x: <$edit-text tag="input" tiddler="screenshot-7-anchor-2-x"/> + +screenshot-7-anchor-2-y: <$edit-text tag="input" tiddler="screenshot-7-anchor-2-y"/> + +<$innerwiki template="$:/plugins/tiddlywiki/innerwiki/template" filename="screenshot-7.png" width="1200" height="400" style="width:100%;"> + <$anchor x="screenshot-7-anchor-1-x" y="screenshot-7-anchor-1-y"/> + <$anchor x="screenshot-7-anchor-2-x" y="screenshot-7-anchor-2-y"/> + <$data title="HelloThere" text="! This tiddler is inside a wiki that is inside a wiki"/> + <$data title="$:/DefaultTiddlers" text="HelloThere"/> + <$macrocall $name="big-arrow" x={{screenshot-7-anchor-1-x}} y={{screenshot-7-anchor-1-y}}/> + +"""/> + !! Inception An innerwiki can itself contain an inner-innerwiki: -<$macrocall $name="example" text="""<$innerwiki width="1200" height="600" style="width:100%;" filename="screenshot-7.png"> +<$macrocall $name="example" text="""<$innerwiki width="1200" height="600" style="width:100%;" filename="screenshot-8.png"> <$data title="HelloThere" text="! This tiddler is inside a wiki that is inside a wiki"/> <$data title="$:/DefaultTiddlers" text="HelloThere $:/plugins/tiddlywiki/innerwiki/inner-example"/> <$data $tiddler="$:/plugins/tiddlywiki/innerwiki"/> diff --git a/plugins/tiddlywiki/innerwiki/doc/inner-example.tid b/plugins/tiddlywiki/innerwiki/doc/inner-example.tid index dd5da6832..339450cf1 100644 --- a/plugins/tiddlywiki/innerwiki/doc/inner-example.tid +++ b/plugins/tiddlywiki/innerwiki/doc/inner-example.tid @@ -1,6 +1,8 @@ title: $:/plugins/tiddlywiki/innerwiki/inner-example <$innerwiki width="1200" height="400" style="width:100%;"> + <$anchor x="screenshot-inner-anchor-1-x" y="screenshot-inner-anchor-1-y"/> + <$data title="HelloThere" text="! This tiddler is inside a wiki that is inside a wiki that is inside a wiki"/> <$data title="$:/DefaultTiddlers" text="HelloThere"/> <$data title="$:/palette" text="$:/palettes/SolarFlare"/> diff --git a/plugins/tiddlywiki/innerwiki/doc/readme.tid b/plugins/tiddlywiki/innerwiki/doc/readme.tid index b188d3054..a43608210 100644 --- a/plugins/tiddlywiki/innerwiki/doc/readme.tid +++ b/plugins/tiddlywiki/innerwiki/doc/readme.tid @@ -2,7 +2,7 @@ title: $:/plugins/tiddlywiki/innerwiki/readme !! Introduction -This plugin enables TiddlyWiki to embed a modified copy of itself (an "innerwiki"). The primary motivation is to be able to produce screenshot illustrations that are automatically up-to-date with the appearance of TiddlyWiki as it changes over time, or to produce the same screenshot in different languages. +This plugin enables TiddlyWiki to embed a modified copy of itself (an "innerwiki") with overlaid graphics. The primary motivation is to be able to produce screenshot illustrations that are automatically up-to-date with the appearance of TiddlyWiki as it changes over time, or to produce the same screenshot in different languages. In the browser, innerwikis are displayed as an embedded iframe. Under Node.js [[Google's Puppeteer|https://pptr.dev/]] is used to load the innerwikis as off-screen web pages and then snapshot them as a PNG image. diff --git a/plugins/tiddlywiki/innerwiki/innerwiki.js b/plugins/tiddlywiki/innerwiki/innerwiki.js index b94070b45..a297858dc 100644 --- a/plugins/tiddlywiki/innerwiki/innerwiki.js +++ b/plugins/tiddlywiki/innerwiki/innerwiki.js @@ -15,7 +15,8 @@ Widget to display an innerwiki in an iframe var DEFAULT_INNERWIKI_TEMPLATE = "$:/plugins/tiddlywiki/innerwiki/template"; var Widget = require("$:/core/modules/widgets/widget.js").widget, - DataWidget = require("$:/plugins/tiddlywiki/innerwiki/data.js").data; + DataWidget = require("$:/plugins/tiddlywiki/innerwiki/data.js").data, + dm = $tw.utils.domMaker; var InnerWikiWidget = function(parseTreeNode,options) { this.initialise(parseTreeNode,options); @@ -35,64 +36,189 @@ InnerWikiWidget.prototype.render = function(parent,nextSibling) { this.computeAttributes(); this.execute(); // Create wrapper - var domWrapper = this.document.createElement("div"); - var classes = (this.innerWikiClass || "").split(" "); - classes.push("tc-innerwiki-wrapper"); - domWrapper.className = classes.join(" "); - domWrapper.style = this.innerWikiStyle; - domWrapper.style.overflow = "hidden"; - domWrapper.style.position = "relative"; - domWrapper.style.boxSizing = "content-box"; + this.domWrapper = dm("div",{ + document: this.document, + "class": (this.innerWikiClass || "").split(" ").concat(["tc-innerwiki-wrapper"]).join(" "), + style: { + overflow: "hidden", + position: "relative", + boxSizing: "content-box" + } + }); // Set up the SVG container - var domSVG = this.document.createElementNS("http://www.w3.org/2000/svg","svg"); - domSVG.style = this.innerWikiStyle; - domSVG.style.position = "absolute"; - domSVG.style.zIndex = "1"; - domSVG.style.pointerEvents = "none"; - domSVG.setAttribute("viewBox","0 0 " + this.innerWikiClipWidth + " " + this.innerWikiClipHeight); - domWrapper.appendChild(domSVG); + this.domSVG = dm("svg",{ + namespace: "http://www.w3.org/2000/svg", + document: this.document, + style: { + width: "100%", + position: "absolute", + zIndex: "1", + pointerEvents: "none" + }, + attributes: { + "viewBox": "0 0 " + this.innerWikiClipWidth + " " + this.innerWikiClipHeight + } + }); + this.domWrapper.appendChild(this.domSVG); this.setVariable("namespace","http://www.w3.org/2000/svg"); - // If we're on the real DOM, adjust the wrapper and iframe + // Create the iframe for the browser or image for Node.js if(!this.document.isTiddlyWikiFakeDom) { // Create iframe - var domIFrame = this.document.createElement("iframe"); - domIFrame.className = "tc-innerwiki-iframe"; - domIFrame.style.position = "absolute"; - domIFrame.style.maxWidth = "none"; - domIFrame.style.border = "none"; - domIFrame.width = this.innerWikiWidth; - domIFrame.height = this.innerWikiHeight; - domWrapper.appendChild(domIFrame); + this.domIFrame = dm("iframe",{ + document: this.document, + "class": "tc-innerwiki-iframe", + style: { + position: "absolute", + maxWidth: "none", + border: "none" + }, + attributes: { + width: this.innerWikiWidth, + height: this.innerWikiHeight + } + }); + this.domWrapper.appendChild(this.domIFrame); } else { // Create image placeholder - var domImage = this.document.createElement("img"); - domImage.style = this.innerWikiStyle; - domImage.setAttribute("src",this.innerWikiFilename); - domWrapper.appendChild(domImage); + this.domImage = dm("img",{ + document: this.document, + style: { + width: "100%" + }, + attributes: { + src: this.innerWikiFilename + } + }); + this.domWrapper.appendChild(this.domImage); } // Insert wrapper into the DOM - parent.insertBefore(domWrapper,nextSibling); - this.renderChildren(domSVG,null); - this.domNodes.push(domWrapper); + parent.insertBefore(this.domWrapper,nextSibling); + this.renderChildren(this.domSVG,null); + this.domNodes.push(this.domWrapper); // If we're on the real DOM, finish the initialisation that needs us to be in the DOM if(!this.document.isTiddlyWikiFakeDom) { // Write the HTML - domIFrame.contentWindow.document.open(); - domIFrame.contentWindow.document.write(this.createInnerHTML()); - domIFrame.contentWindow.document.close(); + this.domIFrame.contentDocument.open(); + this.domIFrame.contentDocument.write(this.createInnerHTML()); + this.domIFrame.contentDocument.close(); } // Scale the iframe and adjust the height of the wrapper - var clipLeft = this.innerWikiClipLeft, - clipTop = this.innerWikiClipTop, - clipWidth = this.innerWikiClipWidth, - clipHeight = this.innerWikiClipHeight, - translateX = -clipLeft, - translateY = -clipTop, - scale = domWrapper.clientWidth / clipWidth; + this.clipLeft = this.innerWikiClipLeft; + this.clipTop = this.innerWikiClipTop; + this.clipWidth = this.innerWikiClipWidth; + this.clipHeight = this.innerWikiClipHeight; + this.scale = this.domWrapper.clientWidth / this.clipWidth; + // Display the anchors if(!this.document.isTiddlyWikiFakeDom) { - domIFrame.style.transformOrigin = (-translateX) + "px " + (-translateY) + "px"; - domIFrame.style.transform = "translate(" + translateX + "px," + translateY + "px) scale(" + scale + ")"; - domWrapper.style.height = (clipHeight * scale) + "px"; + this.domAnchorContainer = dm("div",{ + document: this.document, + style: { + position: "relative", + zIndex: "2", + transformOrigin: "0 0", + transform: "scale(" + this.scale + ")" + } + }); + this.domAnchorBackdrop = dm("div",{ + document: this.document, + style: { + position: "absolute", + display: "none" + } + }); + this.domAnchorContainer.appendChild(this.domAnchorBackdrop); + this.domWrapper.insertBefore(this.domAnchorContainer,this.domWrapper.firstChild); + self.createAnchors(); + } + // Scale the iframe and adjust the height of the wrapper + if(!this.document.isTiddlyWikiFakeDom) { + this.domIFrame.style.transformOrigin = this.clipLeft + "px " + this.clipTop + "px"; + this.domIFrame.style.transform = "translate(" + (-this.clipLeft) + "px," + (-this.clipTop) + "px) scale(" + this.scale + ")"; + this.domWrapper.style.height = (this.clipHeight * this.scale) + "px"; + } +}; + +/* +Create the anchors +*/ +InnerWikiWidget.prototype.createAnchors = function() { + var self = this; + this.findDataWidgets(this.children,"anchor",function(widget) { + var anchorWidth = 40, + anchorHeight = 40, + getAnchorCoordinate = function(name) { + return parseInt(self.wiki.getTiddlerText(widget.getAttribute(name)),10) || 0; + }, + setAnchorCoordinate = function(name,value) { + self.wiki.addTiddler({ + title: widget.getAttribute(name), + text: value + "" + }); + }, + domAnchor = dm("img",{ + document: self.document, + style: { + position: "absolute", + width: anchorWidth + "px", + height: anchorHeight + "px", + transformOrigin: "50% 50%", + transform: "scale(" + (1 / self.scale) + ")", + left: (getAnchorCoordinate("x") - anchorWidth / 2) + "px", + top: (getAnchorCoordinate("y") - anchorHeight / 2) + "px" + }, + attributes: { + draggable: false, + src: "data:image/svg+xml," + encodeURIComponent(self.wiki.getTiddlerText("$:/plugins/tiddlywiki/innerwiki/crosshairs.svg")) + } + }); + self.domAnchorContainer.appendChild(domAnchor); + var posX,posY,dragStartX,dragStartY,deltaX,deltaY, + fnMouseDown = function(event) { + self.domAnchorBackdrop.style.width = self.clipWidth + "px"; + self.domAnchorBackdrop.style.height = self.clipHeight + "px"; + self.domAnchorBackdrop.style.display = "block"; + posX = domAnchor.offsetLeft; + posY = domAnchor.offsetTop; + dragStartX = event.clientX; + dragStartY = event.clientY; + deltaX = 0; + deltaY = 0; + self.document.addEventListener("mousemove",fnMouseMove,false); + self.document.addEventListener("mouseup",fnMouseUp,false); + }, + fnMouseMove = function(event) { + deltaX = (event.clientX - dragStartX) / self.scale; + deltaY = (event.clientY - dragStartY) / self.scale; + domAnchor.style.left = (posX + deltaX) + "px"; + domAnchor.style.top = (posY + deltaY) + "px"; + }, + fnMouseUp = function(event) { + var x = getAnchorCoordinate("x") + deltaX, + y = getAnchorCoordinate("y") + deltaY; + if(x >= 0 && x < self.clipWidth && y >= 0 && y < self.clipHeight) { + setAnchorCoordinate("x",x); + setAnchorCoordinate("y",y); + } else { + domAnchor.style.left = posX + "px"; + domAnchor.style.top = posY + "px"; + } + self.domAnchorBackdrop.style.display = "none"; + self.document.removeEventListener("mousemove",fnMouseMove,false); + self.document.removeEventListener("mouseup",fnMouseUp,false); + }; + domAnchor.addEventListener("mousedown",fnMouseDown,false); + }); +}; + +/* +Delete the anchors +*/ +InnerWikiWidget.prototype.deleteAnchors = function() { + for(var index=this.domAnchorContainer.childNodes.length-1; index>=0; index--) { + var node = this.domAnchorContainer.childNodes[index]; + if(node.tagName === "IMG") { + node.parentNode.removeChild(node); + } } }; @@ -107,7 +233,7 @@ InnerWikiWidget.prototype.createInnerHTML = function() { IMPLANT_PREFIX = "<" + "script>\n$tw.preloadTiddlerArray(", IMPLANT_SUFFIX = ");\n\n", parts = html.split(SPLIT_MARKER), - tiddlers = this.findDataWidgets(this.children); + tiddlers = this.readTiddlerDataWidgets(this.children); if(parts.length === 2) { html = parts[0] + IMPLANT_PREFIX + JSON.stringify(tiddlers) + IMPLANT_SUFFIX + SPLIT_MARKER + parts[1]; } @@ -115,30 +241,36 @@ InnerWikiWidget.prototype.createInnerHTML = function() { }; /* -Find the child data widgets +Find child data widgets */ -InnerWikiWidget.prototype.findDataWidgets = function(children) { - var self = this, - results = []; +InnerWikiWidget.prototype.findDataWidgets = function(children,tag,callback) { + var self = this; $tw.utils.each(children,function(child) { - if(child instanceof DataWidget) { - var item = Object.create(null); - $tw.utils.each(child.attributes,function(value,name) { - item[name] = value; - }); - Array.prototype.push.apply(results,self.readDataWidget(child)); + if(child.dataWidgetTag === tag) { + callback(child); } if(child.children) { - results = results.concat(self.findDataWidgets(child.children)); + self.findDataWidgets(child.children,tag,callback); } }); +}; + +/* +Find the child data widgets +*/ +InnerWikiWidget.prototype.readTiddlerDataWidgets = function(children) { + var self = this, + results = []; + this.findDataWidgets(children,"data",function(widget) { + Array.prototype.push.apply(results,self.readTiddlerDataWidget(widget)); + }); return results; }; /* Read the value(s) from a data widget */ -InnerWikiWidget.prototype.readDataWidget = function(dataWidget) { +InnerWikiWidget.prototype.readTiddlerDataWidget = function(dataWidget) { // Start with a blank object var item = Object.create(null); // Read any attributes not prefixed with $ @@ -206,7 +338,12 @@ InnerWikiWidget.prototype.refresh = function(changedTiddlers) { this.refreshSelf(); return true; } else { - return false; + var childrenRefreshed = this.refreshChildren(changedTiddlers); + if(childrenRefreshed) { + this.deleteAnchors(); + this.createAnchors(); + } + return childrenRefreshed } };