1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2025-02-24 06:50:02 +00:00

Split the story macro out into two macros

Now the story macro manages the story element sequence, while the
navigator macro listens for the navigation events, and modifies the
story tiddler as required. Also introduces a history tiddler that
retains the history stack so that we can animate navigation properly
(as distinct from animating the addition and removal of story elements).

Note that the zoomin storyview isn't quite finished, but this is a
stable point to commit these changes.
This commit is contained in:
Jeremy Ruston 2012-06-26 19:54:51 +01:00
parent 663fd2fe10
commit 69a0c46447
7 changed files with 489 additions and 324 deletions

View File

@ -0,0 +1,208 @@
/*\
title: $:/core/modules/macros/navigator.js
type: application/javascript
module-type: macro
Traps navigation events to update a story tiddler and history tiddler. Can also optionally capture navigation target in a specified text reference.
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
exports.info = {
name: "navigator",
params: {
story: {byName: "default", type: "text"}, // Actually a tiddler, but we don't want it to be a dependency
history: {byName: "default", type: "text"}, // Actually a tiddler, but we don't want it to be a dependency
defaultViewTemplate: {byName: true, type: "tiddler"},
defaultEditTemplate: {byName: true, type: "tiddler"},
set: {byName: true, type: "tiddler"}
}
};
exports.getStory = function() {
var storyTiddler = this.wiki.getTiddler(this.params.story);
this.story = {tiddlers: []};
if(storyTiddler && $tw.utils.hop(storyTiddler.fields,"text")) {
this.story = JSON.parse(storyTiddler.fields.text);
}
};
exports.saveStory = function() {
if(this.hasParameter("story")) {
this.wiki.addTiddler(new $tw.Tiddler(this.wiki.getTiddler(this.params.story),{title: this.params.story, text: JSON.stringify(this.story)}));
}
};
exports.getHistory = function() {
var historyTiddler = this.wiki.getTiddler(this.params.history);
this.history = {stack: []};
if(historyTiddler && $tw.utils.hop(historyTiddler.fields,"text")) {
this.history = JSON.parse(historyTiddler.fields.text);
}
};
exports.saveHistory = function() {
if(this.hasParameter("history")) {
this.wiki.addTiddler(new $tw.Tiddler(this.wiki.getTiddler(this.params.history),{title: this.params.history, text: JSON.stringify(this.history)}));
}
};
exports.handleEvent = function(event) {
if(this.eventMap[event.type]) {
this.eventMap[event.type].call(this,event);
}
};
exports.eventMap = {};
// Navigate to a specified tiddler
exports.eventMap["tw-navigate"] = function(event) {
// Update the story tiddler if specified
if(this.hasParameter("story")) {
this.getStory();
var template = this.params.defaultViewTemplate || "$:/templates/ViewTemplate",
t,tiddler,slot;
// See if the tiddler is already there
for(t=0; t<this.story.tiddlers.length; t++) {
if(this.story.tiddlers[t].title === event.navigateTo) {
tiddler = t;
}
}
// If not we need to add it
if(tiddler === undefined) {
// First we try to find the position of the story element we navigated from
var navigateFromTitle;
if(event.navigateFromStoryElement) {
navigateFromTitle = event.navigateFromStoryElement.params.target;
}
slot = 0;
if(navigateFromTitle !== undefined) {
for(t=0; t<this.story.tiddlers.length; t++) {
if(this.story.tiddlers[t].title === navigateFromTitle) {
slot = t + 1;
}
}
}
// Add the tiddler
this.story.tiddlers.splice(slot,0,{title: event.navigateTo});
// Save the story
this.saveStory();
}
}
// Set the tiddler if specified
if(this.hasParameter("set")) {
this.wiki.setTextReference(this.params.set,event.navigateTo);
}
// Add the tiddler to the top of the history stack
this.getHistory();
this.history.stack.push({title: event.navigateTo});
this.saveHistory();
event.stopPropagation();
return false;
};
// Place a tiddler in edit mode
exports.eventMap["tw-EditTiddler"] = function(event) {
if(this.hasParameter("story")) {
var storyTiddler, storyRecord, tiddler, t;
// Put the specified tiddler into edit mode
this.getStory();
for(t=0; t<this.story.tiddlers.length; t++) {
storyRecord = this.story.tiddlers[t];
if(storyRecord.title === event.tiddlerTitle && !storyRecord.draft) {
// Set the story record to the draft of the specified tiddler
storyRecord.draft = "Draft " + (new Date()) + " of " + event.tiddlerTitle;
// Get the current value of the tiddler we're editing
tiddler = this.wiki.getTiddler(event.tiddlerTitle);
// Save the initial value of the draft tiddler
this.wiki.addTiddler(new $tw.Tiddler(
{
text: "Type the text for the tiddler '" + event.tiddlerTitle + "'"
},
tiddler,
{
title: storyRecord.draft,
"draft.title": event.tiddlerTitle,
"draft.of": event.tiddlerTitle
}));
}
}
this.saveStory();
}
event.stopPropagation();
return false;
};
// Take a tiddler out of edit mode, saving the changes
exports.eventMap["tw-SaveTiddler"] = function(event) {
if(this.hasParameter("story")) {
var storyTiddler, storyRecord, tiddler, storyTiddlerModified, t;
this.getStory();
storyTiddlerModified = false;
for(t=0; t<this.story.tiddlers.length; t++) {
storyRecord = this.story.tiddlers[t];
if(storyRecord.draft === event.tiddlerTitle) {
tiddler = this.wiki.getTiddler(storyRecord.draft);
if(tiddler && $tw.utils.hop(tiddler.fields,"draft.title")) {
// Save the draft tiddler as the real tiddler
this.wiki.addTiddler(new $tw.Tiddler(tiddler,{title: tiddler.fields["draft.title"],"draft.title": undefined, "draft.of": undefined}));
// Remove the draft tiddler
this.wiki.deleteTiddler(storyRecord.draft);
// Remove the original tiddler if we're renaming it
if(tiddler.fields["draft.of"] !== tiddler.fields["draft.title"]) {
this.wiki.deleteTiddler(tiddler.fields["draft.of"]);
}
// Make the story record point to the newly saved tiddler
storyRecord.title = tiddler.fields["draft.title"];
storyRecord.draft = undefined;
// Check if we're modifying the story tiddler itself
if(tiddler.fields["draft.title"] === this.params.story) {
storyTiddlerModified = true;
}
}
}
}
if(!storyTiddlerModified) {
this.saveStory();
}
}
event.stopPropagation();
return false;
};
// Close a specified tiddler
exports.eventMap["tw-CloseTiddler"] = function(event) {
if(this.hasParameter("story")) {
var t,storyElement;
this.getStory();
// Look for tiddlers with this title to close
for(t=this.story.tiddlers.length-1; t>=0; t--) {
if(this.story.tiddlers[t].title === event.tiddlerTitle) {
this.story.tiddlers.splice(t,1);
}
}
this.saveStory();
}
event.stopPropagation();
return false;
};
exports.executeMacro = function() {
var attributes = {};
if(this.classes) {
attributes["class"] = this.classes.slice(0);
}
for(var t=0; t<this.content.length; t++) {
this.content[t].execute(this.parents,this.tiddlerTitle);
}
return $tw.Tree.Element("div",attributes,this.content,{
events: ["tw-navigate","tw-EditTiddler","tw-SaveTiddler","tw-CloseTiddler"],
eventHandler: this
});
};
})();

View File

@ -3,15 +3,27 @@ title: $:/core/modules/macros/story/story.js
type: application/javascript type: application/javascript
module-type: macro module-type: macro
Displays a sequence of tiddlers defined in a JSON structure: Displays a sequence of tiddlers defined in two JSON structures. The story tiddler is the sequence of tiddlers currently present in the DOM:
{ {
tiddlers: [ tiddlers: [
{title: <string>, template: <string>} {title: <string>, draft: <string>}
] ]
} }
The storyview is a plugin that extends the story macro to implement different navigation experiences. The optional `draft` member indicates that the tiddler is in edit mode, and the value is the title of the tiddler being used as the draft.
When the story tiddler changes, the story macro adjusts the DOM to match. An optional storyview plugin can be used to visualise the changes.
And the history tiddler is the stack of tiddlers that were navigated to in turn:
{
stack: [
{title: <string>}
]
}
The history stack is updated during navigation, and again the storyview plugin is given an opportunity to animate the navigation.
\*/ \*/
(function(){ (function(){
@ -24,6 +36,7 @@ exports.info = {
name: "story", name: "story",
params: { params: {
story: {byName: "default", type: "tiddler"}, story: {byName: "default", type: "tiddler"},
history: {byName: "default", type: "tiddler"},
defaultViewTemplate: {byName: true, type: "tiddler"}, defaultViewTemplate: {byName: true, type: "tiddler"},
defaultEditTemplate: {byName: true, type: "tiddler"}, defaultEditTemplate: {byName: true, type: "tiddler"},
storyviewTiddler: {byName: true, type: "tiddler"}, storyviewTiddler: {byName: true, type: "tiddler"},
@ -31,213 +44,133 @@ exports.info = {
} }
}; };
/*
Get the data from the JSON story tiddler
*/
exports.getStory = function() { exports.getStory = function() {
var storyTiddler = this.wiki.getTiddler(this.params.story), var storyTiddler = this.wiki.getTiddler(this.params.story);
story = {tiddlers: []}; this.story = {
tiddlers: []
};
if(storyTiddler && $tw.utils.hop(storyTiddler.fields,"text")) { if(storyTiddler && $tw.utils.hop(storyTiddler.fields,"text")) {
return JSON.parse(storyTiddler.fields.text); this.story = JSON.parse(storyTiddler.fields.text);
} else {
return {
tiddlers: []
};
} }
}; };
exports.handleEvent = function(event) { exports.getHistory = function() {
if(this.eventMap[event.type]) { var historyTiddler = this.wiki.getTiddler(this.params.history);
this.eventMap[event.type].call(this,event); this.history = {stack: []};
if(historyTiddler && $tw.utils.hop(historyTiddler.fields,"text")) {
this.history = JSON.parse(historyTiddler.fields.text);
}
};
exports.getViewTemplate = function() {
if(this.hasParameter("defaultViewTemplate")) {
return this.params.defaultViewTemplate;
} else {
return "$:/templates/ViewTemplate";
}
};
exports.getEditTemplate = function() {
if(this.hasParameter("defaultEditTemplate")) {
return this.params.defaultEditTemplate;
} else {
return "$:/templates/EditTemplate";
} }
}; };
/* /*
Return the index of the story element that contains the specified tree node. Returns -1 if none Create a story element representing a given tiddler, optionally being editted
*/ */
exports.findStoryElementContainingNode = function(node) { exports.createStoryElement = function(title,draft) {
// Get the DOM node contained by the target node var node = this.createStoryElementMacro(title,draft),
while(node && !node.domNode) { eventHandler = {handleEvent: function(event) {
node = node.child; // Add context information to the event
event.navigateFromStoryElement = node;
event.navigateFromTitle = title;
return true;
}};
node.execute(this.parents,this.tiddlerTitle);
var storyElement = $tw.Tree.Element("div",{"class": ["tw-story-element"]},[node],{
events: ["tw-navigate","tw-EditTiddler","tw-SaveTiddler","tw-CloseTiddler"],
eventHandler: eventHandler
});
// Save our data inside the story element node
storyElement.storyElementInfo = {title: title};
if(draft) {
storyElement.storyElementInfo.draft = draft;
} }
// Step through the story elements return storyElement;
var slot = -1; };
for(var t=0; t<this.storyNode.children.length; t++) {
if($tw.utils.domContains(this.storyNode.children[t].domNode,node.domNode)) { /*
slot = t; Create the tiddler macro needed to represent a given tiddler and its draft status
*/
exports.createStoryElementMacro = function(title,draft) {
var srcParams;
if(draft) {
srcParams = {target: draft, template: this.getEditTemplate()};
} else {
srcParams = {target: title, template: this.getViewTemplate()};
}
return $tw.Tree.Macro("tiddler",{
srcParams: srcParams,
wiki: this.wiki
});
};
/*
Remove a story element from the story, along with the attendant DOM nodes
*/
exports.removeStoryElement = function(storyElementIndex) {
var storyElement = this.storyNode.children[storyElementIndex];
// Invoke the storyview to animate the removal
if(this.storyview && this.storyview.remove) {
if(!this.storyview.remove(storyElement)) {
// Only delete the DOM element if the storyview.remove() returned false
storyElement.domNode.parentNode.removeChild(storyElement.domNode);
} }
} }
return slot; // Then delete the actual renderer node
this.storyNode.children.splice(storyElementIndex,1);
}; };
/* /*
Return the index of the story element that corresponds to a particular title Return the index of the story element that corresponds to a particular title
startIndex: index to start search (use zero to search from the top) startIndex: index to start search (use zero to search from the top)
tiddlerTitle: tiddler title to seach for tiddlerTitle: tiddler title to seach for
templateTitle: optional template title to search for
*/ */
exports.findStoryElementByTitle = function(startIndex,tiddlerTitle,templateTitle) { exports.findStoryElementByTitle = function(startIndex,tiddlerTitle) {
while(startIndex < this.storyNode.children.length) { while(startIndex < this.storyNode.children.length) {
var params = this.storyNode.children[startIndex].children[0].params; if(this.storyNode.children[startIndex].storyElementInfo.title === tiddlerTitle) {
if(params.target === tiddlerTitle) { return startIndex;
if(!templateTitle || params.template === templateTitle) {
return startIndex;
}
} }
startIndex++; startIndex++;
} }
return undefined; return undefined;
}; };
exports.eventMap = {};
// Navigate to a specified tiddler
exports.eventMap["tw-navigate"] = function(event) {
var template = this.params.defaultViewTemplate || "$:/templates/ViewTemplate",
story = this.getStory(),
navTiddler,t,tiddler,slot;
// See if the tiddler we want is already there
for(t=0; t<story.tiddlers.length; t++) {
if(story.tiddlers[t].title === event.navigateTo) {
navTiddler = t;
}
}
if(typeof(navTiddler) !== "undefined") {
// If we found our tiddler, just tell the storyview to navigate to it
if(this.storyview && this.storyview.navigate) {
this.storyview.navigate(this.storyNode.children[navTiddler],false,event);
}
} else {
// Find the source location in the story
if(event.navigateFrom) {
slot = this.findStoryElementContainingNode(event.navigateFrom) + 1;
} else {
slot = 0;
}
// Add the tiddler to the bottom of the story (subsequently there will be a refreshInDom() call which is when we'll actually do the navigation)
story.tiddlers.splice(slot,0,{title: event.navigateTo, template: template});
this.wiki.addTiddler(new $tw.Tiddler(this.wiki.getTiddler(this.params.story),{text: JSON.stringify(story)}));
// Record the details of the navigation for us to pick up in refreshInDom()
this.lastNavigationEvent = event;
}
event.stopPropagation();
return false;
};
// Place a tiddler in edit mode
exports.eventMap["tw-EditTiddler"] = function(event) {
var template, storyTiddler, story, storyRecord, tiddler, t;
// Put the specified tiddler into edit mode
template = this.params.defaultEditTemplate || "$:/templates/EditTemplate";
story = this.getStory();
for(t=0; t<story.tiddlers.length; t++) {
storyRecord = story.tiddlers[t];
if(storyRecord.title === event.tiddlerTitle && storyRecord.template !== template) {
storyRecord.title = "Draft " + (new Date()) + " of " + event.tiddlerTitle;
storyRecord.template = template;
tiddler = this.wiki.getTiddler(event.tiddlerTitle);
this.wiki.addTiddler(new $tw.Tiddler(
{
text: "Type the text for the tiddler '" + event.tiddlerTitle + "'"
},
tiddler,
{
title: storyRecord.title,
"draft.title": event.tiddlerTitle,
"draft.of": event.tiddlerTitle
}));
}
}
this.wiki.addTiddler(new $tw.Tiddler(this.wiki.getTiddler(this.params.story),{text: JSON.stringify(story)}));
event.stopPropagation();
return false;
};
// Take a tiddler out of edit mode, saving the changes
exports.eventMap["tw-SaveTiddler"] = function(event) {
var template, storyTiddler, story, storyRecord, tiddler, storyTiddlerModified, t;
template = this.params.defaultEditTemplate || "$:/templates/ViewTemplate";
story = this.getStory();
storyTiddlerModified = false;
for(t=0; t<story.tiddlers.length; t++) {
storyRecord = story.tiddlers[t];
if(storyRecord.title === event.tiddlerTitle && storyRecord.template !== template) {
tiddler = this.wiki.getTiddler(storyRecord.title);
if(tiddler && $tw.utils.hop(tiddler.fields,"draft.title")) {
// Save the draft tiddler as the real tiddler
this.wiki.addTiddler(new $tw.Tiddler(tiddler,{title: tiddler.fields["draft.title"],"draft.title": undefined, "draft.of": undefined}));
// Remove the draft tiddler
this.wiki.deleteTiddler(storyRecord.title);
// Remove the original tiddler if we're renaming it
if(tiddler.fields["draft.of"] !== tiddler.fields["draft.title"]) {
this.wiki.deleteTiddler(tiddler.fields["draft.of"]);
}
// Make the story record point to the newly saved tiddler
storyRecord.title = tiddler.fields["draft.title"];
storyRecord.template = template;
// Check if we're modifying the story tiddler itself
if(tiddler.fields["draft.title"] === this.params.story) {
storyTiddlerModified = true;
}
}
}
}
if(!storyTiddlerModified) {
this.wiki.addTiddler(new $tw.Tiddler(this.wiki.getTiddler(this.params.story),{text: JSON.stringify(story)}));
}
event.stopPropagation();
return false;
};
// Take a tiddler out of edit mode, saving the changes
exports.eventMap["tw-CloseTiddler"] = function(event) {
var story,t,storyElement;
story = this.getStory();
// Look for tiddlers with this title to close
for(t=story.tiddlers.length-1; t>=0; t--) {
if(story.tiddlers[t].title === event.tiddlerTitle) {
storyElement = this.storyNode.children[t];
// Invoke the storyview to animate the closure
if(this.storyview && this.storyview.close) {
if(!this.storyview.close(storyElement,event)) {
// Only delete the DOM element if the storyview.close() returned false
storyElement.domNode.parentNode.removeChild(storyElement.domNode);
}
}
// Remove the story element node
this.storyNode.children.splice(t,1);
// Remove the record in the story
story.tiddlers.splice(t,1);
}
}
this.wiki.addTiddler(new $tw.Tiddler(this.wiki.getTiddler(this.params.story),{text: JSON.stringify(story)}));
event.stopPropagation();
return false;
};
exports.executeMacro = function() { exports.executeMacro = function() {
// Get the story object
this.getStory();
// Create the story frame // Create the story frame
var story = this.getStory(); var attributes = {"class": "tw-story-frame"};
this.contentNode = $tw.Tree.Element("div",{"class": "tw-story-content"},this.content); this.storyNode = $tw.Tree.Element("div",attributes,[]);
this.contentNode.execute(this.parents,this.tiddlerTitle);
this.storyNode = $tw.Tree.Element("div",{"class": "tw-story-frame"},[]);
// Create each story element // Create each story element
for(var t=0; t<story.tiddlers.length; t++) { for(var t=0; t<this.story.tiddlers.length; t++) {
var m = $tw.Tree.Macro("tiddler",{ this.storyNode.children.push(this.createStoryElement(this.story.tiddlers[t].title,this.story.tiddlers[t].draft));
srcParams: {target: story.tiddlers[t].title,template: story.tiddlers[t].template},
wiki: this.wiki
});
m.execute(this.parents,this.tiddlerTitle);
this.storyNode.children.push($tw.Tree.Element("div",{"class": "tw-story-element"},[m]));
} }
var attributes = {};
if(this.classes) { if(this.classes) {
attributes["class"] = this.classes.slice(0); $tw.utils.pushTop(attributes["class"],this.classes);
} }
return $tw.Tree.Element("div",attributes,[this.contentNode,this.storyNode],{ return this.storyNode;
events: ["tw-navigate","tw-EditTiddler","tw-SaveTiddler","tw-CloseTiddler"],
eventHandler: this
});
}; };
exports.postRenderInDom = function() { exports.postRenderInDom = function() {
// Reset the record of the previous history stack
this.prevHistory = {stack: []};
// Instantiate the story view // Instantiate the story view
var storyviewName; var storyviewName;
if(this.hasParameter("storyviewTiddler")) { if(this.hasParameter("storyviewTiddler")) {
@ -259,7 +192,6 @@ exports.postRenderInDom = function() {
}; };
exports.refreshInDom = function(changes) { exports.refreshInDom = function(changes) {
var t;
// If the storyview has changed we'll have to completely re-execute the macro // If the storyview has changed we'll have to completely re-execute the macro
if(this.hasParameter("storyviewTiddler") && $tw.utils.hop(changes,this.params.storyviewTiddler)) { if(this.hasParameter("storyviewTiddler") && $tw.utils.hop(changes,this.params.storyviewTiddler)) {
// This logic should be reused from the base macro class, and not duplicated // This logic should be reused from the base macro class, and not duplicated
@ -274,73 +206,105 @@ exports.refreshInDom = function(changes) {
this.renderInDom(parentDomNode,insertBefore); this.renderInDom(parentDomNode,insertBefore);
return; return;
} }
/*jslint browser: true */ // If the story tiddler has changed we need to sync the story elements
if(this.dependencies.hasChanged(changes,this.tiddlerTitle)) { if(this.hasParameter("story") && $tw.utils.hop(changes,this.params.story)) {
// Get the tiddlers we're supposed to be displaying this.processStoryChange(changes);
var self = this, } else {
story = this.getStory(), // If the story didn't change then we must refresh our content
template = this.params.template, this.child.refreshInDom(changes);
n,domNode, }
findTiddler = function (childIndex,tiddlerTitle,templateTitle) { // If the history tiddler has changed we may need to visualise something
while(childIndex < self.storyNode.children.length) { if(this.hasParameter("history") && $tw.utils.hop(changes,this.params.history)) {
var params = self.storyNode.children[childIndex].children[0].params; this.processHistoryChange();
if(params.target === tiddlerTitle) { }
if(!templateTitle || params.template === templateTitle) { };
return childIndex;
} exports.processStoryChange = function(changes) {
} // Get the tiddlers we're supposed to be displaying
childIndex++; var self = this,storyElement,
} t,n,domNode;
return null; // Get the story object
}; this.getStory();
for(t=0; t<story.tiddlers.length; t++) { // Check through each tiddler in the story
// See if the node we want is already there for(t=0; t<this.story.tiddlers.length; t++) {
var tiddlerNode = findTiddler(t,story.tiddlers[t].title,story.tiddlers[t].template); // See if the node we want is already there
if(tiddlerNode === null) { var tiddlerNode = this.findStoryElementByTitle(t,this.story.tiddlers[t].title);
// If not, render the tiddler if(tiddlerNode === undefined) {
var m = $tw.Tree.Element("div",{"class": "tw-story-element"},[ // If not, render the tiddler
$tw.Tree.Macro("tiddler",{ this.storyNode.children.splice(t,0,this.createStoryElement(this.story.tiddlers[t].title,this.story.tiddlers[t].draft));
srcParams: {target: story.tiddlers[t].title,template: story.tiddlers[t].template}, this.storyNode.children[t].renderInDom(this.storyNode.domNode,this.storyNode.domNode.childNodes[t]);
wiki: this.wiki // Invoke the storyview to animate the navigation
}) if(this.storyview && this.storyview.insert) {
]); this.storyview.insert(this.storyNode.children[t]);
m.execute(this.parents,this.tiddlerTitle); }
m.renderInDom(this.storyNode.domNode,this.storyNode.domNode.childNodes[t]); } else {
this.storyNode.children.splice(t,0,m); // Delete any nodes preceding the one we want
// Invoke the storyview to animate the navigation if(tiddlerNode > t) {
if(this.storyview && this.storyview.navigate) { for(n=tiddlerNode-1; n>=t; n--) {
this.storyview.navigate(this.storyNode.children[t],true,this.lastNavigationEvent); this.removeStoryElement(n);
} }
}
storyElement = this.storyNode.children[t];
// Check that the edit status matches
if(this.story.tiddlers[t].draft !== storyElement.storyElementInfo.draft) {
// If not, we'll have to recreate the story element
storyElement.children[0] = this.createStoryElementMacro(this.story.tiddlers[t].title,this.story.tiddlers[t].draft);
// Remove the DOM node in the story element
storyElement.domNode.removeChild(storyElement.domNode.firstChild);
// Reexecute the story element
storyElement.children[0].execute(this.parents,this.tiddlerTitle);
// Render the story element in the DOM
storyElement.children[0].renderInDom(storyElement.domNode);
// Reset the information in the story element
storyElement.storyElementInfo = {title: this.story.tiddlers[t].title, draft: this.story.tiddlers[t].draft};
} else { } else {
// Delete any nodes preceding the one we want // If the draft status matches then just refresh the DOM node we're reusing
if(tiddlerNode > t) {
// First delete the DOM nodes
for(n=t; n<tiddlerNode; n++) {
domNode = this.storyNode.children[n].domNode;
domNode.parentNode.removeChild(domNode);
}
// Then delete the actual renderer nodes
this.storyNode.children.splice(t,tiddlerNode-t);
}
// Refresh the DOM node we're reusing
this.storyNode.children[t].refreshInDom(changes); this.storyNode.children[t].refreshInDom(changes);
} }
} }
// Remove any left over nodes }
if(this.storyNode.children.length > story.tiddlers.length) { // Remove any left over nodes
for(t=story.tiddlers.length; t<this.storyNode.children.length; t++) { if(this.storyNode.children.length > this.story.tiddlers.length) {
domNode = this.storyNode.children[t].domNode; for(t=this.storyNode.children.length-1; t>=this.story.tiddlers.length; t--) {
domNode.parentNode.removeChild(domNode); this.removeStoryElement(t);
}
this.storyNode.children.splice(story.tiddlers.length,this.storyNode.children.length-story.tiddlers.length);
}
} else {
if(this.child) {
this.child.refreshInDom(changes);
} }
} }
// Clear the details of the last navigation };
this.lastNavigationEvent = undefined;
/*
Respond to a change in the history tiddler. The basic idea is to issue forward/back navigation commands to the story view that correspond to the tiddlers that need to be popped on or off the stack
*/
exports.processHistoryChange = function() {
// Read the history tiddler
this.getHistory();
if(this.storyview) {
var t,index,
topCommon = Math.min(this.history.stack.length,this.prevHistory.stack.length);
// Find the common heritage of the new history stack and the previous one
for(t=0; t<topCommon; t++) {
if(this.history.stack[t].title !== this.prevHistory.stack[t].title) {
topCommon = t;
break;
}
}
// We now navigate backwards through the previous history to get back to the common ancestor
for(t=this.prevHistory.stack.length-1; t>=topCommon; t--) {
index = this.findStoryElementByTitle(0,this.prevHistory.stack[t].title);
if(index !== undefined && this.storyview.navigateBack) {
this.storyview.navigateBack(this.storyNode.children[index]);
}
}
// And now we navigate forwards through the new history to get to the latest tiddler
for(t=topCommon; t<this.history.stack.length; t++) {
index = this.findStoryElementByTitle(0,this.history.stack[t].title);
if(index !== undefined && this.storyview.navigateForward) {
this.storyview.navigateForward(this.storyNode.children[index]);
}
}
}
// Record the history stack for next time
this.prevHistory = this.history;
this.history = undefined;
}; };
})(); })();

View File

@ -16,48 +16,46 @@ function ClassicScroller(story) {
this.story = story; this.story = story;
} }
/* ClassicScroller.prototype.remove = function(storyElementNode) {
Visualise navigation to the specified tiddler macro, optionally specifying a source node for the visualisation var targetElement = storyElementNode.domNode;
targetTiddlerNode: tree node of the tiddler macro we're navigating to
isNew: true if the node we're navigating to has just been added to the DOM
sourceNode: optional tree node that initiated the navigation
*/
ClassicScroller.prototype.navigate = function(targetTiddlerNode,isNew,sourceEvent) {
$tw.utils.scrollIntoView(targetTiddlerNode.domNode);
};
ClassicScroller.prototype.close = function(targetTiddlerNode,sourceEvent) {
var targetElement = targetTiddlerNode.domNode;
// Get the current height of the tiddler // Get the current height of the tiddler
var currHeight = targetElement.offsetHeight; var currHeight = targetElement.offsetHeight;
// Put a wrapper around the dom node we're closing // Put a wrapper around the dom node we're closing
var wrapperElement = document.createElement("div"); var wrapperElement = document.createElement("div");
targetElement.parentNode.insertBefore(wrapperElement,targetElement); targetElement.parentNode.insertBefore(wrapperElement,targetElement);
wrapperElement.appendChild(targetElement); wrapperElement.appendChild(targetElement);
// Attach an event handler for the end of the transition
wrapperElement.addEventListener($tw.browser.transitionEnd,function(event) {
if(wrapperElement.parentNode) {
wrapperElement.parentNode.removeChild(wrapperElement);
}
},false);
// Animate the closure // Animate the closure
var d = ($tw.config.preferences.animationDuration/1000).toFixed(8) + "s"; var d = ($tw.config.preferences.animationDuration/1000).toFixed(8) + "s";
wrapperElement.style[$tw.browser.transition] = "-" + $tw.browser.prefix.toLowerCase() + "-transform " + d + " ease-in-out, " +
"opacity " + d + " ease-out, " +
"height " + d + " ease-in-out";
wrapperElement.style[$tw.browser.transformorigin] = "0% 0%"; wrapperElement.style[$tw.browser.transformorigin] = "0% 0%";
wrapperElement.style[$tw.browser.transform] = "translateX(0px)"; wrapperElement.style[$tw.browser.transform] = "translateX(0px)";
wrapperElement.style.opacity = "1.0"; wrapperElement.style.opacity = "1.0";
wrapperElement.style.height = currHeight + "px"; wrapperElement.style.height = currHeight + "px";
wrapperElement.style[$tw.browser.transition] = "-" + $tw.browser.prefix.toLowerCase() + "-transform " + d + " ease-in-out, " +
"opacity " + d + " ease-out, " +
"height " + d + " ease-in-out";
$tw.utils.nextTick(function() { $tw.utils.nextTick(function() {
wrapperElement.style[$tw.browser.transform] = "translateX(" + window.innerWidth + "px)"; wrapperElement.style[$tw.browser.transform] = "translateX(" + window.innerWidth + "px)";
wrapperElement.style.opacity = "0.0"; wrapperElement.style.opacity = "0.0";
wrapperElement.style.height = "0px"; wrapperElement.style.height = "0px";
}); });
// Attach an event handler for th eend of the transition
wrapperElement.addEventListener($tw.browser.transitionEnd,function(event) {
if(wrapperElement.parentNode) {
wrapperElement.parentNode.removeChild(wrapperElement);
}
},true);
// Returning true causes the DOM node not to be deleted // Returning true causes the DOM node not to be deleted
return true; return true;
}; };
ClassicScroller.prototype.navigateBack = function(storyElementNode) {
$tw.utils.scrollIntoView(storyElementNode.domNode);
};
ClassicScroller.prototype.navigateForward = function(storyElementNode) {
$tw.utils.scrollIntoView(storyElementNode.domNode);
};
exports.classic = ClassicScroller; exports.classic = ClassicScroller;
})(); })();

