├── bootstrap.js ├── icon.png ├── install.rdf └── scripts ├── helper.js └── utils.js /bootstrap.js: -------------------------------------------------------------------------------- 1 | /* ***** BEGIN LICENSE BLOCK ***** 2 | * Version: MPL 1.1/GPL 2.0/LGPL 2.1 3 | * 4 | * The contents of this file are subject to the Mozilla Public License Version 5 | * 1.1 (the "License"); you may not use this file except in compliance with 6 | * the License. You may obtain a copy of the License at 7 | * http://www.mozilla.org/MPL/ 8 | * 9 | * Software distributed under the License is distributed on an "AS IS" basis, 10 | * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License 11 | * for the specific language governing rights and limitations under the 12 | * License. 13 | * 14 | * The Original Code is Twitter Address Bar Search. 15 | * 16 | * The Initial Developer of the Original Code is The Mozilla Foundation. 17 | * Portions created by the Initial Developer are Copyright (C) 2011 18 | * the Initial Developer. All Rights Reserved. 19 | * 20 | * Contributor(s): 21 | * Edward Lee 22 | * 23 | * Alternatively, the contents of this file may be used under the terms of 24 | * either the GNU General Public License Version 2 or later (the "GPL"), or 25 | * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), 26 | * in which case the provisions of the GPL or the LGPL are applicable instead 27 | * of those above. If you wish to allow use of your version of this file only 28 | * under the terms of either the GPL or the LGPL, and not to allow others to 29 | * use your version of this file under the terms of the MPL, indicate your 30 | * decision by deleting the provisions above and replace them with the notice 31 | * and other provisions required by the GPL or the LGPL. If you do not delete 32 | * the provisions above, a recipient may use your version of this file under 33 | * the terms of any one of the MPL, the GPL or the LGPL. 34 | * 35 | * ***** END LICENSE BLOCK ***** */ 36 | 37 | "use strict"; 38 | const global = this; 39 | 40 | const {classes: Cc, interfaces: Ci, manager: Cm, utils: Cu} = Components; 41 | Cu.import("resource://gre/modules/AddonManager.jsm"); 42 | Cu.import("resource://gre/modules/Services.jsm"); 43 | Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 44 | 45 | // Remember if we were just installed 46 | let justInstalled = false; 47 | 48 | // Remember if we're on Firefox or Fennec 49 | let platform = Services.appinfo.name == "Firefox" ? "desktop" : "mobile"; 50 | 51 | // Add functionality to search from the location bar and hook up autocomplete 52 | function addTwitterAddressBarSearch(window) { 53 | let {change} = makeWindowHelpers(window); 54 | let {BrowserUI, gBrowser, gURLBar} = window; 55 | 56 | // Check the input to see if the twitter icon should be shown 57 | let lastIcon = ""; 58 | function checkInput() { 59 | if (skipCheck()) 60 | return; 61 | 62 | // Only allow single word #tag and @user 63 | let icon = ""; 64 | if (twitterLike(urlbar.value)) 65 | icon = TWITTER_ICON; 66 | 67 | // Remember that the icon is showing 68 | lastIcon = icon; 69 | setIcon(icon); 70 | } 71 | 72 | // Convert to twitter urls if necessary 73 | function getTwitterUrl(input) { 74 | // Only fix up the input if we're indicating that it's a twitter term 75 | return lastIcon == TWITTER_ICON ? toTwitterUrl(input, "bar") : input; 76 | } 77 | 78 | // Figure out how to implement various functions depending on the platform 79 | let setIcon, skipCheck, urlbar; 80 | if (gBrowser == null) { 81 | setIcon = function(url) BrowserUI._updateIcon(url); 82 | skipCheck = function() false; 83 | urlbar = BrowserUI._edit; 84 | 85 | // Check the input on various events 86 | listen(window, BrowserUI._edit, "input", checkInput); 87 | 88 | // Convert inputs to twitter urls 89 | change(window.Browser, "getShortcutOrURI", function(orig) { 90 | return function(uri, data) { 91 | uri = getTwitterUrl(uri); 92 | return orig.call(this, uri, data); 93 | }; 94 | }); 95 | } 96 | else { 97 | setIcon = function(url) window.PageProxySetIcon(url); 98 | skipCheck = function() gURLBar.getAttribute("pageproxystate") == "valid" && 99 | !gURLBar.hasAttribute("focused"); 100 | urlbar = gURLBar; 101 | 102 | // Check the input on various events 103 | listen(window, gURLBar, "input", checkInput); 104 | listen(window, gBrowser.tabContainer, "TabSelect", checkInput); 105 | 106 | // Convert inputs to twitter urls 107 | change(gURLBar, "_canonizeURL", function(orig) { 108 | return function(event) { 109 | this.value = getTwitterUrl(this.value); 110 | return orig.call(this, event); 111 | }; 112 | }); 113 | } 114 | 115 | // Provide a way to set the autocomplete search engines and initialize 116 | function setSearch(engines) { 117 | urlbar.setAttribute("autocompletesearch", engines); 118 | urlbar.mSearchNames = null; 119 | urlbar.initSearchNames(); 120 | }; 121 | 122 | // Add in the twitter search and remove on cleanup 123 | let origSearch = urlbar.getAttribute("autocompletesearch"); 124 | setSearch("twitter " + origSearch); 125 | unload(function() setSearch(origSearch)); 126 | } 127 | 128 | // Add an autocomplete search engine to provide location bar suggestions 129 | function addTwitterAutocomplete() { 130 | const contract = "@mozilla.org/autocomplete/search;1?name=twitter"; 131 | const desc = "Twitter Autocomplete"; 132 | const uuid = Components.ID("42778970-8fae-454d-ad3f-eea88b945af1"); 133 | 134 | // Keep a timer to send a delayed no match 135 | let timer; 136 | function clearTimer() { 137 | if (timer != null) 138 | timer.cancel(); 139 | timer = null; 140 | } 141 | function setTimer(callback) { 142 | timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); 143 | timer.initWithCallback({ 144 | notify: function() { 145 | timer = null; 146 | callback(); 147 | } 148 | }, 1000, timer.TYPE_ONE_SHOT); 149 | } 150 | 151 | // Implement the autocomplete search that handles twitter queries 152 | let search = { 153 | createInstance: function(outer, iid) search.QueryInterface(iid), 154 | 155 | QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteSearch]), 156 | 157 | // Handle searches from the location bar 158 | startSearch: function(query, param, previous, listener) { 159 | // Always clear the timer on a new search 160 | clearTimer(); 161 | 162 | // Specially handle twitter-like queries 163 | if (twitterLike(query)) { 164 | let label; 165 | if (query[0] == "#") 166 | label = "Search for " + query; 167 | else 168 | label = "View account for " + query; 169 | 170 | // Call the listener immediately with one result 171 | listener.onSearchResult(search, { 172 | getCommentAt: function() "Twitter: " + query, 173 | 174 | getImageAt: function() TWITTER_ICON, 175 | 176 | getLabelAt: function() label, 177 | 178 | getValueAt: function() toTwitterUrl(query, "autocomplete"), 179 | 180 | getStyleAt: function() "favicon", 181 | 182 | get matchCount() 1, 183 | 184 | QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult]), 185 | 186 | removeValueAt: function() {}, 187 | 188 | searchResult: Ci.nsIAutoCompleteResult.RESULT_SUCCESS, 189 | 190 | get searchString() query, 191 | }); 192 | } 193 | // Send a delayed NOMATCH so the autocomplete doesn't close early 194 | else { 195 | setTimer(function() { 196 | listener.onSearchResult(search, { 197 | searchResult: Ci.nsIAutoCompleteResult.RESULT_NOMATCH, 198 | }); 199 | }); 200 | } 201 | }, 202 | 203 | // Nothing to cancel other than a delayed search as results are synchronous 204 | stopSearch: function() { 205 | clearTimer(); 206 | }, 207 | }; 208 | 209 | // Register this autocomplete search service and clean up when necessary 210 | const registrar = Ci.nsIComponentRegistrar; 211 | Cm.QueryInterface(registrar).registerFactory(uuid, desc, contract, search); 212 | unload(function() { 213 | Cm.QueryInterface(registrar).unregisterFactory(uuid, search); 214 | }); 215 | } 216 | 217 | // Add a default search engine and move it to the right place 218 | function addTwitterSearchEngine() { 219 | // Hide any existing "Twitter" searches 220 | let origEngine = Services.search.getEngineByName("Twitter"); 221 | if (origEngine != null) { 222 | origEngine.hidden = true; 223 | unload(function() origEngine.hidden = false); 224 | } 225 | 226 | // Add the "Twitter " search engine if necessary 227 | let engineName = "Twitter "; 228 | try { 229 | Services.search.addEngineWithDetails(engineName, TWITTER_ICON, "", "", 230 | "GET", getTwitterBase("search/{searchTerms}", "search")); 231 | } 232 | catch(ex) {} 233 | 234 | // Get the just-added or existing engine 235 | let engine = Services.search.getEngineByName(engineName); 236 | if (engine == null) 237 | return; 238 | 239 | // Move it to position #2 after Google for the partner package 240 | Services.search.moveEngine(engine, 1); 241 | 242 | // Clean up when disabling 243 | unload(function() Services.search.removeEngine(engine)); 244 | } 245 | 246 | // Make sure the window has an app tab set to Twitter 247 | function ensureTwitterAppTab(window) { 248 | // Only bother if we were just installed and support app tabs 249 | if (!justInstalled || platform != "desktop") 250 | return; 251 | 252 | // Try again after a short delay if session store is initializing 253 | let {__SSi, __SS_restoreID, gBrowser, setTimeout} = window; 254 | if (__SSi == null || __SS_restoreID != null) { 255 | setTimeout(function() ensureTwitterAppTab(window), 1000); 256 | return; 257 | } 258 | 259 | // Figure out if we already have a pinned twitter 260 | let twitterTab = findOpenTab(gBrowser, function(tab, URI) { 261 | return tab.pinned && URI.host == "twitter.com"; 262 | }); 263 | 264 | // Always remove the twitter tab when uninstalling 265 | unload(function() gBrowser.removeTab(twitterTab)); 266 | 267 | // No need to add! 268 | if (twitterTab != null) 269 | return; 270 | 271 | // Add the tab and pin it as the last app tab 272 | twitterTab = gBrowser.addTab(getTwitterBase("", "apptab")); 273 | gBrowser.pinTab(twitterTab); 274 | } 275 | 276 | // Open a new tab for the landing page and select it 277 | function showLandingPage(window) { 278 | // Only bother if we were just installed and haven't shown yet 279 | if (!justInstalled || showLandingPage.shown) 280 | return; 281 | 282 | // Do the appropriate thing on each platform 283 | if (platform == "desktop") { 284 | // Try again after a short delay if session store is initializing 285 | let {__SSi, __SS_restoreID, gBrowser, setTimeout} = window; 286 | if (__SSi == null || __SS_restoreID != null) { 287 | setTimeout(function() showLandingPage(window), 1000); 288 | return; 289 | } 290 | 291 | // Figure out if we already have a landing page 292 | let landingTab = findOpenTab(gBrowser, function(tab, URI) { 293 | return URI.spec == LANDING_PAGE; 294 | }); 295 | 296 | // Always remove the landing page when uninstalling 297 | unload(function() gBrowser.removeTab(landingTab)); 298 | 299 | // Add the landing page if not open yet 300 | if (landingTab == null) 301 | landingTab = gBrowser.loadOneTab(LANDING_PAGE); 302 | 303 | // Make sure it's focused 304 | gBrowser.selectedTab = landingTab; 305 | } 306 | else { 307 | let {BrowserUI} = window; 308 | let tab = BrowserUI.newTab(LANDING_PAGE); 309 | unload(function() BrowserUI.closeTab(tab)); 310 | } 311 | 312 | // Only show the landing page once 313 | showLandingPage.shown = true; 314 | } 315 | 316 | /** 317 | * Handle the add-on being activated on install/enable 318 | */ 319 | function startup({id}, reason) AddonManager.getAddonByID(id, function(addon) { 320 | // Load various javascript includes for helper functions 321 | ["helper", "utils"].forEach(function(fileName) { 322 | let fileURI = addon.getResourceURI("scripts/" + fileName + ".js"); 323 | Services.scriptloader.loadSubScript(fileURI.spec, global); 324 | }); 325 | 326 | // Add twitter support to the browser 327 | watchWindows(addTwitterAddressBarSearch); 328 | addTwitterAutocomplete(); 329 | addTwitterSearchEngine(); 330 | watchWindows(ensureTwitterAppTab); 331 | watchWindows(showLandingPage); 332 | 333 | // We're no longer just installed after we get some windows loaded 334 | watchWindows(function(window) { 335 | if (justInstalled) 336 | window.setTimeout(function() justInstalled = false, 5000); 337 | }); 338 | }) 339 | 340 | /** 341 | * Handle the add-on being deactivated on uninstall/disable 342 | */ 343 | function shutdown(data, reason) { 344 | // Clean up with unloaders when we're deactivating 345 | if (reason != APP_SHUTDOWN) 346 | unload(); 347 | } 348 | 349 | /** 350 | * Handle the add-on being installed 351 | */ 352 | function install(data, reason) { 353 | justInstalled = reason == ADDON_INSTALL; 354 | } 355 | 356 | /** 357 | * Handle the add-on being uninstalled 358 | */ 359 | function uninstall(data, reason) {} 360 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/twitter-address-bar-search/73ef74d6dc823ce53ac9adc0677e1fc0ffb1223a/icon.png -------------------------------------------------------------------------------- /install.rdf: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | Twitter 6 | Use Twitter to search for #hashtag and @mention from the address bar. 7 | https://twitter.com/ 8 | twitter.address.bar.search@firefox.twitter 9 | Twitter Address Bar Search 10 | 1 11 | 12 | true 13 | 2 14 | 15 | 16 | 17 | {ec8030f7-c20a-464f-9b0e-13a3a9e97384} 18 | 4.0 19 | 7.0a1 20 | 21 | 22 | 23 | 24 | 25 | {a23983c0-fd0e-11dc-95ff-0800200c9a66} 26 | 4.0 27 | 7.0a1 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /scripts/helper.js: -------------------------------------------------------------------------------- 1 | /* ***** BEGIN LICENSE BLOCK ***** 2 | * Version: MPL 1.1/GPL 2.0/LGPL 2.1 3 | * 4 | * The contents of this file are subject to the Mozilla Public License Version 5 | * 1.1 (the "License"); you may not use this file except in compliance with 6 | * the License. You may obtain a copy of the License at 7 | * http://www.mozilla.org/MPL/ 8 | * 9 | * Software distributed under the License is distributed on an "AS IS" basis, 10 | * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License 11 | * for the specific language governing rights and limitations under the 12 | * License. 13 | * 14 | * The Original Code is Twitter Address Bar Search Helper Functions. 15 | * 16 | * The Initial Developer of the Original Code is The Mozilla Foundation. 17 | * Portions created by the Initial Developer are Copyright (C) 2011 18 | * the Initial Developer. All Rights Reserved. 19 | * 20 | * Contributor(s): 21 | * Edward Lee 22 | * 23 | * Alternatively, the contents of this file may be used under the terms of 24 | * either the GNU General Public License Version 2 or later (the "GPL"), or 25 | * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), 26 | * in which case the provisions of the GPL or the LGPL are applicable instead 27 | * of those above. If you wish to allow use of your version of this file only 28 | * under the terms of either the GPL or the LGPL, and not to allow others to 29 | * use your version of this file under the terms of the MPL, indicate your 30 | * decision by deleting the provisions above and replace them with the notice 31 | * and other provisions required by the GPL or the LGPL. If you do not delete 32 | * the provisions above, a recipient may use your version of this file under 33 | * the terms of any one of the MPL, the GPL or the LGPL. 34 | * 35 | * ***** END LICENSE BLOCK ***** */ 36 | 37 | "use strict"; 38 | 39 | const LANDING_PAGE = "https://twitter.com/download/firefox/welcome"; 40 | const TWITTER_ICON = "data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2Fv7%2BD%2F7%2B%2Fj%2F%2B%2Fv5g%2Fv7%2BYP7%2B%2FmD%2B%2Fv5I%2Fv7%2BKP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2B%2Fv4H%2Fv7%2BUPbv4pHgx47B1K9Y3tWwWN7Ur1je3sKCx%2BrbuKj%2B%2Fv5n%2Fv7%2BGP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2B%2Fv4Y%2BfbweM2ycMe2iB7%2FvI0f%2F8STIf%2FKlyL%2FzJki%2F8yZIv%2FLmCL%2F0ahK5%2FHp1JH%2B%2Fv4Y%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A7OTTaquHN%2BCujkXPs5ZTv6N6G%2F%2B2iB7%2FxpUh%2F8yZIv%2FMmSL%2FzJki%2F8yZIv%2FKmy738OjUi%2F%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAMKtfY7w6%2BEf%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A3sqbp8iWIf%2FMmSL%2FzJki%2F8yZIv%2FMmSL%2Fy5gi%2F8mePO7%2B%2Fv4w%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2B%2Fv4H%2Fv7%2BV9CtWN3KmCL%2FzJki%2F8yZIv%2FMmSL%2FzJki%2F8yZIv%2FJlyH%2F5tSqp%2F7%2B%2FmD%2B%2Fv4%2F%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2BPXvJtGyZdXNnS%2F3y5gi%2F8qYIv%2FLmCL%2FzJki%2F8yZIv%2FMmSL%2Fy5gi%2F82iPO7LqVfe0byMmf%2F%2F%2FwD%2F%2F%2F8A%2Fv7%2BD%2FDo1JHKmy73ypci%2F8KSIP%2B%2FjyD%2FxpQh%2F8uYIv%2FMmSL%2FzJki%2F8qYIv%2B%2FjyD%2FrIEd%2F9nKqH7%2F%2F%2F8A%2F%2F%2F%2FAPPu4TzAlSz3wZEg%2F7mLH%2F%2BsgR3%2FuZdGz7mLH%2F%2FJlyH%2FzJki%2F8yZIv%2FGlSH%2Fto0r9eXbxD%2FVx6dg%2F%2F%2F%2FAP7%2B%2Fh%2Fp38WhtIsq9al%2FHP%2BkfyjuybaKgf%2F%2F%2FwCzjzjlwJAg%2F8qYIv%2FJlyH%2Fu4wf%2F8CkYrn%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwDj2sRMnHUa%2F7meYa7Vx6dg%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A2MmnYK6DHf%2B%2BjiD%2Fvo4g%2F62CHf%2Fk2sQ%2F%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A8OvhH%2Ff07w%2F%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwC%2Fp3Cfpnwc%2F66GKvPg1LZ8%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FANXHp2DJtoqByLWKgf%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F8AAP%2F%2FAADgPwAAwA8AAIAHAAB4BwAA%2BAMAAPAAAADgAQAA4AMAAMEDAADPhwAA%2F48AAP%2FnAAD%2F%2FwAA%2F%2F8AAA%3D%3D"; 41 | 42 | // Look through tabs in the browser to see if any match 43 | function findOpenTab(browser, checkTabAndURI) { 44 | let foundTab; 45 | Array.some(browser.tabs, function(tab) { 46 | // Check if there's an existing page 47 | try { 48 | // Use an activate navigation if it's still loading 49 | let {currentURI, webNavigation, __SS_data} = tab.linkedBrowser; 50 | let channel = webNavigation.documentChannel; 51 | if (channel != null) 52 | currentURI = channel.originalURI 53 | 54 | // Use the session restore entry if it's still restoring 55 | if (currentURI.spec == "about:blank" && __SS_data != null) 56 | currentURI = Services.io.newURI(__SS_data.entries[0].url, null, null); 57 | 58 | // Short circuit now that we found it 59 | if (checkTabAndURI(tab, currentURI)) { 60 | foundTab = tab; 61 | return true; 62 | } 63 | } 64 | catch(ex) {} 65 | }); 66 | return foundTab; 67 | } 68 | 69 | // Get a twitter url with a partner code 70 | function getTwitterBase(path, from) { 71 | return "https://twitter.com/" + path + "?partner=mozilla&source=" + 72 | platform + "-" + from; 73 | } 74 | 75 | // Take a window and create various helper properties and functions 76 | function makeWindowHelpers(window) { 77 | let {clearTimeout, setTimeout} = window; 78 | 79 | // Call a function after waiting a little bit 80 | function async(callback, delay) { 81 | let timer = setTimeout(function() { 82 | stopTimer(); 83 | callback(); 84 | }, delay); 85 | 86 | // Provide a way to stop an active timer 87 | function stopTimer() { 88 | if (timer == null) 89 | return; 90 | clearTimeout(timer); 91 | timer = null; 92 | unUnload(); 93 | } 94 | 95 | // Make sure to stop the timer when unloading 96 | let unUnload = unload(stopTimer, window); 97 | 98 | // Give the caller a way to cancel the timer 99 | return stopTimer; 100 | } 101 | 102 | // Replace a value with another value or a function of the original value 103 | function change(obj, prop, val) { 104 | let orig = obj[prop]; 105 | obj[prop] = typeof val == "function" ? val(orig) : val; 106 | unload(function() obj[prop] = orig, window); 107 | } 108 | 109 | return { 110 | async: async, 111 | change: change, 112 | }; 113 | } 114 | 115 | // Convert a query to a url 116 | function toTwitterUrl(query, from) { 117 | // Replace the #tag or @user with a url + referral code 118 | let path = encodeURIComponent(query). 119 | replace(/^%23/, "search/%23").replace(/^%40/, ""); 120 | return getTwitterBase(path, from); 121 | } 122 | 123 | // Check if a query is a twitter-like input 124 | function twitterLike(query) { 125 | return query.search(/^[@#][^ ]*$/) == 0; 126 | } 127 | -------------------------------------------------------------------------------- /scripts/utils.js: -------------------------------------------------------------------------------- 1 | /* ***** BEGIN LICENSE BLOCK ***** 2 | * Version: MPL 1.1/GPL 2.0/LGPL 2.1 3 | * 4 | * The contents of this file are subject to the Mozilla Public License Version 5 | * 1.1 (the "License"); you may not use this file except in compliance with 6 | * the License. You may obtain a copy of the License at 7 | * http://www.mozilla.org/MPL/ 8 | * 9 | * Software distributed under the License is distributed on an "AS IS" basis, 10 | * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License 11 | * for the specific language governing rights and limitations under the 12 | * License. 13 | * 14 | * The Original Code is Home Dash Utility. 15 | * 16 | * The Initial Developer of the Original Code is The Mozilla Foundation. 17 | * Portions created by the Initial Developer are Copyright (C) 2011 18 | * the Initial Developer. All Rights Reserved. 19 | * 20 | * Contributor(s): 21 | * Edward Lee 22 | * 23 | * Alternatively, the contents of this file may be used under the terms of 24 | * either the GNU General Public License Version 2 or later (the "GPL"), or 25 | * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), 26 | * in which case the provisions of the GPL or the LGPL are applicable instead 27 | * of those above. If you wish to allow use of your version of this file only 28 | * under the terms of either the GPL or the LGPL, and not to allow others to 29 | * use your version of this file under the terms of the MPL, indicate your 30 | * decision by deleting the provisions above and replace them with the notice 31 | * and other provisions required by the GPL or the LGPL. If you do not delete 32 | * the provisions above, a recipient may use your version of this file under 33 | * the terms of any one of the MPL, the GPL or the LGPL. 34 | * 35 | * ***** END LICENSE BLOCK ***** */ 36 | 37 | "use strict"; 38 | 39 | /** 40 | * Get a localized string with string replacement arguments filled in and 41 | * correct plural form picked if necessary. 42 | * 43 | * @note: Initialize the strings to use with getString.init(addon). 44 | * 45 | * @usage getString(name): Get the localized string for the given name. 46 | * @param [string] name: Corresponding string name in the properties file. 47 | * @return [string]: Localized string for the string name. 48 | * 49 | * @usage getString(name, arg): Replace %S references in the localized string. 50 | * @param [string] name: Corresponding string name in the properties file. 51 | * @param [any] arg: Value to insert for instances of %S. 52 | * @return [string]: Localized string with %S references replaced. 53 | * 54 | * @usage getString(name, args): Replace %1$S references in localized string. 55 | * @param [string] name: Corresponding string name in the properties file. 56 | * @param [array of any] args: Array of values to replace references like %1$S. 57 | * @return [string]: Localized string with %N$S references replaced. 58 | * 59 | * @usage getString(name, args, plural): Pick the correct plural form. 60 | * @param [string] name: Corresponding string name in the properties file. 61 | * @param [array of any] args: Array of values to replace references like %1$S. 62 | * @param [number] plural: Number to decide what plural form to use. 63 | * @return [string]: Localized string of the correct plural form. 64 | */ 65 | function getString(name, args, plural) { 66 | // Use the cached bundle to retrieve the string 67 | let str; 68 | try { 69 | str = getString.bundle.GetStringFromName(name); 70 | } 71 | // Use the fallback in-case the string isn't localized 72 | catch(ex) { 73 | str = getString.fallback.GetStringFromName(name); 74 | } 75 | 76 | // Pick out the correct plural form if necessary 77 | if (plural != null) 78 | str = getString.plural(plural, str); 79 | 80 | // Fill in the arguments if necessary 81 | if (args != null) { 82 | // Convert a string or something not array-like to an array 83 | if (typeof args == "string" || args.length == null) 84 | args = [args]; 85 | 86 | // Assume %S refers to the first argument 87 | str = str.replace(/%s/gi, args[0]); 88 | 89 | // Replace instances of %N$S where N is a 1-based number 90 | Array.forEach(args, function(replacement, index) { 91 | str = str.replace(RegExp("%" + (index + 1) + "\\$S", "gi"), replacement); 92 | }); 93 | } 94 | 95 | return str; 96 | } 97 | 98 | /** 99 | * Initialize getString() for the provided add-on. 100 | * 101 | * @usage getString.init(addon): Load properties file for the add-on. 102 | * @param [object] addon: Add-on object from AddonManager 103 | * 104 | * @usage getString.init(addon, getAlternate): Load properties with alternate. 105 | * @param [object] addon: Add-on object from AddonManager 106 | * @param [function] getAlternate: Convert a locale to an alternate locale 107 | */ 108 | getString.init = function(addon, getAlternate) { 109 | // Set a default get alternate function if it doesn't exist 110 | if (typeof getAlternate != "function") 111 | getAlternate = function() "en-US"; 112 | 113 | // Get the bundled properties file for the app's locale 114 | function getBundle(locale) { 115 | let propertyPath = "locales/" + locale + ".properties"; 116 | let propertyFile = addon.getResourceURI(propertyPath); 117 | 118 | // Get a bundle and test if it's able to do simple things 119 | try { 120 | // Avoid caching issues by always getting a new file 121 | let uniqueFileSpec = propertyFile.spec + "#" + Math.random(); 122 | let bundle = Services.strings.createBundle(uniqueFileSpec); 123 | bundle.getSimpleEnumeration(); 124 | return bundle; 125 | } 126 | catch(ex) {} 127 | 128 | // The locale must not exist, so give nothing 129 | return null; 130 | } 131 | 132 | // Use the current locale or the alternate as the primary bundle 133 | let locale = Cc["@mozilla.org/chrome/chrome-registry;1"]. 134 | getService(Ci.nsIXULChromeRegistry).getSelectedLocale("global"); 135 | getString.bundle = getBundle(locale) || getBundle(getAlternate(locale)); 136 | 137 | // Create a fallback in-case a string is missing 138 | getString.fallback = getBundle("en-US"); 139 | 140 | // Get the appropriate plural form getter 141 | Cu.import("resource://gre/modules/PluralForm.jsm"); 142 | let rule = getString("pluralRule"); 143 | [getString.plural] = PluralForm.makeGetter(rule); 144 | } 145 | 146 | /** 147 | * Helper that adds event listeners and remembers to remove on unload 148 | */ 149 | function listen(window, node, event, func, capture) { 150 | // Default to use capture 151 | if (capture == null) 152 | capture = true; 153 | 154 | node.addEventListener(event, func, capture); 155 | function undoListen() { 156 | node.removeEventListener(event, func, capture); 157 | } 158 | 159 | // Undo the listener on unload and provide a way to undo everything 160 | let undoUnload = unload(undoListen, window); 161 | return function() { 162 | undoListen(); 163 | undoUnload(); 164 | }; 165 | } 166 | 167 | /** 168 | * Save callbacks to run when unloading. Optionally scope the callback to a 169 | * container, e.g., window. Provide a way to run all the callbacks. 170 | * 171 | * @usage unload(): Run all callbacks and release them. 172 | * 173 | * @usage unload(callback): Add a callback to run on unload. 174 | * @param [function] callback: 0-parameter function to call on unload. 175 | * @return [function]: A 0-parameter function that undoes adding the callback. 176 | * 177 | * @usage unload(callback, container) Add a scoped callback to run on unload. 178 | * @param [function] callback: 0-parameter function to call on unload. 179 | * @param [node] container: Remove the callback when this container unloads. 180 | * @return [function]: A 0-parameter function that undoes adding the callback. 181 | */ 182 | function unload(callback, container) { 183 | // Initialize the array of unloaders on the first usage 184 | let unloaders = unload.unloaders; 185 | if (unloaders == null) 186 | unloaders = unload.unloaders = []; 187 | 188 | // Calling with no arguments runs all the unloader callbacks 189 | if (callback == null) { 190 | unloaders.slice().forEach(function(unloader) unloader()); 191 | unloaders.length = 0; 192 | return; 193 | } 194 | 195 | // The callback is bound to the lifetime of the container if we have one 196 | if (container != null) { 197 | // Remove the unloader when the container unloads 198 | container.addEventListener("unload", removeUnloader, false); 199 | 200 | // Wrap the callback to additionally remove the unload listener 201 | let origCallback = callback; 202 | callback = function() { 203 | container.removeEventListener("unload", removeUnloader, false); 204 | origCallback(); 205 | } 206 | } 207 | 208 | // Wrap the callback in a function that ignores failures 209 | function unloader() { 210 | try { 211 | callback(); 212 | } 213 | catch(ex) {} 214 | } 215 | unloaders.push(unloader); 216 | 217 | // Provide a way to remove the unloader 218 | function removeUnloader() { 219 | let index = unloaders.indexOf(unloader); 220 | if (index != -1) 221 | unloaders.splice(index, 1); 222 | } 223 | return removeUnloader; 224 | } 225 | 226 | /** 227 | * Apply a callback to each open and new browser windows. 228 | * 229 | * @usage watchWindows(callback): Apply a callback to each browser window. 230 | * @param [function] callback: 1-parameter function that gets a browser window. 231 | */ 232 | function watchWindows(callback) { 233 | // Wrap the callback in a function that ignores failures 234 | function watcher(window) { 235 | try { 236 | // Now that the window has loaded, only handle browser windows 237 | let {documentElement} = window.document; 238 | if (documentElement.getAttribute("windowtype") == "navigator:browser") 239 | callback(window); 240 | } 241 | catch(ex) {} 242 | } 243 | 244 | // Wait for the window to finish loading before running the callback 245 | function runOnLoad(window) { 246 | // Listen for one load event before checking the window type 247 | window.addEventListener("load", function runOnce() { 248 | window.removeEventListener("load", runOnce, false); 249 | watcher(window); 250 | }, false); 251 | } 252 | 253 | // Add functionality to existing windows 254 | let windows = Services.wm.getEnumerator(null); 255 | while (windows.hasMoreElements()) { 256 | // Only run the watcher immediately if the window is completely loaded 257 | let window = windows.getNext(); 258 | if (window.document.readyState == "complete") 259 | watcher(window); 260 | // Wait for the window to load before continuing 261 | else 262 | runOnLoad(window); 263 | } 264 | 265 | // Watch for new browser windows opening then wait for it to load 266 | function windowWatcher(subject, topic) { 267 | if (topic == "domwindowopened") 268 | runOnLoad(subject); 269 | } 270 | Services.ww.registerNotification(windowWatcher); 271 | 272 | // Make sure to stop watching for windows if we're unloading 273 | unload(function() Services.ww.unregisterNotification(windowWatcher)); 274 | } 275 | --------------------------------------------------------------------------------