1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2024-07-08 21:14:25 +00:00
TiddlyWiki5/core/modules/widgets/list/list.js
Jeremy Ruston d0dff80d6d Fix problem with insertBefore() on Win7/IE10
The problem is that insertBefore() on Win7/IE10 crashes if the second
parameter is undefined, rather than behaving as if the parameter is
missing, as all other browsers do. Aaargh.
2013-08-24 23:28:17 +01:00

409 lines
12 KiB
JavaScript

/*\
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";
var ListWidget = function(renderer) {
// Save state
this.renderer = renderer;
// Initialise the listviews if they've not been done already
if(!this.listViews) {
ListWidget.prototype.listViews = {};
$tw.modules.applyMethods("listview",this.listViews);
}
// Generate widget elements
this.generate();
};
var typeInfoByType = {
plain: {
frame: {
block: "div", inline: "span"
},
member: {
block: "div", inline: "span"
}
},
ul: {
frame: {
block: "ul", inline: "ul"
},
member: {
block: "li", inline: "li"
}
},
ol: {
frame: {
block: "ol", inline: "ol"
},
member: {
block: "li", inline: "li"
}
}
};
ListWidget.prototype.generate = function() {
// Get our attributes
this.macro = this.renderer.getAttribute("macro");
this.type = this.renderer.getAttribute("type","plain");
this.itemClass = this.renderer.getAttribute("itemClass");
this.template = this.renderer.getAttribute("template");
this.editTemplate = this.renderer.getAttribute("editTemplate");
this.emptyMessage = this.renderer.getAttribute("emptyMessage");
this["class"] = this.renderer.getAttribute("class");
// Get our type information
this.typeInfo = typeInfoByType[this.type] || typeInfoByType.plain;
// Set up the classes
var classes = ["tw-list-frame"];
if(this["class"]) {
$tw.utils.pushTop(classes,this["class"]);
}
// 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
this.tag = this.renderer.parseTreeNode.isBlock ? this.typeInfo.frame.block : this.typeInfo.frame.inline;
this.attributes = {
"class": classes.join(" ")
};
this.children = this.renderer.renderTree.createRenderers(this.renderer,listMembers);
};
ListWidget.prototype.getTiddlerList = function() {
var filter;
if(this.renderer.hasAttribute("filter")) {
filter = this.renderer.getAttribute("filter");
}
if(!filter) {
filter = "[!is[system]]";
}
this.list = this.renderer.renderTree.wiki.filterTiddlers(filter,this.renderer.tiddlerTitle);
};
/*
Create and execute the nodes representing the empty message
*/
ListWidget.prototype.getEmptyMessage = function() {
return {
type: "element",
tag: "span",
children: this.renderer.renderTree.wiki.parseText("text/vnd.tiddlywiki",this.emptyMessage,{parseAsInline: true}).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: this.renderer.parseTreeNode.isBlock ? this.typeInfo.member.block : this.typeInfo.member.inline,
attributes: {
"class": {type: "string", value: classes.join(" ")}
},
children: [this.createListElementParseTree(title)],
events: [
{name: "tw-navigate", handlerFunction: handleEvent},
{name: "tw-edit-tiddler", handlerFunction: handleEvent},
{name: "tw-save-tiddler", handlerFunction: handleEvent},
{name: "tw-close-tiddler", handlerFunction: handleEvent},
{name: "tw-new-tiddler", handlerFunction: handleEvent}
]
};
};
/*
Create the parse tree nodes needed to represent a given list element
*/
ListWidget.prototype.createListElementParseTree = function(title) {
if(this.macro) {
return this.createListElementMacro(title);
} else {
return this.createListElementTransclusion(title);
}
};
/*
Create a macro call to represent a list element
*/
ListWidget.prototype.createListElementMacro = function(title) {
// Create the macrocall rendertree node
return {
type: "macrocall",
name: this.macro,
params: [
{name: "title", value: title}
]
};
};
/*
Create a transclusion to represent a list element
*/
ListWidget.prototype.createListElementTransclusion = 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: "element",
tag: "$view",
attributes: {
field: {type: "string", value: "title"},
format: {type: "string", value: "link"}
}
}];
}
}
// Create the element widgets
if(this.renderer.hasAttribute("hackTemplate")) {
return {
type: "element",
tag: "$transclude",
isBlock: this.renderer.parseTreeNode.isBlock,
attributes: {
title: {type: "string", value: title}
}
};
} else {
if(!templateTree) {
templateTree = [{
type: "element",
tag: "$transclude",
attributes: {
title: {type: "string", value: template}
},
children: templateTree
}];
}
return {
type: "element",
tag: "$tiddler",
isBlock: this.renderer.parseTreeNode.isBlock,
attributes: {
title: {type: "string", value: title}
},
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[index];
// Invoke the listview to animate the removal
if(this.listview && this.listview.remove) {
if(!this.listview.remove(index)) {
// Only delete the DOM element if the listview.remove() returned false
listElement.domNode.parentNode.removeChild(listElement.domNode);
}
} else {
// Always remove the DOM node if we didn't invoke the listview
listElement.domNode.parentNode.removeChild(listElement.domNode);
}
// Then delete the actual renderer node
this.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) {
var testNode = this.macro ? function(node) {
// We're looking for a macro list element
return node.widget.children[0].parseTreeNode.params[0].value === title;
} : (this.renderer.hasAttribute("hackTemplate") ? function(node) {
// We're looking for a transclusion list element
return node.widget.children[0].attributes.title === title;
} : function(node) {
// We're looking for a transclusion list element
return node.widget.children[0].attributes.title === title;
});
// Search for the list element
while(startIndex < this.children.length) {
if(testNode(this.children[startIndex])) {
return startIndex;
}
startIndex++;
}
return undefined;
};
ListWidget.prototype.postRenderInDom = function() {
this.listview = this.chooseListView();
this.history = [];
};
/*
Select the appropriate list viewer
*/
ListWidget.prototype.chooseListView = function() {
// Instantiate the list view
var listviewName = this.renderer.getAttribute("listview");
var ListView = this.listViews[listviewName];
return ListView ? new ListView(this) : null;
};
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 || changedAttributes.history || changedAttributes.listview) {
// Regenerate and rerender the widget and replace the existing DOM node
this.generate();
var oldDomNode = this.renderer.domNode,
newDomNode = this.renderer.renderInDom();
oldDomNode.parentNode.replaceChild(newDomNode,oldDomNode);
} else {
// Handle any changes to the list, and refresh any nodes we're reusing
this.handleListChanges(changedTiddlers);
// Update the history list
var history = this.renderer.getAttribute("history");
if(history && changedTiddlers[history]) {
this.handleHistoryChanges();
}
}
};
ListWidget.prototype.handleListChanges = function(changedTiddlers) {
var t,
prevListLength = this.list.length,
self = this;
// 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
this.children = this.renderer.renderTree.createRenderers(this.renderer,[this.getEmptyMessage()]);
$tw.utils.each(this.children,function(node) {
if(node.renderInDom) {
self.renderer.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
this.children.splice(t,0,this.renderer.renderTree.createRenderer(this.renderer,this.createListElement(this.list[t])));
var before = this.renderer.domNode.childNodes[t],
newNode = this.children[t].renderInDom();
if(before) {
this.renderer.domNode.insertBefore(newNode,before);
} else {
this.renderer.domNode.appendChild(newNode);
}
// Ask the listview to animate the insertion
if(this.listview && this.listview.insert) {
this.listview.insert(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
this.children[t].refreshInDom(changedTiddlers);
}
}
// Remove any left over elements
for(t=this.children.length-1; t>=this.list.length; t--) {
this.removeListElement(t);
}
};
/*
Handle any changes to the history list
*/
ListWidget.prototype.handleHistoryChanges = function() {
// Get the history data
var historyAtt = this.renderer.getAttribute("history"),
newHistory = this.renderer.renderTree.wiki.getTiddlerData(historyAtt,[]);
// Ignore any entries of the history that match the previous history
var entry = 0;
while(entry < newHistory.length && entry < this.history.length && newHistory[entry].title === this.history[entry].title) {
entry++;
}
// Navigate forwards to each of the new tiddlers
while(entry < newHistory.length) {
if(this.listview && this.listview.navigateTo) {
this.listview.navigateTo(newHistory[entry]);
}
entry++;
}
// Update the history
this.history = newHistory;
};
exports.list = ListWidget;
})();