TiddlyWiki5/core/modules/storyviews/zoomin.js

231 lines
8.2 KiB
JavaScript

/*\
title: $:/core/modules/storyviews/zoomin.js
type: application/javascript
module-type: storyview
Zooms between individual tiddlers
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var easing = "cubic-bezier(0.645, 0.045, 0.355, 1)"; // From http://easings.net/#easeInOutCubic
var ZoominListView = function(listWidget) {
var self = this;
this.listWidget = listWidget;
this.textNodeLogger = new $tw.utils.Logger("zoomin story river view", {
enable: true,
colour: 'red'
});
// Get the index of the tiddler that is at the top of the history
var history = this.listWidget.wiki.getTiddlerDataCached(this.listWidget.historyTitle,[]),
targetTiddler;
if(history.length > 0) {
targetTiddler = history[history.length-1].title;
}
// Make all the tiddlers position absolute, and hide all but the top (or first) one
$tw.utils.each(this.listWidget.children,function(itemWidget,index) {
var domNode = itemWidget.findFirstDomNode();
// Abandon if the list entry isn't a DOM element (it might be a text node)
if(!(domNode instanceof Element)) {
return;
}
if((targetTiddler && targetTiddler !== itemWidget.parseTreeNode.itemTitle) || (!targetTiddler && index)) {
domNode.style.display = "none";
} else {
self.currentTiddlerDomNode = domNode;
}
$tw.utils.addClass(domNode,"tc-storyview-zoomin-tiddler");
});
};
ZoominListView.prototype.navigateTo = function(historyInfo) {
var duration = $tw.utils.getAnimationDuration(),
listElementIndex = this.listWidget.findListItem(0,historyInfo.title);
if(listElementIndex === undefined) {
return;
}
var listItemWidget = this.listWidget.children[listElementIndex],
targetElement = listItemWidget.findFirstDomNode();
// Abandon if the list entry isn't a DOM element (it might be a text node)
if(!targetElement) {
return;
} else if (targetElement.nodeType === Node.TEXT_NODE) {
this.logTextNodeRoot(targetElement);
return;
}
// Make the new tiddler be position absolute and visible so that we can measure it
$tw.utils.addClass(targetElement,"tc-storyview-zoomin-tiddler");
$tw.utils.setStyle(targetElement,[
{display: "block"},
{transformOrigin: "0 0"},
{transform: "translateX(0px) translateY(0px) scale(1)"},
{transition: "none"},
{opacity: "0.0"}
]);
// Get the position of the source node, or use the centre of the window as the source position
var sourceBounds = historyInfo.fromPageRect || {
left: window.innerWidth/2 - 2,
top: window.innerHeight/2 - 2,
width: window.innerWidth/8,
height: window.innerHeight/8
};
// Try to find the title node in the target tiddler
var titleDomNode = findTitleDomNode(listItemWidget) || listItemWidget.findFirstDomNode(),
zoomBounds = titleDomNode.getBoundingClientRect();
// Compute the transform for the target tiddler to make the title lie over the source rectange
var targetBounds = targetElement.getBoundingClientRect(),
scale = sourceBounds.width / zoomBounds.width,
x = sourceBounds.left - targetBounds.left - (zoomBounds.left - targetBounds.left) * scale,
y = sourceBounds.top - targetBounds.top - (zoomBounds.top - targetBounds.top) * scale;
// Transform the target tiddler to its starting position
$tw.utils.setStyle(targetElement,[
{transform: "translateX(" + x + "px) translateY(" + y + "px) scale(" + scale + ")"}
]);
// Force layout
$tw.utils.forceLayout(targetElement);
// Apply the ending transitions with a timeout to ensure that the previously applied transformations are applied first
var self = this,
prevCurrentTiddler = this.currentTiddlerDomNode;
this.currentTiddlerDomNode = targetElement;
// Transform the target tiddler to its natural size
$tw.utils.setStyle(targetElement,[
{transition: $tw.utils.roundTripPropertyName("transform") + " " + duration + "ms " + easing + ", opacity " + duration + "ms " + easing},
{opacity: "1.0"},
{transform: "translateX(0px) translateY(0px) scale(1)"},
{zIndex: "500"},
]);
// Transform the previous tiddler out of the way and then hide it
if(prevCurrentTiddler && prevCurrentTiddler !== targetElement) {
scale = zoomBounds.width / sourceBounds.width;
x = zoomBounds.left - targetBounds.left - (sourceBounds.left - targetBounds.left) * scale;
y = zoomBounds.top - targetBounds.top - (sourceBounds.top - targetBounds.top) * scale;
$tw.utils.setStyle(prevCurrentTiddler,[
{transition: $tw.utils.roundTripPropertyName("transform") + " " + duration + "ms " + easing + ", opacity " + duration + "ms " + easing},
{opacity: "0.0"},
{transformOrigin: "0 0"},
{transform: "translateX(" + x + "px) translateY(" + y + "px) scale(" + scale + ")"},
{zIndex: "0"}
]);
// Hide the tiddler when the transition has finished
setTimeout(function() {
if(self.currentTiddlerDomNode !== prevCurrentTiddler) {
prevCurrentTiddler.style.display = "none";
}
},duration);
}
// Scroll the target into view
// $tw.pageScroller.scrollIntoView(targetElement);
};
/*
Find the first child DOM node of a widget that has the class "tc-title"
*/
function findTitleDomNode(widget,targetClass) {
targetClass = targetClass || "tc-title";
var domNode = widget.findFirstDomNode();
if(domNode && domNode.querySelector) {
return domNode.querySelector("." + targetClass);
}
return null;
}
ZoominListView.prototype.insert = function(widget) {
var targetElement = widget.findFirstDomNode();
// Abandon if the list entry isn't a DOM element (it might be a text node)
if(!targetElement) {
return;
} else if (targetElement.nodeType === Node.TEXT_NODE) {
this.logTextNodeRoot(targetElement);
return;
}
// Make the newly inserted node position absolute and hidden
$tw.utils.addClass(targetElement,"tc-storyview-zoomin-tiddler");
$tw.utils.setStyle(targetElement,[
{display: "none"}
]);
};
ZoominListView.prototype.remove = function(widget) {
var targetElement = widget.findFirstDomNode(),
duration = $tw.utils.getAnimationDuration(),
removeElement = function() {
widget.removeChildDomNodes();
};
// Abandon if the list entry isn't a DOM element (it might be a text node)
if(!targetElement || targetElement.nodeType === Node.TEXT_NODE) {
removeElement();
return;
}
// Abandon if hidden
if(targetElement.style.display != "block" ) {
removeElement();
return;
}
// Set up the tiddler that is being closed
$tw.utils.addClass(targetElement,"tc-storyview-zoomin-tiddler");
$tw.utils.setStyle(targetElement,[
{display: "block"},
{transformOrigin: "50% 50%"},
{transform: "translateX(0px) translateY(0px) scale(1)"},
{transition: "none"},
{zIndex: "0"}
]);
// We'll move back to the previous or next element in the story
var toWidget = widget.previousSibling();
if(!toWidget) {
toWidget = widget.nextSibling();
}
var toWidgetDomNode = toWidget && toWidget.findFirstDomNode();
// Set up the tiddler we're moving back in
if(toWidgetDomNode) {
if (toWidgetDomNode.nodeType === Node.TEXT_NODE) {
this.logTextNodeRoot(toWidgetDomNode);
toWidgetDomNode = null;
} else {
$tw.utils.addClass(toWidgetDomNode,"tc-storyview-zoomin-tiddler");
$tw.utils.setStyle(toWidgetDomNode,[
{display: "block"},
{transformOrigin: "50% 50%"},
{transform: "translateX(0px) translateY(0px) scale(10)"},
{transition: $tw.utils.roundTripPropertyName("transform") + " " + duration + "ms " + easing + ", opacity " + duration + "ms " + easing},
{opacity: "0"},
{zIndex: "500"}
]);
this.currentTiddlerDomNode = toWidgetDomNode;
}
}
// Animate them both
// Force layout
$tw.utils.forceLayout(this.listWidget.parentDomNode);
// First, the tiddler we're closing
$tw.utils.setStyle(targetElement,[
{transformOrigin: "50% 50%"},
{transform: "translateX(0px) translateY(0px) scale(0.1)"},
{transition: $tw.utils.roundTripPropertyName("transform") + " " + duration + "ms " + easing + ", opacity " + duration + "ms " + easing},
{opacity: "0"},
{zIndex: "0"}
]);
setTimeout(removeElement,duration);
// Now the tiddler we're going back to
if(toWidgetDomNode) {
$tw.utils.setStyle(toWidgetDomNode,[
{transform: "translateX(0px) translateY(0px) scale(1)"},
{opacity: "1"}
]);
}
return true; // Indicate that we'll delete the DOM node
};
ZoominListView.prototype.logTextNodeRoot = function(node) {
this.textNodeLogger.log($tw.language.getString("Error/ZoominTextNode") + " " + node.textContent);
};
exports.zoomin = ZoominListView;
})();