From 58d291c116a2843cf0a5bf45366c66c1c9fefab6 Mon Sep 17 00:00:00 2001 From: "jeremy@jermolene.com" Date: Mon, 22 Feb 2021 19:20:58 +0000 Subject: [PATCH 1/3] First commit --- core/modules/savers/postmessage.js | 66 +++++++++++++++ core/modules/startup/favicon.js | 18 +++-- core/modules/startup/render.js | 9 ++- core/modules/utils/messaging.js | 125 +++++++++++++++++++++++++++++ 4 files changed, 208 insertions(+), 10 deletions(-) create mode 100644 core/modules/savers/postmessage.js create mode 100644 core/modules/utils/messaging.js diff --git a/core/modules/savers/postmessage.js b/core/modules/savers/postmessage.js new file mode 100644 index 000000000..6483edc43 --- /dev/null +++ b/core/modules/savers/postmessage.js @@ -0,0 +1,66 @@ +/*\ +title: $:/core/modules/savers/postmessage.js +type: application/javascript +module-type: saver + +Handles saving changes via window.postMessage() to the window.parent + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +/* +Select the appropriate saver module and set it up +*/ +var PostMessageSaver = function(wiki) { + this.publisher = new $tw.utils.BrowserMessagingPublisher({type: "SAVE"}); +}; + +PostMessageSaver.prototype.save = function(text,method,callback,options) { + // Fail if the publisher hasn't been fully initialised + if(!this.publisher.canSend()) { + return false; + } + // Send the save request + this.publisher.send({ + verb: "SAVE", + body: text + },function(err) { + if(err) { + callback("PostMessageSaver Error: " + err); + } else { + callback(null); + } + }); + // Indicate that we handled the save + return true; +}; + +/* +Information about this saver +*/ +PostMessageSaver.prototype.info = { + name: "postmessage", + capabilities: ["save", "autosave"], + priority: 100 +}; + +/* +Static method that returns true if this saver is capable of working +*/ +exports.canSave = function(wiki) { + // Provisionally say that we can save + return true; +}; + +/* +Create an instance of this saver +*/ +exports.create = function(wiki) { + return new PostMessageSaver(wiki); +}; + +})(); diff --git a/core/modules/startup/favicon.js b/core/modules/startup/favicon.js index 0e730905f..38ea4ddd5 100644 --- a/core/modules/startup/favicon.js +++ b/core/modules/startup/favicon.js @@ -22,6 +22,16 @@ exports.synchronous = true; var FAVICON_TITLE = "$:/favicon.ico"; exports.startup = function() { + var setFavicon = function() { + var tiddler = $tw.wiki.getTiddler(FAVICON_TITLE); + if(tiddler) { + var faviconLink = document.getElementById("faviconLink"), + dataURI = $tw.utils.makeDataUri(tiddler.fields.text,tiddler.fields.type,tiddler.fields._canonical_uri); + faviconLink.setAttribute("href",dataURI); + $tw.faviconPublisher.send({verb: "FAVICON",body: dataURI}); + } + } + $tw.faviconPublisher = new $tw.utils.BrowserMessagingPublisher({type: "FAVICON", onsubscribe: setFavicon}); // Set up the favicon setFavicon(); // Reset the favicon when the tiddler changes @@ -32,12 +42,4 @@ exports.startup = function() { }); }; -function setFavicon() { - var tiddler = $tw.wiki.getTiddler(FAVICON_TITLE); - if(tiddler) { - var faviconLink = document.getElementById("faviconLink"); - faviconLink.setAttribute("href",$tw.utils.makeDataUri(tiddler.fields.text,tiddler.fields.type,tiddler.fields._canonical_uri)); - } -} - })(); diff --git a/core/modules/startup/render.js b/core/modules/startup/render.js index fa4d21003..b68e100c9 100644 --- a/core/modules/startup/render.js +++ b/core/modules/startup/render.js @@ -32,10 +32,15 @@ exports.startup = function() { $tw.titleWidgetNode = $tw.wiki.makeTranscludeWidget(PAGE_TITLE_TITLE,{document: $tw.fakeDocument, parseAsInline: true}); $tw.titleContainer = $tw.fakeDocument.createElement("div"); $tw.titleWidgetNode.render($tw.titleContainer,null); - document.title = $tw.titleContainer.textContent; + $tw.titlePublisher = new $tw.utils.BrowserMessagingPublisher({type: "PAGETITLE", onsubscribe: publishTitle}); + var publishTitle = function() { + $tw.titlePublisher.send({verb: "PAGETITLE",body: document.title}); + document.title = $tw.titleContainer.textContent; + }; + publishTitle(); $tw.wiki.addEventListener("change",function(changes) { if($tw.titleWidgetNode.refresh(changes,$tw.titleContainer,null)) { - document.title = $tw.titleContainer.textContent; + publishTitle(); } }); // Set up the styles diff --git a/core/modules/utils/messaging.js b/core/modules/utils/messaging.js new file mode 100644 index 000000000..6f1139d33 --- /dev/null +++ b/core/modules/utils/messaging.js @@ -0,0 +1,125 @@ +/*\ +title: $:/core/modules/utils/messaging.js +type: application/javascript +module-type: utils-browser + +Messaging utilities for use with window.postMessage() etc. + +This module intentionally has no dependencies so that it can be included in non-TiddlyWiki projects + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +var RESPONSE_TIMEOUT = 2 * 1000; + +/* +Class to handle subscribing to publishers + +target: Target window (eg iframe.contentWindow) +type: String indicating type of item for which subscriptions are being provided (eg "SAVING") +onsubscribe: Function to be invoked with err parameter when the subscription is established, or there is a timeout +onmessage: Function to be invoked when a new message arrives. Returns the results to be replied +*/ +function BrowserMessagingSubscriber(options) { + var self = this; + this.target = options.target; + this.type = options.type; + this.onsubscribe = options.onsubscribe || function() {}; + this.onmessage = options.onmessage; + this.hasConfirmed = false; + this.channel = new MessageChannel(); + this.channel.port1.addEventListener("message",function(event) { + if(this.timerID) { + clearTimeout(this.timerID); + this.timerID = null; + } + if(event.data) { + if(event.data.verb === "SUBSCRIBED") { + self.hasConfirmed = true; + self.onsubscribe(null); + } else if(event.data.verb === self.type) { + var response = self.onmessage(event.data); + // Send the response back on the supplied port, and then close it + event.ports[0].postMessage(response); + event.ports[0].close(); + } + } + }); + // Set a timer so that if we don't hear from the iframe before a timeout we alert the user + this.timerID = setTimeout(function() { + if(!self.hasConfirmed) { + self.onsubscribe("NO_RESPONSE"); + } + },RESPONSE_TIMEOUT); + this.channel.port1.start(); + this.target.postMessage({verb: "SUBSCRIBE",to: self.type},"*",[this.channel.port2]); +} + +exports.BrowserMessagingSubscriber = BrowserMessagingSubscriber; + +/* +Class to handle publishing subscriptions + +type: String indicating type of item for which subscriptions are being provided (eg "SAVING") +onsubscribe: Function to be invoked when a subscription occurs +*/ +function BrowserMessagingPublisher(options) { + var self = this; + this.type = options.type; + this.hostIsListening = false; + this.port = null; + // Listen to connection requests from the host + window.addEventListener("message",function(event) { + if(event.data && event.data.verb === "SUBSCRIBE" && event.data.to === self.type) { + self.hostIsListening = true; + // Acknowledge + self.port = event.ports[0]; + self.port.postMessage({verb: "SUBSCRIBED", to: self.type}); + if(options.onsubscribe) { + options.onsubscribe(event.data); + } + } + }); +} + +BrowserMessagingPublisher.prototype.canSend = function() { + return !!this.hostIsListening && !!this.port; +}; + +BrowserMessagingPublisher.prototype.send = function(data,callback) { + var self = this; + callback = callback || function() {}; + // Check that we've been initialised by the host + if(!this.hostIsListening || !this.port) { + return false; + } + // Create a channel for the confirmation + var channel = new MessageChannel(); + channel.port1.addEventListener("message",function(event) { + if(event.data && event.data.verb === "OK") { + callback(null); + } else { + callback("BrowserMessagingPublisher for " + self.type + " error: " + (event.data || {}).verb); + } + channel.port1.close(); + }); + channel.port1.start(); + // Send the save request with the port for the response + this.port.postMessage(data,[channel.port2]); +}; + +BrowserMessagingPublisher.prototype.close = function() { + if(this.port) { + this.port.close(); + this.hostIsListening = false; + this.port = null; + } +}; + +exports.BrowserMessagingPublisher = BrowserMessagingPublisher; + +})(); From e31a201269a13855ab88d446e7f9df2e819b1f18 Mon Sep 17 00:00:00 2001 From: "jeremy@jermolene.com" Date: Mon, 22 Feb 2021 22:39:37 +0000 Subject: [PATCH 2/3] Subscriber: Make onmessage be async --- core/modules/utils/messaging.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/core/modules/utils/messaging.js b/core/modules/utils/messaging.js index 6f1139d33..c7467f8b9 100644 --- a/core/modules/utils/messaging.js +++ b/core/modules/utils/messaging.js @@ -22,7 +22,7 @@ Class to handle subscribing to publishers target: Target window (eg iframe.contentWindow) type: String indicating type of item for which subscriptions are being provided (eg "SAVING") onsubscribe: Function to be invoked with err parameter when the subscription is established, or there is a timeout -onmessage: Function to be invoked when a new message arrives. Returns the results to be replied +onmessage: Function to be invoked when a new message arrives, invoked with (data,callback). The callback is invoked with the argument (response) */ function BrowserMessagingSubscriber(options) { var self = this; @@ -42,10 +42,11 @@ function BrowserMessagingSubscriber(options) { self.hasConfirmed = true; self.onsubscribe(null); } else if(event.data.verb === self.type) { - var response = self.onmessage(event.data); - // Send the response back on the supplied port, and then close it - event.ports[0].postMessage(response); - event.ports[0].close(); + self.onmessage(event.data,function(response) { + // Send the response back on the supplied port, and then close it + event.ports[0].postMessage(response); + event.ports[0].close(); + }); } } }); From 33d4e8ea266687190743fbf3d72989179fcc772b Mon Sep 17 00:00:00 2001 From: "jeremy@jermolene.com" Date: Wed, 24 Feb 2021 15:27:58 +0000 Subject: [PATCH 3/3] Don't use a variable before it's defined... --- core/modules/startup/render.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/modules/startup/render.js b/core/modules/startup/render.js index b68e100c9..49c9c33aa 100644 --- a/core/modules/startup/render.js +++ b/core/modules/startup/render.js @@ -32,11 +32,11 @@ exports.startup = function() { $tw.titleWidgetNode = $tw.wiki.makeTranscludeWidget(PAGE_TITLE_TITLE,{document: $tw.fakeDocument, parseAsInline: true}); $tw.titleContainer = $tw.fakeDocument.createElement("div"); $tw.titleWidgetNode.render($tw.titleContainer,null); - $tw.titlePublisher = new $tw.utils.BrowserMessagingPublisher({type: "PAGETITLE", onsubscribe: publishTitle}); var publishTitle = function() { $tw.titlePublisher.send({verb: "PAGETITLE",body: document.title}); document.title = $tw.titleContainer.textContent; }; + $tw.titlePublisher = new $tw.utils.BrowserMessagingPublisher({type: "PAGETITLE", onsubscribe: publishTitle}); publishTitle(); $tw.wiki.addEventListener("change",function(changes) { if($tw.titleWidgetNode.refresh(changes,$tw.titleContainer,null)) {