diff --git a/core/modules/widgets/list.js b/core/modules/widgets/list.js index a49bf01ea..426adb307 100755 --- a/core/modules/widgets/list.js +++ b/core/modules/widgets/list.js @@ -61,6 +61,7 @@ ListWidget.prototype.execute = function() { this.template = this.getAttribute("template"); this.editTemplate = this.getAttribute("editTemplate"); this.variableName = this.getAttribute("variable","currentTiddler"); + this.indexName = this.getAttribute("index"); this.storyViewName = this.getAttribute("storyview"); this.historyTitle = this.getAttribute("history"); // Compose the list elements @@ -72,7 +73,7 @@ ListWidget.prototype.execute = function() { members = this.getEmptyMessage(); } else { $tw.utils.each(this.list,function(title,index) { - members.push(self.makeItemTemplate(title)); + members.push(self.makeItemTemplate(title,index)); }); } // Construct the child widgets @@ -105,7 +106,7 @@ ListWidget.prototype.getEmptyMessage = function() { /* 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 var tiddler = this.wiki.getTiddler(title), isDraft = tiddler && tiddler.hasField("draft.of"), @@ -128,7 +129,14 @@ ListWidget.prototype.makeItemTemplate = function(title) { } } // 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); } // 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(); result = true; } else { @@ -211,23 +219,41 @@ ListWidget.prototype.handleListChanges = function(changedTiddlers) { this.removeChildDomNodes(); this.children = []; } - // Cycle through the list, inserting and removing list items as needed - var hasRefreshed = false; - for(var t=0; t=t; n--) { - this.removeListItem(n); + // If we are providing an index variable then we must refresh the items, otherwise we can rearrange them + var hasRefreshed = false,t; + if(this.indexName) { + // Cycle through the list and remove and re-insert the first item that has changed, and all the remaining items + for(t=0; t=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 @@ -257,7 +283,7 @@ Insert a new list item at the specified index */ ListWidget.prototype.insertListItem = function(index,title) { // 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 this.children.splice(index,0,widget); var nextSibling = widget.findNextSiblingDomNode(); @@ -311,6 +337,11 @@ Compute the internal state of the widget ListItemWidget.prototype.execute = function() { // Set the current list item title 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 this.makeChildWidgets(); }; diff --git a/editions/test/tiddlers/tests/test-widget.js b/editions/test/tiddlers/tests/test-widget.js index f3d500489..72ef2f9b4 100755 --- a/editions/test/tiddlers/tests/test-widget.js +++ b/editions/test/tiddlers/tests/test-widget.js @@ -350,6 +350,123 @@ describe("Widget module", function() { 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=<>/><$text text=<>/><$text text=<>/>"; + 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("

Lemon Squash0yesnoJolly Old World1nonoGolly Gosh2nonoWorldly Old Jelly3noyes

"); + // 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("

Jalapeno Peppers0yesnoLemon Squash1nonoJolly Old World2nonoGolly Gosh3nonoWorldly Old Jelly4noyes

"); + // 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("

Jalapeno Peppers0yesnoLemon Squash1nonoJolly Old World2nonoWorldly Old Jelly3noyes

"); + // 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("

Jalapeno Peppers0yesnoLemon Squash1nonoJolly Old World2nonoSomething3nonoWorldly Old Jelly4noyes

"); + // 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() { var wiki = new $tw.Wiki(); // Add some tiddlers diff --git a/editions/tw5.com/tiddlers/widgets/ListWidget.tid b/editions/tw5.com/tiddlers/widgets/ListWidget.tid index 272df8d87..363d87e2d 100644 --- a/editions/tw5.com/tiddlers/widgets/ListWidget.tid +++ b/editions/tw5.com/tiddlers/widgets/ListWidget.tid @@ -1,6 +1,6 @@ caption: list created: 20131024141900000 -modified: 20190608162410684 +modified: 20210416175333981 tags: Widgets Lists title: ListWidget 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. | |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'' | +|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 | |storyview |Optional name of module responsible for animating/processing the list | |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: + +* `-first` is set to `yes` for the first entry in the list, `no` for the others +* `-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"> +
+<>: ''<$text text=<>/>'' (is first: <>, is last: <>) +
+ +``` + +Displays as: + +<<< +<$list filter="[tag[About]sort[title]]" index="index"> +
+<>: ''<$text text=<>/>'' (is first: <>, is last: <>) +
+ +<<< + +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 The `<$list>` widget can optionally render draft tiddlers through a different template to handle editing, see DraftMechanism.