mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2024-09-19 18:59:42 +00:00
d0dff80d6d
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.
409 lines
12 KiB
JavaScript
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;
|
|
|
|
})();
|