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