diff --git a/core/modules/widgets/list.js b/core/modules/widgets/list.js index faedf72cc..78976f69a 100755 --- a/core/modules/widgets/list.js +++ b/core/modules/widgets/list.js @@ -50,8 +50,8 @@ ListWidget.prototype.render = function(parent,nextSibling) { $tw.modules.applyMethods("storyview",this.storyViews); } this.parentDomNode = parent; - this.computeAttributes(); - this.execute(); + var changedAttributes = this.computeAttributes(); + this.execute(changedAttributes); this.renderChildren(parent,nextSibling); // Construct the storyview var StoryView = this.storyViews[this.storyViewName]; @@ -71,7 +71,7 @@ ListWidget.prototype.render = function(parent,nextSibling) { /* Compute the internal state of the widget */ -ListWidget.prototype.execute = function() { +ListWidget.prototype.execute = function(changedAttributes) { var self = this; // Get our attributes this.template = this.getAttribute("template"); @@ -80,6 +80,10 @@ ListWidget.prototype.execute = function() { this.counterName = this.getAttribute("counter"); this.storyViewName = this.getAttribute("storyview"); this.historyTitle = this.getAttribute("history"); + // Create join template only if needed + if(this.join === undefined || (changedAttributes && changedAttributes.join)) { + this.join = this.makeJoinTemplate(); + } // Compose the list elements this.list = this.getTiddlerList(); var members = [], @@ -102,6 +106,7 @@ ListWidget.prototype.findExplicitTemplates = function() { var self = this; this.explicitListTemplate = null; this.explicitEmptyTemplate = null; + this.explicitJoinTemplate = null; this.hasTemplateInBody = false; var searchChildren = function(childNodes) { $tw.utils.each(childNodes,function(node) { @@ -109,6 +114,8 @@ ListWidget.prototype.findExplicitTemplates = function() { self.explicitListTemplate = node.children; } else if(node.type === "list-empty") { self.explicitEmptyTemplate = node.children; + } else if(node.type === "list-join") { + self.explicitJoinTemplate = node.children; } else if(node.type === "element" && node.tag === "p") { searchChildren(node.children); } else { @@ -152,6 +159,24 @@ ListWidget.prototype.getEmptyMessage = function() { } }; +/* +Compose the template for a join between list items +*/ +ListWidget.prototype.makeJoinTemplate = function() { + var parser, + join = this.getAttribute("join",""); + if(join) { + parser = this.wiki.parseText("text/vnd.tiddlywiki",join,{parseAsInline:true}) + if(parser) { + return parser.tree; + } else { + return []; + } + } else { + return this.explicitJoinTemplate; // May be null, and that's fine + } +}; + /* Compose the template for a list item */ @@ -160,6 +185,7 @@ ListWidget.prototype.makeItemTemplate = function(title,index) { var tiddler = this.wiki.getTiddler(title), isDraft = tiddler && tiddler.hasField("draft.of"), template = this.template, + join = this.join, templateTree; if(isDraft && this.editTemplate) { template = this.editTemplate; @@ -185,12 +211,12 @@ ListWidget.prototype.makeItemTemplate = function(title,index) { } } // Return the list item - var parseTreeNode = {type: "listitem", itemTitle: title, variableName: this.variableName, children: templateTree}; + var parseTreeNode = {type: "listitem", itemTitle: title, variableName: this.variableName, children: templateTree, join: join}; + parseTreeNode.isLast = index === this.list.length - 1; if(this.counterName) { parseTreeNode.counter = (index + 1).toString(); parseTreeNode.counterName = this.counterName; parseTreeNode.isFirst = index === 0; - parseTreeNode.isLast = index === this.list.length - 1; } return parseTreeNode; }; @@ -206,7 +232,7 @@ ListWidget.prototype.refresh = function(changedTiddlers) { this.storyview.refreshStart(changedTiddlers,changedAttributes); } // Completely refresh if any of our attributes have changed - if(changedAttributes.filter || changedAttributes.variable || changedAttributes.counter || changedAttributes.template || changedAttributes.editTemplate || changedAttributes.emptyMessage || changedAttributes.storyview || changedAttributes.history) { + if(changedAttributes.filter || changedAttributes.variable || changedAttributes.counter || changedAttributes.template || changedAttributes.editTemplate || changedAttributes.join || changedAttributes.emptyMessage || changedAttributes.storyview || changedAttributes.history) { this.refreshSelf(); result = true; } else { @@ -310,10 +336,29 @@ ListWidget.prototype.handleListChanges = function(changedTiddlers) { } } else { // Cycle through the list, inserting and removing list items as needed + var mustRecreateLastItem = false; + if(this.join && this.join.length) { + if(this.children.length !== this.list.length) { + mustRecreateLastItem = true; + } else if(prevList[prevList.length-1] !== this.list[this.list.length-1]) { + mustRecreateLastItem = true; + } + } + var isLast = false, wasLast = false; for(t=0; t0) { + // First re-create previosly-last item that will no longer be last + this.removeListItem(t-1); + this.insertListItem(t-1,this.list[t-1]); + } this.insertListItem(t,this.list[t]); hasRefreshed = true; } else { @@ -322,9 +367,15 @@ ListWidget.prototype.handleListChanges = function(changedTiddlers) { 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, or recreate if necessary + if(mustRecreateLastItem && (isLast || wasLast)) { + this.removeListItem(t); + this.insertListItem(t,this.list[t]); + hasRefreshed = true; + } else { + var refreshed = this.children[t].refresh(changedTiddlers); + hasRefreshed = hasRefreshed || refreshed; + } } } } @@ -414,8 +465,17 @@ ListItemWidget.prototype.execute = function() { this.setVariable(this.parseTreeNode.counterName + "-first",this.parseTreeNode.isFirst ? "yes" : "no"); this.setVariable(this.parseTreeNode.counterName + "-last",this.parseTreeNode.isLast ? "yes" : "no"); } + // Add join if needed + var children = this.parseTreeNode.children, + join = this.parseTreeNode.join; + if(join && join.length && !this.parseTreeNode.isLast) { + children = children.slice(0); + $tw.utils.each(join,function(joinNode) { + children.push(joinNode); + }) + } // Construct the child widgets - this.makeChildWidgets(); + this.makeChildWidgets(children); }; /* @@ -450,4 +510,14 @@ ListEmptyWidget.prototype.refresh = function() { return false; } exports["list-empty"] = ListEmptyWidget; +var ListJoinWidget = function(parseTreeNode,options) { + // Main initialisation inherited from widget.js + this.initialise(parseTreeNode,options); +}; +ListJoinWidget.prototype = new Widget(); +ListJoinWidget.prototype.render = function() {} +ListJoinWidget.prototype.refresh = function() { return false; } + +exports["list-join"] = ListJoinWidget; + })(); diff --git a/core/templates/html-json-skinny-tiddler.tid b/core/templates/html-json-skinny-tiddler.tid index 1e3c032f3..6f5b7ff35 100644 --- a/core/templates/html-json-skinny-tiddler.tid +++ b/core/templates/html-json-skinny-tiddler.tid @@ -1,4 +1,3 @@ title: $:/core/templates/html-json-skinny-tiddler -<$list filter="[compare:number:gteq[1]] ~[!match[1]]">`,`<$text text=<>/> <$jsontiddler tiddler=<> exclude="text" escapeUnsafeScriptChars="yes"/> diff --git a/core/templates/html-json-tiddler.tid b/core/templates/html-json-tiddler.tid index 6b62b4ac9..2e12290a7 100644 --- a/core/templates/html-json-tiddler.tid +++ b/core/templates/html-json-tiddler.tid @@ -1,3 +1,3 @@ title: $:/core/templates/html-json-tiddler -<$list filter="[!match[1]]">`,`<$text text=<>/><$jsontiddler tiddler=<> escapeUnsafeScriptChars="yes"/> \ No newline at end of file +<$jsontiddler tiddler=<> escapeUnsafeScriptChars="yes"/> \ No newline at end of file diff --git a/core/templates/store.area.template.html.tid b/core/templates/store.area.template.html.tid index 84dd0c432..b148a2ff3 100644 --- a/core/templates/store.area.template.html.tid +++ b/core/templates/store.area.template.html.tid @@ -6,14 +6,14 @@ title: $:/core/templates/store.area.template.html <$list filter="[[storeAreaFormat]is[variable]getvariable[]else[json]match[json]]"> `` `` diff --git a/core/ui/ViewTemplate/subtitle.tid b/core/ui/ViewTemplate/subtitle.tid index a0436b095..a7c010287 100644 --- a/core/ui/ViewTemplate/subtitle.tid +++ b/core/ui/ViewTemplate/subtitle.tid @@ -4,11 +4,8 @@ tags: $:/tags/ViewTemplate \whitespace trim <$reveal type="nomatch" stateTitle=<> text="hide" tag="div" retain="yes" animate="yes">
-<$list filter="[all[shadows+tiddlers]tag[$:/tags/ViewTemplate/Subtitle]!has[draft.of]]" variable="subtitleTiddler" counter="indexSubtitleTiddler"> -<$list filter="[match[no]]" variable="ignore"> -  - -<$transclude tiddler=<> mode="inline"/> +<$list filter="[all[shadows+tiddlers]tag[$:/tags/ViewTemplate/Subtitle]!has[draft.of]]" variable="subtitleTiddler"> +<$transclude tiddler=<> mode="inline"/><$list-join> 
diff --git a/editions/prerelease/tiddlers/Release 5.3.2.tid b/editions/prerelease/tiddlers/Release 5.3.2.tid index ced4957f2..adcff9e67 100644 --- a/editions/prerelease/tiddlers/Release 5.3.2.tid +++ b/editions/prerelease/tiddlers/Release 5.3.2.tid @@ -41,6 +41,33 @@ description: Under development Note that the <<.attr "emptyMessage">> and <<.attr "template">> attributes take precedence if they are present. +!! Joiners for the ListWidget + +<<.link-badge-added "https://github.com/Jermolene/TiddlyWiki5/pull/7694">> a <<.attr "join">> attribute to the <<.wid "ListWidget">> widget to insert a short piece of text between list items. This is both easier to use and faster than using the <<.attr "counter">> attribute for the same purpose. So if your list looked like this: + +``` +<$list filter=<> counter="counter" variable="item"> +<$text text=<>/><$list filter="[match[no]]" variable="ignore"><$text text=", "/> + +``` + +You can replace it with: + +``` +<$list filter=<> variable="item" join=", "> +<$text text=<>/> + +``` + +If the joiner text that you need is long and awkward to write in an attribute, you can use the new `<$list-join>` widget. Like `<$list-template>` and `<$list-empty>`, it must be an immediate child of the <<.wid "ListWidget">>: + +``` +<$list filter=<> variable="item"> +<$text text=<>/> +<$list-join>, and also let's not forget + +``` + !! jsonset operator <<.link-badge-added "https://github.com/Jermolene/TiddlyWiki5/pull/7742">> [[jsonset Operator]] for setting values within JSON objects diff --git a/editions/test/tiddlers/tests/data/list-widget/WithJoinTemplate.tid b/editions/test/tiddlers/tests/data/list-widget/WithJoinTemplate.tid new file mode 100644 index 000000000..f1b6f25e9 --- /dev/null +++ b/editions/test/tiddlers/tests/data/list-widget/WithJoinTemplate.tid @@ -0,0 +1,30 @@ +title: ListWidget/WithJoinTemplate +description: List widget with join template and $list-empty +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + ++ +title: Output + +\whitespace trim + +\procedure test(filter) +<$list filter=<>> + Item:<> + + <$list-empty> + None! + + + <$list-join>, + +\end + +<> + +<> + ++ +title: ExpectedResult + +

