From 215bd4e015f3069e007c14ab57937d0eade7ba88 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Wed, 15 Nov 2023 05:10:58 +0700 Subject: [PATCH 01/45] Avoid skipping extra whitespace in wikiparser.js (#7835) When wikiparser parses text looking for a pragma block, it skips whitespace before looking for the next pragma. If no pragma is found, we should return the parse position to the original location so that the skipped whitespace can be parsed as a text node. This allows the attribute `join=" and "` to parse as " and " rather than "and ". --- core/modules/parsers/wikiparser/wikiparser.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/modules/parsers/wikiparser/wikiparser.js b/core/modules/parsers/wikiparser/wikiparser.js index 293b7d3d3..1606d982d 100644 --- a/core/modules/parsers/wikiparser/wikiparser.js +++ b/core/modules/parsers/wikiparser/wikiparser.js @@ -194,6 +194,7 @@ Parse any pragmas at the beginning of a block of parse text WikiParser.prototype.parsePragmas = function() { var currentTreeBranch = this.tree; while(true) { + var savedPos = this.pos; // Skip whitespace this.skipWhitespace(); // Check for the end of the text @@ -204,6 +205,7 @@ WikiParser.prototype.parsePragmas = function() { var nextMatch = this.findNextMatch(this.pragmaRules,this.pos); // If not, just exit if(!nextMatch || nextMatch.matchIndex !== this.pos) { + this.pos = savedPos; break; } // Process the pragma rule From e4bf7c5f440cc5eb1fc499cec29eac37382add12 Mon Sep 17 00:00:00 2001 From: Mario Pietsch Date: Sat, 18 Nov 2023 17:13:27 +0100 Subject: [PATCH 02/45] fix the add field button tooltip (#7842) --- core/ui/EditTemplate/fields.tid | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/ui/EditTemplate/fields.tid b/core/ui/EditTemplate/fields.tid index e4381cbe7..0edc33505 100644 --- a/core/ui/EditTemplate/fields.tid +++ b/core/ui/EditTemplate/fields.tid @@ -54,7 +54,7 @@ $:/config/EditTemplateFields/Visibility/$(currentField)$ \whitespace trim <$vars name={{{ [get[text]] }}}> <$reveal type="nomatch" text="" default=<>> -<$button tooltip=<>> +<$button tooltip={{$:/language/EditTemplate/Fields/Add/Button/Hint}}> <$action-sendmessage $message="tm-add-field" $name=<> $value={{{ [subfilterget[text]] }}}/> From bf8b3cff03adee1c76fa03914bdfd22b34e790f4 Mon Sep 17 00:00:00 2001 From: Saq Imtiaz Date: Mon, 20 Nov 2023 09:38:04 +0100 Subject: [PATCH 03/45] Fixes Text Parser being impacted by overrides to codeblock widget (#7844) * fix: overriding codeblock widget should not impact text parser * fix: whitespace changes --- core/modules/parsers/textparser.js | 9 +++++---- ...tomWidget-CodeblockOverride-TextParser.tid | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 editions/test/tiddlers/tests/data/transclude/CustomWidget-CodeblockOverride-TextParser.tid diff --git a/core/modules/parsers/textparser.js b/core/modules/parsers/textparser.js index 06b08f30f..17f9bde10 100644 --- a/core/modules/parsers/textparser.js +++ b/core/modules/parsers/textparser.js @@ -14,10 +14,12 @@ The plain text parser processes blocks of source text into a degenerate parse tr var TextParser = function(type,text,options) { this.tree = [{ - type: "codeblock", + type: "genesis", attributes: { - code: {type: "string", value: text}, - language: {type: "string", value: type} + $type: {name: "$type", type: "string", value: "$codeblock"}, + code: {name: "code", type: "string", value: text}, + language: {name: "language", type: "string", value: type}, + $remappable: {name: "$remappable", type:"string", value: "no"} } }]; this.source = text; @@ -32,4 +34,3 @@ exports["text/css"] = TextParser; exports["application/x-tiddler-dictionary"] = TextParser; })(); - diff --git a/editions/test/tiddlers/tests/data/transclude/CustomWidget-CodeblockOverride-TextParser.tid b/editions/test/tiddlers/tests/data/transclude/CustomWidget-CodeblockOverride-TextParser.tid new file mode 100644 index 000000000..484f0c4a3 --- /dev/null +++ b/editions/test/tiddlers/tests/data/transclude/CustomWidget-CodeblockOverride-TextParser.tid @@ -0,0 +1,20 @@ +title: Transclude/CustomWidget/CodeblockOverride-TextParser +description: Test that overriding codeblock widget does not impact text parser +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\whitespace trim +\widget $codeblock(code) +<$transclude $variable="copy-to-clipboard" src=<>/> +<$genesis $type="$codeblock" $remappable="no" code=<>/> +\end + +\procedure myvariable() hello + +<$transclude $variable="myvariable" $type="text/plain" $output="text/plain"/> ++ +title: ExpectedResult + +

hello

\ No newline at end of file From ab72cc7b097a504fc2ab6edadfb0e987075f1475 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Tue, 21 Nov 2023 18:05:13 +0700 Subject: [PATCH 04/45] Allow negative indexes in json operators (#7849) * Add unit tests for negative indexes in json ops * Allow negative indexes in JSON operators Negative indexes will be treated as counting from the end, so -1 means last item of the array, -2 means next-to-last item, and so on. * Add documentation for negative indexes --- core/modules/filters/json-ops.js | 24 +++++++++++++++---- .../test/tiddlers/tests/test-json-filters.js | 21 ++++++++++++++++ .../tw5.com/tiddlers/filters/jsonextract.tid | 8 +++++++ editions/tw5.com/tiddlers/filters/jsonget.tid | 8 +++++++ editions/tw5.com/tiddlers/filters/jsonset.tid | 8 +++++++ .../tw5.com/tiddlers/filters/jsontype.tid | 8 +++++++ 6 files changed, 72 insertions(+), 5 deletions(-) diff --git a/core/modules/filters/json-ops.js b/core/modules/filters/json-ops.js index 51e509432..75a34e94a 100644 --- a/core/modules/filters/json-ops.js +++ b/core/modules/filters/json-ops.js @@ -213,6 +213,18 @@ function getDataItemType(data,indexes) { } } +function getItemAtIndex(item,index) { + if($tw.utils.hop(item,index)) { + return item[index]; + } else if($tw.utils.isArray(item)) { + index = $tw.utils.parseInt(index); + if(index < 0) { index = index + item.length }; + return item[index]; // Will be undefined if index was out-of-bounds + } else { + return undefined; + } +} + /* Given a JSON data structure and an array of index strings, return the value at the end of the index chain, or "undefined" if any of the index strings are invalid */ @@ -225,7 +237,7 @@ function getDataItem(data,indexes) { for(var i=0; i> operator uses multiple operands to specify the indexes o [jsonextract[d],[g]] --> {"x":"max","y":"may","z":"maize"} ``` +<<.from-version "5.3.2">> Negative indexes into an array are counted from the end, so -1 means the last item, -2 the next-to-last item, and so on: + +``` +[jsonextract[d],[f],[-1]] --> null +[jsonextract[d],[f],[-2]] --> false +[jsonextract[d],[f],[-4]] --> "six" +``` + Indexes can be dynamically composed from variables and transclusions: ``` diff --git a/editions/tw5.com/tiddlers/filters/jsonget.tid b/editions/tw5.com/tiddlers/filters/jsonget.tid index d9caa680e..c50cbd6f2 100644 --- a/editions/tw5.com/tiddlers/filters/jsonget.tid +++ b/editions/tw5.com/tiddlers/filters/jsonget.tid @@ -51,6 +51,14 @@ The <<.op jsonget>> operator uses multiple operands to specify the indexes of th [jsonget[d],[f],[0]] --> "five" ``` +<<.from-version "5.3.2">> Negative indexes into an array are counted from the end, so -1 means the last item, -2 the next-to-last item, and so on: + +``` +[jsonget[d],[f],[-1]] --> null +[jsonget[d],[f],[-2]] --> false +[jsonget[d],[f],[-4]] --> "six" +``` + Indexes can be dynamically composed from variables and transclusions: ``` diff --git a/editions/tw5.com/tiddlers/filters/jsonset.tid b/editions/tw5.com/tiddlers/filters/jsonset.tid index 9f70f6eb4..81552c7a1 100644 --- a/editions/tw5.com/tiddlers/filters/jsonset.tid +++ b/editions/tw5.com/tiddlers/filters/jsonset.tid @@ -51,6 +51,14 @@ The <<.op jsonset>> operator uses multiple operands to specify the indexes of th [jsonset[d],[f],[Panther]] --> {"a": "one","b": "","c": "three","d": "{"e": "four","f": "Panther","g": {"x": "max","y": "may","z": "maize"}}"} ``` +Negative indexes into an array are counted from the end, so -1 means the last item, -2 the next-to-last item, and so on: + +``` +[jsonset[d],[f],[-1],[Elephant]] --> {"a": "one","b": "","c": "three","d": "{"e": "four","f": ["five","six",true,false,"Elephant"],"g": {"x": "max","y": "may","z": "maize"}}"} +[jsonset[d],[f],[-2],[Elephant]] --> {"a": "one","b": "","c": "three","d": "{"e": "four","f": ["five","six",true,"Elephant",null],"g": {"x": "max","y": "may","z": "maize"}}"} +[jsonset[d],[f],[-4],[Elephant]] --> {"a": "one","b": "","c": "three","d": "{"e": "four","f": ["five","Elephant",true,false,null],"g": {"x": "max","y": "may","z": "maize"}}"} +``` + Indexes can be dynamically composed from variables and transclusions: ``` diff --git a/editions/tw5.com/tiddlers/filters/jsontype.tid b/editions/tw5.com/tiddlers/filters/jsontype.tid index b88f865dd..6bff01914 100644 --- a/editions/tw5.com/tiddlers/filters/jsontype.tid +++ b/editions/tw5.com/tiddlers/filters/jsontype.tid @@ -61,6 +61,14 @@ The <<.op jsontype>> operator uses multiple operands to specify the indexes of t [jsontype[d],[f],[2]] --> "boolean" ``` +<<.from-version "5.3.2">> Negative indexes into an array are counted from the end, so -1 means the last item, -2 the next-to-last item, and so on: + +``` +[jsontype[d],[f],[-1]] --> "null" +[jsontype[d],[f],[-2]] --> "boolean" +[jsontype[d],[f],[-4]] --> "string" +``` + Indexes can be dynamically composed from variables and transclusions: ``` From 9012d36859d33db23183d93d14d2bbc351dd235f Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Tue, 21 Nov 2023 11:23:10 +0000 Subject: [PATCH 05/45] Allow scrollable widget to bind scroll position to a tiddler (#7649) * Initial Commit * Update version number --- core/modules/widgets/scrollable.js | 40 +++++++++++++++++++ .../system/temp-my-scroll-position.tid | 3 ++ .../tiddlers/widgets/ScrollableWidget.tid | 29 +++++++++++++- 3 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 editions/prerelease/tiddlers/system/temp-my-scroll-position.tid diff --git a/core/modules/widgets/scrollable.js b/core/modules/widgets/scrollable.js index 15b61e0c8..c95820ef3 100644 --- a/core/modules/widgets/scrollable.js +++ b/core/modules/widgets/scrollable.js @@ -171,6 +171,42 @@ ScrollableWidget.prototype.render = function(parent,nextSibling) { parent.insertBefore(this.outerDomNode,nextSibling); this.renderChildren(this.innerDomNode,null); this.domNodes.push(this.outerDomNode); + // If the scroll position is bound to a tiddler + if(this.scrollableBind) { + // After a delay for rendering, scroll to the bound position + setTimeout(this.updateScrollPositionFromBoundTiddler.bind(this),50); + // Save scroll position on DOM scroll event + this.outerDomNode.addEventListener("scroll",function(event) { + var existingTiddler = self.wiki.getTiddler(self.scrollableBind), + newTiddlerFields = { + title: self.scrollableBind, + "scroll-left": self.outerDomNode.scrollLeft.toString(), + "scroll-top": self.outerDomNode.scrollTop.toString() + }; + if(!existingTiddler || (existingTiddler.fields["scroll-left"] !== newTiddlerFields["scroll-left"] || existingTiddler.fields["scroll-top"] !== newTiddlerFields["scroll-top"])) { + self.wiki.addTiddler(new $tw.Tiddler(existingTiddler,newTiddlerFields)); + } + }); + } +}; + +ScrollableWidget.prototype.updateScrollPositionFromBoundTiddler = function() { + var tiddler = this.wiki.getTiddler(this.scrollableBind); + if(tiddler) { + var scrollLeftTo = this.outerDomNode.scrollLeft; + if(parseFloat(tiddler.fields["scroll-left"]).toString() === tiddler.fields["scroll-left"]) { + scrollLeftTo = parseFloat(tiddler.fields["scroll-left"]); + } + var scrollTopTo = this.outerDomNode.scrollTop; + if(parseFloat(tiddler.fields["scroll-top"]).toString() === tiddler.fields["scroll-top"]) { + scrollTopTo = parseFloat(tiddler.fields["scroll-top"]); + } + this.outerDomNode.scrollTo({ + top: scrollTopTo, + left: scrollLeftTo, + behavior: "instant" + }) + } }; /* @@ -178,6 +214,7 @@ Compute the internal state of the widget */ ScrollableWidget.prototype.execute = function() { // Get attributes + this.scrollableBind = this.getAttribute("bind"); this.fallthrough = this.getAttribute("fallthrough","yes"); this["class"] = this.getAttribute("class"); // Make child widgets @@ -193,6 +230,9 @@ ScrollableWidget.prototype.refresh = function(changedTiddlers) { this.refreshSelf(); return true; } + if(changedAttributes.bind || changedTiddlers[this.getAttribute("bind")]) { + this.updateScrollPositionFromBoundTiddler(); + } return this.refreshChildren(changedTiddlers); }; diff --git a/editions/prerelease/tiddlers/system/temp-my-scroll-position.tid b/editions/prerelease/tiddlers/system/temp-my-scroll-position.tid new file mode 100644 index 000000000..c4a164070 --- /dev/null +++ b/editions/prerelease/tiddlers/system/temp-my-scroll-position.tid @@ -0,0 +1,3 @@ +title: $:/my-scroll-position +scroll-left: 0 +scroll-top: 100 diff --git a/editions/tw5.com/tiddlers/widgets/ScrollableWidget.tid b/editions/tw5.com/tiddlers/widgets/ScrollableWidget.tid index 6fda3a974..d31eb6e31 100644 --- a/editions/tw5.com/tiddlers/widgets/ScrollableWidget.tid +++ b/editions/tw5.com/tiddlers/widgets/ScrollableWidget.tid @@ -1,6 +1,6 @@ caption: scrollable created: 20140324223413403 -modified: 20220620115347910 +modified: 20230731100903977 tags: Widgets title: ScrollableWidget type: text/vnd.tiddlywiki @@ -16,12 +16,15 @@ The content of the `<$scrollable>` widget is displayed within a pair of wrapper |!Attribute |!Description | |class |The CSS class(es) to be applied to the outer DIV | |fallthrough |See below | +|bind |<<.from-version "5.3.2">> Optional title of tiddler to which the scroll position should be bound | + +Binding the scroll position to a tiddler automatically copies the scroll coordinates into the `scroll-left` and `scroll-top` fields as scrolling occurs. Conversely, setting those field values will automatically cause the scrollable to scroll if it can. <$macrocall $name=".note" _="""If a scrollable widget can't handle the `tm-scroll` message because the inner DIV fits within the outer DIV, then by default the message falls through to the parent widget. Setting the ''fallthrough'' attribute to `no` prevents this behaviour."""/> ! Examples -This example requires the following CSS definitions from [[$:/_tw5.com-styles]]: +These examples require the following CSS definitions from [[$:/_tw5.com-styles]]: ``` .tc-scrollable-demo { @@ -33,6 +36,8 @@ This example requires the following CSS definitions from [[$:/_tw5.com-styles]]: } ``` +!! Simple Usage + This wiki text shows how to display a list within the scrollable widget: < @@ -46,3 +51,23 @@ This wiki text shows how to display a list within the scrollable widget: ">> +!! Binding scroll position to a tiddler + +[[Current scroll position|$:/my-scroll-position]]: {{$:/my-scroll-position!!scroll-left}}, {{$:/my-scroll-position!!scroll-top}} + +<$button> +<$action-setfield $tiddler="$:/my-scroll-position" scroll-left="100" scroll-top="100"/> +Set current scroll position to 100,100 + + +< +<$list filter='[tag[Reference]]'> + +<$view field='title'/>: <$list filter='[all[current]links[]sort[title]]' storyview='pop'> +<$link><$view field='title'/> + + + + +">> + From 145a8d699253fcfadfc0b46658fdd13680d9a0b0 Mon Sep 17 00:00:00 2001 From: Scott Sauyet Date: Tue, 21 Nov 2023 06:24:17 -0500 Subject: [PATCH 06/45] Simplify Permalink/Permaview URLs (#7729) * Simplify Permalink/Permaview URLs * Fix lint warnings by removing arrow functions * Remove commented sample code * Remove post-ES5 code * Add many more allowable non-percent-encodedcharacters * Fix more ES6+ stuff, add end-of-sentence padding character. * Fix to match standards * Move the new code from boot to util * Change from custom map/filter to $tw.utils.each * Make `each` blocks multi-line * Move the permalink handling to its own file * Remove auto-navigation * Revert "Remove auto-navigation" This reverts commit ca1e5cf387922d8dd2883cb37f8d85e187158c90. --- core/modules/startup/story.js | 14 +- core/modules/utils/twuri-encoding.js | 128 ++++++++++++++++++ .../tw5.com/tiddlers/concepts/PermaLinks.tid | 16 +++ 3 files changed, 151 insertions(+), 7 deletions(-) create mode 100644 core/modules/utils/twuri-encoding.js diff --git a/core/modules/startup/story.js b/core/modules/startup/story.js index 734f6ae76..da2df6542 100644 --- a/core/modules/startup/story.js +++ b/core/modules/startup/story.js @@ -122,10 +122,10 @@ function openStartupTiddlers(options) { var hash = $tw.locationHash.substr(1), split = hash.indexOf(":"); if(split === -1) { - target = $tw.utils.decodeURIComponentSafe(hash.trim()); + target = $tw.utils.decodeTWURITarget(hash.trim()); } else { - target = $tw.utils.decodeURIComponentSafe(hash.substr(0,split).trim()); - storyFilter = $tw.utils.decodeURIComponentSafe(hash.substr(split + 1).trim()); + target = $tw.utils.decodeTWURITarget(hash.substr(0,split).trim()); + storyFilter = $tw.utils.decodeTWURIList(hash.substr(split + 1).trim()); } } // If the story wasn't specified use the current tiddlers or a blank story @@ -198,19 +198,19 @@ function updateLocationHash(options) { // Assemble the location hash switch(options.updateAddressBar) { case "permalink": - $tw.locationHash = "#" + encodeURIComponent(targetTiddler); + $tw.locationHash = "#" + $tw.utils.encodeTiddlerTitle(targetTiddler); break; case "permaview": - $tw.locationHash = "#" + encodeURIComponent(targetTiddler) + ":" + encodeURIComponent($tw.utils.stringifyList(storyList)); + $tw.locationHash = "#" + $tw.utils.encodeTiddlerTitle(targetTiddler) + ":" + $tw.utils.encodeFilterPath($tw.utils.stringifyList(storyList)); break; } // Copy URL to the clipboard switch(options.copyToClipboard) { case "permalink": - $tw.utils.copyToClipboard($tw.utils.getLocationPath() + "#" + encodeURIComponent(targetTiddler)); + $tw.utils.copyToClipboard($tw.utils.getLocationPath() + "#" + $tw.utils.encodeTiddlerTitle(targetTiddler)); break; case "permaview": - $tw.utils.copyToClipboard($tw.utils.getLocationPath() + "#" + encodeURIComponent(targetTiddler) + ":" + encodeURIComponent($tw.utils.stringifyList(storyList))); + $tw.utils.copyToClipboard($tw.utils.getLocationPath() + "#" + $tw.utils.encodeTiddlerTitle(targetTiddler) + ":" + $tw.utils.encodeFilterPath($tw.utils.stringifyList(storyList))); break; } // Only change the location hash if we must, thus avoiding unnecessary onhashchange events diff --git a/core/modules/utils/twuri-encoding.js b/core/modules/utils/twuri-encoding.js new file mode 100644 index 000000000..f4a5bd947 --- /dev/null +++ b/core/modules/utils/twuri-encoding.js @@ -0,0 +1,128 @@ +/*\ +title: $:/core/modules/utils/twuri-encoding.js +type: application/javascript +module-type: utils + +Utility functions related to permalink/permaview encoding/decoding. + +\*/ +(function(){ + + // The character that will substitute for a space in the URL + var SPACE_SUBSTITUTE = "_"; + + // The character added to the end to avoid ending with `.`, `?`, `!` or the like + var TRAILER = "_"; + + // The character that will separate out the list elements in the URL + var CONJUNCTION = ";"; + + // Those of the allowed url characters claimed by TW + var CLAIMED = [SPACE_SUBSTITUTE, ":", CONJUNCTION]; + + // Non-alphanumeric characters allowed in a URL fragment + // More information at https://www.rfc-editor.org/rfc/rfc3986#appendix-A + var VALID_IN_URL_FRAGMENT = "-._~!$&'()*+,;=:@/?".split(""); + + // The subset of the pchars we will not percent-encode in permalinks/permaviews + var SUBSTITUTES = [] + $tw.utils.each(VALID_IN_URL_FRAGMENT, function(c) { + if (CLAIMED.indexOf(c) === -1) { + SUBSTITUTES.push(c) + } + }); + + // A regex to match the percent-encoded characters we will want to replace. + // Something similar to the following, depending on SPACE and CONJUNCTION + // /(%2D|%2E|%7E|%21|%24|%26|%27|%28|%29|%2A|%2B|%3B|%3D|%40|%2F|%3F)/g + + var CHAR_MATCH_STR = [] + $tw.utils.each(SUBSTITUTES, function(c) { + CHAR_MATCH_STR.push("%" + c.charCodeAt(0).toString(16).toUpperCase()) + }) + var CHAR_MATCH = new RegExp("(" + CHAR_MATCH_STR.join("|") + ")", "g"); + + // A regex to match the SPACE_SUBSTITUTE character + var SPACE_MATCH = new RegExp("(\\" + SPACE_SUBSTITUTE + ")", "g"); + + // A regex to match URLs ending with sentence-ending punctuation + var SENTENCE_ENDING = new RegExp("(\\.|\\!|\\?|\\" + TRAILER + ")$", "g"); + + // A regex to match URLs ending with sentence-ending punctuation plus the TRAILER + var SENTENCE_TRAILING = new RegExp("(\\.|\\!|\\?|\\" + TRAILER + ")\\" + TRAILER + "$", "g"); + + // An object mapping the percent encodings back to their source characters + var PCT_CHAR_MAP = SUBSTITUTES.reduce(function (a, c) { + a["%" + c.charCodeAt(0).toString(16).toUpperCase()] = c + return a + }, {}); + + // Convert a URI List Component encoded string (with the `SPACE_SUBSTITUTE` + // value as an allowed replacement for the space character) to a string + exports.decodeTWURIList = function(s) { + var parts = s.replace(SENTENCE_TRAILING, "$1").split(CONJUNCTION); + var withSpaces = [] + $tw.utils.each(parts, function(s) { + withSpaces.push(s.replace(SPACE_MATCH, " ")) + }); + var withBrackets = [] + $tw.utils.each(withSpaces, function(s) { + withBrackets .push(s.indexOf(" ") >= 0 ? "[[" + s + "]]" : s) + }); + return $tw.utils.decodeURIComponentSafe(withBrackets.join(" ")); + }; + + // Convert a URI Target Component encoded string (with the `SPACE_SUBSTITUTE` + // value as an allowed replacement for the space character) to a string + exports.decodeTWURITarget = function(s) { + return $tw.utils.decodeURIComponentSafe( + s.replace(SENTENCE_TRAILING, "$1").replace(SPACE_MATCH, " ") + ) + }; + + // Convert a URIComponent encoded title string (with the `SPACE_SUBSTITUTE` + // value as an allowed replacement for the space character) to a string + exports.encodeTiddlerTitle = function(s) { + var extended = s.replace(SENTENCE_ENDING, "$1" + TRAILER) + var encoded = encodeURIComponent(extended); + var substituted = encoded.replace(/\%20/g, SPACE_SUBSTITUTE); + return substituted.replace(CHAR_MATCH, function(_, c) { + return PCT_CHAR_MAP[c]; + }); + }; + + // Convert a URIComponent encoded filter string (with the `SPACE_SUBSTITUTE` + // value as an allowed replacement for the space character) to a string + exports.encodeFilterPath = function(s) { + var parts = s.replace(SENTENCE_ENDING, "$1" + TRAILER) + .replace(/\[\[(.+?)\]\]/g, function (_, t) {return t.replace(/ /g, SPACE_SUBSTITUTE )}) + .split(" "); + var nonEmptyParts = [] + $tw.utils.each(parts, function(p) { + if (p) { + nonEmptyParts.push (p) + } + }); + var trimmed = []; + $tw.utils.each(nonEmptyParts, function(s) { + trimmed.push(s.trim()) + }); + var encoded = []; + $tw.utils.each(trimmed, function(s) { + encoded.push(encodeURIComponent(s)) + }); + var substituted = []; + $tw.utils.each(encoded, function(s) { + substituted.push(s.replace(/\%20/g, SPACE_SUBSTITUTE)) + }); + var replaced = [] + $tw.utils.each(substituted, function(s) { + replaced.push(s.replace(CHAR_MATCH, function(_, c) { + return PCT_CHAR_MAP[c]; + })) + }); + return replaced.join(CONJUNCTION); + }; + +})(); + \ No newline at end of file diff --git a/editions/tw5.com/tiddlers/concepts/PermaLinks.tid b/editions/tw5.com/tiddlers/concepts/PermaLinks.tid index 40c7a1925..1b15460fa 100644 --- a/editions/tw5.com/tiddlers/concepts/PermaLinks.tid +++ b/editions/tw5.com/tiddlers/concepts/PermaLinks.tid @@ -40,6 +40,22 @@ There are technical restrictions on the legal characters in an URL fragment. To Both the target tiddler title and the story filter should be URL encoded (but not the separating colon). TiddlyWiki generates properly encoded URLs which can look quite ugly. However, in practice browsers will usually perfectly happily process arbitrary characters in URL fragments. Thus when creating permalinks manually you can choose to ignore URL encoding. +!! Simpler URLS + +<<.from-version "5.3.2">> The URLs generated are simplified from the hard-to-read percent encoding when feasible. Spaces are replaced with underscores (`_`), many punctuation characters are allowed to remain unencoded, and permaview filters receive a simpler encoding. For example the tiddler "Hard Linebreaks with CSS - Example", which percent-encoded would look like + +> @@font-family:monospace;#Hard%20Linebreaks%20with%20CSS%20-%20Example@@ + +instead looks like + +> @@font-family:monospace;#Hard_Linebreaks_with_CSS_-_Example@@ + +Existing story filter URLs like + +> @@font-family:monospace;#:[tag[Features]]%20+[limit[5]]@@ + +will continue to work. + ! Permalink Behaviour Two important aspects of TiddlyWiki's behaviour with permalinks can be controlled via options in the [[control panel|$:/ControlPanel]] <<.icon $:/core/images/options-button>> ''Settings'' tab: From a4850ba3d9864317979fee308888b01584dd092b Mon Sep 17 00:00:00 2001 From: Mario Pietsch Date: Tue, 21 Nov 2023 12:30:05 +0100 Subject: [PATCH 07/45] make all list-* macros readable for easier future improvements (#7551) * make all list-* widgets readable for easier future improvements * remove whitespace on closing braces --- core/wiki/macros/list.tid | 173 +++++++++++++++++++++----------------- 1 file changed, 95 insertions(+), 78 deletions(-) diff --git a/core/wiki/macros/list.tid b/core/wiki/macros/list.tid index 5464ecad1..c9dd2ad71 100644 --- a/core/wiki/macros/list.tid +++ b/core/wiki/macros/list.tid @@ -4,17 +4,17 @@ tags: $:/tags/Macro \define list-links(filter,type:"ul",subtype:"li",class:"",emptyMessage,field:"caption") \whitespace trim <$genesis $type=<<__type__>> class=<<__class__>>> -<$list filter=<<__filter__>> emptyMessage=<<__emptyMessage__>>> -<$genesis $type=<<__subtype__>>> -<$link to={{!!title}}> -<$let tv-wikilinks="no"> -<$transclude field=<<__field__>>> -<$view field="title"/> - - - - - + <$list filter=<<__filter__>> emptyMessage=<<__emptyMessage__>>> + <$genesis $type=<<__subtype__>>> + <$link to={{!!title}}> + <$let tv-wikilinks="no"> + <$transclude field=<<__field__>>> + <$view field="title"/> + + + + + \end @@ -25,34 +25,42 @@ tags: $:/tags/Macro \define list-links-draggable(tiddler,field:"list",emptyMessage,type:"ul",subtype:"li",class:"",itemTemplate) \whitespace trim -<$vars targetTiddler="""$tiddler$""" targetField="""$field$"""> -<$genesis $type=<<__type__>> class="$class$"> -<$list filter="[list[$tiddler$!!$field$]]" emptyMessage=<<__emptyMessage__>>> -<$droppable actions=<> tag="""$subtype$""" enable=<>> -
-
-<$transclude tiddler="""$itemTemplate$"""> -<$link to={{!!title}}> -<$let tv-wikilinks="no"> -<$transclude field="caption"> -<$view field="title"/> - - - - -
- - -<$tiddler tiddler=""> -<$droppable actions=<> tag="div" enable=<>> -
-{{$:/core/images/blank}} -
-
- - - - + <$vars targetTiddler="""$tiddler$""" targetField="""$field$"""> + <$genesis $type=<<__type__>> class="$class$"> + <$list filter="[list[$tiddler$!!$field$]]" emptyMessage=<<__emptyMessage__>>> + <$droppable + actions=<> + tag="""$subtype$""" + enable=<> + > +
+
+ <$transclude tiddler="""$itemTemplate$"""> + <$link to={{!!title}}> + <$let tv-wikilinks="no"> + <$transclude field="caption"> + <$view field="title"/> + + + + +
+ + + <$tiddler tiddler=""> + <$droppable + actions=<> + tag="div" + enable=<> + > +
+ {{$:/core/images/blank}} +
+
+ + + + \end @@ -60,50 +68,59 @@ tags: $:/tags/Macro \whitespace trim <$set name="order" filter="[<__tag__>tagging[]]"> - -<$list filter="[<__tag__>tagging[]]"> -<$action-deletefield $field="list-before"/> -<$action-deletefield $field="list-after"/> - - -<$action-listops $tiddler=<<__tag__>> $field="list" $filter="+[enlist] +[insertbefore,]"/> - - - - -<$list filter="[!contains:tags<__tag__>]"> -<$fieldmangler tiddler=<>> -<$action-sendmessage $message="tm-add-tag" $param=<<__tag__>>/> - - + + <$list filter="[<__tag__>tagging[]]"> + <$action-deletefield $field="list-before"/> + <$action-deletefield $field="list-after"/> + + + <$action-listops $tiddler=<<__tag__>> $field="list" $filter="+[enlist] +[insertbefore,]"/> + + + + + <$list filter="[!contains:tags<__tag__>]"> + <$fieldmangler tiddler=<>> + <$action-sendmessage $message="tm-add-tag" $param=<<__tag__>>/> + + \end \define list-tagged-draggable(tag,subFilter,emptyMessage,itemTemplate,elementTag:"div",storyview:"") \whitespace trim -<$set name="tag" value=<<__tag__>>> -<$list filter="[<__tag__>tagging[]$subFilter$]" emptyMessage=<<__emptyMessage__>> storyview=<<__storyview__>>> -<$genesis $type=<<__elementTag__>> class="tc-menu-list-item"> -<$droppable actions="""<$macrocall $name="list-tagged-draggable-drop-actions" tag=<<__tag__>>/>""" enable=<>> -<$genesis $type=<<__elementTag__>> class="tc-droppable-placeholder"/> -<$genesis $type=<<__elementTag__>>> -<$transclude tiddler="""$itemTemplate$"""> -<$link to={{!!title}}> -<$view field="title"/> - - - - - - -<$tiddler tiddler=""> -<$droppable actions="""<$macrocall $name="list-tagged-draggable-drop-actions" tag=<<__tag__>>/>""" enable=<>> -<$genesis $type=<<__elementTag__>> class="tc-droppable-placeholder"/> -<$genesis $type=<<__elementTag__>> style="height:0.5em;"> - - - - + <$set name="tag" value=<<__tag__>>> + <$list + filter="[<__tag__>tagging[]$subFilter$]" + emptyMessage=<<__emptyMessage__>> + storyview=<<__storyview__>> + > + <$genesis $type=<<__elementTag__>> class="tc-menu-list-item"> + <$droppable + actions="""<$macrocall $name="list-tagged-draggable-drop-actions" tag=<<__tag__>>/>""" + enable=<> + > + <$genesis $type=<<__elementTag__>> class="tc-droppable-placeholder"/> + <$genesis $type=<<__elementTag__>>> + <$transclude tiddler="""$itemTemplate$"""> + <$link to={{!!title}}> + <$view field="title"/> + + + + + + + <$tiddler tiddler=""> + <$droppable + actions="""<$macrocall $name="list-tagged-draggable-drop-actions" tag=<<__tag__>>/>""" + enable=<> + > + <$genesis $type=<<__elementTag__>> class="tc-droppable-placeholder"/> + <$genesis $type=<<__elementTag__>> style="height:0.5em;"/> + + + \end From 37c625384af79deba0934e3071bcfc5ebe9f4349 Mon Sep 17 00:00:00 2001 From: Mario Pietsch Date: Tue, 21 Nov 2023 12:42:17 +0100 Subject: [PATCH 08/45] add archive HTML wikis, add index.html + css (#7776) * add archive HTML wikis, add index.html + css * only build archive for release version * tested and add more docs * fix indent * fix spacing * add $TW5_BUILD_OUTPUT_ARCHIVE env variable for testing * use $TW5_BUILD_OUTPUT_ARCHIVE to check if archive should be built * use TW5_BUILD_ARCHIVE as requested --- .github/workflows/ci.yml | 1 + bin/build-site.sh | 20 +++++++++++++++++++- editions/tw5.com/tiddlywiki.info | 7 ++++++- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a146d15a8..737d523ef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,6 +60,7 @@ jobs: TW5_BUILD_TIDDLYWIKI: "./node_modules/tiddlywiki/tiddlywiki.js" TW5_BUILD_MAIN_EDITION: "./editions/tw5.com" TW5_BUILD_OUTPUT: "./output" + TW5_BUILD_ARCHIVE: "./output" steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 diff --git a/bin/build-site.sh b/bin/build-site.sh index aa8a29f63..5b36de4e1 100755 --- a/bin/build-site.sh +++ b/bin/build-site.sh @@ -84,10 +84,27 @@ echo -e -n "title: $:/build\ncommit: $TW5_BUILD_COMMIT\n\n$TW5_BUILD_DETAILS\n" ###################################################### # -# Core distribution +# Core distributions # ###################################################### +# Conditionally build archive if $TW5_BUILD_ARCHIVE variable is set, otherwise do nothing +# +# /archive/Empty-TiddlyWiki-.html Empty archived version +# /archive/TiddlyWiki-.html Full archived version + +if [ -n "$TW5_BUILD_ARCHIVE" ]; then + +node $TW5_BUILD_TIDDLYWIKI \ + $TW5_BUILD_MAIN_EDITION \ + --verbose \ + --version \ + --load $TW5_BUILD_OUTPUT/build.tid \ + --output $TW5_BUILD_ARCHIVE \ + --build archive \ + || exit 1 +fi + # /index.html Main site # /favicon.ico Favicon for main site # /static.html Static rendering of default tiddlers @@ -95,6 +112,7 @@ echo -e -n "title: $:/build\ncommit: $TW5_BUILD_COMMIT\n\n$TW5_BUILD_DETAILS\n" # /static/* Static single tiddlers # /static/static.css Static stylesheet # /static/favicon.ico Favicon for static pages + node $TW5_BUILD_TIDDLYWIKI \ $TW5_BUILD_MAIN_EDITION \ --verbose \ diff --git a/editions/tw5.com/tiddlywiki.info b/editions/tw5.com/tiddlywiki.info index 2065260bc..5ce9a2f1b 100644 --- a/editions/tw5.com/tiddlywiki.info +++ b/editions/tw5.com/tiddlywiki.info @@ -54,7 +54,12 @@ "--render","$:/core/templates/static.template.css","static/static.css","text/plain"], "external-js": [ "--render","$:/core/save/offline-external-js","[[external-]addsuffixaddsuffix[.html]]","text/plain", - "--render","$:/core/templates/tiddlywiki5.js","[[tiddlywikicore-]addsuffixaddsuffix[.js]]","text/plain"] + "--render","$:/core/templates/tiddlywiki5.js","[[tiddlywikicore-]addsuffixaddsuffix[.js]]","text/plain"], + "archive":[ + "--render","$:/core/save/all","[[archive/TiddlyWiki-]addsuffixaddsuffix[.html]]","text/plain", + "--render","$:/editions/tw5.com/download-empty","[[archive/Empty-TiddlyWiki-]addsuffixaddsuffix[.html]]","text/plain", + "--render","[[TiddlyWiki Archive]]","archive/index.html","text/plain","$:/core/templates/static.tiddler.html", + "--render","$:/core/templates/static.template.css","archive/static.css","text/plain"] }, "config": { "retain-original-tiddler-path": true From 0cd3c9a8ac42c0c9ac7d4fe80379a96583c1fd33 Mon Sep 17 00:00:00 2001 From: Eric Haberstroh Date: Tue, 21 Nov 2023 12:44:39 +0100 Subject: [PATCH 09/45] Add usemap attribute to image widget (#7634) * Add usemap attribute to image macro Allow for a usemap attribute on the $image macro call which is passed through to the resulting img tag. This makes the use of HTML image maps [1] possible. [1]: * Document new usemap attribute in ImageWidget * Update version docs --------- Co-authored-by: Jeremy Ruston --- core/modules/widgets/image.js | 6 +++++- editions/tw5.com/tiddlers/widgets/ImageWidget.tid | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/core/modules/widgets/image.js b/core/modules/widgets/image.js index 533b657cc..52496fd74 100644 --- a/core/modules/widgets/image.js +++ b/core/modules/widgets/image.js @@ -100,6 +100,9 @@ ImageWidget.prototype.render = function(parent,nextSibling) { if(this.imageClass) { domNode.setAttribute("class",this.imageClass); } + if(this.imageUsemap) { + domNode.setAttribute("usemap",this.imageUsemap); + } if(this.imageWidth) { domNode.setAttribute("width",this.imageWidth); } @@ -139,6 +142,7 @@ ImageWidget.prototype.execute = function() { this.imageWidth = this.getAttribute("width"); this.imageHeight = this.getAttribute("height"); this.imageClass = this.getAttribute("class"); + this.imageUsemap = this.getAttribute("usemap"); this.imageTooltip = this.getAttribute("tooltip"); this.imageAlt = this.getAttribute("alt"); this.lazyLoading = this.getAttribute("loading"); @@ -149,7 +153,7 @@ Selectively refreshes the widget if needed. Returns true if the widget or any of */ ImageWidget.prototype.refresh = function(changedTiddlers) { var changedAttributes = this.computeAttributes(); - if(changedAttributes.source || changedAttributes.width || changedAttributes.height || changedAttributes["class"] || changedAttributes.tooltip || changedTiddlers[this.imageSource]) { + if(changedAttributes.source || changedAttributes.width || changedAttributes.height || changedAttributes["class"] || changedAttributes.usemap || changedAttributes.tooltip || changedTiddlers[this.imageSource]) { this.refreshSelf(); return true; } else { diff --git a/editions/tw5.com/tiddlers/widgets/ImageWidget.tid b/editions/tw5.com/tiddlers/widgets/ImageWidget.tid index c888c3a31..0f4bd9012 100644 --- a/editions/tw5.com/tiddlers/widgets/ImageWidget.tid +++ b/editions/tw5.com/tiddlers/widgets/ImageWidget.tid @@ -1,6 +1,6 @@ caption: image created: 20140416160234142 -modified: 20220721102303815 +modified: 20231121114351165 tags: Widgets title: ImageWidget type: text/vnd.tiddlywiki @@ -21,6 +21,7 @@ Any content of the `<$image>` widget is ignored. |alt |The alternative text to be associated with the image | |class |CSS classes to be assigned to the `` element | |loading|<<.from-version "5.2.3">>Optional. Set to `lazy` to enable lazy loading of images loaded from an external URI | +|usemap|<<.from-version "5.3.2">>Optional usemap attribute to be assigned to the `` element for use with HTML image maps | The width and the height can be specified as pixel values (eg "23" or "23px") or percentages (eg "23%"). They are both optional; if not provided the browser will use CSS rules to size the image. From 06b1cc4bca54f8b9a2d205d7cf194fb0caa79a81 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Tue, 21 Nov 2023 11:49:34 +0000 Subject: [PATCH 10/45] Scrollable widget: Fix crash in CI --- core/modules/widgets/scrollable.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/modules/widgets/scrollable.js b/core/modules/widgets/scrollable.js index c95820ef3..58597461b 100644 --- a/core/modules/widgets/scrollable.js +++ b/core/modules/widgets/scrollable.js @@ -191,6 +191,10 @@ ScrollableWidget.prototype.render = function(parent,nextSibling) { }; ScrollableWidget.prototype.updateScrollPositionFromBoundTiddler = function() { + // Bail if we're running on the fakedom + if(!this.outerDomNode.scrollTo) { + return; + } var tiddler = this.wiki.getTiddler(this.scrollableBind); if(tiddler) { var scrollLeftTo = this.outerDomNode.scrollLeft; From de6b866f22a8262c8c531e8fbace999563148aa2 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Tue, 21 Nov 2023 11:54:37 +0000 Subject: [PATCH 11/45] Fix detection of DOM properties Alternate fix for #7714 --- core/modules/utils/dom/dom.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/modules/utils/dom/dom.js b/core/modules/utils/dom/dom.js index 9c06fc8b0..338d96280 100644 --- a/core/modules/utils/dom/dom.js +++ b/core/modules/utils/dom/dom.js @@ -313,7 +313,7 @@ exports.collectDOMVariables = function(selectedNode,domNode,event) { variables["dom-" + attribute.name] = attribute.value.toString(); }); - if(selectedNode.offsetLeft) { + if("offsetLeft" in selectedNode) { // Add variables with a (relative and absolute) popup coordinate string for the selected node var nodeRect = { left: selectedNode.offsetLeft, @@ -338,12 +338,12 @@ exports.collectDOMVariables = function(selectedNode,domNode,event) { } } - if(domNode && domNode.offsetWidth) { + if(domNode && ("offsetWidth" in domNode)) { variables["tv-widgetnode-width"] = domNode.offsetWidth.toString(); variables["tv-widgetnode-height"] = domNode.offsetHeight.toString(); } - if(event && event.clientX && event.clientY) { + if(event && ("clientX" in event) && ("clientY" in event)) { if(selectedNode) { // Add variables for event X and Y position relative to selected node selectedNodeRect = selectedNode.getBoundingClientRect(); From 4c2979286b6836f4331b31649e3d17b7b074ca6f Mon Sep 17 00:00:00 2001 From: yaisog Date: Tue, 21 Nov 2023 12:58:12 +0100 Subject: [PATCH 12/45] Change separators to match doc (#7303) --- core/modules/wiki.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/modules/wiki.js b/core/modules/wiki.js index 3eae3902d..430c46466 100755 --- a/core/modules/wiki.js +++ b/core/modules/wiki.js @@ -1287,7 +1287,7 @@ exports.search = function(text,options) { console.log("Regexp error parsing /(" + text + ")/" + flags + ": ",e); } } else if(options.some) { - terms = text.trim().split(/ +/); + terms = text.trim().split(/[^\S\xA0]+/); if(terms.length === 1 && terms[0] === "") { searchTermsRegExps = null; } else { @@ -1298,7 +1298,7 @@ exports.search = function(text,options) { searchTermsRegExps.push(new RegExp("(" + regExpStr + ")",flags)); } } else { // default: words - terms = text.split(/ +/); + terms = text.split(/[^\S\xA0]+/); if(terms.length === 1 && terms[0] === "") { searchTermsRegExps = null; } else { From 62bb8affa48aec958fc73104b060f6a90efc8e1b Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Wed, 22 Nov 2023 20:05:40 +0000 Subject: [PATCH 13/45] Add data attribute support to button and other widgets (#7769) * Add data attribute support to button widget * Fix typo * Refactor ready for making mechanism more generic * Apply more generic implementation to multiplate widgets * Refactor to use existing widget.assignAttributes() method * Fix typo * Clarify docs * Update docs * Update select widget to support style.* attributes * Remove obsolete comment * Fixes refresh issues for checkbox and links widgets for data attributes (#7846) * fix: refresh issues with checkbox and links widgets * fix: indenting * Feat: add support for data attributes to Draggable and Droppable widgets (#7845) * Docs clarification * docs: add style and data attributes to Draggable and Droppable widget docs (#7850) * Refactors Select widget to directly create DOM node (#7848) * fix: refactored SelectWidget to directly create DOM nodes * fix: refactored SelectWidget to directly create DOM nodes * fix: improve refresh handling for select widget * Fixes issues in the PR "Button widget data attributes" (#7852) * fix: fixed ordered attributes handling and improved tests to catch event attributes * fix: clean up code from testing * fix: more tests and refactoring * fix: use lowercase when checking for event attribute prefix * fix: use lowercase when checking for event attribute prefix * fix: changed comment wording * fix: minor refactoring * refactor: for brevity --------- Co-authored-by: Saq Imtiaz --- core/modules/utils/fakedom.js | 6 +- core/modules/widgets/browse.js | 10 ++++ core/modules/widgets/button.js | 18 +++++- core/modules/widgets/checkbox.js | 11 ++++ core/modules/widgets/draggable.js | 21 +++++-- core/modules/widgets/droppable.js | 16 ++++- core/modules/widgets/link.js | 14 ++++- core/modules/widgets/radio.js | 4 ++ core/modules/widgets/range.js | 4 ++ core/modules/widgets/select.js | 60 +++++++++++-------- core/modules/widgets/widget.js | 54 +++++++++++------ core/wiki/macros/tabs.tid | 10 +++- .../ButtonWidget-DataAttributes.tid | 27 +++++++++ .../CheckboxWidget-DataAttributes.tid | 22 +++++++ .../DraggableWidget-DataAttributes.tid | 27 +++++++++ .../DroppableWidget-DataAttributes.tid | 27 +++++++++ .../LinkWidget-DataAttributes.tid | 27 +++++++++ .../DataAttributes/OrderedStyleAttributes.tid | 15 +++++ .../SelectWidget-DataAttributes.tid | 27 +++++++++ .../widgets/ElementWidgetEventAttributes.tid | 15 +++++ .../widgets/ElementWidgetStyleAttributes.tid | 15 +++++ .../tw5.com/tiddlers/widgets/BrowseWidget.tid | 4 +- .../tw5.com/tiddlers/widgets/ButtonWidget.tid | 4 +- .../tiddlers/widgets/CheckboxWidget.tid | 4 +- .../tiddlers/widgets/DraggableWidget.tid | 5 +- .../tiddlers/widgets/DroppableWidget.tid | 4 +- .../tw5.com/tiddlers/widgets/LinkWidget.tid | 4 +- .../tw5.com/tiddlers/widgets/RadioWidget.tid | 4 +- .../tw5.com/tiddlers/widgets/RangeWidget.tid | 4 +- .../tw5.com/tiddlers/widgets/SelectWidget.tid | 5 +- 30 files changed, 399 insertions(+), 69 deletions(-) create mode 100644 editions/test/tiddlers/tests/data/widgets/DataAttributes/ButtonWidget-DataAttributes.tid create mode 100644 editions/test/tiddlers/tests/data/widgets/DataAttributes/CheckboxWidget-DataAttributes.tid create mode 100644 editions/test/tiddlers/tests/data/widgets/DataAttributes/DraggableWidget-DataAttributes.tid create mode 100644 editions/test/tiddlers/tests/data/widgets/DataAttributes/DroppableWidget-DataAttributes.tid create mode 100644 editions/test/tiddlers/tests/data/widgets/DataAttributes/LinkWidget-DataAttributes.tid create mode 100644 editions/test/tiddlers/tests/data/widgets/DataAttributes/OrderedStyleAttributes.tid create mode 100644 editions/test/tiddlers/tests/data/widgets/DataAttributes/SelectWidget-DataAttributes.tid create mode 100644 editions/test/tiddlers/tests/data/widgets/ElementWidgetEventAttributes.tid create mode 100644 editions/test/tiddlers/tests/data/widgets/ElementWidgetStyleAttributes.tid diff --git a/core/modules/utils/fakedom.js b/core/modules/utils/fakedom.js index d28161ac6..0c1f5fa54 100755 --- a/core/modules/utils/fakedom.js +++ b/core/modules/utils/fakedom.js @@ -104,7 +104,11 @@ TW_Element.prototype.setAttribute = function(name,value) { if(this.isRaw) { throw "Cannot setAttribute on a raw TW_Element"; } - this.attributes[name] = value + ""; + if(name === "style") { + this.style = value; + } else { + this.attributes[name] = value + ""; + } }; TW_Element.prototype.setAttributeNS = function(namespace,name,value) { diff --git a/core/modules/widgets/browse.js b/core/modules/widgets/browse.js index de3c91fb8..8130825b0 100644 --- a/core/modules/widgets/browse.js +++ b/core/modules/widgets/browse.js @@ -70,6 +70,11 @@ BrowseWidget.prototype.render = function(parent,nextSibling) { } return false; },false); + // Assign data- attributes + this.assignAttributes(domNode,{ + sourcePrefix: "data-", + destPrefix: "data-" + }); // Insert element parent.insertBefore(domNode,nextSibling); this.renderChildren(domNode,null); @@ -95,6 +100,11 @@ BrowseWidget.prototype.execute = function() { Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering */ BrowseWidget.prototype.refresh = function(changedTiddlers) { + var changedAttributes = this.computeAttributes(); + if($tw.utils.count(changedAttributes) > 0) { + this.refreshSelf(); + return true; + } return false; }; diff --git a/core/modules/widgets/button.js b/core/modules/widgets/button.js index a724d8448..958b6f6da 100644 --- a/core/modules/widgets/button.js +++ b/core/modules/widgets/button.js @@ -59,6 +59,11 @@ ButtonWidget.prototype.render = function(parent,nextSibling) { $tw.utils.pushTop(classes,"tc-popup-handle"); } domNode.className = classes.join(" "); + // Assign data- attributes + this.assignAttributes(domNode,{ + sourcePrefix: "data-", + destPrefix: "data-" + }); // Assign other attributes if(this.style) { domNode.setAttribute("style",this.style); @@ -250,7 +255,7 @@ ButtonWidget.prototype.updateDomNodeClasses = function() { //Add new classes from updated class attribute. $tw.utils.pushTop(domNodeClasses,newClasses); this.domNode.className = domNodeClasses.join(" "); -} +}; /* Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering @@ -260,8 +265,15 @@ ButtonWidget.prototype.refresh = function(changedTiddlers) { if(changedAttributes.actions || changedAttributes.to || changedAttributes.message || changedAttributes.param || changedAttributes.set || changedAttributes.setTo || changedAttributes.popup || changedAttributes.hover || changedAttributes.selectedClass || changedAttributes.style || changedAttributes.dragFilter || changedAttributes.dragTiddler || (this.set && changedTiddlers[this.set]) || (this.popup && changedTiddlers[this.popup]) || (this.popupTitle && changedTiddlers[this.popupTitle]) || changedAttributes.popupAbsCoords || changedAttributes.setTitle || changedAttributes.setField || changedAttributes.setIndex || changedAttributes.popupTitle || changedAttributes.disabled || changedAttributes["default"]) { this.refreshSelf(); return true; - } else if(changedAttributes["class"]) { - this.updateDomNodeClasses(); + } else { + if(changedAttributes["class"]) { + this.updateDomNodeClasses(); + } + this.assignAttributes(this.domNodes[0],{ + changedAttributes: changedAttributes, + sourcePrefix: "data-", + destPrefix: "data-" + }); } return this.refreshChildren(changedTiddlers); }; diff --git a/core/modules/widgets/checkbox.js b/core/modules/widgets/checkbox.js index fc987d815..e07513b0a 100644 --- a/core/modules/widgets/checkbox.js +++ b/core/modules/widgets/checkbox.js @@ -53,6 +53,11 @@ CheckboxWidget.prototype.render = function(parent,nextSibling) { this.labelDomNode.appendChild(this.inputDomNode); this.spanDomNode = this.document.createElement("span"); this.labelDomNode.appendChild(this.spanDomNode); + // Assign data- attributes + this.assignAttributes(this.inputDomNode,{ + sourcePrefix: "data-", + destPrefix: "data-" + }); // Add a click event handler $tw.utils.addEventListeners(this.inputDomNode,[ {name: "change", handlerObject: this, handlerMethod: "handleChangeEvent"} @@ -325,6 +330,11 @@ CheckboxWidget.prototype.refresh = function(changedTiddlers) { $tw.utils.removeClass(this.labelDomNode,"tc-checkbox-checked"); } } + this.assignAttributes(this.inputDomNode,{ + changedAttributes: changedAttributes, + sourcePrefix: "data-", + destPrefix: "data-" + }); return this.refreshChildren(changedTiddlers) || refreshed; } }; @@ -332,3 +342,4 @@ CheckboxWidget.prototype.refresh = function(changedTiddlers) { exports.checkbox = CheckboxWidget; })(); + \ No newline at end of file diff --git a/core/modules/widgets/draggable.js b/core/modules/widgets/draggable.js index f759ab121..22fdc37e9 100644 --- a/core/modules/widgets/draggable.js +++ b/core/modules/widgets/draggable.js @@ -52,6 +52,11 @@ DraggableWidget.prototype.render = function(parent,nextSibling) { classes.push("tc-draggable"); } domNode.setAttribute("class",classes.join(" ")); + // Assign data- attributes and style. attributes + this.assignAttributes(domNode,{ + sourcePrefix: "data-", + destPrefix: "data-" + }); // Insert the node into the DOM and render any children parent.insertBefore(domNode,nextSibling); this.renderChildren(domNode,null); @@ -108,13 +113,19 @@ DraggableWidget.prototype.updateDomNodeClasses = function() { Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering */ DraggableWidget.prototype.refresh = function(changedTiddlers) { - var changedAttributes = this.computeAttributes(), - changedAttributesCount = $tw.utils.count(changedAttributes); - if(changedAttributesCount === 1 && changedAttributes["class"]) { - this.updateDomNodeClasses(); - } else if(changedAttributesCount > 0) { + var changedAttributes = this.computeAttributes(); + if(changedAttributes.tag || changedAttributes.selector || changedAttributes.dragimagetype || changedAttributes.enable || changedAttributes.startactions || changedAttributes.endactions) { this.refreshSelf(); return true; + } else { + if(changedAttributes["class"]) { + this.assignDomNodeClasses(); + } + this.assignAttributes(this.domNodes[0],{ + changedAttributes: changedAttributes, + sourcePrefix: "data-", + destPrefix: "data-" + }); } return this.refreshChildren(changedTiddlers); }; diff --git a/core/modules/widgets/droppable.js b/core/modules/widgets/droppable.js index 104503b25..0dcba1688 100644 --- a/core/modules/widgets/droppable.js +++ b/core/modules/widgets/droppable.js @@ -42,6 +42,11 @@ DroppableWidget.prototype.render = function(parent,nextSibling) { domNode = this.document.createElement(tag); this.domNode = domNode; this.assignDomNodeClasses(); + // Assign data- attributes and style. attributes + this.assignAttributes(domNode,{ + sourcePrefix: "data-", + destPrefix: "data-" + }); // Add event handlers if(this.droppableEnable) { $tw.utils.addEventListeners(domNode,[ @@ -166,8 +171,15 @@ DroppableWidget.prototype.refresh = function(changedTiddlers) { if(changedAttributes.tag || changedAttributes.enable || changedAttributes.disabledClass || changedAttributes.actions || changedAttributes.effect) { this.refreshSelf(); return true; - } else if(changedAttributes["class"]) { - this.assignDomNodeClasses(); + } else { + if(changedAttributes["class"]) { + this.assignDomNodeClasses(); + } + this.assignAttributes(this.domNodes[0],{ + changedAttributes: changedAttributes, + sourcePrefix: "data-", + destPrefix: "data-" + }); } return this.refreshChildren(changedTiddlers); }; diff --git a/core/modules/widgets/link.js b/core/modules/widgets/link.js index 6f199d395..0d89ee22d 100755 --- a/core/modules/widgets/link.js +++ b/core/modules/widgets/link.js @@ -43,6 +43,11 @@ LinkWidget.prototype.render = function(parent,nextSibling) { } else { // Just insert the link text var domNode = this.document.createElement("span"); + // Assign data- attributes + this.assignAttributes(domNode,{ + sourcePrefix: "data-", + destPrefix: "data-" + }); parent.insertBefore(domNode,nextSibling); this.renderChildren(domNode,null); this.domNodes.push(domNode); @@ -138,6 +143,11 @@ LinkWidget.prototype.renderLink = function(parent,nextSibling) { widget: this }); } + // Assign data- attributes + this.assignAttributes(domNode,{ + sourcePrefix: "data-", + destPrefix: "data-" + }); // Insert the link into the DOM and render any children parent.insertBefore(domNode,nextSibling); this.renderChildren(domNode,null); @@ -207,8 +217,7 @@ Selectively refreshes the widget if needed. Returns true if the widget or any of */ LinkWidget.prototype.refresh = function(changedTiddlers) { var changedAttributes = this.computeAttributes(); - if(changedAttributes.to || changedTiddlers[this.to] || changedAttributes["aria-label"] || changedAttributes.tooltip || - changedAttributes["class"] || changedAttributes.tabindex || changedAttributes.draggable || changedAttributes.tag) { + if($tw.utils.count(changedAttributes) > 0) { this.refreshSelf(); return true; } @@ -218,3 +227,4 @@ LinkWidget.prototype.refresh = function(changedTiddlers) { exports.link = LinkWidget; })(); + \ No newline at end of file diff --git a/core/modules/widgets/radio.js b/core/modules/widgets/radio.js index 363836227..aa7a32cf1 100644 --- a/core/modules/widgets/radio.js +++ b/core/modules/widgets/radio.js @@ -40,6 +40,10 @@ RadioWidget.prototype.render = function(parent,nextSibling) { ); this.inputDomNode = this.document.createElement("input"); this.inputDomNode.setAttribute("type","radio"); + this.assignAttributes(this.inputDomNode,{ + sourcePrefix: "data-", + destPrefix: "data-" + }); if(isChecked) { this.inputDomNode.checked = true; } diff --git a/core/modules/widgets/range.js b/core/modules/widgets/range.js index 4dd55dc3c..db2699cc4 100644 --- a/core/modules/widgets/range.js +++ b/core/modules/widgets/range.js @@ -50,6 +50,10 @@ RangeWidget.prototype.render = function(parent,nextSibling) { this.inputDomNode.setAttribute("disabled",true); } this.inputDomNode.value = this.getValue(); + this.assignAttributes(this.inputDomNode,{ + sourcePrefix: "data-", + destPrefix: "data-" + }); // Add a click event handler $tw.utils.addEventListeners(this.inputDomNode,[ {name:"mousedown", handlerObject:this, handlerMethod:"handleMouseDownEvent"}, diff --git a/core/modules/widgets/select.js b/core/modules/widgets/select.js index ab9bef74e..f1ea3b331 100644 --- a/core/modules/widgets/select.js +++ b/core/modules/widgets/select.js @@ -40,7 +40,31 @@ SelectWidget.prototype.render = function(parent,nextSibling) { this.parentDomNode = parent; this.computeAttributes(); this.execute(); - this.renderChildren(parent,nextSibling); + //Create element + var domNode = this.document.createElement("select"); + if(this.selectClass) { + domNode.classname = this.selectClass; + } + // Assign data- attributes + this.assignAttributes(domNode,{ + sourcePrefix: "data-", + destPrefix: "data-" + }); + if(this.selectMultiple) { + domNode.setAttribute("multiple","multiple"); + } + if(this.selectSize) { + domNode.setAttribute("size",this.selectSize); + } + if(this.selectTabindex) { + domNode.setAttribute("tabindex",this.selectTabindex); + } + if(this.selectTooltip) { + domNode.setAttribute("title",this.selectTooltip); + } + this.renderChildren(domNode,nextSibling); + this.parentDomNode.insertBefore(domNode,nextSibling); + this.domNodes.push(domNode); this.setSelectValue(); if(this.selectFocus == "yes") { this.getSelectDomNode().focus(); @@ -113,7 +137,7 @@ SelectWidget.prototype.setSelectValue = function() { Get the DOM node of the select element */ SelectWidget.prototype.getSelectDomNode = function() { - return this.children[0].domNodes[0]; + return this.domNodes[0]; }; // Return an array of the selected opion values @@ -149,27 +173,7 @@ SelectWidget.prototype.execute = function() { this.selectTooltip = this.getAttribute("tooltip"); this.selectFocus = this.getAttribute("focus"); // Make the child widgets - var selectNode = { - type: "element", - tag: "select", - children: this.parseTreeNode.children - }; - if(this.selectClass) { - $tw.utils.addAttributeToParseTreeNode(selectNode,"class",this.selectClass); - } - if(this.selectMultiple) { - $tw.utils.addAttributeToParseTreeNode(selectNode,"multiple","multiple"); - } - if(this.selectSize) { - $tw.utils.addAttributeToParseTreeNode(selectNode,"size",this.selectSize); - } - if(this.selectTabindex) { - $tw.utils.addAttributeToParseTreeNode(selectNode,"tabindex",this.selectTabindex); - } - if(this.selectTooltip) { - $tw.utils.addAttributeToParseTreeNode(selectNode,"title",this.selectTooltip); - } - this.makeChildWidgets([selectNode]); + this.makeChildWidgets(); }; /* @@ -178,17 +182,21 @@ Selectively refreshes the widget if needed. Returns true if the widget or any of SelectWidget.prototype.refresh = function(changedTiddlers) { var changedAttributes = this.computeAttributes(); // If we're using a different tiddler/field/index then completely refresh ourselves - if(changedAttributes.tiddler || changedAttributes.field || changedAttributes.index || changedAttributes.tooltip) { + if(changedAttributes.tiddler || changedAttributes.field || changedAttributes.index || changedAttributes.tooltip || changedAttributes.tabindex) { this.refreshSelf(); return true; - // If the target tiddler value has changed, just update setting and refresh the children } else { if(changedAttributes.class) { this.selectClass = this.getAttribute("class"); this.getSelectDomNode().setAttribute("class",this.selectClass); } - + this.assignAttributes(this.getSelectDomNode(),{ + changedAttributes: changedAttributes, + sourcePrefix: "data-", + destPrefix: "data-" + }); var childrenRefreshed = this.refreshChildren(changedTiddlers); + // If the target tiddler value has changed, just update setting and refresh the children if(changedTiddlers[this.selectTitle] || childrenRefreshed) { this.setSelectValue(); } diff --git a/core/modules/widgets/widget.js b/core/modules/widgets/widget.js index 6c3997179..af4892b9e 100755 --- a/core/modules/widgets/widget.js +++ b/core/modules/widgets/widget.js @@ -413,16 +413,34 @@ Widget.prototype.getAttribute = function(name,defaultText) { }; /* -Assign the computed attributes of the widget to a domNode +Assign the common attributes of the widget to a domNode options include: -excludeEventAttributes: ignores attributes whose name begins with "on" +sourcePrefix: prefix of attributes that are to be directly assigned (defaults to the empty string meaning all attributes) +destPrefix: prefix to be applied to attribute names that are to be directly assigned (defaults to the emtpy string which means no prefix is added) +changedAttributes: hashmap by attribute name of attributes to process (if missing, process all attributes) +excludeEventAttributes: ignores attributes whose name would begin with "on" */ Widget.prototype.assignAttributes = function(domNode,options) { options = options || {}; - var self = this; + var self = this, + changedAttributes = options.changedAttributes || this.attributes, + sourcePrefix = options.sourcePrefix || "", + destPrefix = options.destPrefix || "", + EVENT_ATTRIBUTE_PREFIX = "on"; var assignAttribute = function(name,value) { + // Process any style attributes before considering sourcePrefix and destPrefix + if(name.substr(0,6) === "style." && name.length > 6) { + domNode.style[$tw.utils.unHyphenateCss(name.substr(6))] = value; + return; + } + // Check if the sourcePrefix is a match + if(name.substr(0,sourcePrefix.length) === sourcePrefix) { + name = destPrefix + name.substr(sourcePrefix.length); + } else { + value = undefined; + } // Check for excluded attribute names - if(options.excludeEventAttributes && name.substr(0,2) === "on") { + if(options.excludeEventAttributes && name.substr(0,2).toLowerCase() === EVENT_ATTRIBUTE_PREFIX) { value = undefined; } if(value !== undefined) { @@ -432,26 +450,24 @@ Widget.prototype.assignAttributes = function(domNode,options) { namespace = "http://www.w3.org/1999/xlink"; name = name.substr(6); } - // Handle styles - if(name.substr(0,6) === "style." && name.length > 6) { - domNode.style[$tw.utils.unHyphenateCss(name.substr(6))] = value; - } else { - // Setting certain attributes can cause a DOM error (eg xmlns on the svg element) - try { - domNode.setAttributeNS(namespace,name,value); - } catch(e) { - } + // Setting certain attributes can cause a DOM error (eg xmlns on the svg element) + try { + domNode.setAttributeNS(namespace,name,value); + } catch(e) { } } - } - // Not all parse tree nodes have the orderedAttributes property + }; + // If the parse tree node has the orderedAttributes property then use that order if(this.parseTreeNode.orderedAttributes) { $tw.utils.each(this.parseTreeNode.orderedAttributes,function(attribute,index) { - assignAttribute(attribute.name,self.attributes[attribute.name]); - }); + if(attribute.name in changedAttributes) { + assignAttribute(attribute.name,self.getAttribute(attribute.name)); + } + }); + // Otherwise update each changed attribute irrespective of order } else { - $tw.utils.each(Object.keys(self.attributes).sort(),function(name) { - assignAttribute(name,self.attributes[name]); + $tw.utils.each(changedAttributes,function(value,name) { + assignAttribute(name,self.getAttribute(name)); }); } }; diff --git a/core/wiki/macros/tabs.tid b/core/wiki/macros/tabs.tid index bc8a0255f..1805bc9be 100644 --- a/core/wiki/macros/tabs.tid +++ b/core/wiki/macros/tabs.tid @@ -4,7 +4,15 @@ code-body: yes \define tabs-button() \whitespace trim -<$button set=<> setTo=<> default=<<__default__>> selectedClass="tc-tab-selected" tooltip={{!!tooltip}} role="switch"> +<$button + set=<> + setTo=<> + default=<<__default__>> + selectedClass="tc-tab-selected" + tooltip={{!!tooltip}} + role="switch" + data-tab-title=<> +> <$tiddler tiddler=<>> <$set name="tv-wikilinks" value="no"> <$transclude tiddler=<<__buttonTemplate__>> mode="inline"> diff --git a/editions/test/tiddlers/tests/data/widgets/DataAttributes/ButtonWidget-DataAttributes.tid b/editions/test/tiddlers/tests/data/widgets/DataAttributes/ButtonWidget-DataAttributes.tid new file mode 100644 index 000000000..da3d7080a --- /dev/null +++ b/editions/test/tiddlers/tests/data/widgets/DataAttributes/ButtonWidget-DataAttributes.tid @@ -0,0 +1,27 @@ +title: Widgets/DataAttributes/ButtonWidget +description: Data Attributes for ButtonWidget +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\whitespace trim +<$button tag="div" class="myclass" data-title="mytiddler" style.color="red" onclick="clicked"> +my tiddler + +<$button tag="div" class="myclass" data-title={{Temp}} style.color={{{ [[Temp]get[color]] }}}> +hello + ++ +title: Actions + +<$action-setfield $tiddler="Temp" $field="text" $value="Title2" color="red"/> ++ +title: Temp +color: black + +Title1 ++ +title: ExpectedResult + +

my tiddler
hello

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/widgets/DataAttributes/CheckboxWidget-DataAttributes.tid b/editions/test/tiddlers/tests/data/widgets/DataAttributes/CheckboxWidget-DataAttributes.tid new file mode 100644 index 000000000..521fa3a13 --- /dev/null +++ b/editions/test/tiddlers/tests/data/widgets/DataAttributes/CheckboxWidget-DataAttributes.tid @@ -0,0 +1,22 @@ +title: Widgets/DataAttributes/CheckboxWidget +description: Data Attributes for CheckboxWidget +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\whitespace trim +<$checkbox tag="done" data-title={{Temp}} style.color={{{ [[Temp]get[color]] }}} onclick="clicked"> Is it done? ++ +title: Actions + +<$action-setfield $tiddler="Temp" $field="text" $value="Title2" color="red"/> ++ +title: Temp +color: black + +Title1 ++ +title: ExpectedResult + +

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/widgets/DataAttributes/DraggableWidget-DataAttributes.tid b/editions/test/tiddlers/tests/data/widgets/DataAttributes/DraggableWidget-DataAttributes.tid new file mode 100644 index 000000000..feeb89ded --- /dev/null +++ b/editions/test/tiddlers/tests/data/widgets/DataAttributes/DraggableWidget-DataAttributes.tid @@ -0,0 +1,27 @@ +title: Widgets/DataAttributes/DraggableWidget +description: Data Attributes for DraggableWidget +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\whitespace trim +<$draggable tag="div" class="myclass" data-title="mytiddler" style.color="red" onclick="clicked"> +my tiddler + +<$draggable tag="div" class="myclass" data-title={{Temp}} style.color={{{ [[Temp]get[color]] }}}> +hello + ++ +title: Actions + +<$action-setfield $tiddler="Temp" $field="text" $value="Title2" color="red"/> ++ +title: Temp +color: black + +Title1 ++ +title: ExpectedResult + +

my tiddler
hello

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/widgets/DataAttributes/DroppableWidget-DataAttributes.tid b/editions/test/tiddlers/tests/data/widgets/DataAttributes/DroppableWidget-DataAttributes.tid new file mode 100644 index 000000000..3c7284eb1 --- /dev/null +++ b/editions/test/tiddlers/tests/data/widgets/DataAttributes/DroppableWidget-DataAttributes.tid @@ -0,0 +1,27 @@ +title: Widgets/DataAttributes/DroppableWidget +description: Data Attributes for DroppableWidget +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\whitespace trim +<$droppable tag="div" class="myclass" data-title="mytiddler" style.color="red" onclick="clicked"> +my tiddler + +<$droppable tag="div" class="myclass" data-title={{Temp}} style.color={{{ [[Temp]get[color]] }}}> +hello + ++ +title: Actions + +<$action-setfield $tiddler="Temp" $field="text" $value="Title2" color="red"/> ++ +title: Temp +color: black + +Title1 ++ +title: ExpectedResult + +

my tiddler
hello

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/widgets/DataAttributes/LinkWidget-DataAttributes.tid b/editions/test/tiddlers/tests/data/widgets/DataAttributes/LinkWidget-DataAttributes.tid new file mode 100644 index 000000000..e99e265bb --- /dev/null +++ b/editions/test/tiddlers/tests/data/widgets/DataAttributes/LinkWidget-DataAttributes.tid @@ -0,0 +1,27 @@ +title: Widgets/DataAttributes/LinkWidget +description: Data Attributes for LinkWidget +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\whitespace trim +<$link data-id="mytiddler" style.color="red" to="Temp" onclick="clicked"> +link to Temp + +<$link tag="button" data-id={{Temp}} style.color={{{ [[Temp]get[color]] }}} to="SomeTiddler"> +some tiddler + ++ +title: Actions + +<$action-setfield $tiddler="Temp" $field="text" $value="Title2" color="red"/> ++ +title: Temp +color: black + +Title1 ++ +title: ExpectedResult + +

link to Temp

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/widgets/DataAttributes/OrderedStyleAttributes.tid b/editions/test/tiddlers/tests/data/widgets/DataAttributes/OrderedStyleAttributes.tid new file mode 100644 index 000000000..2f6d2cb1a --- /dev/null +++ b/editions/test/tiddlers/tests/data/widgets/DataAttributes/OrderedStyleAttributes.tid @@ -0,0 +1,15 @@ +title: Widgets/DataAttributes/OrderedStyleAttributes +description: Ordered style attributes +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\whitespace trim +
+hello +
++ +title: ExpectedResult + +

hello

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/widgets/DataAttributes/SelectWidget-DataAttributes.tid b/editions/test/tiddlers/tests/data/widgets/DataAttributes/SelectWidget-DataAttributes.tid new file mode 100644 index 000000000..de2c9995e --- /dev/null +++ b/editions/test/tiddlers/tests/data/widgets/DataAttributes/SelectWidget-DataAttributes.tid @@ -0,0 +1,27 @@ +title: Widgets/DataAttributes/SelectWidget +description: Data Attributes for SelectWidget +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\whitespace trim +<$select tiddler='New Tiddler' field='text' default='Choose a new text' data-title={{Temp}} style.color={{{ [[Temp]get[color]] }}} onclick="clicked"> + + + + + ++ +title: Actions + +<$action-setfield $tiddler="Temp" $field="text" $value="Title2" color="red"/> ++ +title: Temp +color: black + +Title1 ++ +title: ExpectedResult + +

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/widgets/ElementWidgetEventAttributes.tid b/editions/test/tiddlers/tests/data/widgets/ElementWidgetEventAttributes.tid new file mode 100644 index 000000000..4c2f6eb04 --- /dev/null +++ b/editions/test/tiddlers/tests/data/widgets/ElementWidgetEventAttributes.tid @@ -0,0 +1,15 @@ +title: Widgets/ElementWidgetEventAttributes +description: Element widget should not support event attributes starting with "on" +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\whitespace trim +
+TiddlyWiki +
++ +title: ExpectedResult + +

TiddlyWiki

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/widgets/ElementWidgetStyleAttributes.tid b/editions/test/tiddlers/tests/data/widgets/ElementWidgetStyleAttributes.tid new file mode 100644 index 000000000..a36a51323 --- /dev/null +++ b/editions/test/tiddlers/tests/data/widgets/ElementWidgetStyleAttributes.tid @@ -0,0 +1,15 @@ +title: Widgets/ElementWidgetStyleAttributes +description: Element widget should support style.* attributes +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\whitespace trim +
+TiddlyWiki +
++ +title: ExpectedResult + +

TiddlyWiki

\ No newline at end of file diff --git a/editions/tw5.com/tiddlers/widgets/BrowseWidget.tid b/editions/tw5.com/tiddlers/widgets/BrowseWidget.tid index 28012bd68..b0364a71a 100644 --- a/editions/tw5.com/tiddlers/widgets/BrowseWidget.tid +++ b/editions/tw5.com/tiddlers/widgets/BrowseWidget.tid @@ -1,6 +1,6 @@ caption: browse created: 20131024141900000 -modified: 20200421221304177 +modified: 20231113093304323 tags: Widgets title: BrowseWidget type: text/vnd.tiddlywiki @@ -20,6 +20,8 @@ The content of the <<.wid BrowseWidget>> widget is ignored. |accept |<<.from-version "5.1.23">> Optional comma delimited [[list of file accepted extensions|https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#Unique_file_type_specifiers]] and/or MIME types | |message |Optional override of widget message to be generated. The files will be passed in the JavaScript object `event.target.files` | |param |Optional parameter to be passed with the custom message | +|data-* |<<.from-version "5.3.2">> Optional [[data attributes|https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes]] to be assigned to the HTML element | +|style.* |<<.from-version "5.3.2">> Optional [[CSS properties|https://developer.mozilla.org/en-US/docs/Web/CSS/Reference]] to be assigned to the HTML element | On iPhone/iPad choosing the multiple option will remove the ability to take photographs/videos directly into TiddlyWiki. diff --git a/editions/tw5.com/tiddlers/widgets/ButtonWidget.tid b/editions/tw5.com/tiddlers/widgets/ButtonWidget.tid index da61838af..d74c09575 100644 --- a/editions/tw5.com/tiddlers/widgets/ButtonWidget.tid +++ b/editions/tw5.com/tiddlers/widgets/ButtonWidget.tid @@ -1,6 +1,6 @@ caption: button created: 20131024141900000 -modified: 20220810192251345 +modified: 20231113093304323 tags: Widgets TriggeringWidgets title: ButtonWidget type: text/vnd.tiddlywiki @@ -41,6 +41,8 @@ The content of the `<$button>` widget is displayed within the button. |aria-label |Optional [[Accessibility]] label | |tooltip |Optional tooltip | |class |An optional CSS class name to be assigned to the HTML element| +|data-* |<<.from-version "5.3.2">> Optional [[data attributes|https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes]] to be assigned to the HTML element | +|style.* |<<.from-version "5.3.2">> Optional [[CSS properties|https://developer.mozilla.org/en-US/docs/Web/CSS/Reference]] to be assigned to the HTML element | |style |An optional CSS style attribute to be assigned to the HTML element | |tag |An optional html tag to use instead of the default "button" | |dragTiddler |An optional tiddler title making the button draggable and identifying the payload tiddler. See DraggableWidget for details | diff --git a/editions/tw5.com/tiddlers/widgets/CheckboxWidget.tid b/editions/tw5.com/tiddlers/widgets/CheckboxWidget.tid index 47e83e875..00ecbb6f8 100644 --- a/editions/tw5.com/tiddlers/widgets/CheckboxWidget.tid +++ b/editions/tw5.com/tiddlers/widgets/CheckboxWidget.tid @@ -3,7 +3,7 @@ colors: red orange yellow blue created: 20131024141900000 fruits: bananas oranges grapes list: [[CheckboxWidget (tag Mode)]] [[CheckboxWidget (field Mode)]] [[CheckboxWidget (listField Mode)]] [[CheckboxWidget (index Mode)]] [[CheckboxWidget (listIndex Mode)]] [[CheckboxWidget (filter Mode)]] [[CheckboxWidget (indeterminate)]] -modified: 20230316192632667 +modified: 20231113093304323 tags: Widgets TriggeringWidgets title: CheckboxWidget type: text/vnd.tiddlywiki @@ -38,5 +38,7 @@ The content of the `<$checkbox>` widget is displayed within an HTML `
\end \define image-picker-include-tagged-images(actions) -<$macrocall $name="image-picker" filter="[all[shadows+tiddlers]is[image]] [all[shadows+tiddlers]tag[$:/tags/Image]] -[type[application/pdf]] +[!has[draft.of]sort[title]]" actions="""$actions$"""/> +<$macrocall $name="image-picker" filter="[all[shadows+tiddlers]is[image]] [all[shadows+tiddlers]tag[$:/tags/Image]] -[type[application/pdf]] +[!has[draft.of]sort[title]]" actions=<<__actions__>>/> \end diff --git a/core/wiki/macros/tag-picker.tid b/core/wiki/macros/tag-picker.tid index e1b1a7139..ede53ec26 100644 --- a/core/wiki/macros/tag-picker.tid +++ b/core/wiki/macros/tag-picker.tid @@ -16,7 +16,7 @@ second-search-filter: [tags[]is[system]search:titlesort[]] emptyMessage="<$action-listops $tiddler=<> $field=<<__tagField__>> $subfilter='-[]'/>" > <$action-listops $tiddler=<> $field=<<__tagField__>> $subfilter="[trim[]]"/> - $actions$ + <$transclude $variable="__actions__"/> <> @@ -102,7 +102,7 @@ second-search-filter: [tags[]is[system]search:titlesort[]] <$set name="tag" value={{{ [get[text]] }}}> <$button set=<> setTo="" class=""> <$action-listops $tiddler=<> $field=<<__tagField__>> $subfilter="[trim[]]"/> - $actions$ + <$transclude $variable="__actions__"/> <$set name="currentTiddlerCSSEscaped" value={{{ [escapecss[]] }}}> <><$action-sendmessage $message="tm-focus-selector" $param=<>/> From 1cb607249e50344829dcbc9a4ed49335842e5058 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Fri, 24 Nov 2023 13:02:09 +0000 Subject: [PATCH 19/45] Fix syncer race condition (#7843) * Initial commit * Log task choosing * A tiny bit more logging * Typo * Restructure syncer to use a single state machine --- core/modules/syncer.js | 286 +++++++++++++++++++++++------------------ 1 file changed, 164 insertions(+), 122 deletions(-) diff --git a/core/modules/syncer.js b/core/modules/syncer.js index c06fcb143..58b087b8d 100644 --- a/core/modules/syncer.js +++ b/core/modules/syncer.js @@ -24,7 +24,7 @@ Syncer.prototype.titleSyncPollingInterval = "$:/config/SyncPollingInterval"; Syncer.prototype.titleSyncDisableLazyLoading = "$:/config/SyncDisableLazyLoading"; Syncer.prototype.titleSavedNotification = "$:/language/Notifications/Save/Done"; Syncer.prototype.titleSyncThrottleInterval = "$:/config/SyncThrottleInterval"; -Syncer.prototype.taskTimerInterval = 1 * 1000; // Interval for sync timer +Syncer.prototype.taskTimerInterval = 0.25 * 1000; // Interval for sync timer Syncer.prototype.throttleInterval = 1 * 1000; // Defer saving tiddlers if they've changed in the last 1s... Syncer.prototype.errorRetryInterval = 5 * 1000; // Interval to retry after an error Syncer.prototype.fallbackInterval = 10 * 1000; // Unless the task is older than 10s @@ -74,9 +74,11 @@ function Syncer(options) { this.titlesHaveBeenLazyLoaded = {}; // Hashmap of titles of tiddlers that have already been lazily loaded from the server // Timers this.taskTimerId = null; // Timer for task dispatch - this.pollTimerId = null; // Timer for polling server // Number of outstanding requests this.numTasksInProgress = 0; + // True when we want to force an immediate sync from the server + this.forceSyncFromServer = false; + this.timestampLastSyncFromServer = new Date(); // Listen out for changes to tiddlers this.wiki.addEventListener("change",function(changes) { // Filter the changes to just include ones that are being synced @@ -187,6 +189,7 @@ Syncer.prototype.readTiddlerInfo = function() { // Record information for known tiddlers var self = this, tiddlers = this.getSyncedTiddlers(); + this.logger.log("Initialising tiddlerInfo for " + tiddlers.length + " tiddlers"); $tw.utils.each(tiddlers,function(title) { var tiddler = self.wiki.getTiddler(title); if(tiddler) { @@ -203,33 +206,38 @@ Syncer.prototype.readTiddlerInfo = function() { Checks whether the wiki is dirty (ie the window shouldn't be closed) */ Syncer.prototype.isDirty = function() { - this.logger.log("Checking dirty status"); - // Check tiddlers that are in the store and included in the filter function - var titles = this.getSyncedTiddlers(); - for(var index=0; index tiddlerInfo.changeCount) { + var self = this; + function checkIsDirty() { + // Check tiddlers that are in the store and included in the filter function + var titles = self.getSyncedTiddlers(); + for(var index=0; index tiddlerInfo.changeCount) { + return true; + } + } else { + // If the tiddler isn't known on the server then it needs to be saved to the server return true; } - } else { - // If the tiddler isn't known on the server then it needs to be saved to the server + } + } + // Check tiddlers that are known from the server but not currently in the store + titles = Object.keys(self.tiddlerInfo); + for(index=0; index 0 || updates.deletions.length > 0) { - self.processTaskQueue(); - } - } - }); - } else if(this.syncadaptor && this.syncadaptor.getSkinnyTiddlers) { - this.logger.log("Retrieving skinny tiddler list"); - cancelNextSync(); - this.syncadaptor.getSkinnyTiddlers(function(err,tiddlers) { - triggerNextSync(); - // Check for errors - if(err) { - self.displayError($tw.language.getString("Error/RetrievingSkinny"),err); - return; - } - // Keep track of which tiddlers we already know about have been reported this time - var previousTitles = Object.keys(self.tiddlerInfo); - // Process each incoming tiddler - for(var t=0; t= (this.timestampLastSyncFromServer.valueOf() + this.pollTimerInterval)))) { + return new SyncFromServerTask(this); + } + // Third, we check tiddlers that are known from the server but not currently in the store, and so need deleting on the server titles = Object.keys(this.tiddlerInfo); for(index=0; index Date: Fri, 24 Nov 2023 15:11:21 +0000 Subject: [PATCH 20/45] Update release note --- .../prerelease/tiddlers/Release 5.3.2.tid | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/editions/prerelease/tiddlers/Release 5.3.2.tid b/editions/prerelease/tiddlers/Release 5.3.2.tid index 17058d731..ced4957f2 100644 --- a/editions/prerelease/tiddlers/Release 5.3.2.tid +++ b/editions/prerelease/tiddlers/Release 5.3.2.tid @@ -49,7 +49,7 @@ Note that the <<.attr "emptyMessage">> and <<.attr "template">> attributes take <<.link-badge-extended "https://github.com/Jermolene/TiddlyWiki5/pull/7746">> QR Code plugin to be able to read QR codes and a number of other bar code formats -! Translation improvement +! Translation improvements Improvements to the following translations: @@ -61,10 +61,13 @@ Improvements to the following translations: * <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/commit/1be8f0a9336952d4745d2bd4f2327e353580a272">> Comments Plugin to use predefined palette colours * <<.link-badge-improved "https://github.com/Jermolene/TiddlyWiki5/pull/7785">> Evernote Importer Plugin to support images and other attachments +* <<.link-badge-added "https://github.com/Jermolene/TiddlyWiki5/pull/7790">> `$floating` attribute to Dynannotate Plugin to support popups that do not disappear when another part of the screen is clicked. Instead they have to dismissed manually ! Widget Improvements -* +* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/pull/7734">> ImageWidget encoding for more image types +* <<.link-badge-extended "https://github.com/Jermolene/TiddlyWiki5/pull/7634">> ImageWidget to add a "usemap" attribute +* <<.link-badge-improved "https://github.com/Jermolene/TiddlyWiki5/pull/7649">> the ScrollableWidget to allow the scroll position to be bound to a tiddler, so that changes to the tiddler affect the scroll position, and vice versa ! Usability Improvements @@ -73,8 +76,10 @@ Improvements to the following translations: ! Hackability Improvements -* <<.link-badge-added "https://github.com/Jermolene/TiddlyWiki5/pull/7737">> an automatic build of the external core TiddlyWiki at https://tiddlywiki.com/empty-external-core.html +* <<.link-badge-extended "https://github.com/Jermolene/TiddlyWiki5/pull/7769">> all the relevant core widgets to allow arbitrary `data-*` attributes and `style.*` attributes to be applied to the generated DOM nodes. This is useful for passing data to the EventCatcherWidget +* <<.link-badge-extended "https://github.com/Jermolene/TiddlyWiki5/pull/7849">> [[jsonextract Operator]], [[jsonget Operator]], [[jsonset Operator]] and [[jsontype Operator]] to allow negative indexes into arrays to be counted from the end of the array * <<.link-badge-improved "https://github.com/Jermolene/TiddlyWiki5/pull/7690">> the default page layout to better support CSS grid and flexbox layouts +* <<.link-bage-improved "https://github.com/Jermolene/TiddlyWiki5/pull/7787">> the editor to use grid layout, simplifying customisation ! Bug Fixes @@ -87,10 +92,20 @@ Improvements to the following translations: * <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/pull/6088">> upgrade download link in Firefox * <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/pull/7698">> refreshing of transcluded functions * <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/pull/7789">> resizing of height of textareas in control panel +* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/pull/7683">> [[encodebase64 Operator]] and [[decodebase64 Operator]] to work properly with binary data +* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/pull/7708">> [[WidgetMessage: tm-open-window]] when opening an existing window to bring it to the front and focus it +* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/pull/7809">> behaviour of [[last Operator]] when zero items selected +* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/pull/7806">> incorrectly setting focus on field name input field when deleting field using the delete field button +* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/pull/7802">> [[Table-of-Contents Macros]] to not show expander icon for a sublist that has all children excluded +* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/pull/7794">> overflow of [[CodeMirror Plugin]] editor within grid container +* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/pull/7835">> wikitest parser removing whitespace when parsing pragmas +* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/pull/7842">> tooltip for editor add field button +* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/pull/7844">> plain text parser being susceptible to the CodeBlockWidget being redefined +* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/pull/7855">> pragmas not working within the action string of several core macros ! Node.js Improvements -* +* <> a significant flaw in the synchronisation algorithm used by the client-server configuration. The flaw could lead to tiddlers temporarily disappearing from the browser ! Performance Improvements @@ -100,6 +115,12 @@ Improvements to the following translations: ! Developer Improvements * <<.link-badge-improved "https://github.com/Jermolene/TiddlyWiki5/pull/7751">> global hook handling to support removing hooks +* <<.link-badge-added "https://github.com/Jermolene/TiddlyWiki5/pull/7539">> some useful npm scripts to `package.json` + +! Infrastructure Improvements + +* <<.link-badge-improved "https://github.com/Jermolene/TiddlyWiki5/pull/7820">> Continuous Integration tests to use Playwright to run our browser-based tests +* <<.link-badge-added "https://github.com/Jermolene/TiddlyWiki5/pull/7737">> an automatic build of the external core TiddlyWiki at https://tiddlywiki.com/empty-external-core.html ! Acknowledgements @@ -110,13 +131,19 @@ AnthonyMuscio BramChen BuckarooBanzay BurningTreeC +CrossEye EvidentlyCube +Gk0Wk joebordes kookma linonetwo mateuszwilczek +oflig +pille1842 pmario rmunn +saqimtiaz simonbaird T1mL3arn +yaisog """>> From 64812f5c062e3eaeaa8ef158851ffcece4babb13 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Sat, 25 Nov 2023 16:35:05 +0700 Subject: [PATCH 21/45] Add join attribute to list widget (#7694) * Add join attribute to list widget * Use new join attribute in HTML saving templates This simplifies the logic involved in saving tiddlers in JSON format into TW html files, and should also slightly speed up the saving process depending on how often that list widget gets refreshed. * Unit tests for list widget's new join attribute * Add `<$list-join>` widget Allows specifying complicated join text more easily than an attribute --- core/modules/widgets/list.js | 90 ++++++++++++++++--- core/templates/html-json-skinny-tiddler.tid | 1 - core/templates/html-json-tiddler.tid | 2 +- core/templates/store.area.template.html.tid | 8 +- core/ui/ViewTemplate/subtitle.tid | 7 +- .../prerelease/tiddlers/Release 5.3.2.tid | 27 ++++++ .../data/list-widget/WithJoinTemplate.tid | 30 +++++++ .../WithJoinTemplateInBlockMode.tid | 32 +++++++ editions/test/tiddlers/tests/test-widget.js | 39 ++++++++ .../tw5.com/tiddlers/widgets/ListWidget.tid | 22 ++++- .../tiddlyweb/html-json-skinny-tiddler.tid | 1 - .../tiddlyweb/html-json-tiddler.tid | 3 +- 12 files changed, 237 insertions(+), 25 deletions(-) create mode 100644 editions/test/tiddlers/tests/data/list-widget/WithJoinTemplate.tid create mode 100644 editions/test/tiddlers/tests/data/list-widget/WithJoinTemplateInBlockMode.tid 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"/> From 233b871fdf7406c5a50e1c0f391e4aed1bbb180a Mon Sep 17 00:00:00 2001 From: Saq Imtiaz Date: Sun, 26 Nov 2023 18:42:53 +0100 Subject: [PATCH 22/45] Update help for CommandsCommand to avoid deduplication (#7858) --- core/language/en-GB/Help/commands.tid | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/language/en-GB/Help/commands.tid b/core/language/en-GB/Help/commands.tid index 454159b44..7551885f0 100644 --- a/core/language/en-GB/Help/commands.tid +++ b/core/language/en-GB/Help/commands.tid @@ -10,7 +10,7 @@ Sequentially run the command tokens returned from a filter Examples ``` ---commands "[enlist{$:/build-commands-as-text}]" +--commands "[enlist:raw{$:/build-commands-as-text}]" ``` ``` From 6bd69cc53ff95efc36fdbea615825ef0dcc5de1e Mon Sep 17 00:00:00 2001 From: Maurycy Zarzycki Date: Mon, 27 Nov 2023 22:53:42 +0100 Subject: [PATCH 23/45] Add changes to PL translation from 233b871fdf7406c5a50e1c0f391e4aed1bbb180a (#7862) --- languages/pl-PL/Help/commands.tid | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/languages/pl-PL/Help/commands.tid b/languages/pl-PL/Help/commands.tid index a0bc5932d..1085ac0a5 100644 --- a/languages/pl-PL/Help/commands.tid +++ b/languages/pl-PL/Help/commands.tid @@ -10,7 +10,7 @@ Uruchamia komendy zwrócone przez filtr. Dla przykładu: ``` ---commands "[enlist{$:/build-commands-as-text}]" +--commands "[enlist:raw{$:/build-commands-as-text}]" ``` ``` From 8f661423f7a36257ffabc31ffc1b8a0eb95f1ae8 Mon Sep 17 00:00:00 2001 From: Bram Chen Date: Tue, 28 Nov 2023 06:01:25 +0800 Subject: [PATCH 24/45] Update chinese translations (#7859) * Update help for CommandsCommand --- languages/zh-Hans/Help/commands.tid | 2 +- languages/zh-Hant/Help/commands.tid | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/languages/zh-Hans/Help/commands.tid b/languages/zh-Hans/Help/commands.tid index 68f86f20c..5a6754ec6 100644 --- a/languages/zh-Hans/Help/commands.tid +++ b/languages/zh-Hans/Help/commands.tid @@ -10,7 +10,7 @@ description: 运行从筛选器传回的命令 示例 ``` ---commands "[enlist{$:/build-commands-as-text}]" +--commands "[enlist:raw{$:/build-commands-as-text}]" ``` ``` diff --git a/languages/zh-Hant/Help/commands.tid b/languages/zh-Hant/Help/commands.tid index afeaa1e49..eead14103 100644 --- a/languages/zh-Hant/Help/commands.tid +++ b/languages/zh-Hant/Help/commands.tid @@ -10,7 +10,7 @@ description: 執行從篩選器傳回的命令 範例 ``` ---commands "[enlist{$:/build-commands-as-text}]" +--commands "[enlist:raw{$:/build-commands-as-text}]" ``` ``` From a21f45b93a0935b6f84daba8b773771b21b59071 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Tue, 28 Nov 2023 10:53:38 +0000 Subject: [PATCH 25/45] Release note: fix typos --- editions/prerelease/tiddlers/Release 5.3.2.tid | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/editions/prerelease/tiddlers/Release 5.3.2.tid b/editions/prerelease/tiddlers/Release 5.3.2.tid index adcff9e67..ce1338ba0 100644 --- a/editions/prerelease/tiddlers/Release 5.3.2.tid +++ b/editions/prerelease/tiddlers/Release 5.3.2.tid @@ -106,7 +106,7 @@ Improvements to the following translations: * <<.link-badge-extended "https://github.com/Jermolene/TiddlyWiki5/pull/7769">> all the relevant core widgets to allow arbitrary `data-*` attributes and `style.*` attributes to be applied to the generated DOM nodes. This is useful for passing data to the EventCatcherWidget * <<.link-badge-extended "https://github.com/Jermolene/TiddlyWiki5/pull/7849">> [[jsonextract Operator]], [[jsonget Operator]], [[jsonset Operator]] and [[jsontype Operator]] to allow negative indexes into arrays to be counted from the end of the array * <<.link-badge-improved "https://github.com/Jermolene/TiddlyWiki5/pull/7690">> the default page layout to better support CSS grid and flexbox layouts -* <<.link-bage-improved "https://github.com/Jermolene/TiddlyWiki5/pull/7787">> the editor to use grid layout, simplifying customisation +* <<.link-badge-improved "https://github.com/Jermolene/TiddlyWiki5/pull/7787">> the editor to use grid layout, simplifying customisation ! Bug Fixes @@ -132,7 +132,7 @@ Improvements to the following translations: ! Node.js Improvements -* <> a significant flaw in the synchronisation algorithm used by the client-server configuration. The flaw could lead to tiddlers temporarily disappearing from the browser +* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/pull/7843">> a significant flaw in the synchronisation algorithm used by the client-server configuration. The flaw could lead to tiddlers temporarily disappearing from the browser ! Performance Improvements From 3b84088b27b33d8e2057e52f0cc6810f100c667e Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Tue, 28 Nov 2023 11:44:21 +0000 Subject: [PATCH 26/45] Syncer: Reduce logging intensity --- core/modules/syncer.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/modules/syncer.js b/core/modules/syncer.js index 58b087b8d..12781ad5a 100644 --- a/core/modules/syncer.js +++ b/core/modules/syncer.js @@ -425,7 +425,7 @@ Syncer.prototype.processTaskQueue = function() { if((!this.syncadaptor.isReady || this.syncadaptor.isReady()) && this.numTasksInProgress === 0) { // Choose the next task to perform var task = this.chooseNextTask(); - self.logger.log("Chosen next task " + task); + // self.logger.log("Chosen next task " + task); // Perform the task if we had one if(typeof task === "object" && task !== null) { this.numTasksInProgress += 1; @@ -551,14 +551,14 @@ SaveTiddlerTask.prototype.run = function(callback) { revision: revision, timestampLastSaved: new Date() }; - self.syncer.logger.log("Updating tiddler info in SaveTiddlerTask.run for " + self.title + " " + JSON.stringify(self.syncer.tiddlerInfo[self.title])); + // self.syncer.logger.log("Updating tiddler info in SaveTiddlerTask.run for " + self.title + " " + JSON.stringify(self.syncer.tiddlerInfo[self.title])); // Invoke the callback callback(null); },{ tiddlerInfo: self.syncer.tiddlerInfo[self.title] }); } else { - this.syncer.logger.log(" Not Dispatching 'save' task:",this.title,"tiddler does not exist"); + // this.syncer.logger.log(" Not Dispatching 'save' task:",this.title,"tiddler does not exist"); $tw.utils.nextTick(callback(null)); } }; @@ -582,7 +582,7 @@ DeleteTiddlerTask.prototype.run = function(callback) { return callback(err); } // Remove the info stored about this tiddler - self.syncer.logger.log("Deleting tiddler info in DeleteTiddlerTask.run for " + self.title); + // self.syncer.logger.log("Deleting tiddler info in DeleteTiddlerTask.run for " + self.title); delete self.syncer.tiddlerInfo[self.title]; // Invoke the callback callback(null); @@ -659,7 +659,7 @@ SyncFromServerTask.prototype.run = function(callback) { } else if(this.syncer.syncadaptor.getSkinnyTiddlers) { this.syncer.logger.log("Retrieving skinny tiddler list"); this.syncer.syncadaptor.getSkinnyTiddlers(function(err,tiddlers) { - self.syncer.logger.log("Retrieved skinny tiddler list"); + // self.syncer.logger.log("Retrieved skinny tiddler list"); // Check for errors if(err) { self.syncer.displayError($tw.language.getString("Error/RetrievingSkinny"),err); From 53d493b8766b4bb28fd3c3b1d75d40e330032e5b Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Tue, 28 Nov 2023 11:51:56 +0000 Subject: [PATCH 27/45] Conditional shortcut docs: highlight use of "condition" variable --- .../tw5.com/tiddlers/wikitext/Conditional Shortcut Syntax.tid | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/editions/tw5.com/tiddlers/wikitext/Conditional Shortcut Syntax.tid b/editions/tw5.com/tiddlers/wikitext/Conditional Shortcut Syntax.tid index 6cdfb1517..3ea99433e 100644 --- a/editions/tw5.com/tiddlers/wikitext/Conditional Shortcut Syntax.tid +++ b/editions/tw5.com/tiddlers/wikitext/Conditional Shortcut Syntax.tid @@ -6,6 +6,8 @@ type: text/vnd.tiddlywiki <<.from-version "5.3.2">> The conditional shortcut syntax provides a convenient way to express if-then-else logic within WikiText. It evaluates a filter and considers the condition to be true if there is at least one result (regardless of the value of that result). +Within an "if" or "elseif" clause, the variable `condition` contains the value of the first result of evaluating the filter condition. + A simple example: <$macrocall $name='wikitext-example-without-html' @@ -55,7 +57,6 @@ src='\procedure test(animal) Notes: * Clauses are parsed in inline mode by default. Force block mode parsing by following the opening `<% if %>`, `<% elseif %>` or `<% else %>` with two line breaks -* Within an "if" or "elseif" clause, the variable `condition` contains the value of the first result of evaluating the filter condition * Widgets and HTML elements must be within a single conditional clause; it is not possible to start an element in one conditional clause and end it in another * The conditional shortcut syntax cannot contain pragmas such as procedure definitions From c2e61fffe0fd2cb0abbff377cf72c211e02363b5 Mon Sep 17 00:00:00 2001 From: Mario Pietsch Date: Tue, 28 Nov 2023 14:49:13 +0100 Subject: [PATCH 28/45] German translations (#7864) --- languages/de-DE/Exporters.multids | 2 +- languages/de-DE/Help/commands.tid | 4 ++-- languages/de-DE/Help/server.tid | 2 +- languages/de-DE/Types/image_x-icon.tid | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/languages/de-DE/Exporters.multids b/languages/de-DE/Exporters.multids index 6103f370d..6663dd17b 100644 --- a/languages/de-DE/Exporters.multids +++ b/languages/de-DE/Exporters.multids @@ -3,4 +3,4 @@ title: $:/language/Exporters/ StaticRiver: HTML - Statisch JsonFile: JSON - Format CsvFile: CSV - Format -TidFile: .tid - Format +TidFile: TID - Text Format diff --git a/languages/de-DE/Help/commands.tid b/languages/de-DE/Help/commands.tid index b7de1b86f..55aefa16e 100644 --- a/languages/de-DE/Help/commands.tid +++ b/languages/de-DE/Help/commands.tid @@ -1,7 +1,7 @@ title: $:/language/Help/commands description: Ausführen von Befehlen aus einem Tiddler -Sequentielle Abarbeitung von Befehlen aus einem Tiddler. +Sequentielle Abarbeitung von Befehlen aus einem Tiddler. ``` --commands @@ -9,6 +9,6 @@ Sequentielle Abarbeitung von Befehlen aus einem Tiddler. Beispiele -`--commands "[enlist{$:/build-commands-as-text}]"` +`--commands "[enlist:raw{$:/build-commands-as-text}]"` `--commands "[{$:/build-commands-as-json}jsonindexes[]] :map[{$:/build-commands-as-json}jsonget]"` \ No newline at end of file diff --git a/languages/de-DE/Help/server.tid b/languages/de-DE/Help/server.tid index 94b40acc6..2bfbbdb47 100644 --- a/languages/de-DE/Help/server.tid +++ b/languages/de-DE/Help/server.tid @@ -1,5 +1,5 @@ title: $:/language/Help/server -description: Stellt einen HTTP server für TiddlyWiki zur Verfügung. (Dieser Befehl ist abgekündigt! - Neu ist: "listen") +description: (Dieser Befehl ist abgekündigt! - Neu ist: "listen") -- Stellt einen HTTP server für TiddlyWiki zur Verfügung. TiddlyWiki bringt einen einfachen Web-Server mit. diff --git a/languages/de-DE/Types/image_x-icon.tid b/languages/de-DE/Types/image_x-icon.tid index d75ef6fad..043071d97 100644 --- a/languages/de-DE/Types/image_x-icon.tid +++ b/languages/de-DE/Types/image_x-icon.tid @@ -1,4 +1,4 @@ title: $:/language/Docs/Types/image/x-icon -description: ICO - Piktogramm Format +description: ICO - Piktogramm (Icon) name: image/x-icon group: Bilder From 5dc468f1eae62b5656087cf4106ea60a46690466 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Tue, 28 Nov 2023 17:43:57 +0000 Subject: [PATCH 29/45] Fix release note typo Apologies again @oflg --- editions/prerelease/tiddlers/Release 5.3.2.tid | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editions/prerelease/tiddlers/Release 5.3.2.tid b/editions/prerelease/tiddlers/Release 5.3.2.tid index ce1338ba0..fcf9a5844 100644 --- a/editions/prerelease/tiddlers/Release 5.3.2.tid +++ b/editions/prerelease/tiddlers/Release 5.3.2.tid @@ -165,7 +165,7 @@ joebordes kookma linonetwo mateuszwilczek -oflig +oflg pille1842 pmario rmunn From fe17f16675431af8ab84e5847736db845c2c6865 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Wed, 29 Nov 2023 09:31:19 +0000 Subject: [PATCH 30/45] Fix syncer not exiting when used on CLI Fixes #7867 --- core/modules/syncer.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/core/modules/syncer.js b/core/modules/syncer.js index 12781ad5a..66beaf591 100644 --- a/core/modules/syncer.js +++ b/core/modules/syncer.js @@ -189,7 +189,7 @@ Syncer.prototype.readTiddlerInfo = function() { // Record information for known tiddlers var self = this, tiddlers = this.getSyncedTiddlers(); - this.logger.log("Initialising tiddlerInfo for " + tiddlers.length + " tiddlers"); + // this.logger.log("Initialising tiddlerInfo for " + tiddlers.length + " tiddlers"); $tw.utils.each(tiddlers,function(title) { var tiddler = self.wiki.getTiddler(title); if(tiddler) { @@ -302,10 +302,16 @@ Syncer.prototype.getStatus = function(callback) { Synchronise from the server by reading the skinny tiddler list and queuing up loads for any tiddlers that we don't already have up to date */ Syncer.prototype.syncFromServer = function() { - this.forceSyncFromServer = true; - this.processTaskQueue(); + if(this.canSyncFromServer()) { + this.forceSyncFromServer = true; + this.processTaskQueue(); + } }; +Syncer.prototype.canSyncFromServer = function() { + return !!this.syncadaptor.getUpdatedTiddlers || !!this.syncadaptor.getSkinnyTiddlers; +} + /* Force load a tiddler from the server */ @@ -447,7 +453,7 @@ Syncer.prototype.processTaskQueue = function() { // And trigger a timeout if there is a pending task if(task === true) { this.triggerTimeout(this.taskTimerInterval); - } else { + } else if(this.canSyncFromServer()) { this.triggerTimeout(this.pollTimerInterval); } } From fc1e9b6c434b774159dbbe77b17075526bd6d293 Mon Sep 17 00:00:00 2001 From: Mateusz Wilczek <36714554+mateuszwilczek@users.noreply.github.com> Date: Wed, 29 Nov 2023 11:06:47 +0100 Subject: [PATCH 31/45] Update forum link in update wizard (#7865) * Update forum link in upgrade wizard * Update links to forum in es-ES and de-AT editons --- editions/de-AT/tiddlers/HelloThere.tid | 4 ++-- editions/de-AT/tiddlers/community/Fur_Entwickler.tid | 2 +- editions/es-ES/tiddlers/Forums.tid | 9 ++++----- editions/es-ES/tiddlers/HelloThere.tid | 4 ++-- editions/es-ES/tiddlers/Typography.tid | 2 +- plugins/tiddlywiki/upgrade/UpgradeWizard.tid | 2 +- 6 files changed, 11 insertions(+), 12 deletions(-) diff --git a/editions/de-AT/tiddlers/HelloThere.tid b/editions/de-AT/tiddlers/HelloThere.tid index 60c2147f8..aa8bf52e7 100644 --- a/editions/de-AT/tiddlers/HelloThere.tid +++ b/editions/de-AT/tiddlers/HelloThere.tid @@ -21,8 +21,8 @@ Willkommen bei ''~TiddlyWiki'', dem einzigartigen [[nicht-linearen|Philosophy vo Anders, als bei herkömmlichen Online-Diensten, lässt Ihnen ~TiddlyWiki die Freiheit, wo sie ihre Daten speichern. Da ~TiddlyWiki alle Daten als simplen Text speichert, sind Notizen, die Sie heute machen, garantiert in Jahrzehnten noch einfach lesbar.
- -{{$:/core/images/mail}} ~TiddlyWiki Mailing List + +{{$:/core/images/help}} ~TiddlyWiki Forum {{$:/core/images/twitter}} @~TiddlyWiki on Twitter diff --git a/editions/de-AT/tiddlers/community/Fur_Entwickler.tid b/editions/de-AT/tiddlers/community/Fur_Entwickler.tid index 0c656c8dc..2dbc185d7 100644 --- a/editions/de-AT/tiddlers/community/Fur_Entwickler.tid +++ b/editions/de-AT/tiddlers/community/Fur_Entwickler.tid @@ -7,5 +7,5 @@ type: text/vnd.tiddlywiki Es gibt mehrere Ressourcen für Entwickler, um mehr über das TiddlyWiki Projekt zu erfahren, zu diskutieren und vor allem mitzuhelfen. * [[tiddlywiki.com/dev|https://tiddlywiki.com/dev]] Offizielle Entwickler Doku. -* [[TiddlyWikiDev group|http://groups.google.com/group/TiddlyWikiDev]] Google Diskussionsforum für Entwickler. +* [[TiddlyWikiDev group|https://talk.tiddlywiki.org/c/devs/]] Diskussionsforum für Entwickler. * https://github.com/Jermolene/TiddlyWiki5 .. Github Repository. diff --git a/editions/es-ES/tiddlers/Forums.tid b/editions/es-ES/tiddlers/Forums.tid index 63ecc4339..57af7d0f2 100644 --- a/editions/es-ES/tiddlers/Forums.tid +++ b/editions/es-ES/tiddlers/Forums.tid @@ -12,7 +12,9 @@ Son listas de correo en las que hablamos de ~TiddlyWiki: pedimos ayuda, anunciam Puedes participar a través de la página web asociada, o suscribirte via mail. -!!Usuarios +!! Usuarios + +[[Foro oficial de TiddlyWiki| https://talk.tiddlywiki.org/]] [[Grupo principal de TiddlyWiki| http://groups.google.com/group/TiddlyWiki]] @@ -25,10 +27,7 @@ o síguenos [[en Twitter|http://twitter.com/TiddlyWiki]] si quieres recibir las !! Desarrolladores -[[Grupo de desarrollo de TiddlyWiki|http://groups.google.com/group/TiddlyWikiDev]] - ->No necesitas tener cuenta en Google para acceder al grupo. Suscríbete igualmente enviando un mail a: -*mailto:tiddlywikidev+subscribe@googlegroups.com. +[[Foro de desarrollo de TiddlyWiki|https://talk.tiddlywiki.org/c/devs]] Accede a nuestra [[página de desarrollo|https://github.com/Jermolene/TiddlyWiki5]] en GitHub y haz tu contribución. diff --git a/editions/es-ES/tiddlers/HelloThere.tid b/editions/es-ES/tiddlers/HelloThere.tid index 0b1cea4bb..c08a27e7d 100644 --- a/editions/es-ES/tiddlers/HelloThere.tid +++ b/editions/es-ES/tiddlers/HelloThere.tid @@ -20,8 +20,8 @@ BIenvenido a TiddlyWiki, un bloc de notas [[no lineal|Philosophy of Tiddlers]] Al revés que los servicios online convencionales, TiddlyWiki te deja escoger dónde quieres guardar tus datos, garantizándote que, por más que pase el tiempo, podrás seguir usando en el futuro las notas que tomes hoy.
- -{{$:/core/images/mail}} ~TiddlyWiki en Google Groups + +{{$:/core/images/mail}} Foro oficial de ~TiddlyWiki {{$:/core/images/video}} ~TiddlyWiki en ~YouTube diff --git a/editions/es-ES/tiddlers/Typography.tid b/editions/es-ES/tiddlers/Typography.tid index 58edf9220..47d3a05ce 100644 --- a/editions/es-ES/tiddlers/Typography.tid +++ b/editions/es-ES/tiddlers/Typography.tid @@ -8,7 +8,7 @@ type: text/vnd.tiddlywiki Se recomienda el uso de las [[macros de documentación|Documentation Macros]] para facilitar las futuras tareas de mantenimiento del texto frente a nuevos cambios y actualizaciones. -Se recomienda precaución en el uso arbitrario de estilos directos de formato (''negrita'', //cursiva// ...etc). Si se puede usar una macro, conviene usarla. Si no existe la macro adecuada, se puede crear o, si no se sabe cómo, pedir su creación en el [[Grupo de Google|http://groups.google.com/group/TiddlyWiki]]. +Se recomienda precaución en el uso arbitrario de estilos directos de formato (''negrita'', //cursiva// ...etc). Si se puede usar una macro, conviene usarla. Si no existe la macro adecuada, se puede crear o, si no se sabe cómo, pedir su creación en el [[Foro de TiddlyWiki|https://talk.tiddlywiki.org/]]. Por el mismo motivo, se aconseja el uso de acentos graves `...` para transcribir fragmentos de código y ~WikiText, pero no para nombres de cosas tales como campos, operadores, variables o widgets. Estas tienen su macro correspondiente. diff --git a/plugins/tiddlywiki/upgrade/UpgradeWizard.tid b/plugins/tiddlywiki/upgrade/UpgradeWizard.tid index 922441bd6..8e398156a 100644 --- a/plugins/tiddlywiki/upgrade/UpgradeWizard.tid +++ b/plugins/tiddlywiki/upgrade/UpgradeWizard.tid @@ -44,7 +44,7 @@ Make sure that you keep a safe copy of your previous ~TiddlyWiki file. Close this browser window to prevent others from being able to access your data. -For help and support, visit [[the TiddlyWiki discussion forum|http://groups.google.com/group/TiddlyWiki]]. +For help and support, visit [[the TiddlyWiki discussion forum|https://talk.tiddlywiki.org]]. From f56f5dcc5655563cda7928aea7f65359857024fd Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Wed, 29 Nov 2023 11:23:57 +0000 Subject: [PATCH 32/45] Fix savetiddlers handling of tiddlers with no text field --- core/modules/commands/savetiddlers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/modules/commands/savetiddlers.js b/core/modules/commands/savetiddlers.js index d3b82d726..9c750e204 100644 --- a/core/modules/commands/savetiddlers.js +++ b/core/modules/commands/savetiddlers.js @@ -46,7 +46,7 @@ Command.prototype.execute = function() { type = tiddler.fields.type || "text/vnd.tiddlywiki", contentTypeInfo = $tw.config.contentTypeInfo[type] || {encoding: "utf8"}, filename = path.resolve(pathname,$tw.utils.encodeURIComponentExtended(title)); - fs.writeFileSync(filename,tiddler.fields.text,contentTypeInfo.encoding); + fs.writeFileSync(filename,tiddler.fields.text || "",contentTypeInfo.encoding); }); return null; }; From 622512c380e5855fb5bba5d01119c2749e8e2618 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Wed, 29 Nov 2023 11:24:54 +0000 Subject: [PATCH 33/45] Further reduce syncer logging The rationale is that the deeper logs are only useful for debugging the syncer logic, and are overwhelming for most users --- core/modules/syncer.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/core/modules/syncer.js b/core/modules/syncer.js index 66beaf591..9769d9674 100644 --- a/core/modules/syncer.js +++ b/core/modules/syncer.js @@ -189,7 +189,6 @@ Syncer.prototype.readTiddlerInfo = function() { // Record information for known tiddlers var self = this, tiddlers = this.getSyncedTiddlers(); - // this.logger.log("Initialising tiddlerInfo for " + tiddlers.length + " tiddlers"); $tw.utils.each(tiddlers,function(title) { var tiddler = self.wiki.getTiddler(title); if(tiddler) { @@ -236,7 +235,6 @@ Syncer.prototype.isDirty = function() { return false; } var dirtyStatus = checkIsDirty(); - this.logger.log("Dirty status was " + dirtyStatus); return dirtyStatus; }; @@ -266,7 +264,6 @@ Syncer.prototype.storeTiddler = function(tiddlerFields) { adaptorInfo: this.syncadaptor.getTiddlerInfo(tiddler), changeCount: this.wiki.getChangeCount(tiddlerFields.title) }; - this.logger.log("Updating tiddler info in syncer.storeTiddler for " + tiddlerFields.title + " " + JSON.stringify(this.tiddlerInfo[tiddlerFields.title])); }; Syncer.prototype.getStatus = function(callback) { @@ -431,7 +428,6 @@ Syncer.prototype.processTaskQueue = function() { if((!this.syncadaptor.isReady || this.syncadaptor.isReady()) && this.numTasksInProgress === 0) { // Choose the next task to perform var task = this.chooseNextTask(); - // self.logger.log("Chosen next task " + task); // Perform the task if we had one if(typeof task === "object" && task !== null) { this.numTasksInProgress += 1; @@ -557,14 +553,12 @@ SaveTiddlerTask.prototype.run = function(callback) { revision: revision, timestampLastSaved: new Date() }; - // self.syncer.logger.log("Updating tiddler info in SaveTiddlerTask.run for " + self.title + " " + JSON.stringify(self.syncer.tiddlerInfo[self.title])); // Invoke the callback callback(null); },{ tiddlerInfo: self.syncer.tiddlerInfo[self.title] }); } else { - // this.syncer.logger.log(" Not Dispatching 'save' task:",this.title,"tiddler does not exist"); $tw.utils.nextTick(callback(null)); } }; @@ -588,7 +582,6 @@ DeleteTiddlerTask.prototype.run = function(callback) { return callback(err); } // Remove the info stored about this tiddler - // self.syncer.logger.log("Deleting tiddler info in DeleteTiddlerTask.run for " + self.title); delete self.syncer.tiddlerInfo[self.title]; // Invoke the callback callback(null); @@ -642,7 +635,6 @@ SyncFromServerTask.prototype.run = function(callback) { callback(null); }; if(this.syncer.syncadaptor.getUpdatedTiddlers) { - self.syncer.logger.log("Retrieving updated tiddler list"); this.syncer.syncadaptor.getUpdatedTiddlers(self,function(err,updates) { if(err) { self.syncer.displayError($tw.language.getString("Error/RetrievingSkinny"),err); @@ -663,9 +655,7 @@ SyncFromServerTask.prototype.run = function(callback) { return successCallback(); }); } else if(this.syncer.syncadaptor.getSkinnyTiddlers) { - this.syncer.logger.log("Retrieving skinny tiddler list"); this.syncer.syncadaptor.getSkinnyTiddlers(function(err,tiddlers) { - // self.syncer.logger.log("Retrieved skinny tiddler list"); // Check for errors if(err) { self.syncer.displayError($tw.language.getString("Error/RetrievingSkinny"),err); From c282208668ee89af07b81cf93a21ba68430affc0 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Wed, 29 Nov 2023 12:06:40 +0000 Subject: [PATCH 34/45] Fix jsonset crash when applied to primitive types See https://talk.tiddlywiki.org/t/final-checks-before-release-of-v5-3-2/8560/7 --- core/modules/filters/json-ops.js | 5 ++++- editions/test/tiddlers/tests/test-json-filters.js | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/core/modules/filters/json-ops.js b/core/modules/filters/json-ops.js index 75a34e94a..0c58964eb 100644 --- a/core/modules/filters/json-ops.js +++ b/core/modules/filters/json-ops.js @@ -273,7 +273,10 @@ function setDataItem(data,indexes,value) { lastIndex = $tw.utils.parseInt(lastIndex); if(lastIndex < 0) { lastIndex = lastIndex + current.length }; } - current[lastIndex] = value; + // Only set indexes on objects and arrays + if(typeof current === "object") { + current[lastIndex] = value; + } return data; } diff --git a/editions/test/tiddlers/tests/test-json-filters.js b/editions/test/tiddlers/tests/test-json-filters.js index a8903970a..bfb8a4504 100644 --- a/editions/test/tiddlers/tests/test-json-filters.js +++ b/editions/test/tiddlers/tests/test-json-filters.js @@ -124,6 +124,7 @@ describe("json filter tests", function() { }); it("should support the jsonset operator", function() { + expect(wiki.filterTiddlers("[jsonset[a],[aa]]")).toEqual(['"First"','"Second"','"Third"']); expect(wiki.filterTiddlers("[{First}jsonset[]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]}}']); expect(wiki.filterTiddlers("[{First}jsonset[],[Antelope]]")).toEqual(['"Antelope"']); expect(wiki.filterTiddlers("[{First}jsonset:number[],[not a number]]")).toEqual(['0']); From 6b47cbed32005382b274525f76fb8323ed2cfdb4 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Wed, 29 Nov 2023 14:36:58 +0000 Subject: [PATCH 35/45] Scrollable: write bound value if title of bound tiddler changes Thanks @yaisog --- core/modules/widgets/scrollable.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/modules/widgets/scrollable.js b/core/modules/widgets/scrollable.js index 58597461b..7733308a8 100644 --- a/core/modules/widgets/scrollable.js +++ b/core/modules/widgets/scrollable.js @@ -183,7 +183,7 @@ ScrollableWidget.prototype.render = function(parent,nextSibling) { "scroll-left": self.outerDomNode.scrollLeft.toString(), "scroll-top": self.outerDomNode.scrollTop.toString() }; - if(!existingTiddler || (existingTiddler.fields["scroll-left"] !== newTiddlerFields["scroll-left"] || existingTiddler.fields["scroll-top"] !== newTiddlerFields["scroll-top"])) { + if(!existingTiddler || (existingTiddler.fields["title"] !== newTiddlerFields["title"]) || (existingTiddler.fields["scroll-left"] !== newTiddlerFields["scroll-left"] || existingTiddler.fields["scroll-top"] !== newTiddlerFields["scroll-top"])) { self.wiki.addTiddler(new $tw.Tiddler(existingTiddler,newTiddlerFields)); } }); From c61c34e9df09bab7215a4c0b7a5c04d239341088 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Wed, 29 Nov 2023 14:45:34 +0000 Subject: [PATCH 36/45] Debounce scrollable widget scroll handler --- core/modules/widgets/scrollable.js | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/core/modules/widgets/scrollable.js b/core/modules/widgets/scrollable.js index 7733308a8..c2acc563d 100644 --- a/core/modules/widgets/scrollable.js +++ b/core/modules/widgets/scrollable.js @@ -176,16 +176,23 @@ ScrollableWidget.prototype.render = function(parent,nextSibling) { // After a delay for rendering, scroll to the bound position setTimeout(this.updateScrollPositionFromBoundTiddler.bind(this),50); // Save scroll position on DOM scroll event + var timeout; this.outerDomNode.addEventListener("scroll",function(event) { - var existingTiddler = self.wiki.getTiddler(self.scrollableBind), - newTiddlerFields = { - title: self.scrollableBind, - "scroll-left": self.outerDomNode.scrollLeft.toString(), - "scroll-top": self.outerDomNode.scrollTop.toString() - }; - if(!existingTiddler || (existingTiddler.fields["title"] !== newTiddlerFields["title"]) || (existingTiddler.fields["scroll-left"] !== newTiddlerFields["scroll-left"] || existingTiddler.fields["scroll-top"] !== newTiddlerFields["scroll-top"])) { - self.wiki.addTiddler(new $tw.Tiddler(existingTiddler,newTiddlerFields)); + if(timeout) { + window.cancelAnimationFrame(timeout); + timeout = null; } + timeout = window.requestAnimationFrame(function() { + var existingTiddler = self.wiki.getTiddler(self.scrollableBind), + newTiddlerFields = { + title: self.scrollableBind, + "scroll-left": self.outerDomNode.scrollLeft.toString(), + "scroll-top": self.outerDomNode.scrollTop.toString() + }; + if(!existingTiddler || (existingTiddler.fields["title"] !== newTiddlerFields["title"]) || (existingTiddler.fields["scroll-left"] !== newTiddlerFields["scroll-left"] || existingTiddler.fields["scroll-top"] !== newTiddlerFields["scroll-top"])) { + self.wiki.addTiddler(new $tw.Tiddler(existingTiddler,newTiddlerFields)); + } + }); }); } }; From f7359671aa827c868b896def60fe2e903dd974e9 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Wed, 29 Nov 2023 18:06:54 +0000 Subject: [PATCH 37/45] Defer scrollable widget updating bound tiddler for 100ms See discussion https://talk.tiddlywiki.org/t/5-3-2pre-scroll-binding/8570/3?u=jeremyruston --- core/modules/widgets/scrollable.js | 8 +++++--- editions/tw5.com/tiddlers/widgets/ScrollableWidget.tid | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/core/modules/widgets/scrollable.js b/core/modules/widgets/scrollable.js index c2acc563d..b77d6a12b 100644 --- a/core/modules/widgets/scrollable.js +++ b/core/modules/widgets/scrollable.js @@ -12,6 +12,8 @@ Scrollable widget /*global $tw: false */ "use strict"; +var DEBOUNCE_INTERVAL = 100; // Delay after last scroll event before updating the bound tiddler + var Widget = require("$:/core/modules/widgets/widget.js").widget; var ScrollableWidget = function(parseTreeNode,options) { @@ -179,10 +181,10 @@ ScrollableWidget.prototype.render = function(parent,nextSibling) { var timeout; this.outerDomNode.addEventListener("scroll",function(event) { if(timeout) { - window.cancelAnimationFrame(timeout); + clearTimeout(timeout); timeout = null; } - timeout = window.requestAnimationFrame(function() { + timeout = setTimeout(function() { var existingTiddler = self.wiki.getTiddler(self.scrollableBind), newTiddlerFields = { title: self.scrollableBind, @@ -192,7 +194,7 @@ ScrollableWidget.prototype.render = function(parent,nextSibling) { if(!existingTiddler || (existingTiddler.fields["title"] !== newTiddlerFields["title"]) || (existingTiddler.fields["scroll-left"] !== newTiddlerFields["scroll-left"] || existingTiddler.fields["scroll-top"] !== newTiddlerFields["scroll-top"])) { self.wiki.addTiddler(new $tw.Tiddler(existingTiddler,newTiddlerFields)); } - }); + },DEBOUNCE_INTERVAL); }); } }; diff --git a/editions/tw5.com/tiddlers/widgets/ScrollableWidget.tid b/editions/tw5.com/tiddlers/widgets/ScrollableWidget.tid index d31eb6e31..6c52f0025 100644 --- a/editions/tw5.com/tiddlers/widgets/ScrollableWidget.tid +++ b/editions/tw5.com/tiddlers/widgets/ScrollableWidget.tid @@ -18,7 +18,7 @@ The content of the `<$scrollable>` widget is displayed within a pair of wrapper |fallthrough |See below | |bind |<<.from-version "5.3.2">> Optional title of tiddler to which the scroll position should be bound | -Binding the scroll position to a tiddler automatically copies the scroll coordinates into the `scroll-left` and `scroll-top` fields as scrolling occurs. Conversely, setting those field values will automatically cause the scrollable to scroll if it can. +Binding the scroll position to a tiddler automatically copies the scroll coordinates into the `scroll-left` and `scroll-top` fields after scrolling occurs. Conversely, setting those field values will automatically cause the scrollable to scroll if it can. <$macrocall $name=".note" _="""If a scrollable widget can't handle the `tm-scroll` message because the inner DIV fits within the outer DIV, then by default the message falls through to the parent widget. Setting the ''fallthrough'' attribute to `no` prevents this behaviour."""/> From e60ddf0b0ab668201997b06c64b94577673622a6 Mon Sep 17 00:00:00 2001 From: yaisog Date: Thu, 30 Nov 2023 19:26:26 +0100 Subject: [PATCH 38/45] Handle switching the bound tiddler (#7868) --- core/modules/widgets/scrollable.js | 53 ++++++++++++++++++------------ 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/core/modules/widgets/scrollable.js b/core/modules/widgets/scrollable.js index b77d6a12b..f6cb5e67b 100644 --- a/core/modules/widgets/scrollable.js +++ b/core/modules/widgets/scrollable.js @@ -177,28 +177,28 @@ ScrollableWidget.prototype.render = function(parent,nextSibling) { if(this.scrollableBind) { // After a delay for rendering, scroll to the bound position setTimeout(this.updateScrollPositionFromBoundTiddler.bind(this),50); - // Save scroll position on DOM scroll event - var timeout; - this.outerDomNode.addEventListener("scroll",function(event) { - if(timeout) { - clearTimeout(timeout); - timeout = null; - } - timeout = setTimeout(function() { - var existingTiddler = self.wiki.getTiddler(self.scrollableBind), - newTiddlerFields = { - title: self.scrollableBind, - "scroll-left": self.outerDomNode.scrollLeft.toString(), - "scroll-top": self.outerDomNode.scrollTop.toString() - }; - if(!existingTiddler || (existingTiddler.fields["title"] !== newTiddlerFields["title"]) || (existingTiddler.fields["scroll-left"] !== newTiddlerFields["scroll-left"] || existingTiddler.fields["scroll-top"] !== newTiddlerFields["scroll-top"])) { - self.wiki.addTiddler(new $tw.Tiddler(existingTiddler,newTiddlerFields)); - } - },DEBOUNCE_INTERVAL); - }); + // Set up event listener + this.currentListener = this.listenerFunction.bind(this); + this.outerDomNode.addEventListener("scroll", this.currentListener); } }; +ScrollableWidget.prototype.listenerFunction = function(event) { + self = this; + clearTimeout(this.timeout); + this.timeout = setTimeout(function() { + var existingTiddler = self.wiki.getTiddler(self.scrollableBind), + newTiddlerFields = { + title: self.scrollableBind, + "scroll-left": self.outerDomNode.scrollLeft.toString(), + "scroll-top": self.outerDomNode.scrollTop.toString() + }; + if(!existingTiddler || (existingTiddler.fields["title"] !== newTiddlerFields["title"]) || (existingTiddler.fields["scroll-left"] !== newTiddlerFields["scroll-left"] || existingTiddler.fields["scroll-top"] !== newTiddlerFields["scroll-top"])) { + self.wiki.addTiddler(new $tw.Tiddler(existingTiddler,newTiddlerFields)); + } + }, DEBOUNCE_INTERVAL); +} + ScrollableWidget.prototype.updateScrollPositionFromBoundTiddler = function() { // Bail if we're running on the fakedom if(!this.outerDomNode.scrollTo) { @@ -243,8 +243,19 @@ ScrollableWidget.prototype.refresh = function(changedTiddlers) { this.refreshSelf(); return true; } - if(changedAttributes.bind || changedTiddlers[this.getAttribute("bind")]) { - this.updateScrollPositionFromBoundTiddler(); + // If the bound tiddler has changed, update the eventListener and update scroll position + if(changedAttributes["bind"]) { + if(this.currentListener) { + this.outerDomNode.removeEventListener("scroll", this.currentListener, false); + } + this.scrollableBind = this.getAttribute("bind"); + this.currentListener = this.listenerFunction.bind(this); + this.outerDomNode.addEventListener("scroll", this.currentListener); + setTimeout(this.updateScrollPositionFromBoundTiddler.bind(this),50); + } + // If a new scroll position was written into the tiddler, update scroll position + if(changedTiddlers[this.getAttribute("bind")]) { + setTimeout(this.updateScrollPositionFromBoundTiddler.bind(this),50); } return this.refreshChildren(changedTiddlers); }; From 4e67aafeb784265a8304f6de976089cdd106e9bf Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Sat, 2 Dec 2023 08:58:35 +0000 Subject: [PATCH 39/45] Scrollable hotfix: Avoid setTimeout See #7869 --- core/modules/widgets/scrollable.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/core/modules/widgets/scrollable.js b/core/modules/widgets/scrollable.js index f6cb5e67b..227c455c3 100644 --- a/core/modules/widgets/scrollable.js +++ b/core/modules/widgets/scrollable.js @@ -176,7 +176,7 @@ ScrollableWidget.prototype.render = function(parent,nextSibling) { // If the scroll position is bound to a tiddler if(this.scrollableBind) { // After a delay for rendering, scroll to the bound position - setTimeout(this.updateScrollPositionFromBoundTiddler.bind(this),50); + this.updateScrollPositionFromBoundTiddler(); // Set up event listener this.currentListener = this.listenerFunction.bind(this); this.outerDomNode.addEventListener("scroll", this.currentListener); @@ -251,13 +251,14 @@ ScrollableWidget.prototype.refresh = function(changedTiddlers) { this.scrollableBind = this.getAttribute("bind"); this.currentListener = this.listenerFunction.bind(this); this.outerDomNode.addEventListener("scroll", this.currentListener); - setTimeout(this.updateScrollPositionFromBoundTiddler.bind(this),50); } - // If a new scroll position was written into the tiddler, update scroll position - if(changedTiddlers[this.getAttribute("bind")]) { - setTimeout(this.updateScrollPositionFromBoundTiddler.bind(this),50); + // Refresh children + var result = this.refreshChildren(changedTiddlers); + // If the bound tiddler has changed, update scroll position + if(changedAttributes["bind"] || changedTiddlers[this.getAttribute("bind")]) { + this.updateScrollPositionFromBoundTiddler(); } - return this.refreshChildren(changedTiddlers); + return result; }; exports.scrollable = ScrollableWidget; From 155db0f6f8a7bf65cdc2030b6f489915a82c13d1 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Mon, 4 Dec 2023 08:13:23 +0000 Subject: [PATCH 40/45] Improve release note --- editions/prerelease/tiddlers/Release 5.3.2.tid | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/editions/prerelease/tiddlers/Release 5.3.2.tid b/editions/prerelease/tiddlers/Release 5.3.2.tid index fcf9a5844..e1194c06c 100644 --- a/editions/prerelease/tiddlers/Release 5.3.2.tid +++ b/editions/prerelease/tiddlers/Release 5.3.2.tid @@ -12,7 +12,9 @@ description: Under development !! Conditional Shortcut Syntax -<<.link-badge-added "https://github.com/Jermolene/TiddlyWiki5/pull/7710">> a new [[shortcut syntax|Conditional Shortcut Syntax]] for concisely expressing if-then-else logic. For example: +<<.link-badge-added "https://github.com/Jermolene/TiddlyWiki5/pull/7710">> a new [[shortcut syntax|Conditional Shortcut Syntax]] for concisely expressing if-then-else logic. This is the first of a new type of wikitext syntax based on tokens delimited with `<%` and `%>`. We plan to introduce other structures using the same format such as a "case" statement. + +These new token-based shortcuts allow a richer structure and expressivity than existing features such as widgets or pragmas. For example: ``` <% if [match[Elephant]] %> @@ -24,9 +26,15 @@ description: Under development <% endif %> ``` +Behind the scenes, the conditional shortcut syntax is rendered as the equivalent [[ListWidgets|ListWidget]]. + !! Explicit Templates for the ListWidget -<<.link-badge-added "https://github.com/Jermolene/TiddlyWiki5/pull/7784">> support for `<$list-template>` and `<$list-empty>` as immediate children of the <<.wid "ListWidget">> widget to specify the list item template and/or the empty template. For example: +<<.link-badge-added "https://github.com/Jermolene/TiddlyWiki5/pull/7784">> support for `<$list-template>` and `<$list-empty>` as immediate children of the <<.wid "ListWidget">> widget to specify the list item template and/or the empty template. + +This new feature is designed to replace a common pattern of using the `emptyMessage` attribute of the ListWidget to render complex wikitext that thus has to be quoted. Working with wikitext within quotes is awkward and error prone. The new structure can be somewhat faster because it allows the empty message to be parsed in advanced of rendering. + +For example: ``` <$list filter=<>> From 2b0675cac5c7e5467d375a71ca112e7f625ebe8b Mon Sep 17 00:00:00 2001 From: Saq Imtiaz Date: Mon, 4 Dec 2023 09:53:24 +0100 Subject: [PATCH 41/45] Docs: fixes typos in conditonal shortcut syntax docs (#7872) * Docs: Conditional Shortcut Syntax corrections * Update Conditional Shortcut Syntax.tid Add a link to Filter Expression tiddler --- .../tiddlers/wikitext/Conditional Shortcut Syntax.tid | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/editions/tw5.com/tiddlers/wikitext/Conditional Shortcut Syntax.tid b/editions/tw5.com/tiddlers/wikitext/Conditional Shortcut Syntax.tid index 3ea99433e..8cef3acfb 100644 --- a/editions/tw5.com/tiddlers/wikitext/Conditional Shortcut Syntax.tid +++ b/editions/tw5.com/tiddlers/wikitext/Conditional Shortcut Syntax.tid @@ -4,14 +4,14 @@ tags: WikiText title: Conditional Shortcut Syntax type: text/vnd.tiddlywiki -<<.from-version "5.3.2">> The conditional shortcut syntax provides a convenient way to express if-then-else logic within WikiText. It evaluates a filter and considers the condition to be true if there is at least one result (regardless of the value of that result). +<<.from-version "5.3.2">> The conditional shortcut syntax provides a convenient way to express if-then-else logic within WikiText. It evaluates a [[filter expression|Filter Expression]] and considers the condition to be true if there is at least one result (regardless of the value of that result). Within an "if" or "elseif" clause, the variable `condition` contains the value of the first result of evaluating the filter condition. A simple example: <$macrocall $name='wikitext-example-without-html' -src='<% if [{$:/$:/info/url/protocol}match[file:]] %> +src='<% if [{$:/info/url/protocol}match[file:]] %> Loaded from a file URI <% else %> Not loaded from a file URI @@ -21,11 +21,11 @@ src='<% if [{$:/$:/info/url/protocol}match[file:]] %> One or more `<% elseif %>` clauses may be included before the `<% else %>` clause: <$macrocall $name='wikitext-example-without-html' -src='<% if [{$:/$:/info/url/protocol}match[file:]] %> +src='<% if [{$:/info/url/protocol}match[file:]] %> Loaded from a file URI -<% elseif [{$:/$:/info/url/protocol}match[https:]] %> +<% elseif [{$:/info/url/protocol}match[https:]] %> Loaded from an HTTPS URI -<% elseif [{$:/$:/info/url/protocol}match[http:]] %> +<% elseif [{$:/info/url/protocol}match[http:]] %> Loaded from an HTTP URI <% else %> Loaded from an unknown protocol From 5578fa5f942011861d9f18494600197316d0cdeb Mon Sep 17 00:00:00 2001 From: Mateusz Wilczek <36714554+mateuszwilczek@users.noreply.github.com> Date: Mon, 4 Dec 2023 16:24:33 +0100 Subject: [PATCH 42/45] Improve `jsonset` operator docs (#7873) * Update docs of jsonset operator * Move jsonset examples into a separate tiddler * Update jsonset operator docs --- .../tiddlers/filters/examples/jsonset.tid | 59 +++++++++++++ editions/tw5.com/tiddlers/filters/jsonset.tid | 88 +++++-------------- 2 files changed, 83 insertions(+), 64 deletions(-) create mode 100644 editions/tw5.com/tiddlers/filters/examples/jsonset.tid diff --git a/editions/tw5.com/tiddlers/filters/examples/jsonset.tid b/editions/tw5.com/tiddlers/filters/examples/jsonset.tid new file mode 100644 index 000000000..8cd1d1d61 --- /dev/null +++ b/editions/tw5.com/tiddlers/filters/examples/jsonset.tid @@ -0,0 +1,59 @@ +created: 20231204112944341 +modified: 20231204115056732 +tags: [[Operator Examples]] [[jsonset Operator]] +title: jsonset Operator (Examples) + +<$let object-a="""{ + "a": "one", + "b": "", + "c": "three", + "d": { + "e": "four", + "f": [ + "five", + "six", + true, + false, + null + ], + "g": { + "x": "max", + "y": "may", + "z": "maize" + } + } +} +""" +object-b="""{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]}}"""> + +The examples below assume the following JSON object is contained in the variable `object-a`: + +
<>
+ +<<.operator-example 1 "[jsonset[d],[Jaguar]]">> +<<.operator-example 2 "[jsonset[d],[f],[Panther]]">> +<<.operator-example 3 "[jsonset[d],[f],[-1],[Elephant]]">> +<<.operator-example 4 "[jsonset[d],[f],[-2],[Elephant]]">> +<<.operator-example 5 "[jsonset[d],[f],[-4],[Elephant]]">> +<<.operator-example 6 "[jsonset[Panther]]" "If only a single parameter is specified, it replaces the entire JSON object">> +<<.operator-example 7 "[jsonset[]]" "If only a single blank parameter is specified, no changes are made to the JSON object">> + + +The examples below assume the following JSON object is contained in the variable `object-b`: + +
<>
+ +<<.operator-example 8 "[jsonset[]]" "If only a single blank parameter is specified, no changes are made to the JSON object">> +<<.operator-example 9 "[jsonset[],[Antelope]]" "If the property to be set is blank, the entire JSON object is replaced">> +<<.operator-example 10 "[jsonset:number[],[not a number]]" "invalid numbers are interpreted as zero">> +<<.operator-example 11 "[jsonset[id],[Antelope]]" "nonexistent top level properties are added to the object">> +<<.operator-example 19 "[jsonset[missing],[id],[Antelope]]" "nonexistent nested properties are are ignored">> +<<.operator-example 12 "[jsonset:notatype[id],[Antelope]]" "invalid type suffix is interpreted as the default string type">> +<<.operator-example 13 "[jsonset:boolean[id],[false]]">> +<<.operator-example 14 "[jsonset:boolean[id],[Antelope]]" "invalid boolean value causes no assignment to be made">> +<<.operator-example 15 "[jsonset:number[id],[42]]">> +<<.operator-example 16 "[jsonset:null[id]]">> +<<.operator-example 17 "[jsonset:array[d],[f],[5]]">> +<<.operator-example 18 "[jsonset:object[d],[f],[5]]">> + +<<.operator-example 20 "[] [] :and[jsonset[b],[two]]" "If the input consists of multiple JSON objects with matching properties, the value is set for all of them">> \ No newline at end of file diff --git a/editions/tw5.com/tiddlers/filters/jsonset.tid b/editions/tw5.com/tiddlers/filters/jsonset.tid index 81552c7a1..1cfd076c2 100644 --- a/editions/tw5.com/tiddlers/filters/jsonset.tid +++ b/editions/tw5.com/tiddlers/filters/jsonset.tid @@ -1,22 +1,31 @@ +caption: jsonset created: 20230915121010948 -modified: 20230915121010948 +modified: 20231204115203428 +op-input: a selection of JSON objects +op-output: the JSON objects with the specified value assigned to the specified property +op-parameter: one or more indexes of the property to modify, if applicable followed by the value to be assigned +op-purpose: set the value of a property in JSON objects +op-suffix: data type of the value to be assigned to the property tags: [[Filter Operators]] [[JSON Operators]] title: jsonset Operator -caption: jsonset -op-purpose: set the value of a property in JSON strings -op-input: a selection of JSON strings -op-parameter: one or more indexes of the property to retrieve and sometimes a value to assign -op-output: the JSON strings with the specified property assigned -<<.from-version "5.3.2">> See [[JSON in TiddlyWiki]] for background. - -The <<.op jsonset>> operator is used to set a property value in JSON strings. See also the following related operators: +<<.from-version "5.3.2">> The <<.op jsonset>> operator is used to set a property value in JSON strings. See [[JSON in TiddlyWiki]] for background. See also the following related operators: * <<.olink jsonget>> to retrieve the values of a property in JSON data * <<.olink jsontype>> to retrieve the type of a JSON value * <<.olink jsonindexes>> to retrieve the names of the fields of a JSON object, or the indexes of a JSON array * <<.olink jsonextract>> to retrieve a JSON value as a string of JSON +The type of the value to be assigned to the property can be optionally specified with a suffix: + +* ''string'': default, the string is specified as the final operand +* ''boolean'': the boolean value is true if the final operand is the string "true" and false if the final operand is the string "false", any other value for the final string results prevents the property from being assigned +* ''number'': the numeric value is taken from the final operand, invalid numbers are interpreted as zero +* ''json'': the JSON string value is taken from the final operand, invalid JSON prevents the property from being assigned +* ''object'': an empty object is assigned to the property, the final operand is ignored +* ''array'': an empty array is assigned to the property, the final operand is ignored +* ''null'': the special value null is assigned to the property, the final operand is ignored + Properties within a JSON object are identified by a sequence of indexes. In the following example, the value at `[a]` is `one`, and the value at `[d][f][0]` is `five`. ``` @@ -42,63 +51,14 @@ Properties within a JSON object are identified by a sequence of indexes. In the } ``` -The following examples assume that this JSON data is contained in a variable called `jsondata`. +The <<.op jsonset>> operator uses multiple parameters to specify the indexes of the property to set. When used to assign strings (default behaviour if no suffix is specified) the final operand is interpreted as the value to assign. -The <<.op jsonset>> operator uses multiple operands to specify the indexes of the property to set. When used to assign strings the final operand is interpreted as the value to assign. For example: +Negative indexes are counted from the end, so -1 means the last item, -2 the next-to-last item, and so on. -``` -[jsonset[d],[Jaguar]] --> {"a": "one","b": "","c": "three","d": "Jaguar"} -[jsonset[d],[f],[Panther]] --> {"a": "one","b": "","c": "three","d": "{"e": "four","f": "Panther","g": {"x": "max","y": "may","z": "maize"}}"} -``` +Indexes can be dynamically composed from variables and transclusions, e.g. `[jsonset,{!!field},[0],{CurrentResult}]`. -Negative indexes into an array are counted from the end, so -1 means the last item, -2 the next-to-last item, and so on: +In the special case where only a single parameter is defined, the operator replaces the entire input object with the the value of that parameter. If the single parameter is blank, the operation is ignored and no assignment takes place. -``` -[jsonset[d],[f],[-1],[Elephant]] --> {"a": "one","b": "","c": "three","d": "{"e": "four","f": ["five","six",true,false,"Elephant"],"g": {"x": "max","y": "may","z": "maize"}}"} -[jsonset[d],[f],[-2],[Elephant]] --> {"a": "one","b": "","c": "three","d": "{"e": "four","f": ["five","six",true,"Elephant",null],"g": {"x": "max","y": "may","z": "maize"}}"} -[jsonset[d],[f],[-4],[Elephant]] --> {"a": "one","b": "","c": "three","d": "{"e": "four","f": ["five","Elephant",true,false,null],"g": {"x": "max","y": "may","z": "maize"}}"} -``` +If the input consists of multiple JSON objects with matching properties, the value is set for all of them. -Indexes can be dynamically composed from variables and transclusions: - -``` -[jsonset,{!!field},[0],{CurrentResult}] -``` - -The data type of the value to be assigned to the property can be specified with an optional suffix: - -|!Suffix |!Description | -|''string'' |The string is specified as the final operand | -|''boolean'' |The boolean value is true if the final operand is the string "true" and false if the final operand is the string "false". Any other value for the final string results prevents the property from being assigned | -|''number'' |The numeric value is taken from the final operand. Invalid numbers are interpreted as zero | -|''json'' |The JSON string value is taken from the final operand. Invalid JSON prevents the property from being assigned | -|''object'' |An empty object is assigned to the property. The final operand is not used as a value | -|''array'' |An empty array is assigned to the property. The final operand is not used as a value | -|''null'' |The special value null is assigned to the property. The final operand is not used as a value | - -For example: - -``` -Input string: -{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]}} - -[jsonset[]] --> {"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]}} -[jsonset[],[Antelope]] --> "Antelope" -[jsonset:number[],[not a number]] --> 0 -[jsonset[id],[Antelope]] --> {"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]},"id":"Antelope"} -[jsonset:notatype[id],[Antelope]] --> {"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]},"id":"Antelope"} -[jsonset:boolean[id],[false]] --> {"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]},"id":false} -[jsonset:boolean[id],[Antelope]] --> {"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]}} -[jsonset:number[id],[42]] --> {"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]},"id":42} -[jsonset:null[id]] --> {"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]},"id":null} -[jsonset:array[d],[f],[5]] --> {"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null,[]]}} -[jsonset:object[d],[f],[5]] --> {"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null,{}]}} -[jsonset[missing],[id],[Antelope]] --> {"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]}} -``` - -A subtlety is that the special case of a single operand sets the value of that operand as the new JSON string, entirely replacing the input object. If that operand is blank, the operation is ignored and no assignment takes place. Thus: - -``` -[jsonset[Panther]] --> "Panther" -[jsonset[]] --> {"a": "one","b": "","c": "three","d": "{"e": "four","f": ["five", "six", true, false, null],"g": {"x": "max","y": "may","z": "maize"}}"} -``` +<<.operator-examples "jsonset">> \ No newline at end of file From 4e06c31022c39423041cfc38aa7de818ee23213f Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Thu, 7 Dec 2023 15:34:07 +0700 Subject: [PATCH 43/45] Move list-join example onto single line (#7877) It's a little less readable this way, but avoids the whitespace issue. --- editions/prerelease/tiddlers/Release 5.3.2.tid | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/editions/prerelease/tiddlers/Release 5.3.2.tid b/editions/prerelease/tiddlers/Release 5.3.2.tid index e1194c06c..4c7bc8874 100644 --- a/editions/prerelease/tiddlers/Release 5.3.2.tid +++ b/editions/prerelease/tiddlers/Release 5.3.2.tid @@ -62,18 +62,13 @@ Note that the <<.attr "emptyMessage">> and <<.attr "template">> attributes take You can replace it with: ``` -<$list filter=<> variable="item" join=", "> -<$text text=<>/> - +<$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 - +<$list filter=<> variable="item"><$text text=<>/><$list-join>, and also let's not forget ``` !! jsonset operator From 4a9b3009dd2c1128d4fdd957ed91e5649b25cd9c Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Mon, 11 Dec 2023 15:21:03 +0000 Subject: [PATCH 44/45] Further fix for d1c7f79dd2c138afeb6527236a3fcca23b4a1cf3 The plus sign needs escaping on some regex engines --- plugins/tiddlywiki/jasmine/run-wiki-based-tests.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/tiddlywiki/jasmine/run-wiki-based-tests.js b/plugins/tiddlywiki/jasmine/run-wiki-based-tests.js index 93c09c005..439de794d 100644 --- a/plugins/tiddlywiki/jasmine/run-wiki-based-tests.js +++ b/plugins/tiddlywiki/jasmine/run-wiki-based-tests.js @@ -51,7 +51,7 @@ describe("Wiki-based tests", function() { }); function readMultipleTiddlersTiddler(title) { - var rawTiddlers = $tw.wiki.getTiddlerText(title).split(/\r?\n+\r?\n/mg); + var rawTiddlers = $tw.wiki.getTiddlerText(title).split(/\r?\n\+\r?\n/mg); var tiddlers = []; $tw.utils.each(rawTiddlers,function(rawTiddler) { var fields = Object.create(null), From 15e53b8cd1fe4db67389538994ddebe236920655 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Mon, 11 Dec 2023 17:56:11 +0000 Subject: [PATCH 45/45] Revert: #7768 Ensure {{}} doesn't cause a recursion error See https://github.com/Jermolene/TiddlyWiki5/pull/7768#issuecomment-1850578638 --- core/modules/parsers/wikiparser/rules/transcludeblock.js | 3 --- core/modules/parsers/wikiparser/rules/transcludeinline.js | 3 --- editions/prerelease/tiddlers/Release 5.3.2.tid | 1 - 3 files changed, 7 deletions(-) diff --git a/core/modules/parsers/wikiparser/rules/transcludeblock.js b/core/modules/parsers/wikiparser/rules/transcludeblock.js index d6dad6df3..c033c2440 100644 --- a/core/modules/parsers/wikiparser/rules/transcludeblock.js +++ b/core/modules/parsers/wikiparser/rules/transcludeblock.js @@ -81,9 +81,6 @@ exports.parse = function() { } return [tiddlerNode]; } else { - // No template or text reference is provided, so we'll use a blank target. Otherwise we'll generate - // a transclude widget that transcludes the current tiddler, often leading to recursion errors - transcludeNode.attributes["$tiddler"] = {name: "$tiddler", type: "string", value: ""}; return [transcludeNode]; } } diff --git a/core/modules/parsers/wikiparser/rules/transcludeinline.js b/core/modules/parsers/wikiparser/rules/transcludeinline.js index 87529ca8d..3ce9dc78e 100644 --- a/core/modules/parsers/wikiparser/rules/transcludeinline.js +++ b/core/modules/parsers/wikiparser/rules/transcludeinline.js @@ -79,9 +79,6 @@ exports.parse = function() { } return [tiddlerNode]; } else { - // No template or text reference is provided, so we'll use a blank target. Otherwise we'll generate - // a transclude widget that transcludes the current tiddler, often leading to recursion errors - transcludeNode.attributes["$tiddler"] = {name: "$tiddler", type: "string", value: ""}; return [transcludeNode]; } } diff --git a/editions/prerelease/tiddlers/Release 5.3.2.tid b/editions/prerelease/tiddlers/Release 5.3.2.tid index 4c7bc8874..c3de75b36 100644 --- a/editions/prerelease/tiddlers/Release 5.3.2.tid +++ b/editions/prerelease/tiddlers/Release 5.3.2.tid @@ -113,7 +113,6 @@ Improvements to the following translations: ! Bug Fixes -* <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/issues/7665">> `{{}}` generating a recursion error * <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/pull/7758">> ordering of Vanilla stylesheets * <<.link-badge-fixed "https://github.com/Jermolene/TiddlyWiki5/commit/fa9bfa07a095548eb2f8339b0b1b816d2e6794ef">> missing closing tag in tag-pill-inner macro * <<.link-badge-removed "https://github.com/Jermolene/TiddlyWiki5/issues/7732">> invalid "type" attribute from textarea elements generated by the EditTextWidget