Introduce refactored wiki parser and renderer

This is a half-way through a big refactoring of the parsing and
rendering infrastructure. The main change is to separate the parse and
render trees, which makes the code a lot cleaner. The new parser isn't
yet functional enough to replace the existing parser so for the moment
you have to manually invoke it with `$tw.testNewParser()` in your
browser console. I really ought to use branches for this kind of
thing...
This commit is contained in:
Jeremy Ruston
2012-12-13 21:34:31 +00:00
parent 916ca8eecf
commit d338a54370
28 changed files with 2837 additions and 33 deletions
+145
View File
@@ -0,0 +1,145 @@
/*\
title: $:/core/modules/widget/button.js
type: application/javascript
module-type: widget
Implements the button widget.
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var ButtonWidget = function(renderer) {
// Save state
this.renderer = renderer;
// Generate child nodes
this.generateChildNodes();
};
ButtonWidget.prototype.generateChildNodes = function() {
// Get the parameters from the attributes
this.message = this.renderer.getAttribute("message");
this.param = this.renderer.getAttribute("param");
this.set = this.renderer.getAttribute("set");
this.setTo = this.renderer.getAttribute("setTo");
this.popup = this.renderer.getAttribute("popup");
this.hover = this.renderer.getAttribute("hover");
this.qualifyTiddlerTitles = this.renderer.getAttribute("qualifyTiddlerTitles");
this["class"] = this.renderer.getAttribute("class");
// Compose the button
var classes = ["tw-tiddlybutton"];
if(this.classes) {
$tw.utils.pushTop(classes,this.classes);
}
if(this["class"]) {
$tw.utils.pushTop(classes,this["class"]);
}
var events = [{name: "click", handlerObject: this, handlerMethod: "handleClickEvent"}];
if(this.hover === "yes") {
events.push({name: "mouseover", handlerObject: this, handlerMethod: "handleMouseOverOrOutEvent"});
events.push({name: "mouseout", handlerObject: this, handlerMethod: "handleMouseOverOrOutEvent"});
}
this.children = this.renderer.renderTree.createRenderers(this.renderer.renderContext,[{
type: "element",
tag: "button",
attributes: {
"class": {type: "string", value: classes.join(" ")}
},
children: this.renderer.parseTreeNode.children,
events: events
}]);
};
ButtonWidget.prototype.render = function(type) {
var output = [];
$tw.utils.each(this.children,function(node) {
if(node.render) {
output.push(node.render(type));
}
});
return output.join("");
};
ButtonWidget.prototype.renderInDom = function(parentElement) {
this.parentElement = parentElement;
// Render any child nodes
$tw.utils.each(this.children,function(node) {
if(node.renderInDom) {
parentElement.appendChild(node.renderInDom());
}
});
};
ButtonWidget.prototype.dispatchMessage = function(event) {
$tw.utils.dispatchCustomEvent(event.target,this.message,{
param: this.param,
tiddlerTitle: this.renderer.getContextTiddlerTitle()
});
};
ButtonWidget.prototype.triggerPopup = function(event) {
$tw.popup.triggerPopup({
textRef: this.popup,
domNode: this.renderer.domNode,
qualifyTiddlerTitles: this.qualifyTiddlerTitles,
renderContext: this.renderer.renderContext,
wiki: this.renderer.renderTree.wiki
});
};
ButtonWidget.prototype.setTiddler = function() {
var tiddler = this.renderer.renderTree.wiki.getTiddler(this.set);
this.renderer.renderTree.wiki.addTiddler(new $tw.Tiddler(tiddler,{title: this.set, text: this.setTo}));
};
ButtonWidget.prototype.handleClickEvent = function(event) {
if(this.message) {
this.dispatchMessage(event);
}
if(this.popup) {
this.triggerPopup(event);
}
if(this.set && this.setTo) {
this.setTiddler();
}
event.preventDefault();
return false;
};
ButtonWidget.prototype.handleMouseOverOrOutEvent = function(event) {
if(this.popup) {
this.triggerPopup(event);
}
event.preventDefault();
return false;
};
ButtonWidget.prototype.refreshInDom = function(changedAttributes,changedTiddlers) {
// Check if any of our attributes have changed, or if a tiddler we're interested in has changed
if(changedAttributes.message || changedAttributes.param || changedAttributes.set || changedAttributes.setTo || changedAttributes.popup || changedAttributes.hover || changedAttributes.qualifyTiddlerTitles || changedAttributes["class"] || (this.set && changedTiddlers[this.set]) || (this.popup && changedTiddlers[this.popup])) {
// Remove old child nodes
$tw.utils.removeChildren(this.parentElement);
// Regenerate and render children
this.generateChildNodes();
var self = this;
$tw.utils.each(this.children,function(node) {
if(node.renderInDom) {
self.parentElement.appendChild(node.renderInDom());
}
});
} else {
// We don't need to refresh ourselves, so just refresh any child nodes
$tw.utils.each(this.children,function(node) {
if(node.refreshInDom) {
node.refreshInDom(changedTiddlers);
}
});
}
};
exports.button = ButtonWidget;
})();
+147
View File
@@ -0,0 +1,147 @@
/*\
title: $:/core/modules/widget/link.js
type: application/javascript
module-type: widget
Implements the link widget.
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var isLinkExternal = function(to) {
var externalRegExp = /(?:file|http|https|mailto|ftp|irc|news|data):[^\s'"]+(?:\/|\b)/i;
return externalRegExp.test(to);
};
var LinkWidget = function(renderer) {
// Save state
this.renderer = renderer;
// Generate child nodes
this.generateChildNodes();
};
LinkWidget.prototype.generateChildNodes = function() {
// Get the parameters from the attributes
this.to = this.renderer.getAttribute("to");
this.hover = this.renderer.getAttribute("hover");
this.qualifyHoverTitle = this.renderer.getAttribute("qualifyHoverTitle");
// Determine the default link characteristics
this.isExternal = isLinkExternal(this.to);
if(!this.isExternal) {
this.isMissing = !this.renderer.renderTree.wiki.tiddlerExists(this.to);
}
// Compose the link
var classes = ["tw-tiddlylink"]
if(this.isExternal) {
$tw.utils.pushTop(classes,"tw-tiddlylink-external");
} else {
$tw.utils.pushTop(classes,"tw-tiddlylink-internal");
if(this.isMissing) {
$tw.utils.pushTop(classes,"tw-tiddlylink-missing");
} else {
$tw.utils.pushTop(classes,"tw-tiddlylink-resolves");
}
}
if(this.classes) {
$tw.utils.pushTop(classes,this.classes);
}
var events = [{name: "click", handlerObject: this, handlerMethod: "handleClickEvent"}];
if(this.hover) {
events.push({name: "mouseover", handlerObject: this, handlerMethod: "handleMouseOverOrOutEvent"});
events.push({name: "mouseout", handlerObject: this, handlerMethod: "handleMouseOverOrOutEvent"});
}
this.children = this.renderer.renderTree.createRenderers(this.renderer.renderContext,[{
type: "element",
tag: "a",
attributes: {
href: {type: "string", value: this.isExternal ? this.to : encodeURIComponent(this.to)},
"class": {type: "string", value: classes.join(" ")}
},
children: this.renderer.parseTreeNode.children,
events: events
}]);
};
LinkWidget.prototype.render = function(type) {
var output = [];
$tw.utils.each(this.children,function(node) {
if(node.render) {
output.push(node.render(type));
}
});
return output.join("");
};
LinkWidget.prototype.renderInDom = function(parentElement) {
this.parentElement = parentElement;
// Render any child nodes
$tw.utils.each(this.children,function(node) {
if(node.renderInDom) {
parentElement.appendChild(node.renderInDom());
}
});
};
LinkWidget.prototype.handleClickEvent = function(event) {
if(isLinkExternal(this.to)) {
event.target.setAttribute("target","_blank");
return true;
} else {
$tw.utils.dispatchCustomEvent(event.target,"tw-navigate",{
navigateTo: this.to,
navigateFromNode: this,
navigateFromClientRect: this.children[0].domNode.getBoundingClientRect()
});
event.preventDefault();
return false;
}
};
LinkWidget.prototype.handleMouseOverOrOutEvent = function(event) {
if(this.hover) {
$tw.popup.triggerPopup({
textRef: this.hover,
domNode: this.children[0].domNode,
qualifyTiddlerTitles: this.qualifyHoverTitle,
contextTiddlerTitle: this.renderer.getContextTiddlerTitle(),
wiki: this.renderer.renderTree.wiki
});
}
event.preventDefault();
return false;
};
LinkWidget.prototype.refreshInDom = function(changedAttributes,changedTiddlers) {
// Set the class for missing tiddlers
if(this.targetTitle) {
$tw.utils.toggleClass(this.children[0].domNode,"tw-tiddler-missing",!this.renderer.renderTree.wiki.tiddlerExists(this.targetTitle));
}
// Check if any of our attributes have changed, or if a tiddler we're interested in has changed
if(changedAttributes.to || changedAttributes.hover || (this.to && changedTiddlers[this.to]) || (this.hover && changedTiddlers[this.hover])) {
// Remove old child nodes
$tw.utils.removeChildren(this.parentElement);
// Regenerate and render children
this.generateChildNodes();
var self = this;
$tw.utils.each(this.children,function(node) {
if(node.renderInDom) {
self.parentElement.appendChild(node.renderInDom());
}
});
} else {
// We don't need to refresh ourselves, so just refresh any child nodes
$tw.utils.each(this.children,function(node) {
if(node.refreshInDom) {
node.refreshInDom(changedTiddlers);
}
});
}
};
exports.link = LinkWidget;
})();
+295
View File
@@ -0,0 +1,295 @@
/*\
title: $:/core/modules/widgets/list/list.js
type: application/javascript
module-type: widget
The list widget
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
/*
These types are shorthands for particular filters
*/
var typeMappings = {
all: "[!is[shadow]sort[title]]",
recent: "[!is[shadow]sort[modified]]",
missing: "[is[missing]sort[title]]",
orphans: "[is[orphan]sort[title]]",
shadowed: "[is[shadow]sort[title]]"
};
var ListWidget = function(renderer) {
// Save state
this.renderer = renderer;
// Generate child nodes
this.generateChildNodes();
};
ListWidget.prototype.generateChildNodes = function() {
// We'll manage our own dependencies
this.renderer.dependencies = undefined;
// Get our attributes
this.itemClass = this.renderer.getAttribute("itemClass");
this.template = this.renderer.getAttribute("template");
this.editTemplate = this.renderer.getAttribute("editTemplate");
this.emptyMessage = this.renderer.getAttribute("emptyMessage");
// Get the list of tiddlers object
this.getTiddlerList();
// Create the list
var listMembers = [];
if(this.list.length === 0) {
// Check for an empty list
listMembers = [this.getEmptyMessage()];
} else {
// Create the list
for(var t=0; t<this.list.length; t++) {
listMembers.push(this.createListElement(this.list[t]));
}
}
// Create the list frame element
var classes = ["tw-list-frame"];
if(this.classes) {
$tw.utils.pushTop(classes,this.classes);
}
this.children = this.renderer.renderTree.createRenderers(this.renderer.renderContext,[{
type: "element",
tag: "div",
attributes: {
"class": {type: "string", value: classes.join(" ")}
},
children: listMembers
}]);
};
ListWidget.prototype.getTiddlerList = function() {
var filter;
if(this.renderer.hasAttribute("type")) {
filter = typeMappings[this.renderer.getAttribute("type")];
} else if(this.renderer.hasAttribute("filter")) {
filter = this.renderer.getAttribute("filter");
}
if(!filter) {
filter = "[!is[shadow]]";
}
this.list = this.renderer.renderTree.wiki.filterTiddlers(filter,this.renderer.getContextTiddlerTitle());
};
/*
Create and execute the nodes representing the empty message
*/
ListWidget.prototype.getEmptyMessage = function() {
return {
type: "element",
tag: "span",
children: this.renderer.renderTree.wiki.new_parseText("text/vnd.tiddlywiki",this.emptyMessage).tree
};
};
/*
Create a list element representing a given tiddler
*/
ListWidget.prototype.createListElement = function(title) {
// Define an event handler that adds navigation information to the event
var handleEvent = function(event) {
event.navigateFromTitle = title;
return true;
},
classes = ["tw-list-element"];
// Add any specified classes
if(this.itemClass) {
$tw.utils.pushTop(classes,this.itemClass);
}
// Return the list element
return {
type: "element",
tag: "div",
attributes: {
"class": {type: "string", value: classes.join(" ")}
},
children: [this.createListElementMacro(title)],
events: [
{name: "tw-navigate", handlerFunction: handleEvent},
{name: "tw-EditTiddler", handlerFunction: handleEvent},
{name: "tw-SaveTiddler", handlerFunction: handleEvent},
{name: "tw-CloseTiddler", handlerFunction: handleEvent},
{name: "tw-NewTiddler", handlerFunction: handleEvent}
]
};
};
/*
Create the tiddler macro needed to represent a given tiddler
*/
ListWidget.prototype.createListElementMacro = function(title) {
// Check if the tiddler is a draft
var tiddler = this.renderer.renderTree.wiki.getTiddler(title),
isDraft = tiddler ? tiddler.hasField("draft.of") : false;
// Figure out the template to use
var template = this.template,
templateTree = undefined;
if(isDraft && this.editTemplate) {
template = this.editTemplate;
}
// Check for not having a template
if(!template) {
if(this.renderer.parseTreeNode.children && this.renderer.parseTreeNode.children.length > 0) {
// Use our content as the template
templateTree = this.renderer.parseTreeNode.children;
} else {
// Use default content
templateTree = [{
type: "widget",
tag: "view",
attributes: {
field: {type: "string", value: "title"},
format: {type: "string", value: "link"}
}
}];
}
}
// Create the tiddler macro
return {
type: "widget",
tag: "transclude",
attributes: {
target: {type: "string", value: title},
template: {type: "string", value: template}
},
children: templateTree
};
};
/*
Remove a list element from the list, along with the attendant DOM nodes
*/
ListWidget.prototype.removeListElement = function(index) {
// Get the list element
var listElement = this.children[0].children[index];
// Remove the DOM node
listElement.domNode.parentNode.removeChild(listElement.domNode);
// Then delete the actual renderer node
this.children[0].children.splice(index,1);
};
/*
Return the index of the list element that corresponds to a particular title
startIndex: index to start search (use zero to search from the top)
title: tiddler title to seach for
*/
ListWidget.prototype.findListElementByTitle = function(startIndex,title) {
while(startIndex < this.children[0].children.length) {
if(this.children[0].children[startIndex].children[0].attributes.target === title) {
return startIndex;
}
startIndex++;
}
return undefined;
};
ListWidget.prototype.render = function(type) {
var output = [];
$tw.utils.each(this.children,function(node) {
if(node.render) {
output.push(node.render(type));
}
});
return output.join("");
};
ListWidget.prototype.renderInDom = function(parentElement) {
this.parentElement = parentElement;
// Render any child nodes
$tw.utils.each(this.children,function(node) {
if(node.renderInDom) {
parentElement.appendChild(node.renderInDom());
}
});
};
ListWidget.prototype.refreshInDom = function(changedAttributes,changedTiddlers) {
// Reexecute the widget if any of our attributes have changed
if(changedAttributes.itemClass || changedAttributes.template || changedAttributes.editTemplate || changedAttributes.emptyMessage || changedAttributes.type || changedAttributes.filter || changedAttributes.template) {
// Remove old child nodes
$tw.utils.removeChildren(this.parentElement);
// Regenerate and render children
this.generateChildNodes();
var self = this;
$tw.utils.each(this.children,function(node) {
if(node.renderInDom) {
self.parentElement.appendChild(node.renderInDom());
}
});
} else {
// Handle any changes to the list, and refresh any nodes we're reusing
this.handleListChanges(changedTiddlers);
}
};
ListWidget.prototype.handleListChanges = function(changedTiddlers) {
var t,
prevListLength = this.list.length,
frame = this.children[0];
// Get the list of tiddlers, having saved the previous length
this.getTiddlerList();
// Check if the list is empty
if(this.list.length === 0) {
// Check if it was empty before
if(prevListLength === 0) {
// If so, just refresh the empty message
$tw.utils.each(this.children,function(node) {
if(node.refreshInDom) {
node.refreshInDom(changedTiddlers);
}
});
return;
} else {
// If the list wasn't empty before, empty it
for(t=prevListLength-1; t>=0; t--) {
this.removeListElement(t);
}
// Insert the empty message
frame.children = this.renderer.renderTree.createRenderers(this.renderer.renderContext,[this.getEmptyMessage()]);
$tw.utils.each(frame.children,function(node) {
if(node.renderInDom) {
frame.domNode.appendChild(node.renderInDom());
}
});
return;
}
} else {
// If it is not empty now, but was empty previously, then remove the empty message
if(prevListLength === 0) {
this.removeListElement(0);
}
}
// Step through the list and adjust our child list elements appropriately
for(t=0; t<this.list.length; t++) {
// Check to see if the list element is already there
var index = this.findListElementByTitle(t,this.list[t]);
if(index === undefined) {
// The list element isn't there, so we need to insert it
frame.children.splice(t,0,this.renderer.renderTree.createRenderer(this.renderer.renderContext,this.createListElement(this.list[t])));
frame.domNode.insertBefore(frame.children[t].renderInDom(),frame.domNode.childNodes[t]);
} else {
// Delete any list elements preceding the one we want
for(var n=index-1; n>=t; n--) {
this.removeListElement(n);
}
// Refresh the node we're reusing
frame.children[t].refreshInDom(changedTiddlers);
}
}
// Remove any left over elements
for(t=frame.children.length-1; t>=this.list.length; t--) {
this.removeListElement(t);
}
};
exports.list = ListWidget;
})();
+241
View File
@@ -0,0 +1,241 @@
/*\
title: $:/core/modules/widget/navigator.js
type: application/javascript
module-type: widget
Implements the navigator widget.
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var NavigatorWidget = function(renderer) {
// Save state
this.renderer = renderer;
// Generate child nodes
this.generateChildNodes();
};
NavigatorWidget.prototype.generateChildNodes = function() {
// We'll manage our own dependencies
this.renderer.dependencies = undefined;
// Get our parameters
this.storyTitle = this.renderer.getAttribute("story");
this.historyTitle = this.renderer.getAttribute("history");
// Render our children
this.children = this.renderer.renderTree.createRenderers(this.renderer.renderContext,this.renderer.parseTreeNode.children);
};
NavigatorWidget.prototype.render = function(type) {
var output = [];
$tw.utils.each(this.children,function(node) {
if(node.render) {
output.push(node.render(type));
}
});
return output.join("");
};
NavigatorWidget.prototype.renderInDom = function(parentElement) {
this.parentElement = parentElement;
// Render any child nodes
$tw.utils.each(this.children,function(node) {
if(node.renderInDom) {
parentElement.appendChild(node.renderInDom());
}
});
// Attach our event handlers
$tw.utils.addEventListeners(this.renderer.domNode,[
{name: "tw-navigate", handlerObject: this, handlerMethod: "handleNavigateEvent"},
{name: "tw-EditTiddler", handlerObject: this, handlerMethod: "handleEditTiddlerEvent"},
{name: "tw-SaveTiddler", handlerObject: this, handlerMethod: "handleSaveTiddlerEvent"},
{name: "tw-close", handlerObject: this, handlerMethod: "handleCloseTiddlerEvent"},
{name: "tw-NewTiddler", handlerObject: this, handlerMethod: "handleNewTiddlerEvent"}
]);
};
NavigatorWidget.prototype.refreshInDom = function(changedAttributes,changedTiddlers) {
// We don't need to refresh ourselves, so just refresh any child nodes
$tw.utils.each(this.children,function(node) {
if(node.refreshInDom) {
node.refreshInDom(changedTiddlers);
}
});
};
NavigatorWidget.prototype.getStoryList = function() {
var text = this.renderer.renderTree.wiki.getTextReference(this.storyTitle,"");
if(text && text.length > 0) {
this.storyList = text.split("\n");
} else {
this.storyList = [];
}
};
NavigatorWidget.prototype.saveStoryList = function() {
var storyTiddler = this.renderer.renderTree.wiki.getTiddler(this.storyTitle);
this.renderer.renderTree.wiki.addTiddler(new $tw.Tiddler({
title: this.storyTitle
},storyTiddler,{text: this.storyList.join("\n")}));
};
NavigatorWidget.prototype.findTitleInStory = function(title,defaultIndex) {
for(var t=0; t<this.storyList.length; t++) {
if(this.storyList[t] === title) {
return t;
}
}
return defaultIndex;
};
// Navigate to a specified tiddler
NavigatorWidget.prototype.handleNavigateEvent = function(event) {
if(this.storyTitle) {
// Update the story tiddler if specified
this.getStoryList();
// See if the tiddler is already there
var slot = this.findTitleInStory(event.navigateTo,-1);
// If not we need to add it
if(slot === -1) {
// First we try to find the position of the story element we navigated from
slot = this.findTitleInStory(event.navigateFromTitle,-1) + 1;
// Add the tiddler
this.storyList.splice(slot,0,event.navigateTo);
// Save the story
this.saveStoryList();
}
}
// Add a new record to the top of the history stack
if(this.historyTitle) {
var historyList = this.renderer.renderTree.wiki.getTiddlerData(this.historyTitle,[]);
historyList.push({title: event.navigateTo, fromPageRect: event.navigateFromClientRect});
this.renderer.renderTree.wiki.setTiddlerData(this.historyTitle,historyList);
}
event.stopPropagation();
return false;
};
// Close a specified tiddler
NavigatorWidget.prototype.handleCloseTiddlerEvent = function(event) {
this.getStoryList();
// Look for tiddlers with this title to close
var slot = this.findTitleInStory(event.tiddlerTitle,-1);
if(slot !== -1) {
this.storyList.splice(slot,1);
this.saveStoryList();
}
event.stopPropagation();
return false;
};
// Place a tiddler in edit mode
NavigatorWidget.prototype.handleEditTiddlerEvent = function(event) {
this.getStoryList();
// Replace the specified tiddler with a draft in edit mode
for(var t=0; t<this.storyList.length; t++) {
if(this.storyList[t] === event.tiddlerTitle) {
// Compute the title for the draft
var draftTitle = "Draft " + (new Date()) + " of " + event.tiddlerTitle;
this.storyList[t] = draftTitle;
// Get the current value of the tiddler we're editing
var tiddler = this.renderer.renderTree.wiki.getTiddler(event.tiddlerTitle);
// Save the initial value of the draft tiddler
this.renderer.renderTree.wiki.addTiddler(new $tw.Tiddler(
{
text: "Type the text for the tiddler '" + event.tiddlerTitle + "'"
},
tiddler,
{
title: draftTitle,
"draft.title": event.tiddlerTitle,
"draft.of": event.tiddlerTitle
}));
}
}
this.saveStoryList();
event.stopPropagation();
return false;
};
// Take a tiddler out of edit mode, saving the changes
NavigatorWidget.prototype.handleSaveTiddlerEvent = function(event) {
this.getStoryList();
var storyTiddlerModified = false;
for(var t=0; t<this.storyList.length; t++) {
if(this.storyList[t] === event.tiddlerTitle) {
var tiddler = this.renderer.renderTree.wiki.getTiddler(event.tiddlerTitle);
if(tiddler && $tw.utils.hop(tiddler.fields,"draft.title")) {
// Save the draft tiddler as the real tiddler
this.renderer.renderTree.wiki.addTiddler(new $tw.Tiddler(tiddler,{
title: tiddler.fields["draft.title"],
modified: new Date(),
"draft.title": undefined,
"draft.of": undefined
}));
// Remove the draft tiddler
this.renderer.renderTree.wiki.deleteTiddler(event.tiddlerTitle);
// Remove the original tiddler if we're renaming it
if(tiddler.fields["draft.of"] !== tiddler.fields["draft.title"]) {
this.renderer.renderTree.wiki.deleteTiddler(tiddler.fields["draft.of"]);
}
// Make the story record point to the newly saved tiddler
this.storyList[t] = tiddler.fields["draft.title"];
// Check if we're modifying the story tiddler itself
if(tiddler.fields["draft.title"] === this.storyTitle) {
storyTiddlerModified = true;
}
}
}
}
if(!storyTiddlerModified) {
this.saveStoryList();
}
event.stopPropagation();
return false;
};
// Create a new draft tiddler
NavigatorWidget.prototype.handleNewTiddlerEvent = function(event) {
// Get the story details
this.getStoryList();
// Create the new tiddler
var title;
for(var t=0; true; t++) {
title = "New Tiddler" + (t ? " " + t : "");
if(!this.renderer.renderTree.wiki.tiddlerExists(title)) {
break;
}
}
var tiddler = new $tw.Tiddler({
title: title,
text: "Newly created tiddler"
});
this.renderer.renderTree.wiki.addTiddler(tiddler);
// Create the draft tiddler
var draftTitle = "New Tiddler at " + (new Date()),
draftTiddler = new $tw.Tiddler({
text: "Type the text for the new tiddler",
title: draftTitle,
"draft.title": title,
"draft.of": title
});
this.renderer.renderTree.wiki.addTiddler(draftTiddler);
// Update the story to insert the new draft at the top
var slot = this.findTitleInStory(event.navigateFromTitle,-1) + 1;
this.storyList.splice(slot,0,draftTitle);
// Save the updated story
this.saveStoryList();
// Add a new record to the top of the history stack
var history = this.renderer.renderTree.wiki.getTiddlerData(this.historyTitle,[]);
history.push({title: draftTitle});
this.renderer.renderTree.wiki.setTiddlerData(this.historyTitle,historyList);
event.stopPropagation();
return false;
};
exports.navigator = NavigatorWidget;
})();
+150
View File
@@ -0,0 +1,150 @@
/*\
title: $:/core/modules/widgets/transclude.js
type: application/javascript
module-type: widget
The transclude widget includes another tiddler into the tiddler being rendered.
Attributes:
target: the title of the tiddler to transclude
template: the title of the tiddler to use as a template for the transcluded tiddler
The simplest case is to just supply a target tiddler:
{{{
<_transclude target="Foo"/>
}}}
This will render the tiddler Foo within the current tiddler. If the tiddler Foo includes
the view widget (or other widget that reference the fields of the current tiddler), then the
fields of the tiddler Foo will be accessed.
If you want to transclude the tiddler as a template, so that the fields referenced by the view
widget are those of the tiddler doing the transcluding, then you can instead specify the tiddler
as a template:
{{{
<_transclude template="Foo"/>
}}}
The effect is the same as the previous example: the text of the tiddler Foo is rendered. The
difference is that the view widget will access the fields of the tiddler doing the transcluding.
The `target` and `template` attributes may be combined:
{{{
<_transclude template="Bar" target="Foo"/>
}}}
Here, the text of the tiddler `Bar` will be transcluded, with the widgets within it accessing the fields
of the tiddler `Foo`.
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var TranscludeWidget = function(renderer) {
// Save state
this.renderer = renderer;
// Generate child nodes
this.generateChildNodes();
};
TranscludeWidget.prototype.generateChildNodes = function() {
var tr, templateParseTree, templateTiddler;
// We'll manage our own dependencies
this.renderer.dependencies = undefined;
// Get the render target details
this.targetTitle = this.renderer.getAttribute("target",this.renderer.getContextTiddlerTitle());
// Get the render tree for the template
this.templateTitle = undefined;
if(this.renderer.parseTreeNode.children && this.renderer.parseTreeNode.children.length > 0) {
// Use the child nodes as the template if we've got them
templateParseTree = this.renderer.parseTreeNode.children;
} else {
this.templateTitle = this.renderer.getAttribute("template",this.targetTitle);
// Check for recursion
if(this.renderer.checkContextRecursion({
tiddlerTitle: this.targetTitle,
templateTitle: this.templateTitle
})) {
templateParseTree = [{type: "text", text: "Tiddler recursion error in transclude widget"}];
} else {
var parser = this.renderer.renderTree.wiki.new_parseTiddler(this.templateTitle);
templateParseTree = parser ? parser.tree : [];
}
}
// Set up the attributes for the wrapper element
var classes = [];
if(!this.renderer.renderTree.wiki.tiddlerExists(this.targetTitle)) {
$tw.utils.pushTop(classes,"tw-tiddler-missing");
}
// Create the renderers for the wrapper and the children
var newRenderContext = {
tiddlerTitle: this.targetTitle,
templateTitle: this.templateTitle,
parentContext: this.renderer.renderContext
};
this.children = this.renderer.renderTree.createRenderers(newRenderContext,[{
type: "element",
tag: this.renderer.parseTreeNode.isBlock ? "div" : "span",
attributes: {
"class": {type: "string", value: classes.join(" ")}
},
children: templateParseTree
}]);
};
TranscludeWidget.prototype.render = function(type) {
var output = [];
$tw.utils.each(this.children,function(node) {
if(node.render) {
output.push(node.render(type));
}
});
return output.join("");
};
TranscludeWidget.prototype.renderInDom = function(parentElement) {
this.parentElement = parentElement;
// Render any child nodes
$tw.utils.each(this.children,function(node) {
if(node.renderInDom) {
parentElement.appendChild(node.renderInDom());
}
});
};
TranscludeWidget.prototype.refreshInDom = function(changedAttributes,changedTiddlers) {
// Set the class for missing tiddlers
if(this.targetTitle) {
$tw.utils.toggleClass(this.children[0].domNode,"tw-tiddler-missing",!this.renderer.renderTree.wiki.tiddlerExists(this.targetTitle));
}
// Check if any of our attributes have changed, or if a tiddler we're interested in has changed
if(changedAttributes.target || changedAttributes.template || (this.targetTitle && changedTiddlers[this.targetTitle]) || (this.templateTitle && changedTiddlers[this.templateTitle])) {
// Remove old child nodes
$tw.utils.removeChildren(this.parentElement);
// Regenerate and render children
this.generateChildNodes();
var self = this;
$tw.utils.each(this.children,function(node) {
if(node.renderInDom) {
self.parentElement.appendChild(node.renderInDom());
}
});
} else {
// We don't need to refresh ourselves, so just refresh any child nodes
$tw.utils.each(this.children,function(node) {
if(node.refreshInDom) {
node.refreshInDom(changedTiddlers);
}
});
}
};
exports.transclude = TranscludeWidget;
})();
+141
View File
@@ -0,0 +1,141 @@
/*\
title: $:/core/modules/widgets/view.js
type: application/javascript
module-type: widget
The view widget displays a tiddler field.
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
/*
Define the "text" viewer here so that it is always available
*/
var TextViewer = function(viewWidget,tiddler,field,value) {
this.viewWidget = viewWidget;
this.tiddler = tiddler;
this.field = field;
this.value = value;
};
TextViewer.prototype.render = function() {
// Get the value as a string
if(this.field !== "text" && this.tiddler) {
this.value = this.tiddler.getFieldString(this.field);
}
var value = "";
if(this.value !== undefined && this.value !== null) {
value = this.value;
}
return this.viewWidget.renderer.renderTree.createRenderers(this.viewWidget.renderer.renderContext,[{
type: "text",
text: value
}]);
};
// We'll cache the available field viewers here
var fieldViewers = undefined;
var ViewWidget = function(renderer) {
// Save state
this.renderer = renderer;
// Initialise the field viewers if they've not been done already
if(!fieldViewers) {
fieldViewers = {text: TextViewer}; // Start with the built-in text viewer
$tw.modules.applyMethods("newfieldviewer",fieldViewers);
}
// Generate child nodes
this.generateChildNodes();
};
ViewWidget.prototype.generateChildNodes = function() {
// We'll manage our own dependencies
this.renderer.dependencies = undefined;
// Get parameters from our attributes
this.tiddlerTitle = this.renderer.getAttribute("tiddler",this.renderer.getContextTiddlerTitle());
this.fieldName = this.renderer.getAttribute("field","text");
this.format = this.renderer.getAttribute("format","text");
// Get the value to display
var tiddler = this.renderer.renderTree.wiki.getTiddler(this.tiddlerTitle),
value;
if(tiddler) {
if(this.fieldName === "text") {
// Calling getTiddlerText() triggers lazy loading of skinny tiddlers
value = this.renderer.renderTree.wiki.getTiddlerText(this.tiddlerTitle);
} else {
value = tiddler.fields[this.fieldName];
}
} else { // Use a special value if the tiddler is missing
switch(this.fieldName) {
case "title":
value = this.tiddlerTitle;
break;
case "modified":
case "created":
value = new Date();
break;
default:
value = "";
break;
}
}
// Choose the viewer to use
var Viewer = fieldViewers.text;
if($tw.utils.hop(fieldViewers,this.format)) {
Viewer = fieldViewers[this.format];
}
this.viewer = new Viewer(this,tiddler,this.fieldName,value);
// Ask the viewer to create the children
this.children = this.viewer.render();
};
ViewWidget.prototype.render = function(type) {
var output = [];
$tw.utils.each(this.children,function(node) {
if(node.render) {
output.push(node.render(type));
}
});
return output.join("");
};
ViewWidget.prototype.renderInDom = function(parentElement) {
this.parentElement = parentElement;
// Render any child nodes
$tw.utils.each(this.children,function(node) {
if(node.renderInDom) {
parentElement.appendChild(node.renderInDom());
}
});
};
ViewWidget.prototype.refreshInDom = function(changedAttributes,changedTiddlers) {
// Check if any of our attributes have changed, or if a tiddler we're interested in has changed
if(changedAttributes.tiddler || changedAttributes.field || changedAttributes.format || (this.tiddlerTitle && changedTiddlers[this.tiddlerTitle])) {
// Remove old child nodes
$tw.utils.removeChildren(this.parentElement);
// Regenerate and render children
this.generateChildNodes();
var self = this;
$tw.utils.each(this.children,function(node) {
if(node.renderInDom) {
self.parentElement.appendChild(node.renderInDom());
}
});
} else {
// We don't need to refresh ourselves, so just refresh any child nodes
$tw.utils.each(this.children,function(node) {
if(node.refreshInDom) {
node.refreshInDom(changedTiddlers);
}
});
}
};
exports.view = ViewWidget;
})();
+36
View File
@@ -0,0 +1,36 @@
/*\
title: $:/core/modules/widgets/view/viewers/date.js
type: application/javascript
module-type: newfieldviewer
A viewer for viewing tiddler fields as a date
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var DateViewer = function(viewWidget,tiddler,field,value) {
this.viewWidget = viewWidget;
this.tiddler = tiddler;
this.field = field;
this.value = value;
};
DateViewer.prototype.render = function() {
var template = this.viewWidget.renderer.getAttribute("template","DD MMM YYYY"),
value = "";
if(this.value !== undefined) {
value = $tw.utils.formatDateString(this.value,template);
}
return this.viewWidget.renderer.renderTree.createRenderers(this.viewWidget.renderer.renderContext,[{
type: "text",
text: value
}]);
};
exports.date = DateViewer;
})();
+44
View File
@@ -0,0 +1,44 @@
/*\
title: $:/core/modules/widgets/view/viewers/link.js
type: application/javascript
module-type: newfieldviewer
A viewer for viewing tiddler fields as a link
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var LinkViewer = function(viewWidget,tiddler,field,value) {
this.viewWidget = viewWidget;
this.tiddler = tiddler;
this.field = field;
this.value = value;
};
LinkViewer.prototype.render = function() {
var parseTree = [];
if(this.value === undefined) {
parseTree.push({type: "text", text: ""});
} else {
parseTree.push({
type: "widget",
tag: "link",
attributes: {
to: {type: "string", value: this.value}
},
children: [{
type: "text",
text: this.value
}]
})
}
return this.viewWidget.renderer.renderTree.createRenderers(this.viewWidget.renderer.renderContext,parseTree);
};
exports.link = LinkViewer;
})();
@@ -0,0 +1,41 @@
/*\
title: $:/core/modules/widgets/view/viewers/wikified.js
type: application/javascript
module-type: newfieldviewer
A viewer for viewing tiddler fields as wikified text
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var WikifiedViewer = function(viewWidget,tiddler,field,value) {
this.viewWidget = viewWidget;
this.tiddler = tiddler;
this.field = field;
this.value = value;
};
WikifiedViewer.prototype.render = function() {
var parseTree;
// If we're viewing the text field of a tiddler then we'll transclude it
if(this.tiddler && this.field === "text") {
parseTree = [{
type: "widget",
tag: "transclude",
attributes: {
target: {type: "string", value: this.tiddler.fields.title}
}
}];
} else {
parseTree = this.viewWidget.renderer.renderTree.wiki.new_parseText("text/vnd.tiddlywiki",this.value).tree;
}
return this.viewWidget.renderer.renderTree.createRenderers(this.viewWidget.renderer.renderContext,parseTree);
};
exports.wikified = WikifiedViewer;
})();