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"/>
$innerwiki>"""/>
+!! 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}}/>
+
+$innerwiki>"""/>
+
!! 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" + "script>\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
}
};