Extend list widget with "index" attribute (#5611)

* Extend list widget with "index" attribute

* Fix refreshing bug

* Clarify performance note
new-json-store-area
Jeremy Ruston 2021-04-20 09:15:11 +01:00 zatwierdzone przez GitHub
rodzic a725da2b39
commit 85ba7ac041
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
3 zmienionych plików z 202 dodań i 21 usunięć

Wyświetl plik

@ -61,6 +61,7 @@ ListWidget.prototype.execute = function() {
this.template = this.getAttribute("template"); this.template = this.getAttribute("template");
this.editTemplate = this.getAttribute("editTemplate"); this.editTemplate = this.getAttribute("editTemplate");
this.variableName = this.getAttribute("variable","currentTiddler"); this.variableName = this.getAttribute("variable","currentTiddler");
this.indexName = this.getAttribute("index");
this.storyViewName = this.getAttribute("storyview"); this.storyViewName = this.getAttribute("storyview");
this.historyTitle = this.getAttribute("history"); this.historyTitle = this.getAttribute("history");
// Compose the list elements // Compose the list elements
@ -72,7 +73,7 @@ ListWidget.prototype.execute = function() {
members = this.getEmptyMessage(); members = this.getEmptyMessage();
} else { } else {
$tw.utils.each(this.list,function(title,index) { $tw.utils.each(this.list,function(title,index) {
members.push(self.makeItemTemplate(title)); members.push(self.makeItemTemplate(title,index));
}); });
} }
// Construct the child widgets // Construct the child widgets
@ -105,7 +106,7 @@ ListWidget.prototype.getEmptyMessage = function() {
/* /*
Compose the template for a list item Compose the template for a list item
*/ */
ListWidget.prototype.makeItemTemplate = function(title) { ListWidget.prototype.makeItemTemplate = function(title,index) {
// Check if the tiddler is a draft // Check if the tiddler is a draft
var tiddler = this.wiki.getTiddler(title), var tiddler = this.wiki.getTiddler(title),
isDraft = tiddler && tiddler.hasField("draft.of"), isDraft = tiddler && tiddler.hasField("draft.of"),
@ -128,7 +129,14 @@ ListWidget.prototype.makeItemTemplate = function(title) {
} }
} }
// Return the list item // Return the list item
return {type: "listitem", itemTitle: title, variableName: this.variableName, children: templateTree}; var parseTreeNode = {type: "listitem", itemTitle: title, variableName: this.variableName, children: templateTree};
if(this.indexName) {
parseTreeNode.index = index.toString();
parseTreeNode.indexName = this.indexName;
parseTreeNode.isFirst = index === 0;
parseTreeNode.isLast = index === this.list.length - 1;
}
return parseTreeNode;
}; };
/* /*
@ -142,7 +150,7 @@ ListWidget.prototype.refresh = function(changedTiddlers) {
this.storyview.refreshStart(changedTiddlers,changedAttributes); this.storyview.refreshStart(changedTiddlers,changedAttributes);
} }
// Completely refresh if any of our attributes have changed // Completely refresh if any of our attributes have changed
if(changedAttributes.filter || changedAttributes.template || changedAttributes.editTemplate || changedAttributes.emptyMessage || changedAttributes.storyview || changedAttributes.history) { if(changedAttributes.filter || changedAttributes.variable || changedAttributes.index || changedAttributes.template || changedAttributes.editTemplate || changedAttributes.emptyMessage || changedAttributes.storyview || changedAttributes.history) {
this.refreshSelf(); this.refreshSelf();
result = true; result = true;
} else { } else {
@ -211,23 +219,41 @@ ListWidget.prototype.handleListChanges = function(changedTiddlers) {
this.removeChildDomNodes(); this.removeChildDomNodes();
this.children = []; this.children = [];
} }
// Cycle through the list, inserting and removing list items as needed // If we are providing an index variable then we must refresh the items, otherwise we can rearrange them
var hasRefreshed = false; var hasRefreshed = false,t;
for(var t=0; t<this.list.length; t++) { if(this.indexName) {
var index = this.findListItem(t,this.list[t]); // Cycle through the list and remove and re-insert the first item that has changed, and all the remaining items
if(index === undefined) { for(t=0; t<this.list.length; t++) {
// The list item must be inserted if(hasRefreshed || !this.children[t] || this.children[t].parseTreeNode.itemTitle !== this.list[t]) {
this.insertListItem(t,this.list[t]); if(this.children[t]) {
hasRefreshed = true; this.removeListItem(t);
} else { }
// There are intervening list items that must be removed this.insertListItem(t,this.list[t]);
for(var n=index-1; n>=t; n--) {
this.removeListItem(n);
hasRefreshed = true; hasRefreshed = true;
} else {
// Refresh the item we're reusing
var refreshed = this.children[t].refresh(changedTiddlers);
hasRefreshed = hasRefreshed || refreshed;
}
}
} else {
// Cycle through the list, inserting and removing list items as needed
for(t=0; t<this.list.length; t++) {
var index = this.findListItem(t,this.list[t]);
if(index === undefined) {
// The list item must be inserted
this.insertListItem(t,this.list[t]);
hasRefreshed = true;
} else {
// There are intervening list items that must be removed
for(var n=index-1; n>=t; n--) {
this.removeListItem(n);
hasRefreshed = true;
}
// Refresh the item we're reusing
var refreshed = this.children[t].refresh(changedTiddlers);
hasRefreshed = hasRefreshed || refreshed;
} }
// Refresh the item we're reusing
var refreshed = this.children[t].refresh(changedTiddlers);
hasRefreshed = hasRefreshed || refreshed;
} }
} }
// Remove any left over items // Remove any left over items
@ -257,7 +283,7 @@ Insert a new list item at the specified index
*/ */
ListWidget.prototype.insertListItem = function(index,title) { ListWidget.prototype.insertListItem = function(index,title) {
// Create, insert and render the new child widgets // Create, insert and render the new child widgets
var widget = this.makeChildWidget(this.makeItemTemplate(title)); var widget = this.makeChildWidget(this.makeItemTemplate(title,index));
widget.parentDomNode = this.parentDomNode; // Hack to enable findNextSiblingDomNode() to work widget.parentDomNode = this.parentDomNode; // Hack to enable findNextSiblingDomNode() to work
this.children.splice(index,0,widget); this.children.splice(index,0,widget);
var nextSibling = widget.findNextSiblingDomNode(); var nextSibling = widget.findNextSiblingDomNode();
@ -311,6 +337,11 @@ Compute the internal state of the widget
ListItemWidget.prototype.execute = function() { ListItemWidget.prototype.execute = function() {
// Set the current list item title // Set the current list item title
this.setVariable(this.parseTreeNode.variableName,this.parseTreeNode.itemTitle); this.setVariable(this.parseTreeNode.variableName,this.parseTreeNode.itemTitle);
if(this.parseTreeNode.indexName) {
this.setVariable(this.parseTreeNode.indexName,this.parseTreeNode.index);
this.setVariable(this.parseTreeNode.indexName + "-first",this.parseTreeNode.isFirst ? "yes" : "no");
this.setVariable(this.parseTreeNode.indexName + "-last",this.parseTreeNode.isLast ? "yes" : "no");
}
// Construct the child widgets // Construct the child widgets
this.makeChildWidgets(); this.makeChildWidgets();
}; };

Wyświetl plik

@ -350,6 +350,123 @@ describe("Widget module", function() {
expect(wrapper.children[0].children[4].sequenceNumber).toBe(5); expect(wrapper.children[0].children[4].sequenceNumber).toBe(5);
}); });
it("should deal with the list widget using an index variable", function() {
var wiki = new $tw.Wiki();
// Add some tiddlers
wiki.addTiddlers([
{title: "TiddlerOne", text: "Jolly Old World"},
{title: "TiddlerTwo", text: "Worldly Old Jelly"},
{title: "TiddlerThree", text: "Golly Gosh"},
{title: "TiddlerFour", text: "Lemon Squash"}
]);
// Construct the widget node
var text = "<$list index='index'><$view field='text'/><$text text=<<index>>/><$text text=<<index-first>>/><$text text=<<index-last>>/></$list>";
var widgetNode = createWidgetNode(parseText(text,wiki),wiki);
// Render the widget node to the DOM
var wrapper = renderWidgetNode(widgetNode);
// Test the rendering
expect(wrapper.innerHTML).toBe("<p>Lemon Squash0yesnoJolly Old World1nonoGolly Gosh2nonoWorldly Old Jelly3noyes</p>");
// Test the sequence numbers in the DOM
expect(wrapper.sequenceNumber).toBe(0);
expect(wrapper.children[0].sequenceNumber).toBe(1);
expect(wrapper.children[0].children[0].sequenceNumber).toBe(2);
expect(wrapper.children[0].children[1].sequenceNumber).toBe(3);
expect(wrapper.children[0].children[2].sequenceNumber).toBe(4);
expect(wrapper.children[0].children[3].sequenceNumber).toBe(5);
expect(wrapper.children[0].children[4].sequenceNumber).toBe(6);
expect(wrapper.children[0].children[5].sequenceNumber).toBe(7);
expect(wrapper.children[0].children[6].sequenceNumber).toBe(8);
expect(wrapper.children[0].children[7].sequenceNumber).toBe(9);
expect(wrapper.children[0].children[8].sequenceNumber).toBe(10);
expect(wrapper.children[0].children[9].sequenceNumber).toBe(11);
expect(wrapper.children[0].children[10].sequenceNumber).toBe(12);
expect(wrapper.children[0].children[11].sequenceNumber).toBe(13);
expect(wrapper.children[0].children[12].sequenceNumber).toBe(14);
expect(wrapper.children[0].children[13].sequenceNumber).toBe(15);
expect(wrapper.children[0].children[14].sequenceNumber).toBe(16);
expect(wrapper.children[0].children[15].sequenceNumber).toBe(17);
// Add another tiddler
wiki.addTiddler({title: "TiddlerFive", text: "Jalapeno Peppers"});
// Refresh
refreshWidgetNode(widgetNode,wrapper,["TiddlerFive"]);
// Test the refreshing
expect(wrapper.innerHTML).toBe("<p>Jalapeno Peppers0yesnoLemon Squash1nonoJolly Old World2nonoGolly Gosh3nonoWorldly Old Jelly4noyes</p>");
// Test the sequence numbers in the DOM
expect(wrapper.sequenceNumber).toBe(0);
expect(wrapper.children[0].sequenceNumber).toBe(1);
expect(wrapper.children[0].children[0].sequenceNumber).toBe(18);
expect(wrapper.children[0].children[1].sequenceNumber).toBe(19);
expect(wrapper.children[0].children[2].sequenceNumber).toBe(20);
expect(wrapper.children[0].children[3].sequenceNumber).toBe(21);
expect(wrapper.children[0].children[4].sequenceNumber).toBe(22);
expect(wrapper.children[0].children[5].sequenceNumber).toBe(23);
expect(wrapper.children[0].children[6].sequenceNumber).toBe(24);
expect(wrapper.children[0].children[7].sequenceNumber).toBe(25);
expect(wrapper.children[0].children[8].sequenceNumber).toBe(26);
expect(wrapper.children[0].children[9].sequenceNumber).toBe(27);
expect(wrapper.children[0].children[10].sequenceNumber).toBe(28);
expect(wrapper.children[0].children[11].sequenceNumber).toBe(29);
expect(wrapper.children[0].children[12].sequenceNumber).toBe(30);
expect(wrapper.children[0].children[13].sequenceNumber).toBe(31);
expect(wrapper.children[0].children[14].sequenceNumber).toBe(32);
expect(wrapper.children[0].children[15].sequenceNumber).toBe(33);
expect(wrapper.children[0].children[16].sequenceNumber).toBe(34);
expect(wrapper.children[0].children[17].sequenceNumber).toBe(35);
expect(wrapper.children[0].children[18].sequenceNumber).toBe(36);
expect(wrapper.children[0].children[19].sequenceNumber).toBe(37);
// Remove a tiddler
wiki.deleteTiddler("TiddlerThree");
// Refresh
refreshWidgetNode(widgetNode,wrapper,["TiddlerThree"]);
// Test the refreshing
expect(wrapper.innerHTML).toBe("<p>Jalapeno Peppers0yesnoLemon Squash1nonoJolly Old World2nonoWorldly Old Jelly3noyes</p>");
// Test the sequence numbers in the DOM
expect(wrapper.sequenceNumber).toBe(0);
expect(wrapper.children[0].sequenceNumber).toBe(1);
expect(wrapper.children[0].children[0].sequenceNumber).toBe(18);
expect(wrapper.children[0].children[1].sequenceNumber).toBe(19);
expect(wrapper.children[0].children[2].sequenceNumber).toBe(20);
expect(wrapper.children[0].children[3].sequenceNumber).toBe(21);
expect(wrapper.children[0].children[4].sequenceNumber).toBe(22);
expect(wrapper.children[0].children[5].sequenceNumber).toBe(23);
expect(wrapper.children[0].children[6].sequenceNumber).toBe(24);
expect(wrapper.children[0].children[7].sequenceNumber).toBe(25);
expect(wrapper.children[0].children[8].sequenceNumber).toBe(26);
expect(wrapper.children[0].children[9].sequenceNumber).toBe(27);
expect(wrapper.children[0].children[10].sequenceNumber).toBe(28);
expect(wrapper.children[0].children[11].sequenceNumber).toBe(29);
expect(wrapper.children[0].children[12].sequenceNumber).toBe(38);
expect(wrapper.children[0].children[13].sequenceNumber).toBe(39);
expect(wrapper.children[0].children[14].sequenceNumber).toBe(40);
expect(wrapper.children[0].children[15].sequenceNumber).toBe(41);
// Add it back a tiddler
wiki.addTiddler({title: "TiddlerThree", text: "Something"});
// Refresh
refreshWidgetNode(widgetNode,wrapper,["TiddlerThree"]);
// Test the refreshing
expect(wrapper.innerHTML).toBe("<p>Jalapeno Peppers0yesnoLemon Squash1nonoJolly Old World2nonoSomething3nonoWorldly Old Jelly4noyes</p>");
// Test the sequence numbers in the DOM
expect(wrapper.sequenceNumber).toBe(0);
expect(wrapper.children[0].sequenceNumber).toBe(1);
expect(wrapper.children[0].children[0].sequenceNumber).toBe(18);
expect(wrapper.children[0].children[1].sequenceNumber).toBe(19);
expect(wrapper.children[0].children[2].sequenceNumber).toBe(20);
expect(wrapper.children[0].children[3].sequenceNumber).toBe(21);
expect(wrapper.children[0].children[4].sequenceNumber).toBe(22);
expect(wrapper.children[0].children[5].sequenceNumber).toBe(23);
expect(wrapper.children[0].children[6].sequenceNumber).toBe(24);
expect(wrapper.children[0].children[7].sequenceNumber).toBe(25);
expect(wrapper.children[0].children[8].sequenceNumber).toBe(26);
expect(wrapper.children[0].children[9].sequenceNumber).toBe(27);
expect(wrapper.children[0].children[10].sequenceNumber).toBe(28);
expect(wrapper.children[0].children[11].sequenceNumber).toBe(29);
expect(wrapper.children[0].children[12].sequenceNumber).toBe(42);
expect(wrapper.children[0].children[13].sequenceNumber).toBe(43);
expect(wrapper.children[0].children[14].sequenceNumber).toBe(44);
expect(wrapper.children[0].children[15].sequenceNumber).toBe(45);
});
it("should deal with the list widget followed by other widgets", function() { it("should deal with the list widget followed by other widgets", function() {
var wiki = new $tw.Wiki(); var wiki = new $tw.Wiki();
// Add some tiddlers // Add some tiddlers

Wyświetl plik

@ -1,6 +1,6 @@
caption: list caption: list
created: 20131024141900000 created: 20131024141900000
modified: 20190608162410684 modified: 20210416175333981
tags: Widgets Lists tags: Widgets Lists
title: ListWidget title: ListWidget
type: text/vnd.tiddlywiki type: text/vnd.tiddlywiki
@ -82,10 +82,43 @@ The action of the list widget depends on the results of the filter combined with
|template |The title of a template tiddler for transcluding each tiddler in the list. When no template is specified, the body of the ListWidget serves as the item template. With no body, a simple link to the tiddler is returned. | |template |The title of a template tiddler for transcluding each tiddler in the list. When no template is specified, the body of the ListWidget serves as the item template. With no body, a simple link to the tiddler is returned. |
|editTemplate |An alternative template to use for [[DraftTiddlers|DraftMechanism]] in edit mode | |editTemplate |An alternative template to use for [[DraftTiddlers|DraftMechanism]] in edit mode |
|variable |The name for a [[variable|Variables]] in which the title of each listed tiddler is stored. Defaults to ''currentTiddler'' | |variable |The name for a [[variable|Variables]] in which the title of each listed tiddler is stored. Defaults to ''currentTiddler'' |
|index |<<.from-version "5.1.24">> Optional name for a [[variable|Variables]] in which the numeric index of each listed tiddler is stored (see below) |
|emptyMessage |Message to be displayed when the list is empty | |emptyMessage |Message to be displayed when the list is empty |
|storyview |Optional name of module responsible for animating/processing the list | |storyview |Optional name of module responsible for animating/processing the list |
|history |The title of the tiddler containing the navigation history | |history |The title of the tiddler containing the navigation history |
!! `index` attribute
The optional `index` attribute specifies the name of a variable to hold the numeric index of the current item in the list.
Two additional variables are also set to indicate the first and last items in the list:
* `<index-variable-name>-first` is set to `yes` for the first entry in the list, `no` for the others
* `<index-variable-name>-last` is set to `yes` for the last entry in the list, `no` for the others
For example:
```
<$list filter="[tag[About]sort[title]]" index="index">
<div>
<<index>>: ''<$text text=<<currentTiddler>>/>'' (is first: <<index-first>>, is last: <<index-last>>)
</div>
</$list>
```
Displays as:
<<<
<$list filter="[tag[About]sort[title]]" index="index">
<div>
<<index>>: ''<$text text=<<currentTiddler>>/>'' (is first: <<index-first>>, is last: <<index-last>>)
</div>
</$list>
<<<
Note that using the `index` attribute degrades the performance of the list widget because it prevents the optimisation of refreshes by moving list items around instead of rerendering them.
!! Edit mode !! Edit mode
The `<$list>` widget can optionally render draft tiddlers through a different template to handle editing, see DraftMechanism. The `<$list>` widget can optionally render draft tiddlers through a different template to handle editing, see DraftMechanism.