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 55e7891e2..36d9aeec8 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 7206a51d0..f38aac43b 100644 --- a/core/modules/startup/render.js +++ b/core/modules/startup/render.js @@ -36,10 +36,15 @@ exports.startup = function() { }); $tw.titleContainer = $tw.fakeDocument.createElement("div"); $tw.titleWidgetNode.render($tw.titleContainer,null); - document.title = $tw.titleContainer.textContent; + 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)) { - 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..c7467f8b9 --- /dev/null +++ b/core/modules/utils/messaging.js @@ -0,0 +1,126 @@ +/*\ +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, invoked with (data,callback). The callback is invoked with the argument (response) +*/ +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) { + 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(); + }); + } + } + }); + // 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; + +})();