Item:1,Item:2,Item:3

None!

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/list-widget/WithJoinTemplateInBlockMode.tid b/editions/test/tiddlers/tests/data/list-widget/WithJoinTemplateInBlockMode.tid new file mode 100644 index 000000000..c12f4c801 --- /dev/null +++ b/editions/test/tiddlers/tests/data/list-widget/WithJoinTemplateInBlockMode.tid @@ -0,0 +1,32 @@ +title: ListWidget/WithJoinTemplateInBlockMode +description: List widget with join template and $list-empty in block mode +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + ++ +title: Output + +\whitespace trim + +\procedure test(filter) +<$list filter=<>> + + Item:<> + + <$list-empty> + None! + + + <$list-join>
+ +\end + +<> + +<> + ++ +title: ExpectedResult +comment: I wish there was a good way to get rid of these extraneous paragraph elements + +

Item:1


Item:2


Item:3

None! \ No newline at end of file diff --git a/editions/test/tiddlers/tests/test-widget.js b/editions/test/tiddlers/tests/test-widget.js index 4da9e20b0..0d1351f31 100755 --- a/editions/test/tiddlers/tests/test-widget.js +++ b/editions/test/tiddlers/tests/test-widget.js @@ -527,6 +527,45 @@ describe("Widget module", function() { expect(wrapper.children[0].children[15].sequenceNumber).toBe(53); }); + var testListJoin = function(oldList, newList) { + return function() { + var wiki = new $tw.Wiki(); + // Add some tiddlers + wiki.addTiddler({title: "Numbers", text: "", list: oldList}); + var text = "<$list filter='[list[Numbers]]' variable='item' join=', '><>"; + 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("

" + oldList.split(' ').join(', ') + "

"); + // Change the list and ensure new rendering is still right + wiki.addTiddler({title: "Numbers", text: "", list: newList}); + refreshWidgetNode(widgetNode,wrapper,["Numbers"]); + expect(wrapper.innerHTML).toBe("

" + newList.split(' ').join(', ') + "

"); + } + } + + it("the list widget with join should update correctly when empty list gets one item", testListJoin("", "1")); + it("the list widget with join should update correctly when empty list gets two items", testListJoin("", "1 2")); + it("the list widget with join should update correctly when single-item list is appended to", testListJoin("1", "1 2")); + it("the list widget with join should update correctly when single-item list is prepended to", testListJoin("1", "2 1")); + it("the list widget with join should update correctly when list is appended", testListJoin("1 2 3 4", "1 2 3 4 5")); + it("the list widget with join should update correctly when last item is removed", testListJoin("1 2 3 4", "1 2 3")); + it("the list widget with join should update correctly when first item is inserted", testListJoin("1 2 3 4", "0 1 2 3 4")); + it("the list widget with join should update correctly when first item is removed", testListJoin("1 2 3 4", "2 3 4")); + it("the list widget with join should update correctly when first two items are swapped", testListJoin("1 2 3 4", "2 1 3 4")); + it("the list widget with join should update correctly when last two items are swapped", testListJoin("1 2 3 4", "1 2 4 3")); + it("the list widget with join should update correctly when last item is moved to the front", testListJoin("1 2 3 4", "4 1 2 3")); + it("the list widget with join should update correctly when last item is moved to the middle", testListJoin("1 2 3 4", "1 4 2 3")); + it("the list widget with join should update correctly when first item is moved to the back", testListJoin("1 2 3 4", "2 3 4 1")); + it("the list widget with join should update correctly when middle item is moved to the back", testListJoin("1 2 3 4", "1 3 4 2")); + it("the list widget with join should update correctly when the last item disappears at the same time as other edits 1", testListJoin("1 3 4", "1 2 3")); + it("the list widget with join should update correctly when the last item disappears at the same time as other edits 2", testListJoin("1 3 4", "1 3 2")); + it("the list widget with join should update correctly when the last item disappears at the same time as other edits 3", testListJoin("1 3 4", "2 1 3")); + it("the list widget with join should update correctly when the last item disappears at the same time as other edits 4", testListJoin("1 3 4", "2 3 1")); + it("the list widget with join should update correctly when the last item disappears at the same time as other edits 5", testListJoin("1 3 4", "3 1 2")); + it("the list widget with join should update correctly when the last item disappears at the same time as other edits 6", testListJoin("1 3 4", "3 2 1")); + var testCounterLast = function(oldList, newList) { return function() { var wiki = new $tw.Wiki(); diff --git a/editions/tw5.com/tiddlers/widgets/ListWidget.tid b/editions/tw5.com/tiddlers/widgets/ListWidget.tid index 592185d36..ce4389261 100644 --- a/editions/tw5.com/tiddlers/widgets/ListWidget.tid +++ b/editions/tw5.com/tiddlers/widgets/ListWidget.tid @@ -84,6 +84,7 @@ The action of the list widget depends on the results of the filter combined with |limit |<<.from-version "5.3.2">> Optional numeric limit for the number of results that are returned. Negative values will return the results from the end of the list | |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 | +|join |<<.from-version "5.3.2">> Text to include between each list item | |variable |The name for a [[variable|Variables]] in which the title of each listed tiddler is stored. Defaults to ''currentTiddler'' | |counter |<<.from-version "5.2.0">> Optional name for a [[variable|Variables]] in which the 1-based numeric index of each listed tiddler is stored (see below) | |emptyMessage |Message to be displayed when the list is empty | @@ -120,10 +121,29 @@ Displays as: <<< -Note that using the `counter` attribute can reduce performance when working with list items that dynamically reorder or update themselves. The best advice is only to use it when it is really necessary: to obtain a numeric index, or to detect the first or last entries in the list. +Note that using the `counter` attribute can reduce performance when working with list items that dynamically reorder or update themselves. The best advice is only to use it when it is really necessary: to obtain a numeric index, or to detect the first or last entries in the list. Note that if you are only using it to insert something (like a comma) between list items, the `join` attribute performs much better and you should use it instead of `counter`. Setting `counter="transclusion"` is a handy way to make child elements for each list element be identified as unique. A common use case are multiple [[tag macros|tag Macro]] for the same tag generated by a list widget. Refer to [[tag macro examples|tag Macro (Examples)]] for more details. +!! `join` attribute + +<<.from-version "5.3.2">> The optional `join` attribute allow you to insert some [[WikiText]] between each list item without needing to use the `counter` attribute, which can become quite slow if the list is updated frequently. + +<<.from-version "5.3.2">> If the widget `<$list-join>` is found as an immediate child of the <<.wid "ListWidget">> widget then the content of that widget is used as the "join" template, included between two list items. Note that the <<.attr "join">> attribute takes precedence if it is present. + +For example: + + +``` +<$list filter="[tag[About]sort[title]]" join=", " variable="item"><> +``` + +Displays as: + +<<< +<$list filter="[tag[About]sort[title]]" join=", " variable="item"><> +<<< + !! Edit mode The `<$list>` widget can optionally render draft tiddlers through a different template to handle editing, see DraftMechanism. diff --git a/plugins/tiddlywiki/tiddlyweb/html-json-skinny-tiddler.tid b/plugins/tiddlywiki/tiddlyweb/html-json-skinny-tiddler.tid index d4f96fde7..b7329c265 100644 --- a/plugins/tiddlywiki/tiddlyweb/html-json-skinny-tiddler.tid +++ b/plugins/tiddlywiki/tiddlyweb/html-json-skinny-tiddler.tid @@ -1,4 +1,3 @@ title: $:/core/templates/html-json-skinny-tiddler -<$list filter="[compare:number:gteq[1]] ~[!match[1]]">`,`<$text text=<>/> <$jsontiddler tiddler=<> exclude="text" escapeUnsafeScriptChars="yes" $revision=<> $bag="default" $_is_skinny=""/> diff --git a/plugins/tiddlywiki/tiddlyweb/html-json-tiddler.tid b/plugins/tiddlywiki/tiddlyweb/html-json-tiddler.tid index bd7a0deec..f357321fb 100644 --- a/plugins/tiddlywiki/tiddlyweb/html-json-tiddler.tid +++ b/plugins/tiddlywiki/tiddlyweb/html-json-tiddler.tid @@ -1,4 +1,3 @@ title: $:/core/templates/html-json-tiddler -<$list filter="[!match[1]]">`,`<$text text=<>/> -<$jsontiddler tiddler=<> escapeUnsafeScriptChars="yes" $revision=<> $bag="default">/> +<$jsontiddler tiddler=<> escapeUnsafeScriptChars="yes" $revision=<> $bag="default"/>