View File

@ -21,7 +21,7 @@ function setStoryElementStyles(e) {
function SidewaysView(story) { function SidewaysView(story) {
this.story = story; this.story = story;
var wrapper = this.story.child.children[1].domNode; var wrapper = this.story.child.domNode;
// Scroll horizontally // Scroll horizontally
wrapper.style.whiteSpace = "nowrap"; wrapper.style.whiteSpace = "nowrap";
// Make all the tiddlers position absolute, and hide all but the first one // Make all the tiddlers position absolute, and hide all but the first one
@ -31,18 +31,16 @@ function SidewaysView(story) {
} }
/* /*
Visualise navigation to the specified tiddler macro, optionally specifying a source node for the visualisation Visualise insertion of the specified tiddler macro, optionally specifying a source node for the visualisation
targetTiddlerNode: tree node of the tiddler macro we're navigating to storyElementNode: tree node of the tiddler macro we're navigating to
isNew: true if the node we're navigating to has just been added to the DOM
sourceNode: optional tree node that initiated the navigation
*/ */
SidewaysView.prototype.navigate = function(targetTiddlerNode,isNew,sourceEvent) { SidewaysView.prototype.insert = function(storyElementNode) {
setStoryElementStyles(targetTiddlerNode.domNode); setStoryElementStyles(storyElementNode.domNode);
$tw.utils.scrollIntoView(targetTiddlerNode.domNode); $tw.utils.scrollIntoView(storyElementNode.domNode);
}; };
SidewaysView.prototype.close = function(targetTiddlerNode,sourceEvent) { SidewaysView.prototype.remove = function(storyElementNode) {
var targetElement = targetTiddlerNode.domNode; var targetElement = storyElementNode.domNode;
// Get the current width of the tiddler // Get the current width of the tiddler
var currWidth = targetElement.offsetWidth; var currWidth = targetElement.offsetWidth;
// Put a wrapper around the dom node we're closing // Put a wrapper around the dom node we're closing

View File

@ -15,17 +15,17 @@ A storyview that shows a single tiddler and navigates by zooming into links
function Zoomin(story) { function Zoomin(story) {
// Save the story // Save the story
this.story = story; this.story = story;
var wrapper = this.story.child.children[1].domNode; this.storyNode = this.story.child.domNode;
// Make all the tiddlers position absolute, and hide all but the first one // Make all the tiddlers position absolute, and hide all but the first one
wrapper.style.position = "relative"; this.storyNode.style.position = "relative";
for(var t=0; t<wrapper.children.length; t++) { for(var t=0; t<this.storyNode.children.length; t++) {
if(t) { if(t) {
wrapper.children[t].style.display = "none"; this.storyNode.children[t].style.display = "none";
} }
wrapper.children[t].style.position = "absolute"; this.storyNode.children[t].style.position = "absolute";
} }
// Record the current tiddler node // Record the current tiddler node
this.currTiddler = this.story.child.children[1].children[0]; this.currTiddler = this.story.child.children[0];
// Set up the stack of previously viewed tiddlers // Set up the stack of previously viewed tiddlers
this.prevTiddlers = [this.currTiddler.children[0].params.target]; this.prevTiddlers = [this.currTiddler.children[0].params.target];
} }
@ -70,44 +70,37 @@ function getNodeBounds(node) {
} }
/* /*
Visualise navigation to the specified tiddler macro, optionally specifying a source node for the visualisation Visualise removal of the the specified tiddler macro, optionally specifying a source node for the visualisation
targetTiddlerNode: tree node of the tiddler macro we're navigating to storyElementNode: tree node of the tiddler macro we're navigating to
isNew: true if the node we're navigating to has just been added to the DOM
sourceNode: optional tree node that initiated the navigation
*/ */
Zoomin.prototype.navigate = function(targetTiddlerNode,isNew,sourceEvent) { Zoomin.prototype.navigateForward = function(storyElementNode) {
// Do nothing if the target tiddler is already the current tiddler // Do nothing if the target tiddler is already the current tiddler
if(targetTiddlerNode === this.currTiddler) { if(storyElementNode === this.currTiddler) {
return; return;
} }
// Make the new tiddler be position absolute and visible // Make the new tiddler be position absolute and visible
targetTiddlerNode.domNode.style.position = "absolute"; storyElementNode.domNode.style.position = "absolute";
targetTiddlerNode.domNode.style.display = "block"; storyElementNode.domNode.style.display = "block";
targetTiddlerNode.domNode.style[$tw.browser.transformorigin] = "0 0"; storyElementNode.domNode.style[$tw.browser.transformorigin] = "0 0";
targetTiddlerNode.domNode.style[$tw.browser.transform] = "translateX(0px) translateY(0px) scale(1)"; storyElementNode.domNode.style[$tw.browser.transform] = "translateX(0px) translateY(0px) scale(1)";
targetTiddlerNode.domNode.style[$tw.browser.transition] = "none"; storyElementNode.domNode.style[$tw.browser.transition] = "none";
// Get the position of the source node, or use the centre of the window as the source position // Get the position of the source node, or use the centre of the window as the source position
var sourceBounds; var sourceBounds = {
if(sourceEvent && sourceEvent.navigateFrom) {
sourceBounds = getNodeBounds(sourceEvent.navigateFrom);
} else {
sourceBounds = {
left: window.innerWidth/2 - 2, left: window.innerWidth/2 - 2,
top: window.innerHeight/2 - 2, top: window.innerHeight/2 - 2,
width: 4, width: 4,
height: 4 height: 4
}; };
}
// Try to find the title node in the target tiddler // Try to find the title node in the target tiddler
var titleNode = findTitleNode(targetTiddlerNode) || targetTiddlerNode; var titleNode = findTitleNode(storyElementNode) || storyElementNode;
// Compute the transform for the target tiddler to make the title lie over the source rectange // Compute the transform for the target tiddler to make the title lie over the source rectange
var targetBounds = getNodeBounds(targetTiddlerNode), var targetBounds = getNodeBounds(storyElementNode),
titleBounds = getNodeBounds(titleNode), titleBounds = getNodeBounds(titleNode),
scale = sourceBounds.width / titleBounds.width, scale = sourceBounds.width / titleBounds.width,
x = sourceBounds.left - targetBounds.left - (titleBounds.left - targetBounds.left) * scale, x = sourceBounds.left - targetBounds.left - (titleBounds.left - targetBounds.left) * scale,
y = sourceBounds.top - targetBounds.top - (titleBounds.top - targetBounds.top) * scale; y = sourceBounds.top - targetBounds.top - (titleBounds.top - targetBounds.top) * scale;
// Transform the target tiddler // Transform the target tiddler
targetTiddlerNode.domNode.style[$tw.browser.transform] = "translateX(" + x + "px) translateY(" + y + "px) scale(" + scale + ")"; storyElementNode.domNode.style[$tw.browser.transform] = "translateX(" + x + "px) translateY(" + y + "px) scale(" + scale + ")";
// Get the animation duration // Get the animation duration
var d = ($tw.config.preferences.animationDuration/1000).toFixed(8) + "s"; var d = ($tw.config.preferences.animationDuration/1000).toFixed(8) + "s";
// Apply the ending transitions with a timeout to ensure that the previously applied transformations are applied first // Apply the ending transitions with a timeout to ensure that the previously applied transformations are applied first
@ -118,10 +111,10 @@ Zoomin.prototype.navigate = function(targetTiddlerNode,isNew,sourceEvent) {
var currTiddlerBounds = getNodeBounds(currTiddler), var currTiddlerBounds = getNodeBounds(currTiddler),
x = (currTiddlerBounds.left - targetBounds.left), x = (currTiddlerBounds.left - targetBounds.left),
y = (currTiddlerBounds.top - targetBounds.top); y = (currTiddlerBounds.top - targetBounds.top);
targetTiddlerNode.domNode.style[$tw.browser.transition] = "-" + $tw.browser.prefix.toLowerCase() + "-transform " + d + " ease-in-out, opacity " + d + " ease-out"; storyElementNode.domNode.style[$tw.browser.transition] = "-" + $tw.browser.prefix.toLowerCase() + "-transform " + d + " ease-in-out, opacity " + d + " ease-out";
targetTiddlerNode.domNode.style.opacity = "1.0"; storyElementNode.domNode.style.opacity = "1.0";
targetTiddlerNode.domNode.style[$tw.browser.transform] = "translateX(" + x + "px) translateY(" + y + "px) scale(1)"; storyElementNode.domNode.style[$tw.browser.transform] = "translateX(" + x + "px) translateY(" + y + "px) scale(1)";
targetTiddlerNode.domNode.style.zIndex = "500"; storyElementNode.domNode.style.zIndex = "500";
// Transform the current tiddler // Transform the current tiddler
var scale = titleBounds.width / sourceBounds.width; var scale = titleBounds.width / sourceBounds.width;
x = titleBounds.left - targetBounds.left - (sourceBounds.left - currTiddlerBounds.left) * scale; x = titleBounds.left - targetBounds.left - (sourceBounds.left - currTiddlerBounds.left) * scale;
@ -133,15 +126,15 @@ Zoomin.prototype.navigate = function(targetTiddlerNode,isNew,sourceEvent) {
currTiddler.domNode.style.zIndex = "0"; currTiddler.domNode.style.zIndex = "0";
}); });
// Record the new current tiddler // Record the new current tiddler
this.currTiddler = targetTiddlerNode; this.currTiddler = storyElementNode;
// Save the tiddler in the stack // Save the tiddler in the stack
this.prevTiddlers.push(targetTiddlerNode.children[0].params.target); this.prevTiddlers.push(storyElementNode.children[0].params.target);
}; };
/* /*
Visualise closing a tiddler Visualise removing a tiddler
*/ */
Zoomin.prototype.close = function(targetTiddlerNode,sourceEvent) { Zoomin.prototype.remove = function(storyElementNode) {
// Remove the last entry from the navigation stack, which will be to navigate to the current tiddler // Remove the last entry from the navigation stack, which will be to navigate to the current tiddler
this.prevTiddlers.pop(); this.prevTiddlers.pop();
// Find the top entry in the navigation stack that still exists // Find the top entry in the navigation stack that still exists
@ -159,12 +152,12 @@ Zoomin.prototype.close = function(targetTiddlerNode,sourceEvent) {
// Get the animation duration // Get the animation duration
var d = ($tw.config.preferences.animationDuration/1000).toFixed(8) + "s"; var d = ($tw.config.preferences.animationDuration/1000).toFixed(8) + "s";
// Set up the tiddler that is being closed // Set up the tiddler that is being closed
targetTiddlerNode.domNode.style.position = "absolute"; storyElementNode.domNode.style.position = "absolute";
targetTiddlerNode.domNode.style.display = "block"; storyElementNode.domNode.style.display = "block";
targetTiddlerNode.domNode.style[$tw.browser.transformorigin] = "50% 50%"; storyElementNode.domNode.style[$tw.browser.transformorigin] = "50% 50%";
targetTiddlerNode.domNode.style[$tw.browser.transform] = "translateX(0px) translateY(0px) scale(1)"; storyElementNode.domNode.style[$tw.browser.transform] = "translateX(0px) translateY(0px) scale(1)";
targetTiddlerNode.domNode.style[$tw.browser.transition] = "none"; storyElementNode.domNode.style[$tw.browser.transition] = "none";
targetTiddlerNode.domNode.style.zIndex = "0"; storyElementNode.domNode.style.zIndex = "0";
// Set up the tiddler we're moving back in // Set up the tiddler we're moving back in
if(storyElement !== undefined) { if(storyElement !== undefined) {
storyElement.domNode.style.position = "absolute"; storyElement.domNode.style.position = "absolute";
@ -183,15 +176,15 @@ Zoomin.prototype.close = function(targetTiddlerNode,sourceEvent) {
// Animate them both // Animate them both
$tw.utils.nextTick(function() { $tw.utils.nextTick(function() {
// First, the tiddler we're closing // First, the tiddler we're closing
targetTiddlerNode.domNode.style[$tw.browser.transition] = "-" + $tw.browser.prefix.toLowerCase() + "-transform " + d + " ease-in-out, opacity " + d + " ease-out"; storyElementNode.domNode.style[$tw.browser.transition] = "-" + $tw.browser.prefix.toLowerCase() + "-transform " + d + " ease-in-out, opacity " + d + " ease-out";
targetTiddlerNode.domNode.style.opacity = "0.0"; storyElementNode.domNode.style.opacity = "0.0";
targetTiddlerNode.domNode.style[$tw.browser.transformorigin] = "50% 50%"; storyElementNode.domNode.style[$tw.browser.transformorigin] = "50% 50%";
targetTiddlerNode.domNode.style[$tw.browser.transform] = "translateX(0px) translateY(0px) scale(0.1)"; storyElementNode.domNode.style[$tw.browser.transform] = "translateX(0px) translateY(0px) scale(0.1)";
targetTiddlerNode.domNode.style.zIndex = "0"; storyElementNode.domNode.style.zIndex = "0";
targetTiddlerNode.domNode.addEventListener($tw.browser.transitionEnd,function(event) { storyElementNode.domNode.addEventListener($tw.browser.transitionEnd,function(event) {
// Delete the DOM node when the transition is over // Delete the DOM node when the transition is over
if(targetTiddlerNode.domNode.parentNode) { if(storyElementNode.domNode.parentNode) {
targetTiddlerNode.domNode.parentNode.removeChild(targetTiddlerNode.domNode); storyElementNode.domNode.parentNode.removeChild(storyElementNode.domNode);
} }
},true); },true);
// Now the tiddler we're going back to // Now the tiddler we're going back to

View File

@ -1,7 +1,7 @@
title: $:/templates/PageTemplate title: $:/templates/PageTemplate
<div class="container"> <<navigator story:"$:/StoryTiddlers" history:"$:/History" ><
<<story story:"$:/StoryTiddlers" storyviewTiddler:[[$:/CurrentView]] storyview:classic ><
{{navigation-panel{ {{navigation-panel{
<<chooser>< <<chooser><
<<list all>> <<list all>>
@ -35,9 +35,13 @@ title: $:/templates/PageTemplate
</div> </div>
>> >>
>> >>
</div>
</div>
</div>
<div class="container">
<<story story:"$:/StoryTiddlers" history:"$:/History" storyviewTiddler:[[$:/CurrentView]] storyview:classic >>
</div> </div>
</div>
</div>
>> >>
</div>

View File

@ -3,9 +3,9 @@ type: application/json
{ {
"tiddlers": [ "tiddlers": [
{"title": "HelloThere", "template": "$:/templates/ViewTemplate"}, {"title": "HelloThere"},
{"title": "Introduction", "template": "$:/templates/ViewTemplate"}, {"title": "Introduction"},
{"title": "Improvements", "template": "$:/templates/ViewTemplate"}, {"title": "Improvements"},
{"title": "Docs", "template": "$:/templates/ViewTemplate"} {"title": "Docs"}
] ]
} }