Innerwiki: Add support for draggable anchors

allow-filter-duplicates
Jermolene 2019-02-01 10:43:42 +00:00
rodzic 7bc1458749
commit a4eb139f99
9 zmienionych plików z 264 dodań i 61 usunięć

Wyświetl plik

@ -0,0 +1,2 @@
title: screenshot-7-anchor-1-x
text: 100

Wyświetl plik

@ -0,0 +1,2 @@
title: screenshot-7-anchor-1-y
text: 70

Wyświetl plik

@ -0,0 +1,21 @@
title: $:/plugins/tiddlywiki/innerwiki/crosshairs.svg
type: image/svg+xml
<?xml version="1.0" encoding="UTF-8"?>
<svg width="128px" height="128px" viewBox="0 0 128 128" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<path d="M104.938091,68 L64,64 L68,103.85205 L68,108 C68,110.209139 66.209139,112 64,112 C61.790861,112 60,110.209139 60,108 L60,103.159803 L63.9305182,64 L22.9924274,68 L20,68 C17.790861,68 16,66.209139 16,64 C16,61.790861 17.790861,60 20,60 L22.9924274,60 L63.9305182,64 L60,24.8401973 L60,20 C60,17.790861 61.790861,16 64,16 C66.209139,16 68,17.790861 68,20 L68,24.1479497 L64,64 L104.938091,60 L108,60 C110.209139,60 112,61.790861 112,64 C112,66.209139 110.209139,68 108,68 L104.938091,68 Z" id="path-1"></path>
<filter x="-15.6%" y="-15.6%" width="131.2%" height="131.2%" filterUnits="objectBoundingBox" id="filter-2">
<feMorphology radius="2.5" operator="dilate" in="SourceAlpha" result="shadowSpreadOuter1"></feMorphology>
<feOffset dx="0" dy="0" in="shadowSpreadOuter1" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="2.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
</filter>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Combined-Shape">
<use fill="black" fill-opacity="1" filter="url(#filter-2)" xlink:href="#path-1"></use>
<use fill="#FF0000" fill-rule="evenodd" xlink:href="#path-1"></use>
</g>
</g>
</svg>

Wyświetl plik

@ -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;
})();

Wyświetl plik

@ -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.

Wyświetl plik

@ -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}}/>
<circle cx={{screenshot-7-anchor-2-x}} cy={{screenshot-7-anchor-2-y}} r="50" stroke="blue" stroke-width="2" fill="green" />
</$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"/>

Wyświetl plik

@ -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"/>
<circle cx={{screenshot-inner-anchor-1-x}} cy={{screenshot-inner-anchor-1-y}} r="50" stroke="blue" stroke-width="2" fill="green" />
<$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"/>

Wyświetl plik

@ -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.

Wyświetl plik

@ -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
}
};