From 69a0c46447402535d446e86fe9a658beb0541eb6 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Tue, 26 Jun 2012 19:54:51 +0100 Subject: [PATCH] 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. --- core/modules/macros/navigator.js | 208 ++++++++++ core/modules/macros/story/story.js | 436 +++++++++----------- core/modules/macros/story/views/classic.js | 40 +- core/modules/macros/story/views/sideways.js | 18 +- core/modules/macros/story/views/zoomin.js | 89 ++-- core/templates/PageTemplate.tid | 14 +- tw5.com/wiki/StoryTiddlers.tid | 8 +- 7 files changed, 489 insertions(+), 324 deletions(-) create mode 100644 core/modules/macros/navigator.js diff --git a/core/modules/macros/navigator.js b/core/modules/macros/navigator.js new file mode 100644 index 000000000..57d617495 --- /dev/null +++ b/core/modules/macros/navigator.js @@ -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=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, template: } - ] + {title: , draft: } + ] } -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: } + ] + } + +The history stack is updated during navigation, and again the storyview plugin is given an opportunity to animate the navigation. \*/ (function(){ @@ -24,6 +36,7 @@ exports.info = { name: "story", params: { story: {byName: "default", type: "tiddler"}, + history: {byName: "default", type: "tiddler"}, defaultViewTemplate: {byName: true, type: "tiddler"}, defaultEditTemplate: {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() { - var storyTiddler = this.wiki.getTiddler(this.params.story), - story = {tiddlers: []}; + var storyTiddler = this.wiki.getTiddler(this.params.story); + this.story = { + tiddlers: [] + }; if(storyTiddler && $tw.utils.hop(storyTiddler.fields,"text")) { - return JSON.parse(storyTiddler.fields.text); - } else { - return { - tiddlers: [] - }; + this.story = JSON.parse(storyTiddler.fields.text); } }; -exports.handleEvent = function(event) { - if(this.eventMap[event.type]) { - this.eventMap[event.type].call(this,event); +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.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) { - // Get the DOM node contained by the target node - while(node && !node.domNode) { - node = node.child; +exports.createStoryElement = function(title,draft) { + var node = this.createStoryElementMacro(title,draft), + eventHandler = {handleEvent: function(event) { + // 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 - var slot = -1; - for(var t=0; 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() { + // Get the story object + this.getStory(); // Create the story frame - var story = this.getStory(); - this.contentNode = $tw.Tree.Element("div",{"class": "tw-story-content"},this.content); - this.contentNode.execute(this.parents,this.tiddlerTitle); - this.storyNode = $tw.Tree.Element("div",{"class": "tw-story-frame"},[]); + var attributes = {"class": "tw-story-frame"}; + this.storyNode = $tw.Tree.Element("div",attributes,[]); // Create each story element - for(var t=0; t t) { + for(n=tiddlerNode-1; n>=t; n--) { + 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 { - // Delete any nodes preceding the one we want - if(tiddlerNode > t) { - // First delete the DOM nodes - for(n=t; n story.tiddlers.length) { - for(t=story.tiddlers.length; t this.story.tiddlers.length) { + for(t=this.storyNode.children.length-1; t>=this.story.tiddlers.length; t--) { + this.removeStoryElement(t); } } - // 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--) { + 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 -<< +<< + {{navigation-panel{ << <> @@ -35,9 +35,13 @@ title: $:/templates/PageTemplate >> >> + + + +
+<>
- - + >> - + diff --git a/tw5.com/wiki/StoryTiddlers.tid b/tw5.com/wiki/StoryTiddlers.tid index b777a1d79..ef409386b 100644 --- a/tw5.com/wiki/StoryTiddlers.tid +++ b/tw5.com/wiki/StoryTiddlers.tid @@ -3,9 +3,9 @@ type: application/json { "tiddlers": [ - {"title": "HelloThere", "template": "$:/templates/ViewTemplate"}, - {"title": "Introduction", "template": "$:/templates/ViewTemplate"}, - {"title": "Improvements", "template": "$:/templates/ViewTemplate"}, - {"title": "Docs", "template": "$:/templates/ViewTemplate"} + {"title": "HelloThere"}, + {"title": "Introduction"}, + {"title": "Improvements"}, + {"title": "Docs"} ] } \ No newline at end of file