From e4fb532ecda8bbfd2effcaafe64908d54d82979c Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Tue, 27 Nov 2012 16:55:17 +0000 Subject: [PATCH] Introduce zoomable macro Used with Cecily to give us pan and zoom across the Cecily canvas. --- core/modules/macros/list/listviews/cecily.js | 11 ++ core/modules/macros/zoomable.js | 147 ++++++++++++++++++ .../CecilySubView/CecilyHistoryList.tid | 2 + .../samples/CecilySubView/CecilyStoryList.tid | 2 + .../samples/CecilySubView/CecilySubView.tid | 4 +- 5 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 core/modules/macros/zoomable.js create mode 100644 editions/tw5.com/tiddlers/samples/CecilySubView/CecilyHistoryList.tid create mode 100644 editions/tw5.com/tiddlers/samples/CecilySubView/CecilyStoryList.tid diff --git a/core/modules/macros/list/listviews/cecily.js b/core/modules/macros/list/listviews/cecily.js index ded63ae1f1..5cd7d12ad9 100644 --- a/core/modules/macros/list/listviews/cecily.js +++ b/core/modules/macros/list/listviews/cecily.js @@ -75,12 +75,23 @@ CecilyListView.prototype.lookupTiddlerInMap = function(title,domNode) { CecilyListView.prototype.positionTiddler = function(title,domNode) { var pos = this.lookupTiddlerInMap(title,domNode), scale = pos.w/domNode.offsetWidth; + domNode.setAttribute("data-tw-zoom",JSON.stringify(pos)); $tw.utils.setStyle(domNode,[ {transformOrigin: "0% 0%"}, {transform: "translateX(" + pos.x + "px) translateY(" + pos.y + "px) scale(" + scale + ")"} ]); }; +CecilyListView.prototype.navigateTo = function(historyInfo) { + var listElementIndex = this.listMacro.findListElementByTitle(0,historyInfo.title), + listElementNode = this.listMacro.listFrame.children[listElementIndex], + targetElement = listElementNode.domNode; + // Scroll the node into view + var scrollEvent = document.createEvent("Event"); + scrollEvent.initEvent("tw-scroll",true,true); + targetElement.dispatchEvent(scrollEvent); +}; + CecilyListView.prototype.insert = function(index) { var listElementNode = this.listMacro.listFrame.children[index], targetElement = listElementNode.domNode; diff --git a/core/modules/macros/zoomable.js b/core/modules/macros/zoomable.js new file mode 100644 index 0000000000..6426aa01be --- /dev/null +++ b/core/modules/macros/zoomable.js @@ -0,0 +1,147 @@ +/*\ +title: $:/core/modules/macros/zoomable.js +type: application/javascript +module-type: macro + +Creates a zoomable frame around its content + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +exports.info = { + name: "zoomable", + params: { + width: {byName: true, type: "text"}, + height: {byName: true, type: "text"}, + "class": {byName: true, type: "text"} + } +}; + +exports.executeMacro = function() { + var innerClasses = ["tw-scrollable-inner"], + innerAttributes = { + "class": innerClasses, + style: { + overflow: "visible", + position: "relative" + } + }, + outerClasses = ["tw-scrollable","tw-scrollable-outer"], + outerAttributes = { + "class": outerClasses, + style: { + overflow: "hidden", + "white-space": "nowrap" + } + }; + if(this.hasParameter("class")) { + outerClasses.push(this.params["class"]); + } + if(this.classes) { + $tw.utils.pushTop(outerClasses,this.classes); + } + if(this.hasParameter("width")) { + outerAttributes.style.width = this.params.width; + } + if(this.hasParameter("height")) { + outerAttributes.style.height = this.params.height; + } + var self = this; + this.innerFrame = $tw.Tree.Element("div",innerAttributes,this.content); + this.outerFrame = $tw.Tree.Element("div",outerAttributes,[this.innerFrame],{ + events: ["tw-scroll","click"], + eventHandler: this + }); + this.outerFrame.execute(this.parents,this.tiddlerTitle); + this.translateX = 0; + this.translateY = 0; + this.scale = 1; + return this.outerFrame; +}; + +/* +Bring a dom node into view by setting the viewport (translation and scale factors) appropriately. + +The calculations are a little hairy. The problem is that we want to deal with coordinates that take into account CSS transformations applied to elements. The most reliable way to do that is to use getBoundingClientRect, which returns the bounding rectangle in screen coordinates of an element, taking into account any transformations. + +So, we measure the position of the target element and then adjust it to compensate for the current viewport settings. +*/ +exports.setViewPort = function(domNode) { + var zoomPosString = domNode.getAttribute("data-tw-zoom"); + if(zoomPosString) { + var zoomPos = JSON.parse(zoomPosString); + if(zoomPos) { + // Compute the transform for the inner frame + this.translateX = -zoomPos.x; + this.translateY = -zoomPos.y; + this.scale = this.outerFrame.domNode.offsetWidth / zoomPos.w; + $tw.utils.setStyle(this.innerFrame.domNode,[ + {transition: $tw.utils.roundTripPropertyName("transform") + " " + $tw.config.preferences.animationDurationMs + " ease-in-out"}, + {transformOrigin: "0% 0%"}, + {transform: "scale(" + this.scale + ") translateX(" + this.translateX + "px) translateY(" + this.translateY + "px)"} + ]); + } + } +}; + +exports.zoomAll = function() { + var bounds = {left: 0, top: 0, right: 0, bottom: 0}, + scanChildren = function(nodes) { + for(var c=0; c bounds.right) { + bounds.right = zoom.x + zoom.w; + } + if((zoom.y + zoom.h) > bounds.bottom) { + bounds.bottom = zoom.y + zoom.h; + } + } + } + if(node.hasChildNodes()) { + scanChildren(node.childNodes); + } + } + }; + scanChildren(this.innerFrame.domNode.childNodes); + this.translateX = bounds.left; + this.translateY = bounds.top; + this.scale = this.outerFrame.domNode.offsetWidth / (bounds.right - bounds.left); + $tw.utils.setStyle(this.innerFrame.domNode,[ + {transition: $tw.utils.roundTripPropertyName("transform") + " " + $tw.config.preferences.animationDurationMs + " ease-in-out"}, + {transformOrigin: "0% 0%"}, + {transform: "scale(" + this.scale + ") translateX(" + this.translateX + "px) translateY(" + this.translateY + "px)"} + ]); +}; + +exports.handleEvent = function(event) { + if(event.type === "tw-scroll") { + return this.handleScrollEvent(event); + } + if(event.type === "click") { + this.zoomAll(); + return false; + } + return true; +} + +exports.handleScrollEvent = function(event) { +console.log("Zoomable got scroll event to",event.target); + this.setViewPort(event.target); + event.stopPropagation(); + return false; +}; + +})(); diff --git a/editions/tw5.com/tiddlers/samples/CecilySubView/CecilyHistoryList.tid b/editions/tw5.com/tiddlers/samples/CecilySubView/CecilyHistoryList.tid new file mode 100644 index 0000000000..3f56f02d26 --- /dev/null +++ b/editions/tw5.com/tiddlers/samples/CecilySubView/CecilyHistoryList.tid @@ -0,0 +1,2 @@ +title: $:/CecilyHistoryList + diff --git a/editions/tw5.com/tiddlers/samples/CecilySubView/CecilyStoryList.tid b/editions/tw5.com/tiddlers/samples/CecilySubView/CecilyStoryList.tid new file mode 100644 index 0000000000..4271122d8d --- /dev/null +++ b/editions/tw5.com/tiddlers/samples/CecilySubView/CecilyStoryList.tid @@ -0,0 +1,2 @@ +title: $:/CecilyStoryList + diff --git a/editions/tw5.com/tiddlers/samples/CecilySubView/CecilySubView.tid b/editions/tw5.com/tiddlers/samples/CecilySubView/CecilySubView.tid index 8db9d13289..b12b0466e8 100644 --- a/editions/tw5.com/tiddlers/samples/CecilySubView/CecilySubView.tid +++ b/editions/tw5.com/tiddlers/samples/CecilySubView/CecilySubView.tid @@ -4,7 +4,9 @@ title: CecilySubView "Cecily" provides a customisable ZoomableUserInterface to your tiddlers. -<< +[[Docs]] - HelloThere - AllTiddlers + +<< <> >>