Like a universal Turing machine, a universal extension is an extension that can reproduce the behavior of an arbitrary extension. A universal extension does not actually exist because extension bars and toolbar items cannot be created dynamically. These are however the only restrictions, and this extension is a universal extension with one bar and one toolbar item.
In the settings you can specify scripts to be used in the global page and in the extension’s bar. In these scripts it is possible to create context menus, toolbar menus, and toolbar popovers. The extension contains a file popover.html
which includes the popover script specified in the settings and can be used to create popover content (see the example below). The unique identifier for the bar, the unique identifier for the the toolbar item, and the command identifier for the toolbar item are all "universal"
.
file://
protocol cannot be used in an extension’s global page. To load local files, use http://localhost/
instead.safari.application
, safari.extension
, and safari.self
objects can be used in the global script.addContentScript
and addContentStyleSheet
of the safari.extension
object.addContentScriptFromURL
and addContentStyleSheetsFromURL
methods throw an error if the URL is external to the extension. See the example below for a workaround.This is the extension’s global page:
<!DOCTYPE html> <html> <head> <script> if(safari.extension.settings.global) { var scriptElement = document.createElement("script"); scriptElement.src = safari.extension.settings.global + "?time=" + new Date().getTime(); document.head.appendChild(scriptElement); } </script> </head> <body></body> </html>
The universal extension only loads one global script, so a first step to make it more useful is to have that script load any number of other scripts.
var base = "http://localhost/~joeshmoe/scripts/"; function loadScripts() { for(var i = 0; i < arguments.length; i++) { var scriptElement = document.createElement("script"); scriptElement.src = base + arguments[i]; document.head.appendChild(scriptElement); } } loadScripts("globalScript1.js", "globalScript2.js", "globalScript3.js");
This global script can be used to load content scripts and style sheets from arbitrary URLs with a variety of parameters. Note that these scripts will not be sandboxed and the global variables they define will be visible in other scripts. The only way to add a sandboxed script is to make the whole script into a string and use the addContentScript
method directly.
function addContentScripts(scripts, whitelist, blacklist, runAtEnd, defer, async) { if(scripts.length === 0) return; var script = "function loadScripts(){for(var i=0;i<arguments.length;i++){var s=document.createElement('script');" + (async ? "s.async=true;" : "") + (defer ? "s.defer=true;" : "") + "s.src=arguments[i];document.documentElement.appendChild(s);}}loadScripts('" + scripts.join("','") + "');"; return safari.extension.addContentScript(script, whitelist, blacklist, runAtEnd); } function addContentStyleSheets(stylesheets, whitelist, blacklist) { if(stylesheets.length === 0) return; var script = "function loadStyleSheets(){for(var i=0;i<arguments.length;i++){var s=document.createElement('link');s.rel='stylesheet';s.href=arguments[i];document.documentElement.appendChild(s);}}loadStyleSheets('" + stylesheets.join("','") + "');"; return safari.extension.addContentStyleSheet(script, whitelist, blacklist); } // The script runBeforeScript.js will run on every page before the page’s own scripts addContentScripts(["http://localhost/~joeshmoe/runBeforeScript.js"], [], [], false, false, false); // The scripts local.js and remote.js will run on all nonsecure pages without delaying page loading addContentScripts(["http://localhost/~joeshmoe/local.js", "http://example.com/remote.js"], [], ["https://*/*"], false, true, false); // The style sheet pretty.css will be applied to all pages on the domain someuglysite.com addContentStyleSheets(["http://localhost/~joeshmoe/pretty.css"], ["http://someuglysite.com/*"], []);
The following code in the global script assigns a click action and a popover to the toolbar item:
function setPopover(popover) { for(var i = 0; i < safari.extension.toolbarItems.length; i++) { safari.extension.toolbarItems[i].popover = popover; } } var universalPopover = safari.extension.createPopover("universal", safari.extension.baseURI + "popover.html"); setPopover(universalPopover); function onOpen(event) { if(event.target instanceof SafariBrowserWindow) { setPopover(universalPopover); } } function onCommand(event) { if(event.command === "universal") { alert("The toolbar item was clicked!"); } } safari.application.addEventListener("open", onOpen, true); safari.application.addEventListener("command", onCommand, false);
This global script redefines the canPlayType
method of media elements so as to always return ""
on WebM MIME types. This is especially useful for Perian users.
var script = "\ var s = document.createElement('script');\ s.textContent = '\ HTMLMediaElement.prototype.canPlayTypeCopy = HTMLMediaElement.prototype.canPlayType;\ HTMLMediaElement.prototype.canPlayType = function(type) {\ if(/webm/.test(type)) return \"\";\ else return this.canPlayTypeCopy.apply(this, [type]);\ };';\ document.documentElement.appendChild(s);"; safari.extension.addContentScript(script, [], [], false);
This global script prevents HTML5 media elements from preloading until the “Play” button is clicked.
var whitelist = ["trustedsource.com", "anothertrustedsource.com"]; function canLoad(url) { for(var i = 0; i < whitelist.length; i++) { if(url.indexOf(whitelist[i]) !== -1) return true; } return false; } function respondToMessage(event) { if(event.name === "canLoad") event.message = canLoad(event.message); } var script = "document.addEventListener('beforeload',handleBeforeLoadEvent,true);function handleBeforeLoadEvent(event){if(!(event.target instanceof HTMLMediaElement)||safari.self.tab.canLoad(event,event.url))return;event.target.autoplay=false;event.target.preload='none';}"; safari.extension.addContentScript(script, [], [], false); safari.application.addEventListener("message", respondToMessage, false);