From d1f90f075f7cf41531f5e967b02acbc244885904 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Tue, 13 Jun 2023 10:35:55 +0100 Subject: [PATCH 1/8] Add tm-http-request message for making HTTP requests (#7422) * Initial Commit * HttpClient object shouldn't need to know about events * Add support for cancelling HTTP requests * Make the number of outstanding HTTP requests available in a state tiddler * Add a network activity button Click it to cancel outstanding requests * Fix typo Thanks @btheado Co-authored-by: btheado * Fix crash when cancelling more than one HTTP request Thanks @saqimtiaz * Further fixes to cancelling outstanding HTTP requests * Fix missing body --------- Co-authored-by: btheado --- core/images/network-activity.tid | 11 + core/language/en-GB/Buttons.multids | 2 + core/modules/startup/rootwidget.js | 32 +++ core/modules/utils/dom/http.js | 221 +++++++++++++++++- core/modules/wiki.js | 8 + core/palettes/Vanilla.tid | 1 + core/ui/PageControls/network-activity.tid | 16 ++ core/wiki/config/PageControlButtons.multids | 1 + core/wiki/tags/PageControls.tid | 2 +- ...etMessage_ tm-http-cancel-all-requests.tid | 12 + ...essage_ tm-http-request Example Zotero.tid | 115 +++++++++ .../WidgetMessage_ tm-http-request.tid | 51 ++++ .../tiddlers/messages/config-zotero-group.tid | 2 + themes/tiddlywiki/vanilla/base.tid | 4 + 14 files changed, 475 insertions(+), 3 deletions(-) create mode 100644 core/images/network-activity.tid create mode 100644 core/ui/PageControls/network-activity.tid create mode 100644 editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-cancel-all-requests.tid create mode 100644 editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request Example Zotero.tid create mode 100644 editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request.tid create mode 100644 editions/tw5.com/tiddlers/messages/config-zotero-group.tid diff --git a/core/images/network-activity.tid b/core/images/network-activity.tid new file mode 100644 index 000000000..2efdfd4d4 --- /dev/null +++ b/core/images/network-activity.tid @@ -0,0 +1,11 @@ +title: $:/core/images/network-activity +tags: $:/tags/Image + + +<$list filter="[{$:/state/http-requests}match[0]]" variable="ignore"> + + +<$list filter="[{$:/state/http-requests}!match[0]]" variable="ignore"> + + + \ No newline at end of file diff --git a/core/language/en-GB/Buttons.multids b/core/language/en-GB/Buttons.multids index 85a71ac08..fa769d117 100644 --- a/core/language/en-GB/Buttons.multids +++ b/core/language/en-GB/Buttons.multids @@ -67,6 +67,8 @@ More/Caption: more More/Hint: More actions NewHere/Caption: new here NewHere/Hint: Create a new tiddler tagged with this one +NetworkActivity/Caption: network activity +NetworkActivity/Hint: Cancel all network activity NewJournal/Caption: new journal NewJournal/Hint: Create a new journal tiddler NewJournalHere/Caption: new journal here diff --git a/core/modules/startup/rootwidget.js b/core/modules/startup/rootwidget.js index 1175f6f25..f5d90afb5 100644 --- a/core/modules/startup/rootwidget.js +++ b/core/modules/startup/rootwidget.js @@ -20,6 +20,38 @@ exports.before = ["story"]; exports.synchronous = true; exports.startup = function() { + // Install the HTTP client event handler + $tw.httpClient = new $tw.utils.HttpClient(); + var getPropertiesWithPrefix = function(properties,prefix) { + var result = Object.create(null); + $tw.utils.each(properties,function(value,name) { + if(name.indexOf(prefix) === 0) { + result[name.substring(prefix.length)] = properties[name]; + } + }); + return result; + }; + $tw.rootWidget.addEventListener("tm-http-request",function(event) { + var params = event.paramObject || {}; + $tw.httpClient.initiateHttpRequest({ + wiki: event.widget.wiki, + url: params.url, + method: params.method, + body: params.body, + oncompletion: params.oncompletion, + onprogress: params.onprogress, + bindStatus: params["bind-status"], + bindProgress: params["bind-progress"], + variables: getPropertiesWithPrefix(params,"var-"), + headers: getPropertiesWithPrefix(params,"header-"), + passwordHeaders: getPropertiesWithPrefix(params,"password-header-"), + queryStrings: getPropertiesWithPrefix(params,"query-"), + passwordQueryStrings: getPropertiesWithPrefix(params,"password-query-") + }); + }); + $tw.rootWidget.addEventListener("tm-http-cancel-all-requests",function(event) { + $tw.httpClient.cancelAllHttpRequests(); + }); // Install the modal message mechanism $tw.modal = new $tw.utils.Modal($tw.wiki); $tw.rootWidget.addEventListener("tm-modal",function(event) { diff --git a/core/modules/utils/dom/http.js b/core/modules/utils/dom/http.js index 6e07b1040..ba4b3d2a1 100644 --- a/core/modules/utils/dom/http.js +++ b/core/modules/utils/dom/http.js @@ -3,7 +3,7 @@ title: $:/core/modules/utils/dom/http.js type: application/javascript module-type: utils -Browser HTTP support +HTTP support \*/ (function(){ @@ -13,11 +13,204 @@ Browser HTTP support "use strict"; /* -A quick and dirty HTTP function; to be refactored later. Options are: +Manage tm-http-request events. Options include: +wiki: Reference to the wiki to be used for state tiddler tracking +stateTrackerTitle: Title of tiddler to be used for state tiddler tracking +*/ +function HttpClient(options) { + options = options || {}; + this.nextId = 1; + this.wiki = options.wiki || $tw.wiki; + this.stateTrackerTitle = options.stateTrackerTitle || "$:/state/http-requests"; + this.requests = []; // Array of {id: string,request: HttpClientRequest} + this.updateRequestTracker(); +} + +/* +Return the index into this.requests[] corresponding to a given ID. Returns null if not found +*/ +HttpClient.prototype.getRequestIndex = function(targetId) { + var targetIndex = null; + $tw.utils.each(this.requests,function(requestInfo,index) { + if(requestInfo.id === targetId) { + targetIndex = index; + } + }); + return targetIndex; +}; + +/* +Update the state tiddler that is tracking the outstanding requests +*/ +HttpClient.prototype.updateRequestTracker = function() { + this.wiki.addTiddler({title: this.stateTrackerTitle, text: "" + this.requests.length}); +}; + +HttpClient.prototype.initiateHttpRequest = function(options) { + var self = this, + id = this.nextId, + request = new HttpClientRequest(options); + this.nextId += 1; + this.requests.push({id: id, request: request}); + this.updateRequestTracker(); + request.send(function(err) { + var targetIndex = self.getRequestIndex(id); + if(targetIndex !== null) { + self.requests.splice(targetIndex,1); + self.updateRequestTracker(); + } + }); + return id; +}; + +HttpClient.prototype.cancelAllHttpRequests = function() { + var self = this; + if(this.requests.length > 0) { + for(var t=this.requests.length - 1; t--; t>=0) { + var requestInfo = this.requests[t]; + requestInfo.request.cancel(); + } + } + this.requests = []; + this.updateRequestTracker(); +}; + +HttpClient.prototype.cancelHttpRequest = function(targetId) { + var targetIndex = this.getRequestIndex(targetId); + if(targetIndex !== null) { + this.requests[targetIndex].request.cancel(); + this.requests.splice(targetIndex,1); + this.updateRequestTracker(); + } +}; + +/* +Initiate an HTTP request. Options: +wiki: wiki to be used for executing action strings +url: URL for request +method: method eg GET, POST +body: text of request body +oncompletion: action string to be invoked on completion +onprogress: action string to be invoked on progress updates +bindStatus: optional title of tiddler to which status ("pending", "complete", "error") should be written +bindProgress: optional title of tiddler to which the progress of the request (0 to 100) should be bound +variables: hashmap of variable name to string value passed to action strings +headers: hashmap of header name to header value to be sent with the request +passwordHeaders: hashmap of header name to password store name to be sent with the request +queryStrings: hashmap of query string parameter name to parameter value to be sent with the request +passwordQueryStrings: hashmap of query string parameter name to password store name to be sent with the request +*/ +function HttpClientRequest(options) { + var self = this; + console.log("Initiating an HTTP request",options) + this.wiki = options.wiki; + this.completionActions = options.oncompletion; + this.progressActions = options.onprogress; + this.bindStatus = options["bind-status"]; + this.bindProgress = options["bind-progress"]; + this.method = options.method || "GET"; + this.body = options.body || ""; + this.variables = options.variables; + var url = options.url; + $tw.utils.each(options.queryStrings,function(value,name) { + url = $tw.utils.setQueryStringParameter(url,name,value); + }); + $tw.utils.each(options.passwordQueryStrings,function(value,name) { + url = $tw.utils.setQueryStringParameter(url,name,$tw.utils.getPassword(value) || ""); + }); + this.url = url; + this.requestHeaders = {}; + $tw.utils.each(options.headers,function(value,name) { + self.requestHeaders[name] = value; + }); + $tw.utils.each(options.passwordHeaders,function(value,name) { + self.requestHeaders[name] = $tw.utils.getPassword(value) || ""; + }); +} + +HttpClientRequest.prototype.send = function(callback) { + var self = this, + setBinding = function(title,text) { + if(title) { + this.wiki.addTiddler(new $tw.Tiddler({title: title, text: text})); + } + }; + if(this.url) { + setBinding(this.bindStatus,"pending"); + setBinding(this.bindProgress,"0"); + // Set the request tracker tiddler + var requestTrackerTitle = this.wiki.generateNewTitle("$:/temp/HttpRequest"); + this.wiki.addTiddler({ + title: requestTrackerTitle, + tags: "$:/tags/HttpRequest", + text: JSON.stringify({ + url: this.url, + type: this.method, + status: "inprogress", + headers: this.requestHeaders, + data: this.body + }) + }); + this.xhr = $tw.utils.httpRequest({ + url: this.url, + type: this.method, + headers: this.requestHeaders, + data: this.body, + callback: function(err,data,xhr) { + var hasSucceeded = xhr.status >= 200 && xhr.status < 300, + completionCode = hasSucceeded ? "complete" : "error", + headers = {}; + $tw.utils.each(xhr.getAllResponseHeaders().split("\r\n"),function(line) { + var pos = line.indexOf(":"); + if(pos !== -1) { + headers[line.substr(0,pos)] = line.substr(pos + 1).trim(); + } + }); + setBinding(self.bindStatus,completionCode); + setBinding(self.bindProgress,"100"); + var resultVariables = { + status: xhr.status.toString(), + statusText: xhr.statusText, + error: (err || "").toString(), + data: (data || "").toString(), + headers: JSON.stringify(headers) + }; + self.wiki.addTiddler(new $tw.Tiddler(self.wiki.getTiddler(requestTrackerTitle),{ + status: completionCode, + })); + self.wiki.invokeActionString(self.completionActions,undefined,$tw.utils.extend({},self.variables,resultVariables),{parentWidget: $tw.rootWidget}); + callback(hasSucceeded ? null : xhr.statusText); + // console.log("Back!",err,data,xhr); + }, + progress: function(lengthComputable,loaded,total) { + if(lengthComputable) { + setBinding(self.bindProgress,"" + Math.floor((loaded/total) * 100)) + } + self.wiki.invokeActionString(self.progressActions,undefined,{ + lengthComputable: lengthComputable ? "yes" : "no", + loaded: loaded, + total: total + },{parentWidget: $tw.rootWidget}); + } + }); + } +}; + +HttpClientRequest.prototype.cancel = function() { + if(this.xhr) { + this.xhr.abort(); + } +}; + +exports.HttpClient = HttpClient; + +/* +Make an HTTP request. Options are: url: URL to retrieve headers: hashmap of headers to send type: GET, PUT, POST etc callback: function invoked with (err,data,xhr) + progress: optional function invoked with (lengthComputable,loaded,total) returnProp: string name of the property to return as first argument of callback */ exports.httpRequest = function(options) { @@ -83,8 +276,16 @@ exports.httpRequest = function(options) { options.callback($tw.language.getString("Error/XMLHttpRequest") + ": " + this.status,null,this); } }; + // Handle progress + if(options.progress) { + request.onprogress = function(event) { + console.log("Progress event",event) + options.progress(event.lengthComputable,event.loaded,event.total); + }; + } // Make the request request.open(type,url,true); + // Headers if(headers) { $tw.utils.each(headers,function(header,headerTitle,object) { request.setRequestHeader(headerTitle,header); @@ -96,6 +297,7 @@ exports.httpRequest = function(options) { if(!hasHeader("X-Requested-With") && !isSimpleRequest(type,headers)) { request.setRequestHeader("X-Requested-With","TiddlyWiki"); } + // Send data try { request.send(data); } catch(e) { @@ -104,4 +306,19 @@ exports.httpRequest = function(options) { return request; }; +exports.setQueryStringParameter = function(url,paramName,paramValue) { + var URL = $tw.browser ? window.URL : require("url").URL, + newUrl; + try { + newUrl = new URL(url); + } catch(e) { + } + if(newUrl && paramName) { + newUrl.searchParams.set(paramName,paramValue || ""); + return newUrl.toString(); + } else { + return url; + } +}; + })(); diff --git a/core/modules/wiki.js b/core/modules/wiki.js index 8cb12cc39..ca31da8d2 100755 --- a/core/modules/wiki.js +++ b/core/modules/wiki.js @@ -1415,6 +1415,14 @@ exports.checkTiddlerText = function(title,targetText,options) { return text === targetText; } +/* +Execute an action string without an associated context widget +*/ +exports.invokeActionString = function(actions,event,variables,options) { + var widget = this.makeWidget(null,{parentWidget: options.parentWidget}); + widget.invokeActionString(actions,null,event,variables); +}; + /* Read an array of browser File objects, invoking callback(tiddlerFieldsArray) once they're all read */ diff --git a/core/palettes/Vanilla.tid b/core/palettes/Vanilla.tid index d84b4ec83..4c660e912 100644 --- a/core/palettes/Vanilla.tid +++ b/core/palettes/Vanilla.tid @@ -54,6 +54,7 @@ modal-footer-background: #f5f5f5 modal-footer-border: #dddddd modal-header-border: #eeeeee muted-foreground: #bbb +network-activity-foreground: #448844 notification-background: #ffffdd notification-border: #999999 page-background: #f4f4f4 diff --git a/core/ui/PageControls/network-activity.tid b/core/ui/PageControls/network-activity.tid new file mode 100644 index 000000000..763365f37 --- /dev/null +++ b/core/ui/PageControls/network-activity.tid @@ -0,0 +1,16 @@ +title: $:/core/ui/Buttons/network-activity +tags: $:/tags/PageControls +caption: {{$:/core/images/network-activity}} {{$:/language/Buttons/NetworkActivity/Caption}} +description: {{$:/language/Buttons/NetworkActivity/Hint}} + +\whitespace trim +<$button message="tm-http-cancel-all-requests" tooltip={{$:/language/Buttons/NetworkActivity/Hint}} aria-label={{$:/language/Buttons/NetworkActivity/Caption}} class=<>> +<$list filter="[match[yes]]"> +{{$:/core/images/network-activity}} + +<$list filter="[match[yes]]"> + +<$text text={{$:/language/Buttons/NetworkActivity/Caption}}/> + + + \ No newline at end of file diff --git a/core/wiki/config/PageControlButtons.multids b/core/wiki/config/PageControlButtons.multids index a437251f5..b66f11cc0 100644 --- a/core/wiki/config/PageControlButtons.multids +++ b/core/wiki/config/PageControlButtons.multids @@ -13,6 +13,7 @@ core/ui/Buttons/language: hide core/ui/Buttons/tag-manager: hide core/ui/Buttons/manager: hide core/ui/Buttons/more-page-actions: hide +core/ui/Buttons/network-activity: hide core/ui/Buttons/new-journal: hide core/ui/Buttons/new-image: hide core/ui/Buttons/palette: hide diff --git a/core/wiki/tags/PageControls.tid b/core/wiki/tags/PageControls.tid index c6234751c..c0f1cb233 100644 --- a/core/wiki/tags/PageControls.tid +++ b/core/wiki/tags/PageControls.tid @@ -1,2 +1,2 @@ title: $:/tags/PageControls -list: [[$:/core/ui/Buttons/home]] [[$:/core/ui/Buttons/close-all]] [[$:/core/ui/Buttons/fold-all]] [[$:/core/ui/Buttons/unfold-all]] [[$:/core/ui/Buttons/permaview]] [[$:/core/ui/Buttons/new-tiddler]] [[$:/core/ui/Buttons/new-journal]] [[$:/core/ui/Buttons/new-image]] [[$:/core/ui/Buttons/import]] [[$:/core/ui/Buttons/export-page]] [[$:/core/ui/Buttons/control-panel]] [[$:/core/ui/Buttons/advanced-search]] [[$:/core/ui/Buttons/manager]] [[$:/core/ui/Buttons/tag-manager]] [[$:/core/ui/Buttons/language]] [[$:/core/ui/Buttons/palette]] [[$:/core/ui/Buttons/theme]] [[$:/core/ui/Buttons/layout]] [[$:/core/ui/Buttons/storyview]] [[$:/core/ui/Buttons/encryption]] [[$:/core/ui/Buttons/timestamp]] [[$:/core/ui/Buttons/full-screen]] [[$:/core/ui/Buttons/print]] [[$:/core/ui/Buttons/save-wiki]] [[$:/core/ui/Buttons/refresh]] [[$:/core/ui/Buttons/more-page-actions]] +list: [[$:/core/ui/Buttons/home]] [[$:/core/ui/Buttons/close-all]] [[$:/core/ui/Buttons/fold-all]] [[$:/core/ui/Buttons/unfold-all]] [[$:/core/ui/Buttons/permaview]] [[$:/core/ui/Buttons/new-tiddler]] [[$:/core/ui/Buttons/new-journal]] [[$:/core/ui/Buttons/new-image]] [[$:/core/ui/Buttons/import]] [[$:/core/ui/Buttons/export-page]] [[$:/core/ui/Buttons/control-panel]] [[$:/core/ui/Buttons/advanced-search]] [[$:/core/ui/Buttons/manager]] [[$:/core/ui/Buttons/tag-manager]] [[$:/core/ui/Buttons/language]] [[$:/core/ui/Buttons/palette]] [[$:/core/ui/Buttons/theme]] [[$:/core/ui/Buttons/layout]] [[$:/core/ui/Buttons/storyview]] [[$:/core/ui/Buttons/encryption]] [[$:/core/ui/Buttons/timestamp]] [[$:/core/ui/Buttons/full-screen]] [[$:/core/ui/Buttons/print]] [[$:/core/ui/Buttons/save-wiki]] [[$:/core/ui/Buttons/refresh]] [[$:/core/ui/Buttons/network-activity]] [[$:/core/ui/Buttons/more-page-actions]] diff --git a/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-cancel-all-requests.tid b/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-cancel-all-requests.tid new file mode 100644 index 000000000..df94e5a0b --- /dev/null +++ b/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-cancel-all-requests.tid @@ -0,0 +1,12 @@ +caption: tm-http-cancel-all-requests +created: 20230429161453032 +modified: 20230429161453032 +tags: Messages +title: WidgetMessage: tm-http-cancel-all-requests +type: text/vnd.tiddlywiki + +The ''tm-http-cancel-all-requests'' message is used to cancel all outstanding HTTP requests initiated with [[WidgetMessage: tm-http-request]]. + +Note that the state tiddler $:/state/http-requests contains a number representing the number of outstanding HTTP requests in progress. + +It does not take any parameters. diff --git a/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request Example Zotero.tid b/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request Example Zotero.tid new file mode 100644 index 000000000..ea64dd3a2 --- /dev/null +++ b/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request Example Zotero.tid @@ -0,0 +1,115 @@ +title: WidgetMessage: tm-http-request Example - Zotero +tags: $:/tags/Macro + +\procedure select-zotero-group() +Specify the Zotero group ID to import +<$edit-text tiddler="$:/config/zotero-group" tag="input"/> or +<$select tiddler="$:/config/zotero-group"> + + + + +\end + +\procedure zotero-save-item(item) +<$action-createtiddler + $basetitle={{{ =[[_zotero_import ]] =[jsonget[key]] =[[ ]] =[jsonget[title]] +[join[]] }}} + text={{{ [jsonget[title]] }}} + tags="$:/tags/ZoteroImport" +> + <$action-setmultiplefields $tiddler=<> $fields="[jsonindexes[]addprefix[zotero-]]" $values="[jsonindexes[]] :map[jsongetelse[.XXXXX.]]"/> + <$list filter="[jsonindexes[creators]]" variable="creatorIndex"> + <$action-setmultiplefields $tiddler=<> $fields="[jsonget[creators],,[creatorType]addprefix[zotero-]]" $values="[jsonget[creators],,[lastName]] [jsonget[creators],,[firstName]] +[join[, ]] :else[jsonget[creators],,[name]] "/> + + +\end zotero-save-item + +\procedure zotero-save-items(data) +<$list filter="[jsonindexes[]] :map[jsonextract,[data]]" variable="item"> + <$macrocall $name="zotero-save-item" item=<>/> + +\end zotero-save-items + +\procedure zotero-get-items(start:"0",limit:"25") + +\procedure completion() +\import [[$:/core/ui/PageMacros]] [all[shadows+tiddlers]tag[$:/tags/Macro]!has[draft.of]] + <$action-log msg="In completion"/> + <$action-log/> + + <$list filter="[compare:number:gteq[200]compare:number:lteq[299]]" variable="ignore"> + + <$macrocall $name="zotero-save-items" data=<>/> + + <$list filter="[jsonget[total-results]subtractsubtractcompare:number:gt[0]]" variable="ignore"> + <$macrocall $name="zotero-get-items" start={{{ [add] }}} limit=<>/> + + +\end completion + +\procedure progress() +\import [[$:/core/ui/PageMacros]] [all[shadows+tiddlers]tag[$:/tags/Macro]!has[draft.of]] + <$action-log message="In progress-actions"/> +\end progress + +\procedure request-url() +\rules only transcludeinline transcludeblock filteredtranscludeinline filteredtranscludeblock +https://api.zotero.org/groups/{{$:/config/zotero-group}}/items/ +\end request-url + +<$wikify name="url" text=<>> + <$action-sendmessage + $message="tm-http-request" + url=<> + method="GET" + query-format="json" + query-sort="title" + query-start=<> + query-limit=<> + header-accept="application/json" + bind-status="$:/temp/zotero/status" + bind-progress="$:/temp/zotero/progress" + oncompletion=<> + onprogress=<> + var-start=<> + var-limit=<> + /> + +\end + +\procedure zotero-actions() +<$macrocall $name="zotero-get-items" start="0" limit="50"/> +\end + +<> + +<$button actions=<>> +Start import from Zotero group + + +<$button message="tm-http-cancel-all-requests"> +Cancel all HTTP requests + Outstanding requests: {{$:/state/http-requests}} + +<$list filter="[tag[$:/tags/ZoteroImport]limit[1]]" variable="ignore"> + +!! Imported Tiddlers + +<$button> +<$action-deletetiddler $filter="[tag[$:/tags/ZoteroImport]]"/> +Delete these tiddlers + + +Export: <$macrocall $name="exportButton" exportFilter="[tag[$:/tags/ZoteroImport]]" lingoBase="$:/language/Buttons/ExportTiddlers/"/> + + + +
    +<$list filter="[tag[$:/tags/ZoteroImport]]"> +
  1. +<$link> +<$view field="title"/> + +
  2. + +
diff --git a/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request.tid b/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request.tid new file mode 100644 index 000000000..f6c82e760 --- /dev/null +++ b/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request.tid @@ -0,0 +1,51 @@ +caption: tm-http-request +created: 20230429161453032 +modified: 20230429161453032 +tags: Messages +title: WidgetMessage: tm-http-request +type: text/vnd.tiddlywiki + +The ''tm-http-request'' message is used to make an HTTP request to a server. + +It uses the following properties on the `event` object: + +|!Name |!Description | +|param |Not used | +|paramObject |Hashmap of parameters (see below) | + +The following parameters are used: + +|!Name |!Description | +|method |HTTP method (eg "GET", "POST") | +|body |String data to be sent with the request | +|query-* |Query string parameters with string values | +|header-* |Headers with string values | +|password-header-* |Headers with values taken from the password store | +|password-query-* |Query string parameters with values taken from the password store | +|var-* |Variables to be passed to the completion and progress handlers (without the "var-" prefix) | +|bind-status |Title of tiddler to which the status of the request ("pending", "complete", "error") should be bound | +|bind-progress |Title of tiddler to which the progress of the request (0 to 100) should be bound | +|oncompletion |Action strings to be executed when the request completes | +|onprogress |Action strings to be executed when progress is reported | + +The following variables are passed to the completion handler: + +|!Name |!Description | +|status |HTTP result status code (see [[MDN|https://developer.mozilla.org/en-US/docs/Web/HTTP/Status]]) | +|statusText |HTTP result status text | +|error |Error string | +|data |Returned data | +|headers |Response headers as a JSON object | + +The following variables are passed to the progress handler: + +|!Name |!Description | +|lengthComputable |Whether the progress loaded and total figures are valid - "yes" or "no" | +|loaded |Number of bytes loaded so far | +|total |Total number bytes to be loaded | + +Note that the state tiddler $:/state/http-requests contains a number representing the number of outstanding HTTP requests in progress. + +!! Examples + +* [[Zotero's|https://www.zotero.org/]] API for retrieving reference items: [[WidgetMessage: tm-http-request Example - Zotero]] diff --git a/editions/tw5.com/tiddlers/messages/config-zotero-group.tid b/editions/tw5.com/tiddlers/messages/config-zotero-group.tid new file mode 100644 index 000000000..2215c496a --- /dev/null +++ b/editions/tw5.com/tiddlers/messages/config-zotero-group.tid @@ -0,0 +1,2 @@ +title: $:/config/zotero-group +text: 4813312 \ No newline at end of file diff --git a/themes/tiddlywiki/vanilla/base.tid b/themes/tiddlywiki/vanilla/base.tid index 53943f994..a8df11bb3 100644 --- a/themes/tiddlywiki/vanilla/base.tid +++ b/themes/tiddlywiki/vanilla/base.tid @@ -3185,6 +3185,10 @@ span.tc-translink > a:first-child { fill: <>; } +.tc-network-activity-background { + fill: <>; +} + /* ** Flexbox utility classes */ From 106f121133a9c527118231f81a4ab6a14ced988d Mon Sep 17 00:00:00 2001 From: Mario Pietsch Date: Tue, 13 Jun 2023 11:44:34 +0200 Subject: [PATCH 2/8] Table-of-content macros -- make "exclude" an official macro parameter (#7417) * toc make exclude a proper macro parameter using subfilter instead of enlist * add exclude parameter to TOC documentation tiddler * add exclude parameter to toc-tabbed-xx macros * add from-version to exclude parameter --- core/wiki/macros/toc.tid | 26 +++++++++---------- .../tiddlers/macros/TableOfContentsMacro.tid | 16 ++++++++---- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/core/wiki/macros/toc.tid b/core/wiki/macros/toc.tid index 6b8a83295..528c0e63c 100644 --- a/core/wiki/macros/toc.tid +++ b/core/wiki/macros/toc.tid @@ -19,9 +19,9 @@ tags: $:/tags/Macro \define toc-body(tag,sort:"",itemClassFilter,exclude,path) \whitespace trim
    - <$list filter="""[all[shadows+tiddlers]tag<__tag__>!has[draft.of]$sort$] -[<__tag__>] -[enlist<__exclude__>]"""> + <$list filter="""[all[shadows+tiddlers]tag<__tag__>!has[draft.of]$sort$] -[<__tag__>] -[subfilter<__exclude__>]"""> <$let item=<> path={{{ [<__path__>addsuffix[/]addsuffix<__tag__>] }}}> - <$set name="excluded" filter="""[enlist<__exclude__>] [<__tag__>]"""> + <$set name="excluded" filter="[subfilter<__exclude__>] [<__tag__>]"> <$set name="toc-item-class" filter=<<__itemClassFilter__>> emptyValue="toc-item-selected" value="toc-item">
  1. >> <$list filter="[all[current]toc-link[no]]" emptyMessage="<$link to={{{ [get[target]else] }}}><>"> @@ -36,8 +36,8 @@ tags: $:/tags/Macro
\end -\define toc(tag,sort:"",itemClassFilter:"") -<$macrocall $name="toc-body" tag=<<__tag__>> sort=<<__sort__>> itemClassFilter=<<__itemClassFilter__>> /> +\define toc(tag,sort:"",itemClassFilter:"", exclude) +<$macrocall $name="toc-body" tag=<<__tag__>> sort=<<__sort__>> itemClassFilter=<<__itemClassFilter__>> exclude=<<__exclude__>>/> \end \define toc-linked-expandable-body(tag,sort:"",itemClassFilter,exclude,path) @@ -75,7 +75,7 @@ tags: $:/tags/Macro
  • >> <$reveal type="nomatch" stateTitle=<> text="open"> <$button setTitle=<> setTo="open" class="tc-btn-invisible tc-popup-keep"> - <$transclude tiddler=<> /> + <$transclude tiddler=<> /> <> @@ -100,9 +100,9 @@ tags: $:/tags/Macro \define toc-expandable(tag,sort:"",itemClassFilter:"",exclude,path) \whitespace trim <$let tag=<<__tag__>> sort=<<__sort__>> itemClassFilter=<<__itemClassFilter__>> path={{{ [<__path__>addsuffix[/]addsuffix<__tag__>] }}}> - <$set name="excluded" filter="""[enlist<__exclude__>] [<__tag__>]"""> + <$set name="excluded" filter="[subfilter<__exclude__>] [<__tag__>]">
      - <$list filter="""[all[shadows+tiddlers]tag<__tag__>!has[draft.of]$sort$] -[<__tag__>] -[enlist<__exclude__>]"""> + <$list filter="""[all[shadows+tiddlers]tag<__tag__>!has[draft.of]$sort$] -[<__tag__>] -[subfilter<__exclude__>]"""> <$list filter="[all[current]toc-link[no]]" emptyMessage=<> > <$macrocall $name="toc-unlinked-expandable-body" tag=<<__tag__>> sort=<<__sort__>> itemClassFilter="""itemClassFilter""" exclude=<> path=<> /> @@ -174,9 +174,9 @@ tags: $:/tags/Macro \define toc-selective-expandable(tag,sort:"",itemClassFilter,exclude,path) \whitespace trim <$let tag=<<__tag__>> sort=<<__sort__>> itemClassFilter=<<__itemClassFilter__>> path={{{ [<__path__>addsuffix[/]addsuffix<__tag__>] }}}> - <$set name="excluded" filter="[enlist<__exclude__>] [<__tag__>]"> + <$set name="excluded" filter="[subfilter<__exclude__>] [<__tag__>]">
        - <$list filter="""[all[shadows+tiddlers]tag<__tag__>!has[draft.of]$sort$] -[<__tag__>] -[enlist<__exclude__>]"""> + <$list filter="""[all[shadows+tiddlers]tag<__tag__>!has[draft.of]$sort$] -[<__tag__>] -[subfilter<__exclude__>]"""> <$list filter="[all[current]toc-link[no]]" variable="ignore" emptyMessage=<> > <$macrocall $name="toc-unlinked-selective-expandable-body" tag=<<__tag__>> sort=<<__sort__>> itemClassFilter=<<__itemClassFilter__>> exclude=<> path=<>/> @@ -186,13 +186,13 @@ tags: $:/tags/Macro \end -\define toc-tabbed-external-nav(tag,sort:"",selectedTiddler:"$:/temp/toc/selectedTiddler",unselectedText,missingText,template:"") +\define toc-tabbed-external-nav(tag,sort:"",selectedTiddler:"$:/temp/toc/selectedTiddler",unselectedText,missingText,template:"",exclude) \whitespace trim <$tiddler tiddler={{{ [<__selectedTiddler__>get[text]] }}}>
        <$linkcatcher to=<<__selectedTiddler__>>>
        - <$macrocall $name="toc-selective-expandable" tag=<<__tag__>> sort=<<__sort__>> itemClassFilter="[all[current]] -[<__selectedTiddler__>get[text]]"/> + <$macrocall $name="toc-selective-expandable" tag=<<__tag__>> sort=<<__sort__>> itemClassFilter="[all[current]] -[<__selectedTiddler__>get[text]]" exclude=<<__exclude__>>/>
        @@ -210,9 +210,9 @@ tags: $:/tags/Macro \end -\define toc-tabbed-internal-nav(tag,sort:"",selectedTiddler:"$:/temp/toc/selectedTiddler",unselectedText,missingText,template:"") +\define toc-tabbed-internal-nav(tag,sort:"",selectedTiddler:"$:/temp/toc/selectedTiddler",unselectedText,missingText,template:"",exclude) \whitespace trim <$linkcatcher to=<<__selectedTiddler__>>> - <$macrocall $name="toc-tabbed-external-nav" tag=<<__tag__>> sort=<<__sort__>> selectedTiddler=<<__selectedTiddler__>> unselectedText=<<__unselectedText__>> missingText=<<__missingText__>> template=<<__template__>>/> + <$macrocall $name="toc-tabbed-external-nav" tag=<<__tag__>> sort=<<__sort__>> selectedTiddler=<<__selectedTiddler__>> unselectedText=<<__unselectedText__>> missingText=<<__missingText__>> template=<<__template__>> exclude=<<__exclude__>> /> \end diff --git a/editions/tw5.com/tiddlers/macros/TableOfContentsMacro.tid b/editions/tw5.com/tiddlers/macros/TableOfContentsMacro.tid index c813fd1e6..54343bf32 100644 --- a/editions/tw5.com/tiddlers/macros/TableOfContentsMacro.tid +++ b/editions/tw5.com/tiddlers/macros/TableOfContentsMacro.tid @@ -1,5 +1,5 @@ created: 20140919155729620 -modified: 20220819093733569 +modified: 20230427125500432 tags: Macros [[Core Macros]] title: Table-of-Contents Macros type: text/vnd.tiddlywiki @@ -53,15 +53,21 @@ These two parameters are combined into a single [[filter expression|Filter Expre <<.var toc-tabbed-internal-nav>> and <<.var toc-tabbed-external-nav>> take additional parameters: -;selectedTiddler +; selectedTiddler : The title of the [[state tiddler|StateMechanism]] for noting the currently selected tiddler, defaulting to `$:/temp/toc/selectedTiddler`. It is recommended that this be a [[system tiddler|SystemTiddlers]] -;unselectedText + +; unselectedText : The text to display when no tiddler is selected in the tree -;missingText + +; missingText : The text to display if the selected tiddler doesn't exist -;template + +; template : Optionally, the title of a tiddler to use as a [[template|TemplateTiddlers]] for transcluding the selected tiddler into the right-hand panel +; exclude <<.from-version "5.3.0">> +: This optional parameter can be used to exclude tiddlers from the TOC list. It allows a [[Title List]] or a <<.olink subfilter>>. Eg: `exclude:"HelloThere [[Title with spaces]]"` or `exclude:"[has[excludeTOC]]"`. Where the former will exclude two tiddlers and the later would exclude every tiddler that has a field <<.field excludeTOC>> independent of its value.
        ''Be aware'' that eg: `[prefix[H]]` is a shortcut for `[all[tiddlers]prefix[H]]`, which can have a performance impact, if used carelessly. So use $:/AdvancedSearch -> ''Filters'' tab to test the <<.param exclude>> parameter + !! Custom Icons <<.from-version "5.2.4">> From 86d45f1c3d7f617223d1c13e6fd8aa588b13c486 Mon Sep 17 00:00:00 2001 From: btheado Date: Tue, 13 Jun 2023 04:50:00 -0500 Subject: [PATCH 3/8] Request permission to protect local storage from eviction (#7398) * Request the browser to never evict the persistent storage * Store browser storage persisted state in a tiddler * Factor out some code into helper functions * Display status of persistence request in the settings page --- .../tiddlywiki/browser-storage/settings.tid | 10 ++++ plugins/tiddlywiki/browser-storage/startup.js | 49 ++++++++++++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/plugins/tiddlywiki/browser-storage/settings.tid b/plugins/tiddlywiki/browser-storage/settings.tid index 7bf0e26e5..eb2e27940 100644 --- a/plugins/tiddlywiki/browser-storage/settings.tid +++ b/plugins/tiddlywiki/browser-storage/settings.tid @@ -28,6 +28,16 @@ This setting allows a custom alert message to be displayed when an attempt to st <$link to="$:/config/BrowserStorage/QuotaExceededAlert">Quota Exceeded Alert: <$edit-text tiddler="$:/config/BrowserStorage/QuotaExceededAlert" default="" tag="input" size="50"/> +! Prevent browser from evicting local storage + +Permission for local storage persistence: ''{{$:/info/browser/storage/persisted}}'' + +The first time a tiddler is saved to local storage a request will be made to prevent automatic eviction of local storage for this site. This means the data will not be cleared unless the user manually clears it. + +Old browsers may not support this feature. New browsers might not support the feature if the wiki is hosted on a non-localhost unencrypted http connection. + +Some browsers will explicitly prompt the user for permission. Other browsers may automatically grant or deny the request based on site usage or based on whether the site is bookmarked. + ! Startup Log The tiddler $:/temp/BrowserStorage/Log contains a log of the tiddlers that were loaded from local storage at startup: diff --git a/plugins/tiddlywiki/browser-storage/startup.js b/plugins/tiddlywiki/browser-storage/startup.js index 69cc5119e..552de93d2 100644 --- a/plugins/tiddlywiki/browser-storage/startup.js +++ b/plugins/tiddlywiki/browser-storage/startup.js @@ -19,7 +19,8 @@ exports.after = ["startup"]; exports.synchronous = true; var ENABLED_TITLE = "$:/config/BrowserStorage/Enabled", - SAVE_FILTER_TITLE = "$:/config/BrowserStorage/SaveFilter"; + SAVE_FILTER_TITLE = "$:/config/BrowserStorage/SaveFilter", + PERSISTED_STATE_TITLE = "$:/info/browser/storage/persisted"; var BrowserStorageUtil = require("$:/plugins/tiddlywiki/browser-storage/util.js").BrowserStorageUtil; @@ -53,6 +54,48 @@ exports.startup = function() { $tw.wiki.addTiddler({title: ENABLED_TITLE, text: "no"}); $tw.browserStorage.clearLocalStorage(); }); + // Helpers for protecting storage from eviction + var setPersistedState = function(state) { + $tw.wiki.addTiddler({title: PERSISTED_STATE_TITLE, text: state}); + }, + requestPersistence = function() { + setPersistedState("requested"); + navigator.storage.persist().then(function(persisted) { + console.log("Request for persisted storage " + (persisted ? "granted" : "denied")); + setPersistedState(persisted ? "granted" : "denied"); + }); + }, + persistPermissionRequested = false, + requestPersistenceOnFirstSave = function() { + $tw.hooks.addHook("th-saving-tiddler", function(tiddler) { + if (!persistPermissionRequested) { + var filteredChanges = filterFn.call($tw.wiki, function(iterator) { + iterator(tiddler,tiddler.getFieldString("title")); + }); + if (filteredChanges.length > 0) { + // The tiddler will be saved to local storage, so request persistence + requestPersistence(); + persistPermissionRequested = true; + } + } + return tiddler; + }); + }; + // Request the browser to never evict the localstorage. Some browsers such as firefox + // will prompt the user. To make the decision easier for the user only prompt them + // when they click the save button on a tiddler which will be stored to localstorage. + if (navigator.storage && navigator.storage.persist) { + navigator.storage.persisted().then(function(isPersisted) { + if (!isPersisted) { + setPersistedState("not requested yet"); + requestPersistenceOnFirstSave(); + } else { + setPersistedState("granted"); + } + }); + } else { + setPersistedState("feature not available"); + } // Track tiddler changes $tw.wiki.addEventListener("change",function(changes) { // Bail if browser storage is disabled @@ -76,6 +119,10 @@ exports.startup = function() { if(title === ENABLED_TITLE) { return; } + // This should always be queried from the browser, so don't store it in local storage + if(title === PERSISTED_STATE_TITLE) { + return; + } // Save the tiddler $tw.browserStorage.saveTiddlerToLocalStorage(title); }); From f277493acdaa87f4ee926fef8ebc263abc73f0fb Mon Sep 17 00:00:00 2001 From: "jeremy@jermolene.com" Date: Tue, 13 Jun 2023 11:22:11 +0100 Subject: [PATCH 4/8] Improved fix for #7529 The fix in cce23ac6cddbccc88a848dcc5c456e57c01b2c20 was affecting other editor dropdowns --- core/ui/EditorToolbar/link-dropdown.tid | 4 ++-- themes/tiddlywiki/vanilla/base.tid | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/core/ui/EditorToolbar/link-dropdown.tid b/core/ui/EditorToolbar/link-dropdown.tid index e2766935b..d2887a180 100644 --- a/core/ui/EditorToolbar/link-dropdown.tid +++ b/core/ui/EditorToolbar/link-dropdown.tid @@ -18,7 +18,7 @@ title: $:/core/ui/EditorToolbar/link-dropdown \define external-link() \whitespace trim -<$button class="tc-btn-invisible" style="width: auto; display: inline-block; background-colour: inherit;" actions=<>> +<$button class="tc-btn-invisible tc-btn-mini" style="width: auto; display: inline-block; background-colour: inherit;" actions=<>> {{$:/core/images/chevron-right}} \end @@ -45,7 +45,7 @@ title: $:/core/ui/EditorToolbar/link-dropdown <$reveal tag="span" state=<> type="nomatch" text=""> <> -<$button class="tc-btn-invisible" style="width: auto; display: inline-block; background-colour: inherit;"> +<$button class="tc-btn-invisible tc-btn-mini" style="width: auto; display: inline-block; background-colour: inherit;"> <><$set name="cssEscapedTitle" value={{{ [escapecss[]] }}}><$action-sendmessage $message="tm-focus-selector" $param=<>/> {{$:/core/images/close-button}} diff --git a/themes/tiddlywiki/vanilla/base.tid b/themes/tiddlywiki/vanilla/base.tid index a8df11bb3..dcf4a1697 100644 --- a/themes/tiddlywiki/vanilla/base.tid +++ b/themes/tiddlywiki/vanilla/base.tid @@ -1388,9 +1388,8 @@ html body.tc-body.tc-single-tiddler-window { height: 1.2em; } -.tc-editor-toolbar .tc-drop-down a, -.tc-editor-toolbar .tc-drop-down button { - padding: 0; +.tc-editor-toolbar .tc-drop-down button.tc-btn-mini { + padding: 2px 4px; } .tc-editor-toolbar button:hover { From 120c2f8136440ddc0e48656205e04ef837a9f8b7 Mon Sep 17 00:00:00 2001 From: Bram Chen Date: Tue, 13 Jun 2023 21:50:20 +0800 Subject: [PATCH 5/8] Update chinese language files (#7536) * Add chinese translations for the new network activity button --- languages/zh-Hans/Buttons.multids | 2 ++ languages/zh-Hant/Buttons.multids | 2 ++ 2 files changed, 4 insertions(+) diff --git a/languages/zh-Hans/Buttons.multids b/languages/zh-Hans/Buttons.multids index a94a31940..f33169778 100644 --- a/languages/zh-Hans/Buttons.multids +++ b/languages/zh-Hans/Buttons.multids @@ -67,6 +67,8 @@ More/Caption: 更多 More/Hint: 更多操作 NewHere/Caption: 添加子条目 NewHere/Hint: 创建一个标签为此条目名称的新条目 +NetworkActivity/Caption: 网络活动 +NetworkActivity/Hint: 取消所有网络活动 NewJournal/Caption: 添加日志 NewJournal/Hint: 创建一个新的日志条目 NewJournalHere/Caption: 添加子日志 diff --git a/languages/zh-Hant/Buttons.multids b/languages/zh-Hant/Buttons.multids index 7ffc15f50..cc5ebba6b 100644 --- a/languages/zh-Hant/Buttons.multids +++ b/languages/zh-Hant/Buttons.multids @@ -67,6 +67,8 @@ More/Caption: 更多 More/Hint: 更多動作 NewHere/Caption: 新增子條目 NewHere/Hint: 建立一個標籤為此條目名稱的新條目 +NetworkActivity/Caption: 網路活動 +NetworkActivity/Hint: 取消所有網路活動 NewJournal/Caption: 新增日誌 NewJournal/Hint: 建立一個新的日誌條目 NewJournalHere/Caption: 新增子日誌 From 50315310f530a3c081e7f986aa169f57e628bba2 Mon Sep 17 00:00:00 2001 From: buggyj Date: Tue, 13 Jun 2023 16:55:44 +0200 Subject: [PATCH 6/8] Add widget.destroy() function (#7468) --- core/modules/widgets/widget.js | 35 +++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/core/modules/widgets/widget.js b/core/modules/widgets/widget.js index c0d2bc7a6..8ffee0ab7 100755 --- a/core/modules/widgets/widget.js +++ b/core/modules/widgets/widget.js @@ -718,23 +718,44 @@ Widget.prototype.findFirstDomNode = function() { }; /* -Remove any DOM nodes created by this widget or its children +Entry into destroy procedure +*/ +Widget.prototype.destroyChildren = function() { + $tw.utils.each(this.children,function(childWidget) { + childWidget.destroy(); + }); +}; +/* +Legacy entry into destroy procedure */ Widget.prototype.removeChildDomNodes = function() { - // If this widget has directly created DOM nodes, delete them and exit. This assumes that any child widgets are contained within the created DOM nodes, which would normally be the case + this.destroy(); +}; +/* +Default destroy +*/ +Widget.prototype.destroy = function() { + // call children to remove their resources + this.destroyChildren(); + // remove our resources + this.children = []; + this.removeLocalDomNodes(); +}; + +/* +Remove any DOM nodes created by this widget +*/ +Widget.prototype.removeLocalDomNodes = function() { + // If this widget has directly created DOM nodes, delete them and exit. if(this.domNodes.length > 0) { $tw.utils.each(this.domNodes,function(domNode) { domNode.parentNode.removeChild(domNode); }); this.domNodes = []; - } else { - // Otherwise, ask the child widgets to delete their DOM nodes - $tw.utils.each(this.children,function(childWidget) { - childWidget.removeChildDomNodes(); - }); } }; + /* Invoke the action widgets that are descendents of the current widget. */ From 12f7b98c4fba93b6d7db2157a2b8ba03e1fd02a4 Mon Sep 17 00:00:00 2001 From: lin onetwo Date: Tue, 13 Jun 2023 22:57:24 +0800 Subject: [PATCH 7/8] Docs for widget.destroy (#7508) --- .../Widget `destroy` method examples.tid | 36 +++++++++++++++++ .../moduletypes/WidgetModules.tid | 13 ++++-- editions/dev/tiddlers/system/doc-styles.tid | 40 +++++++++++++++++++ .../dev/tiddlers/system/version-macros.tid | 14 +++++++ 4 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 editions/dev/tiddlers/Widget `destroy` method examples.tid create mode 100644 editions/dev/tiddlers/system/doc-styles.tid create mode 100644 editions/dev/tiddlers/system/version-macros.tid diff --git a/editions/dev/tiddlers/Widget `destroy` method examples.tid b/editions/dev/tiddlers/Widget `destroy` method examples.tid new file mode 100644 index 000000000..5ff04bdd0 --- /dev/null +++ b/editions/dev/tiddlers/Widget `destroy` method examples.tid @@ -0,0 +1,36 @@ +created: 20230601123245916 +modified: 20230601125015463 +title: Widget `destroy` method examples +type: text/vnd.tiddlywiki + +!! When using a v-dom library + +Virtual DOM libraries manages its internal state and apply state to DOM periodically, this is so called [["controlled" component|https://react.dev/learn/sharing-state-between-components#controlled-and-uncontrolled-components]]. When Tiddlywiki remove a DOM element controlled by a v-dom library, it may throws error. + +So when creating a plugin providing v-dom library binding, you need to tell v-dom library (for example, React.js) the DOM element is removed. We will use `destroy` method for this. + +```js + render() { + // ...other render related code + if (this.root === undefined || this.containerElement === undefined) { + // initialize the v-dom library + this.root = ReactDom.createRoot(document.createElement('div')); + } + } + + destroy() { + // end the lifecycle of v-dom library + this.root && this.root.unmount(); + } +``` + +The `destroy` method will be called by parent widget. If you widget don't have any child widget, you can just write your own tear down logic. If it may have some child widget, don't forget to call original `destroy` method in the `Widget` class to destroy children widgets. + +```js +Widget.prototype.destroy(); +this.root && this.root.unmount(); +/** if you are using ESNext +super.destroy(); +this.root?.unmount(); +*/ +``` \ No newline at end of file diff --git a/editions/dev/tiddlers/from tw5.com/moduletypes/WidgetModules.tid b/editions/dev/tiddlers/from tw5.com/moduletypes/WidgetModules.tid index 1a8bf5edf..0b0b3f33a 100644 --- a/editions/dev/tiddlers/from tw5.com/moduletypes/WidgetModules.tid +++ b/editions/dev/tiddlers/from tw5.com/moduletypes/WidgetModules.tid @@ -1,7 +1,8 @@ -title: WidgetModules +created: 20131101130700000 +modified: 20230601130631884 tags: dev moduletypes -created: 201311011307 -modified: 201311011307 +title: WidgetModules +type: text/vnd.tiddlywiki ! Introduction @@ -78,4 +79,10 @@ The individual methods defined by the widget object are documented in the source !! Widget `refreshChildren` method !! Widget `findNextSiblingDomNode` method !! Widget `findFirstDomNode` method +!! Widget `destroy` method + +<<.from-version "5.3.0">> Gets called when any parent widget is unmounted from the widget tree. + +[[Examples|Widget `destroy` method examples]] + !! Widget `removeChildDomNodes` method diff --git a/editions/dev/tiddlers/system/doc-styles.tid b/editions/dev/tiddlers/system/doc-styles.tid new file mode 100644 index 000000000..24234d47a --- /dev/null +++ b/editions/dev/tiddlers/system/doc-styles.tid @@ -0,0 +1,40 @@ +created: 20150117152612000 +modified: 20230325101137075 +tags: $:/tags/Stylesheet +title: $:/editions/tw5.com/doc-styles +type: text/vnd.tiddlywiki + +a.doc-from-version.tc-tiddlylink { + display: inline-block; + border-radius: 1em; + background: <>; + color: <>; + fill: <>; + padding: 0 0.4em; + font-size: 0.7em; + text-transform: uppercase; + font-weight: bold; + line-height: 1.5; + vertical-align: text-bottom; +} + +a.doc-deprecated-version.tc-tiddlylink { + display: inline-block; + border-radius: 1em; + background: red; + color: <>; + fill: <>; + padding: 0 0.4em; + font-size: 0.7em; + text-transform: uppercase; + font-weight: bold; + line-height: 1.5; + vertical-align: text-bottom; +} + +.doc-deprecated-version svg, +.doc-from-version svg { + width: 1em; + height: 1em; + vertical-align: text-bottom; +} diff --git a/editions/dev/tiddlers/system/version-macros.tid b/editions/dev/tiddlers/system/version-macros.tid new file mode 100644 index 000000000..0fb7dcf12 --- /dev/null +++ b/editions/dev/tiddlers/system/version-macros.tid @@ -0,0 +1,14 @@ +code-body: yes +created: 20161008085627406 +modified: 20221007122259593 +tags: $:/tags/Macro +title: $:/editions/tw5.com/version-macros +type: text/vnd.tiddlywiki + +\define .from-version(version) +<$link to={{{ [<__version__>addprefix[Release ]] }}} class="doc-from-version">{{$:/core/images/warning}} New in: <$text text=<<__version__>>/> +\end + +\define .deprecated-since(version, superseded:"TODO-Link") +<$link to="Deprecated - What does it mean" class="doc-deprecated-version tc-btn-invisible">{{$:/core/images/warning}} Deprecated since: <$text text=<<__version__>>/> (see <$link to=<<__superseded__>>><$text text=<<__superseded__>>/>) +\end From f90eb386ae7cd7da0f18b8d53fb0e454d9486d75 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Tue, 13 Jun 2023 16:36:07 +0100 Subject: [PATCH 8/8] Bidirectional text improvements (#4541) * Add support for \dir pragma * Add "dir" attribute to reveal, edit, edit-text and edit-codemirror widgets * Add $:/config/DefaultTextDirection hidden setting * Revert accidentally commited test data This reverts some of commit b83c1d160f12813a499872126d637b7f2199a29b. * Remove Codemirror plugin from Prerelease Makes it easier to test things * Fix framed text editor directionality in Firefox * Add direction attribute for edit body template * Missed closing brace * Add docs for \dir pragma * Templates should set text direction from a variable, not a transclusion * Updates to framed.js in the light of PRs that have been merged since this * Restore whitespace trim * Docs dates * Fix typo * Clarify docs --- core/modules/editor/engines/framed.js | 4 ++ core/modules/editor/engines/simple.js | 3 ++ core/modules/editor/factory.js | 3 +- core/modules/parsers/wikiparser/rules/dir.js | 51 +++++++++++++++++++ core/modules/parsers/wikiparser/wikiparser.js | 1 + core/modules/widgets/edit.js | 3 +- core/modules/widgets/reveal.js | 6 ++- core/templates/single.tiddler.window.tid | 3 +- core/ui/EditTemplate.tid | 1 + core/ui/EditTemplate/body-editor.tid | 1 + core/ui/EditTemplate/body/default.tid | 4 +- core/ui/EditTemplate/controls.tid | 2 +- core/ui/EditTemplate/fields.tid | 4 +- core/ui/EditTemplate/shadow.tid | 4 +- core/ui/EditTemplate/tags.tid | 2 +- core/ui/EditTemplate/title.tid | 6 +-- core/ui/EditTemplate/type.tid | 2 +- core/ui/PageTemplate.tid | 3 +- core/ui/ViewTemplate.tid | 2 +- core/ui/ViewTemplate/body.tid | 2 +- core/ui/ViewTemplate/subtitle.tid | 2 +- core/ui/ViewTemplate/tags.tid | 2 +- core/ui/ViewTemplate/title.tid | 2 +- core/wiki/config/DefaultTextDirection.tid | 2 + .../tiddlers/Right-To-Left Languages.tid | 11 ++++ .../tw5.com/tiddlers/pragmas/Pragma_ _dir.tid | 13 +++++ plugins/tiddlywiki/codemirror/engine.js | 3 ++ 27 files changed, 120 insertions(+), 22 deletions(-) create mode 100644 core/modules/parsers/wikiparser/rules/dir.js create mode 100644 core/wiki/config/DefaultTextDirection.tid create mode 100644 editions/tw5.com/tiddlers/Right-To-Left Languages.tid create mode 100644 editions/tw5.com/tiddlers/pragmas/Pragma_ _dir.tid diff --git a/core/modules/editor/engines/framed.js b/core/modules/editor/engines/framed.js index a4cf983b0..01b9974c2 100644 --- a/core/modules/editor/engines/framed.js +++ b/core/modules/editor/engines/framed.js @@ -72,6 +72,9 @@ function FramedEngine(options) { if(this.widget.editRows) { this.domNode.setAttribute("rows",this.widget.editRows); } + if(this.widget.editDir) { + this.domNode.setAttribute("dir",this.widget.editDir); + } if(this.widget.editTabIndex) { this.iframeNode.setAttribute("tabindex",this.widget.editTabIndex); } @@ -120,6 +123,7 @@ FramedEngine.prototype.copyStyles = function() { this.domNode.style["-webkit-text-fill-color"] = "currentcolor"; // Ensure we don't force text direction to LTR this.domNode.style.removeProperty("direction"); + this.domNode.style.removeProperty("unicodeBidi"); }; /* diff --git a/core/modules/editor/engines/simple.js b/core/modules/editor/engines/simple.js index 9840cb623..64c087133 100644 --- a/core/modules/editor/engines/simple.js +++ b/core/modules/editor/engines/simple.js @@ -52,6 +52,9 @@ function SimpleEngine(options) { if(this.widget.editTabIndex) { this.domNode.setAttribute("tabindex",this.widget.editTabIndex); } + if(this.widget.editDir) { + this.domNode.setAttribute("dir",this.widget.editDir); + } if(this.widget.editAutoComplete) { this.domNode.setAttribute("autocomplete",this.widget.editAutoComplete); } diff --git a/core/modules/editor/factory.js b/core/modules/editor/factory.js index 6157ec67f..7e43f709b 100644 --- a/core/modules/editor/factory.js +++ b/core/modules/editor/factory.js @@ -183,6 +183,7 @@ function editTextWidgetFactory(toolbarEngine,nonToolbarEngine) { this.editFocusSelectFromStart = $tw.utils.parseNumber(this.getAttribute("focusSelectFromStart","0")); this.editFocusSelectFromEnd = $tw.utils.parseNumber(this.getAttribute("focusSelectFromEnd","0")); this.editTabIndex = this.getAttribute("tabindex"); + this.editDir = this.getAttribute("dir"); this.editCancelPopups = this.getAttribute("cancelPopups","") === "yes"; this.editInputActions = this.getAttribute("inputActions"); this.editRefreshTitle = this.getAttribute("refreshTitle"); @@ -220,7 +221,7 @@ function editTextWidgetFactory(toolbarEngine,nonToolbarEngine) { EditTextWidget.prototype.refresh = function(changedTiddlers) { var changedAttributes = this.computeAttributes(); // Completely rerender if any of our attributes have changed - if(changedAttributes.tiddler || changedAttributes.field || changedAttributes.index || changedAttributes["default"] || changedAttributes["class"] || changedAttributes.placeholder || changedAttributes.size || changedAttributes.autoHeight || changedAttributes.minHeight || changedAttributes.focusPopup || changedAttributes.rows || changedAttributes.tabindex || changedAttributes.cancelPopups || changedAttributes.inputActions || changedAttributes.refreshTitle || changedAttributes.autocomplete || changedTiddlers[HEIGHT_MODE_TITLE] || changedTiddlers[ENABLE_TOOLBAR_TITLE] || changedTiddlers["$:/palette"] || changedAttributes.disabled || changedAttributes.fileDrop) { + if(changedAttributes.tiddler || changedAttributes.field || changedAttributes.index || changedAttributes["default"] || changedAttributes["class"] || changedAttributes.placeholder || changedAttributes.size || changedAttributes.autoHeight || changedAttributes.minHeight || changedAttributes.focusPopup || changedAttributes.rows || changedAttributes.tabindex || changedAttributes.dir || changedAttributes.cancelPopups || changedAttributes.inputActions || changedAttributes.refreshTitle || changedAttributes.autocomplete || changedTiddlers[HEIGHT_MODE_TITLE] || changedTiddlers[ENABLE_TOOLBAR_TITLE] || changedTiddlers["$:/palette"] || changedAttributes.disabled || changedAttributes.fileDrop) { this.refreshSelf(); return true; } else if (changedTiddlers[this.editRefreshTitle]) { diff --git a/core/modules/parsers/wikiparser/rules/dir.js b/core/modules/parsers/wikiparser/rules/dir.js new file mode 100644 index 000000000..e30fe7a47 --- /dev/null +++ b/core/modules/parsers/wikiparser/rules/dir.js @@ -0,0 +1,51 @@ +/*\ +title: $:/core/modules/parsers/wikiparser/rules/dir.js +type: application/javascript +module-type: wikirule + +Wiki pragma rule for specifying text direction + +``` +\dir rtl +\dir ltr +\dir auto +``` + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +exports.name = "dir"; +exports.types = {pragma: true}; + +/* +Instantiate parse rule +*/ +exports.init = function(parser) { + this.parser = parser; + // Regexp to match + this.matchRegExp = /^\\dir[^\S\n]*(\S+)\r?\n/mg; +}; + +/* +Parse the most recent match +*/ +exports.parse = function() { + var self = this; + // Move past the pragma invocation + this.parser.pos = this.matchRegExp.lastIndex; + // Parse tree nodes to return + return [{ + type: "element", + tag: this.parser.parseAsInline ? "span" : "div", + attributes: { + dir: {type: "string", value: this.match[1]} + }, + children: [] + }]; +}; + +})(); diff --git a/core/modules/parsers/wikiparser/wikiparser.js b/core/modules/parsers/wikiparser/wikiparser.js index bb457b205..9cdb91913 100644 --- a/core/modules/parsers/wikiparser/wikiparser.js +++ b/core/modules/parsers/wikiparser/wikiparser.js @@ -36,6 +36,7 @@ options: see below: */ var WikiParser = function(type,text,options) { this.wiki = options.wiki; + this.parseAsInline = options.parseAsInline; var self = this; // Check for an externally linked tiddler if($tw.browser && (text || "") === "" && options._canonical_uri) { diff --git a/core/modules/widgets/edit.js b/core/modules/widgets/edit.js index e7bd49b93..ce72f0926 100644 --- a/core/modules/widgets/edit.js +++ b/core/modules/widgets/edit.js @@ -48,6 +48,7 @@ EditWidget.prototype.execute = function() { this.editPlaceholder = this.getAttribute("placeholder"); this.editTabIndex = this.getAttribute("tabindex"); this.editFocus = this.getAttribute("focus",""); + this.editDir = this.getAttribute("dir"); this.editCancelPopups = this.getAttribute("cancelPopups",""); this.editInputActions = this.getAttribute("inputActions"); this.editRefreshTitle = this.getAttribute("refreshTitle"); @@ -90,7 +91,7 @@ Selectively refreshes the widget if needed. Returns true if the widget or any of EditWidget.prototype.refresh = function(changedTiddlers) { var changedAttributes = this.computeAttributes(); // Refresh if an attribute has changed, or the type associated with the target tiddler has changed - if(changedAttributes.tiddler || changedAttributes.field || changedAttributes.index || changedAttributes.tabindex || changedAttributes.cancelPopups || changedAttributes.inputActions || changedAttributes.refreshTitle || changedAttributes.autocomplete || (changedTiddlers[this.editTitle] && this.getEditorType() !== this.editorType)) { + if(changedAttributes.tiddler || changedAttributes.field || changedAttributes.index || changedAttributes.tabindex || changedAttributes.dir || changedAttributes.cancelPopups || changedAttributes.inputActions || changedAttributes.refreshTitle || changedAttributes.autocomplete || (changedTiddlers[this.editTitle] && this.getEditorType() !== this.editorType)) { this.refreshSelf(); return true; } else { diff --git a/core/modules/widgets/reveal.js b/core/modules/widgets/reveal.js index 3e3510f75..fde810439 100755 --- a/core/modules/widgets/reveal.js +++ b/core/modules/widgets/reveal.js @@ -42,6 +42,9 @@ RevealWidget.prototype.render = function(parent,nextSibling) { if(this.style) { domNode.setAttribute("style",this.style); } + if(this.direction) { + domNode.setAttribute("dir",this.direction); + } parent.insertBefore(domNode,nextSibling); this.renderChildren(domNode,null); if(!domNode.isTiddlyWikiFakeDom && this.type === "popup" && this.isOpen) { @@ -123,6 +126,7 @@ RevealWidget.prototype.execute = function() { this["default"] = this.getAttribute("default",""); this.animate = this.getAttribute("animate","no"); this.retain = this.getAttribute("retain","no"); + this.direction = this.getAttribute("dir"); this.openAnimation = this.animate === "no" ? undefined : "open"; this.closeAnimation = this.animate === "no" ? undefined : "close"; this.updatePopupPosition = this.getAttribute("updatePopupPosition","no") === "yes"; @@ -214,7 +218,7 @@ Selectively refreshes the widget if needed. Returns true if the widget or any of */ RevealWidget.prototype.refresh = function(changedTiddlers) { var changedAttributes = this.computeAttributes(); - if(changedAttributes.state || changedAttributes.type || changedAttributes.text || changedAttributes.position || changedAttributes.positionAllowNegative || changedAttributes["default"] || changedAttributes.animate || changedAttributes.stateTitle || changedAttributes.stateField || changedAttributes.stateIndex) { + if(changedAttributes.state || changedAttributes.type || changedAttributes.text || changedAttributes.position || changedAttributes.positionAllowNegative || changedAttributes["default"] || changedAttributes.animate || changedAttributes.stateTitle || changedAttributes.stateField || changedAttributes.stateIndex || changedAttributes.dir) { this.refreshSelf(); return true; } else { diff --git a/core/templates/single.tiddler.window.tid b/core/templates/single.tiddler.window.tid index aa5175c01..48ce49247 100644 --- a/core/templates/single.tiddler.window.tid +++ b/core/templates/single.tiddler.window.tid @@ -12,7 +12,8 @@ tc-page-container tc-page-view-$(storyviewTitle)$ tc-language-$(languageTitle)$ tv-config-toolbar-class={{$:/config/Toolbar/ButtonClass}} tv-show-missing-links={{$:/config/MissingLinks}} storyviewTitle={{$:/view}} - languageTitle={{{ [{$:/language}get[name]] }}}> + languageTitle={{{ [{$:/language}get[name]] }}} + tv-text-direction={{$:/config/DefaultTextDirection}}>
        >> diff --git a/core/ui/EditTemplate.tid b/core/ui/EditTemplate.tid index 5aed61a73..4ddea2a62 100644 --- a/core/ui/EditTemplate.tid +++ b/core/ui/EditTemplate.tid @@ -29,6 +29,7 @@ title: $:/core/ui/EditTemplate data-tiddler-title=<> data-tags={{!!tags}} class={{{ [all[shadows+tiddlers]tag[$:/tags/ClassFilters/TiddlerTemplate]!is[draft]] :map:flat[subfilter{!!text}] tc-tiddler-frame tc-tiddler-edit-frame [is[tiddler]then[tc-tiddler-exists]] [is[missing]!is[shadow]then[tc-tiddler-missing]] [is[shadow]then[tc-tiddler-exists tc-tiddler-shadow]] [is[system]then[tc-tiddler-system]] [{!!class}] [tags[]encodeuricomponent[]addprefix[tc-tagged-]] +[join[ ]] }}} + dir=<> role="region" aria-label={{$:/language/EditTemplate/Caption}}> <$fieldmangler> diff --git a/core/ui/EditTemplate/body-editor.tid b/core/ui/EditTemplate/body-editor.tid index 374567acd..8d17c498e 100644 --- a/core/ui/EditTemplate/body-editor.tid +++ b/core/ui/EditTemplate/body-editor.tid @@ -9,6 +9,7 @@ title: $:/core/ui/EditTemplate/body/editor placeholder={{$:/language/EditTemplate/Body/Placeholder}} tabindex={{$:/config/EditTabIndex}} focus={{{ [{$:/config/AutoFocus}match[text]then[true]] ~[[false]] }}} + dir=<> cancelPopups="yes" fileDrop={{{ [{$:/config/DragAndDrop/Enable}match[no]] :else[subfilter{$:/config/Editor/EnableImportFilter}then[yes]else[no]] }}} diff --git a/core/ui/EditTemplate/body/default.tid b/core/ui/EditTemplate/body/default.tid index a2128efb0..069a43d1a 100644 --- a/core/ui/EditTemplate/body/default.tid +++ b/core/ui/EditTemplate/body/default.tid @@ -15,7 +15,7 @@ $:/config/EditorToolbarButtons/Visibility/$(currentTiddler)$ importState=<> > <$dropzone importTitle=<> autoOpenOnImport="no" contentTypesFilter={{$:/config/Editor/ImportContentTypesFilter}} class="tc-dropzone-editor" enable={{{ [{$:/config/DragAndDrop/Enable}match[no]] :else[subfilter{$:/config/Editor/EnableImportFilter}then[yes]else[no]] }}} filesOnly="yes" actions=<> > <$reveal stateTitle=<> type="match" text="yes" tag="div"> -
        +
        >> <$transclude tiddler="$:/core/ui/EditTemplate/body/editor" mode="inline"/> @@ -32,7 +32,7 @@ $:/config/EditorToolbarButtons/Visibility/$(currentTiddler)$
        -<$reveal stateTitle=<> type="nomatch" text="yes" tag="div"> +<$reveal stateTitle=<> type="nomatch" text="yes" tag="div" dir=<>> <$transclude tiddler="$:/core/ui/EditTemplate/body/editor" mode="inline"/> diff --git a/core/ui/EditTemplate/controls.tid b/core/ui/EditTemplate/controls.tid index 3e94d371d..2dd8709ae 100644 --- a/core/ui/EditTemplate/controls.tid +++ b/core/ui/EditTemplate/controls.tid @@ -5,7 +5,7 @@ tags: $:/tags/EditTemplate $:/config/EditToolbarButtons/Visibility/$(listItem)$ \end \whitespace trim -
        +
        >> <$view field="title"/> <$list filter="[all[shadows+tiddlers]tag[$:/tags/EditToolbar]!has[draft.of]]" variable="listItem"><$let tv-config-toolbar-class={{{ [enlist] [encodeuricomponent[]addprefix[tc-btn-]] +[join[ ]]}}}><$reveal type="nomatch" state=<> text="hide"><$transclude tiddler=<>/>
        diff --git a/core/ui/EditTemplate/fields.tid b/core/ui/EditTemplate/fields.tid index 6a767517b..438456455 100644 --- a/core/ui/EditTemplate/fields.tid +++ b/core/ui/EditTemplate/fields.tid @@ -74,7 +74,7 @@ $value={{{ [subfilterget[text]] }}}/> \whitespace trim <$set name="newFieldValueTiddlerPrefix" value=<> emptyValue=<> > -
        +
        >> <$list filter="[all[current]fields[]] +[sort[title]]" variable="currentField" storyview="pop"> @@ -101,7 +101,7 @@ $value={{{ [subfilterget[text]] }}}/> <$fieldmangler> -
        +
        >> <> diff --git a/core/ui/EditTemplate/shadow.tid b/core/ui/EditTemplate/shadow.tid index 3ae3e0a1f..97672750e 100644 --- a/core/ui/EditTemplate/shadow.tid +++ b/core/ui/EditTemplate/shadow.tid @@ -14,7 +14,7 @@ tags: $:/tags/EditTemplate <$list filter="[all[current]shadowsource[]]" variable="pluginTitle"> <$set name="pluginLink" value=<>> -
        +
        >> <> @@ -29,7 +29,7 @@ tags: $:/tags/EditTemplate <$list filter="[all[current]shadowsource[]]" variable="pluginTitle"> <$set name="pluginLink" value=<>> -
        +
        >> <> diff --git a/core/ui/EditTemplate/tags.tid b/core/ui/EditTemplate/tags.tid index 5084478b4..0456e1bb7 100644 --- a/core/ui/EditTemplate/tags.tid +++ b/core/ui/EditTemplate/tags.tid @@ -27,7 +27,7 @@ color:$(foregroundColor)$; \define edit-tags-template(tagField:"tags") \whitespace trim -
        +
        >> <$list filter="[list[!!$tagField$]sort[title]]" storyview="pop"> <$macrocall $name="tag-body" colour={{{ [] :cascade[all[shadows+tiddlers]tag[$:/tags/TiddlerColourFilter]!is[draft]get[text]] }}} palette={{$:/palette}} icon={{{ [] :cascade[all[shadows+tiddlers]tag[$:/tags/TiddlerIconFilter]!is[draft]get[text]] }}} tagField=<<__tagField__>>/> diff --git a/core/ui/EditTemplate/title.tid b/core/ui/EditTemplate/title.tid index 5228ad7c0..fa02db819 100644 --- a/core/ui/EditTemplate/title.tid +++ b/core/ui/EditTemplate/title.tid @@ -2,13 +2,13 @@ title: $:/core/ui/EditTemplate/title tags: $:/tags/EditTemplate \whitespace trim -<$edit-text field="draft.title" class="tc-titlebar tc-edit-texteditor" focus={{{ [{$:/config/AutoFocus}match[title]then[true]] ~[[false]] }}} tabindex={{$:/config/EditTabIndex}} cancelPopups="yes"/> +<$edit-text field="draft.title" class="tc-titlebar tc-edit-texteditor" focus={{{ [{$:/config/AutoFocus}match[title]then[true]] ~[[false]] }}} tabindex={{$:/config/EditTabIndex}} cancelPopups="yes" dir=<>/> <$vars pattern="""[\|\[\]{}]""" bad-chars="""`| [ ] { }`"""> <$list filter="[all[current]regexp:draft.title]" variable="listItem"> -
        +
        >> {{$:/core/images/warning}} {{$:/language/EditTemplate/Title/BadCharacterWarning}} @@ -18,7 +18,7 @@ tags: $:/tags/EditTemplate -<$reveal state="!!draft.title" type="nomatch" text={{!!draft.of}} tag="div"> +<$reveal state="!!draft.title" type="nomatch" text={{!!draft.of}} tag="div" dir=<>> <$list filter="[{!!draft.title}!is[missing]]" variable="listItem"> diff --git a/core/ui/EditTemplate/type.tid b/core/ui/EditTemplate/type.tid index faa89639f..0dca582e1 100644 --- a/core/ui/EditTemplate/type.tid +++ b/core/ui/EditTemplate/type.tid @@ -9,7 +9,7 @@ first-search-filter: [all[shadows+tiddlers]prefix[$:/language/Docs/Types/]sort[d
        <>
        -
        <$fieldmangler> +
        >}><$fieldmangler> <$macrocall $name="keyboard-driven-input" tiddler=<> storeTitle=<> refreshTitle=<> selectionStateTitle=<> field="type" tag="input" default="" placeholder={{$:/language/EditTemplate/Type/Placeholder}} focusPopup=<> class="tc-edit-typeeditor tc-edit-texteditor tc-popup-handle" tabindex={{$:/config/EditTabIndex}} focus={{{ [{$:/config/AutoFocus}match[type]then[true]] ~[[false]] }}} cancelPopups="yes" configTiddlerFilter="[[$:/core/ui/EditTemplate/type]]" inputCancelActions=<>/><$button popup=<> class="tc-btn-invisible tc-btn-dropdown tc-small-gap" tooltip={{$:/language/EditTemplate/Type/Dropdown/Hint}} aria-label={{$:/language/EditTemplate/Type/Dropdown/Caption}}>{{$:/core/images/down-arrow}}<$button message="tm-remove-field" param="type" class="tc-btn-invisible tc-btn-icon" tooltip={{$:/language/EditTemplate/Type/Delete/Hint}} aria-label={{$:/language/EditTemplate/Type/Delete/Caption}}>{{$:/core/images/delete-button}}<$action-deletetiddler $filter="[] [] []"/>
        diff --git a/core/ui/PageTemplate.tid b/core/ui/PageTemplate.tid index f0ab4852a..faf1a06b6 100644 --- a/core/ui/PageTemplate.tid +++ b/core/ui/PageTemplate.tid @@ -13,7 +13,8 @@ icon: $:/core/images/layout-button tv-enable-drag-and-drop={{$:/config/DragAndDrop/Enable}} tv-show-missing-links={{$:/config/MissingLinks}} storyviewTitle={{$:/view}} - languageTitle={{{ [{$:/language}get[name]] }}}> + languageTitle={{{ [{$:/language}get[name]] }}} + tv-text-direction={{$:/config/DefaultTextDirection}}>
        ] [[tc-language-]addsuffix] :and[unique[]join[ ]] }}} > diff --git a/core/ui/ViewTemplate.tid b/core/ui/ViewTemplate.tid index dcba5c953..3f39f3496 100644 --- a/core/ui/ViewTemplate.tid +++ b/core/ui/ViewTemplate.tid @@ -7,7 +7,7 @@ $:/state/folded/$(currentTiddler)$ \define cancel-delete-tiddler-actions(message) <$action-sendmessage $message="tm-$message$-tiddler"/> \import [all[shadows+tiddlers]tag[$:/tags/Macro/View]!is[draft]] [all[shadows+tiddlers]tag[$:/tags/Global/View]!is[draft]] <$vars storyTiddler=<> tiddlerInfoState=<>> -
        > data-tags={{!!tags}} class={{{ [all[shadows+tiddlers]tag[$:/tags/ClassFilters/TiddlerTemplate]!is[draft]] :map:flat[subfilter{!!text}] tc-tiddler-frame tc-tiddler-view-frame [is[tiddler]then[tc-tiddler-exists]] [is[missing]!is[shadow]then[tc-tiddler-missing]] [is[shadow]then[tc-tiddler-exists tc-tiddler-shadow]] [is[shadow]is[tiddler]then[tc-tiddler-overridden-shadow]] [is[system]then[tc-tiddler-system]] [{!!class}] [tags[]encodeuricomponent[]addprefix[tc-tagged-]] +[join[ ]] }}} role="article"> +
        > data-tiddler-title=<> data-tags={{!!tags}} class={{{ [all[shadows+tiddlers]tag[$:/tags/ClassFilters/TiddlerTemplate]!is[draft]] :map:flat[subfilter{!!text}] tc-tiddler-frame tc-tiddler-view-frame [is[tiddler]then[tc-tiddler-exists]] [is[missing]!is[shadow]then[tc-tiddler-missing]] [is[shadow]then[tc-tiddler-exists tc-tiddler-shadow]] [is[shadow]is[tiddler]then[tc-tiddler-overridden-shadow]] [is[system]then[tc-tiddler-system]] [{!!class}] [tags[]encodeuricomponent[]addprefix[tc-tagged-]] +[join[ ]] }}} role="article"> <$list filter="[all[shadows+tiddlers]tag[$:/tags/ViewTemplate]!is[draft]]" variable="listItem"> <$transclude tiddler=<>/> diff --git a/core/ui/ViewTemplate/body.tid b/core/ui/ViewTemplate/body.tid index 34e6aaa38..36cac1e18 100644 --- a/core/ui/ViewTemplate/body.tid +++ b/core/ui/ViewTemplate/body.tid @@ -3,7 +3,7 @@ tags: $:/tags/ViewTemplate \import [all[shadows+tiddlers]tag[$:/tags/Macro/View/Body]!is[draft]] [all[shadows+tiddlers]tag[$:/tags/Global/View/Body]!is[draft]] -<$reveal tag="div" class="tc-tiddler-body" type="nomatch" stateTitle=<> text="hide" retain="yes" animate="yes"> +<$reveal tag="div" class="tc-tiddler-body" type="nomatch" stateTitle=<> text="hide" retain="yes" animate="yes" dir=<>> <$transclude tiddler={{{ [] :cascade[all[shadows+tiddlers]tag[$:/tags/ViewTemplateBodyFilter]!is[draft]get[text]] :and[!is[blank]else[$:/core/ui/ViewTemplate/body/default]] }}} /> diff --git a/core/ui/ViewTemplate/subtitle.tid b/core/ui/ViewTemplate/subtitle.tid index a0436b095..611130869 100644 --- a/core/ui/ViewTemplate/subtitle.tid +++ b/core/ui/ViewTemplate/subtitle.tid @@ -3,7 +3,7 @@ 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">   diff --git a/core/ui/ViewTemplate/tags.tid b/core/ui/ViewTemplate/tags.tid index d1f4e55c9..95b5dbe39 100644 --- a/core/ui/ViewTemplate/tags.tid +++ b/core/ui/ViewTemplate/tags.tid @@ -3,5 +3,5 @@ tags: $:/tags/ViewTemplate \whitespace trim <$reveal type="nomatch" stateTitle=<> text="hide" tag="div" retain="yes" animate="yes"> -
        <$list filter="[all[current]tags[]sort[title]]" template="$:/core/ui/TagTemplate" storyview="pop"/>
        +
        >><$list filter="[all[current]tags[]sort[title]]" template="$:/core/ui/TagTemplate" storyview="pop"/>
        diff --git a/core/ui/ViewTemplate/title.tid b/core/ui/ViewTemplate/title.tid index 98695f6bf..19d375068 100644 --- a/core/ui/ViewTemplate/title.tid +++ b/core/ui/ViewTemplate/title.tid @@ -6,7 +6,7 @@ tags: $:/tags/ViewTemplate fill:$(foregroundColor)$; \end
        -
        +
        >> <$list filter="[all[shadows+tiddlers]tag[$:/tags/ViewToolbar]!has[draft.of]] :filter[lookup[$:/config/ViewToolbarButtons/Visibility/]!match[hide]]" storyview="pop" variable="listItem"><$set name="tv-config-toolbar-class" filter="[] [encodeuricomponent[]addprefix[tc-btn-]]"><$transclude tiddler=<>/> diff --git a/core/wiki/config/DefaultTextDirection.tid b/core/wiki/config/DefaultTextDirection.tid new file mode 100644 index 000000000..6140bbf7b --- /dev/null +++ b/core/wiki/config/DefaultTextDirection.tid @@ -0,0 +1,2 @@ +title: $:/config/DefaultTextDirection +text: auto diff --git a/editions/tw5.com/tiddlers/Right-To-Left Languages.tid b/editions/tw5.com/tiddlers/Right-To-Left Languages.tid new file mode 100644 index 000000000..d32d01524 --- /dev/null +++ b/editions/tw5.com/tiddlers/Right-To-Left Languages.tid @@ -0,0 +1,11 @@ +created: 20230613162508509 +modified: 20230613162508509 +title: Right-To-Left Languages +type: text/vnd.tiddlywiki + +<<.from-version "5.3.0">> The [[language plugins|Languages]] in TiddlyWiki's plugin library apply the appropriate [["right-to-left" setting|https://www.w3.org/International/questions/qa-html-dir]] to the entire document. To set the right to left setting independently for an individual tiddler, use the `\dir` [[pragma|Pragma]] at the top of the tiddler: + +``` +\dir rtl +This text will be displayed with right-to-left formatting +``` diff --git a/editions/tw5.com/tiddlers/pragmas/Pragma_ _dir.tid b/editions/tw5.com/tiddlers/pragmas/Pragma_ _dir.tid new file mode 100644 index 000000000..bc5774e30 --- /dev/null +++ b/editions/tw5.com/tiddlers/pragmas/Pragma_ _dir.tid @@ -0,0 +1,13 @@ +created: 20230613162508509 +modified: 20230613162508509 +tags: Pragmas +title: Pragma: \dir +type: text/vnd.tiddlywiki + +<<.from-version "5.3.0">> The ''\dir'' [[pragma|Pragmas]] is used to set the text direction of text within a tiddler -- see [[Right-To-Left Languages]]. + +The ''\dir'' pragma should be used after any procedure, function, widget or macro definitions. + +* `\dir ltr` – sets text direction to left-to-right +* `\dir rtl` – sets text direction to right-to-left +* `\dir auto` – causes the browser to attempt to automatically deduce the text direction diff --git a/plugins/tiddlywiki/codemirror/engine.js b/plugins/tiddlywiki/codemirror/engine.js index e775e1c95..e8749481d 100755 --- a/plugins/tiddlywiki/codemirror/engine.js +++ b/plugins/tiddlywiki/codemirror/engine.js @@ -109,6 +109,9 @@ function CodeMirrorEngine(options) { if(this.widget.editTabIndex) { config["tabindex"] = this.widget.editTabIndex; } + if(this.widget.editDir) { + config.direction = this.widget.editDir; + } config.editWidget = this.widget; // Create the CodeMirror instance this.cm = window.CodeMirror(function(cmDomNode) {