diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea19771 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Files and directories used during the build process: +/build/firefox-unpacked +/build/firefox-addon-sdk-url.txt +/build/firefox-addon-sdk +/build/xar-url.txt +/build/xar/ +/lib/BabelExtResources.js + +# Files and directories containing unpacked extensions: +/build/Chrome +/build/Firefox/*.png +/build/Firefox/data +/build/Firefox/icons +/build/Safari.safariextension + +# output directory: +/out + +# Files containing sensitive information that must never go in version control: +/build/safari-certs +/build/Chrome.pem +/conf/local_settings.json + +# miscellaneous: +*~ diff --git a/Chrome/.gitignore b/Chrome/.gitignore deleted file mode 100644 index 603b5e1..0000000 --- a/Chrome/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/extension.js -/BabelExt.js diff --git a/Chrome/background.js b/Chrome/background.js deleted file mode 100644 index d7e9671..0000000 --- a/Chrome/background.js +++ /dev/null @@ -1,89 +0,0 @@ -var contextMenuClick = function(info, tab, callbackID) { - chrome.tabs.sendMessage(tab.id, { - requestType: "contextMenu.click", - callbackID: callbackID - }); -}; - -chrome.runtime.onMessage.addListener( - function(request, sender, sendResponse) { - // all requests expect a JSON object with requestType and then the relevant - // companion information... - switch(request.requestType) { - case 'xmlhttpRequest': - var xhr = new XMLHttpRequest(); - xhr.open(request.method, request.url, true); - if (request.method === "POST") { - xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); - // xhr.setRequestHeader("Content-length", request.data.length); - // xhr.setRequestHeader("Connection", "close"); - } - xhr.onreadystatechange = function() { - if (xhr.readyState === 4) { - // Only store 'status' and 'responseText' fields and send them back. - var response = {status: xhr.status, responseText: xhr.responseText}; - sendResponse(response); - } - }; - xhr.send(request.data); - return true; // true must be returned here to indicate successful XHR - break; - case 'createTab': - var newIndex, - focus = (request.background !== true); - - if (typeof(request.index) !== 'undefined') { - newIndex = request.index; - } else { - // If index wasn't specified, get the selected tab so we can get the index of it. - // This allows us to open our new tab as the "next" tab in order rather than at the end. - newIndex = sender.tab.index+1; - } - chrome.tabs.create({url: request.url, selected: focus, index: newIndex}); - sendResponse({status: "success"}); - break; - case 'createNotification': - if (!request.icon) { - // if no icon specified, make a single pixel empty gif so we don't get a broken image link. - request.icon = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=='; - } - var notification = window.webkitNotifications.createNotification( - request.icon, // icon url - can be relative - request.title, // notification title - request.text // notification body text - ); - notification.show(); - break; - case 'localStorage': - switch (request.operation) { - case 'getItem': - sendResponse({status: true, key: request.itemName, value: localStorage.getItem(request.itemName)}); - break; - case 'removeItem': - localStorage.removeItem(request.itemName); - sendResponse({status: true, key: request.itemName, value: null}); - break; - case 'setItem': - localStorage.setItem(request.itemName, request.itemValue); - sendResponse({status: true, key: request.itemName, value: request.itemValue}); - break; - } - break; - case 'addURLToHistory': - chrome.history.addUrl({url: request.url}); - break; - case 'contextMenus.create': - if (typeof request.obj.onclick === 'number') { - var callbackID = request.obj.onclick; - request.obj.onclick = function(info, tab) { - contextMenuClick(info, tab, callbackID); - }; - } - chrome.contextMenus.create(request.obj); - break; - default: - sendResponse({status: "unrecognized request type"}); - break; - } - } -); diff --git a/Chrome/manifest.json b/Chrome/manifest.json deleted file mode 100644 index d35165f..0000000 --- a/Chrome/manifest.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "BabelExt Extension", - "version": "0.95", - "manifest_version": 2, - "description": "An extension created with BabelExt - www.babelext.com", - "update_url": "http://babelext.com/update-chrome.php", - "background": { - "scripts": ["background.js"] - }, - "content_scripts": [ - { - "matches": [ - "http://babelext.com/*" - ], - "js": ["BabelExt.js", "extension.js"] // add others here if you like, i.e. jquery... - } - ], - "icons": { - // "48": "icon48.png", - // "128": "icon128.png" - }, - "permissions": [ - "contextMenus", - "tabs", - "history", - "notifications" - ] -} \ No newline at end of file diff --git a/Firefox/README.md b/Firefox/README.md deleted file mode 100644 index 49a92fc..0000000 --- a/Firefox/README.md +++ /dev/null @@ -1 +0,0 @@ -The Firefox Addon SDK requires you have a README.md present. \ No newline at end of file diff --git a/Firefox/data/.gitignore b/Firefox/data/.gitignore deleted file mode 100644 index 603b5e1..0000000 --- a/Firefox/data/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/extension.js -/BabelExt.js diff --git a/Firefox/lib/main.js b/Firefox/lib/main.js deleted file mode 100644 index 6d09acc..0000000 --- a/Firefox/lib/main.js +++ /dev/null @@ -1,184 +0,0 @@ -// Import the APIs we need. - -var pageMod = require("page-mod"); -var Request = require("request").Request; -var notifications = require("notifications"); -var self = require("self"); -var tabs = require("tabs"); -var ss = require("simple-storage"); -var workers = []; -var contextMenu = require("context-menu"); -var priv = require("private-browsing"); -var windows = require("sdk/windows").browserWindows; - -// require chrome allows us to use XPCOM objects... -const {Cc, Ci, Cu, Cr} = require("chrome"); - -var historyService = Cc["@mozilla.org/browser/history;1"].getService(Ci.mozIAsyncHistory); - -// this function takes in a string (and optional charset, paseURI) and creates an nsURI object, which is required by historyService.addURI... -function makeURI(aURL, aOriginCharset, aBaseURI) { - var ioService = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); - return ioService.newURI(aURL, aOriginCharset, aBaseURI); -} - - -function detachWorker(worker, workerArray) { - var index = workerArray.indexOf(worker); - if(index != -1) { - workerArray.splice(index, 1); - } -} -var localStorage = ss.storage; - -// these aliases are just for simplicity, so that the code here looks just like background code -// for all of the other browsers... -localStorage.getItem = function(key) { - return ss.storage[key]; -}; -localStorage.setItem = function(key, value) { - ss.storage[key] = value; -}; -localStorage.removeItem = function(key) { - delete ss.storage[key]; -}; - -pageMod.PageMod({ - include: ["*.babelext.com"], - contentScriptWhen: 'ready', - contentScriptFile: [self.data.url('BabelExt.js'), self.data.url('extension.js')], - onAttach: function(worker) { - tabs.on('activate', function(tab) { - // run some code when a tab is activated... - }); - - workers.push(worker); - worker.on('detach', function () { - detachWorker(this, workers); - // console.log('worker detached, total now: ' + workers.length); - }); - - worker.on('message', function(data) { - var request = data; - switch(request.requestType) { - case 'xmlhttpRequest': - var responseObj = { - callbackID: request.callbackID, - name: request.requestType - }; - if (request.method == 'POST') { - Request({ - url: request.url, - onComplete: function(response) { - responseObj.response = { - responseText: response.text, - status: response.status - }; - worker.postMessage(responseObj); - }, - headers: request.headers, - content: request.data - }).post(); - } else { - Request({ - url: request.url, - onComplete: function(response) { - responseObj.response = { - responseText: response.text, - status: response.status - }; - worker.postMessage(responseObj); - }, - headers: request.headers, - content: request.data - }).get(); - } - break; - case 'createTab': - var focus = (request.background !== true); - tabs.open({url: request.url, inBackground: !focus }); - worker.postMessage({status: "success"}); - break; - case 'createNotification': - if (!request.icon) { - // if no icon specified, make a single pixel empty gif so we don't get a broken image link. - request.icon = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=='; - } - notifications.notify({ - title: request.title, - text: request.text, - iconURL: request.icon - }); - break; - case 'localStorage': - switch (request.operation) { - case 'getItem': - worker.postMessage({ - name: 'localStorage', - callbackID: request.callbackID, - status: true, - key: request.itemName, - value: localStorage.getItem(request.itemName) - }); - break; - case 'removeItem': - localStorage.removeItem(request.itemName); - worker.postMessage({ - name: 'localStorage', - callbackID: request.callbackID, - status: true, - value: null - }); - break; - case 'setItem': - localStorage.setItem(request.itemName, request.itemValue); - worker.postMessage({ - name: 'localStorage', - callbackID: request.callbackID, - status: true, - key: request.itemName, - value: request.itemValue - }); - break; - } - break; - case 'addURLToHistory': - var isPrivate = priv.isPrivate(windows.activeWindow); - if (isPrivate) { - // do not add to history if in private browsing mode! - return false; - } - var uri = makeURI(request.url); - historyService.updatePlaces({ - uri: uri, - visits: [{ - transitionType: Ci.nsINavHistoryService.TRANSITION_LINK, - visitDate: Date.now() * 1000 - }] - }); - break; - case 'contextMenus.create': - contextMenu.Item({ - label: request.obj.title, - context: contextMenu.PageContext(), - data: request.obj.onclick, - contentScript: 'self.on("click", function (node, data) {' + - 'self.postMessage(data);' + - '});', - onMessage: function(onclick) { - worker.postMessage({ - name: 'contextMenus.click', - callbackID: onclick - }); - } - - }); - break; - default: - worker.postMessage({status: "unrecognized request type"}); - break; - } - - }); - } -}); diff --git a/Firefox/package.json b/Firefox/package.json deleted file mode 100644 index 3379d5d..0000000 --- a/Firefox/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "description": "An extension created with BabelExt - www.babelext.com", - "license": "GPL", - "author": "honestbleeps", - "version": "0.95", - "fullName": "BabelExt", - "id": "jid1-h7LuK9FSeAYouw", - "name": "babelext_your_name_here" -} diff --git a/Opera/config.xml b/Opera/config.xml deleted file mode 100644 index 90a9838..0000000 --- a/Opera/config.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - BabelExt Extension - An extension created with BabelExt - www.babelext.com - - Steve Sobel - - - diff --git a/Opera/includes/.gitignore b/Opera/includes/.gitignore deleted file mode 100644 index 8e35b06..0000000 --- a/Opera/includes/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/extension.user.js -/BabelExt.js \ No newline at end of file diff --git a/Opera/index.html b/Opera/index.html deleted file mode 100644 index 6d22bc6..0000000 --- a/Opera/index.html +++ /dev/null @@ -1,133 +0,0 @@ - - \ No newline at end of file diff --git a/Opera/notification.html b/Opera/notification.html deleted file mode 100644 index 2379514..0000000 --- a/Opera/notification.html +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/README.md b/README.md index 0ae8ba7..6b170fb 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,11 @@ MIT (X11) license. See LICENSE.txt +### Building ### + +To build run ./script/build.sh build chrome|safari|amo + + ### What is BabelExt? ### BabelExt is a library (or perhaps more of a boilerplate) meant to simplify the @@ -34,6 +39,8 @@ has its own function calls and way of working, including, but not limited to: - Accessing and controlling tabs (i.e. opening a link in a new one and choosing if it's focused) - Cross domain http requests (extensions require) - Storing data (using HTML5 localStorage or similar/equivalent engines) +- Managing resources (like large HTML snippets that are hard to read in raw JavaScript) +- Managing add-on preferences (which some browsers call options or settings) - Triggering notifications (desktop or browser, depending on the browser's particular level of support) - Adding URLs to history (to mark links as visited) - Note: this is a bit of a hack in all non-Chrome browsers... @@ -48,10 +55,11 @@ websites or functionality on the web. For this reason, functionality that is no by one or more of the 4 BabelExt browsers (Chrome, Firefox, Opera, Safari) may not be added to BabelExt. -BabelExt also isn't meant to handle building each browser's native settings consoles/panels, etc. -They're just too different from each other to try and abstract into a nice little package, -and with the 4 supported browsers all handling modern HTML/CSS/Javascript so well - it makes -sense (to me, anyhow) to build settings consoles and the like using those technologies. +Because each browser implements preferences in a slightly different way, BabelExt only supports +the baseline functionality that can be supported across all browsers. That might be enough if +you only need a few buttons and options, but with the 4 supported browsers all handling modern +HTML/CSS/Javascript so well - it makes sense (to me, anyhow) to build preference pages into the +site your extension is for. That's what I did with Reddit Enhancement Suite, and it has worked rather well. I am considering adding the automatic form rendering code from RES into BabelExt, but I will need to devote some @@ -61,36 +69,36 @@ thought to how to make it more universally useful. First, download all of the source from Github and put it together within a folder. -In Windows, run `makelinks.bat` to create symlinks to extension.js - these links are not -handled by github, which is why you unfortuntately have to make them yourself. -**NOTE:** You may need to open a command prompt as Administrator for this batch file to -work. +Then, download [PhantomJS](http://phantomjs.org), which is used to build and deploy extensions. -In UNIX-based OSes, you can run `makelinks.sh`. Note that this will make hardlinks. +Next, rename `conf/local_settings.json.example` to `conf/local_settings.json`. You will need +to edit this when you release your extension, but the defaults should be fine for now. -**IMPORTANT OPERA NOTE:** Note that the Opera js file has .user.js in it - that's because without this, -@include and @exclude directives will be ignored and your script will run on every page on -the internet! +In UNIX-based OSes, run `./script/build.sh build ` to build packages for each browser, +and `./script/build.sh release ` to release them to the various extension sites. -**IMPORTANT SAFARI NOTE:** Safari has a "security feature" that is not documented, gives no user -feedback at all, and can be a HUGE time sink if you don't know about it! If you have any -files in your extension folder that are symlinks, Safari will **silently** ignore them. -With Safari, a hard link will work, but a symbolic link will not. If you made the links -yourself instead of using the batch file, and your extension is doing nothing at all in -Safari, double check that! +The build system hasn't been tested under Windows yet - your best bet is probably to look at +the scripts and write a Windows equivalent. If it's any good, please send in a patch! -One last Safari quirk: if the directory does not end in ".safariextension", it will not be -recognized by Safari. Don't remove that from the name! +The build system maintains browser-specific `build` directories based on `conf/settings.json`. +It uses symbolic links where possible, but falls back to hard links for Chrome and Safari +(which silently ignore symlinks). + +It is recommended run `./script/build.sh maintain &` in the background. +This automatically fixes broken hard links and updates `BabelExt.resources` every few seconds. ## Instructions for loading/testing an extension in each browser ## -### Chrome ### +- You need to build the package before you start - the initial build + process configures some files that aren't stored in git + +### Chrome / Opera ### -- Click the wrench icon and choose Tools -> Extensions +- Go to about://extensions -- Check the "Developer Mode" checkbox +- Check "Developer Mode" -- Click "load unpacked extension" and choose the Chrome directory +- Click "load unpacked extension" and choose the build/Chrome directory - You're good to go! If you just want to try out the BabelExt kitchen sink demo, navigate to [http://babelext.com/demo/](http://babelext.com/demo/) @@ -98,26 +106,19 @@ recognized by Safari. Don't remove that from the name! ### Firefox ### -- Download the Firefox Addon SDK from [https://addons.mozilla.org/en-US/developers/builder](https://addons.mozilla.org/en-US/developers/builder) - -- Follow the installation instructions there, and run the appropriate activation script (i.e. bin\activate.bat in windows) +- Go to about:addons, click the "Tools" icon in the top-right and install the add-on from file -- Navigate to the Firefox directory under BabelExt, and type: cfx run +- Go to about:support and click the "Open Directory" to go to your profile directory -- You're good to go! If you just want to try out the BabelExt kitchen sink demo, navigate to [http://babelext.com/demo/](http://babelext.com/demo/) - -- Further Firefox development information can be found at [https://addons.mozilla.org/en-US/developers/docs/sdk/latest/](https://addons.mozilla.org/en-US/developers/docs/sdk/latest/) - -### Opera ### +- Open the "extensions" subdirectory and look for a subdirectory matching the "id" in your settings.json file -- Click Tools -> Extensions -> Manage Extensions +- Delete the file and replace it with a link to your extension's "build/firefox-unpacked" directory -- Find the config.xml file in the Opera directory of BabelExt, and drag it to the Extensions window +- Restart Firefox - You're good to go! If you just want to try out the BabelExt kitchen sink demo, navigate to [http://babelext.com/demo/](http://babelext.com/demo/) -- Further Opera development information can be found at [http://dev.opera.com/addons/extensions/](http://dev.opera.com/addons/extensions/) - +- Further Firefox development information: [Add-on SDK](https://addons.mozilla.org/en-US/developers/docs/sdk/latest/) and [setting up an extension development environment](https://developer.mozilla.org/en-US/Add-ons/Setting_up_extension_development_environment) ### Safari ### @@ -129,8 +130,49 @@ recognized by Safari. Don't remove that from the name! - Click the + button at the bottom left, and choose "Add Extension" -- Choose the Safari.safariextension folder from BabelExt +- Choose the build/Safari.safariextension folder from BabelExt - You're good to go! If you just want to try out the BabelExt kitchen sink demo, navigate to [http://babelext.com/demo/](http://babelext.com/demo/) - Further Safari development information can be found at [https://developer.apple.com/library/safari/#documentation/Tools/Conceptual/SafariExtensionGuide/Introduction/Introduction.html](https://developer.apple.com/library/safari/#documentation/Tools/Conceptual/SafariExtensionGuide/Introduction/Introduction.html) + +#### Certificates #### + +Safari requires all packages to be signed with a private key that's been registered with Apple. +You can develop unpacked extensions without a license, but you will need a (free) Apple Developer +account to build a package. You will also need to create a private key, which you can do with: + + openssl req -new -nodes -newkey rsa:2048 -keyout build/safari-info/id.rsa -out apple-cert.csr + +Apple seems to prefer you have a single private key per Apple Developer account. +If you maintain several projects with one account, consider linking build/safari-certs to a central location. + +BabelExt will automatically register your key and download extra certificates if you pass in your +username and password. Here are the steps if you prefer to do it by hand: + +- Go through [Apple's Certificate Request process](https://developer.apple.com/account/safari/certificate/certificateRequest.action) and save your certificate as `build/safari-certs/local.cer` +- Download [Apple's Worldwide Developer Relations Certificate](https://developer.apple.com/certificationauthority/AppleWWDRCA.) to `build/safari-certs/AppleIncRootCertificate.cer` +- Download [Apple's Root Certificate](https://www.apple.com/appleca/AppleIncRootCertificate.cer) to `build/safari-certs/AppleWWDRCA.cer` +- Download and compile [a modified version of the "xar" tool](http://mackyle.github.io/xar/) as `build/xar` + +Note: some online documentation refers to these keys as `cert00`, `cert01` and `cert02` +(these are the names `xar` uses when extracting them from a package) + +## Resetting extension data ## + +If your extension uses storage or preferences, you will need to test the extension data with +different stored values. Apart from Safari, all the browsers let you create multiple +profiles ("users" in Chrome), so you might want to create throwaway profiles for use during +testing. + +Private browsing isn't much help here, as some private browsing data will be initialised from +your public data. If you find profiles too much effort, Chrome/Opera also let you clear +extension data by deleting all files matching /Local*/** + +## Releasing packages ## + +You need to release the first version of your extension by hand, because each site has slightly +different requirements for their extensions. + +After the initial release, fill in `local_settings.json` and run `script/build.sh release ` +to release and update metadata. diff --git a/Safari.safariextension/.gitignore b/Safari.safariextension/.gitignore deleted file mode 100644 index ab2f236..0000000 --- a/Safari.safariextension/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/extension.js -/BabelExt.js \ No newline at end of file diff --git a/Safari.safariextension/notification.png b/Safari.safariextension/notification.png deleted file mode 100644 index 4abc65b..0000000 Binary files a/Safari.safariextension/notification.png and /dev/null differ diff --git a/build/Chrome/.gitignore b/build/Chrome/.gitignore new file mode 100644 index 0000000..dcaffc0 --- /dev/null +++ b/build/Chrome/.gitignore @@ -0,0 +1 @@ +/*.js diff --git a/build/Chrome/background.js b/build/Chrome/background.js new file mode 100644 index 0000000..4cf2fc7 --- /dev/null +++ b/build/Chrome/background.js @@ -0,0 +1,194 @@ +var contextMenuClick = function(info, tab, callbackID) { + chrome.tabs.sendMessage(tab.id, { + requestType: "contextMenu.click", + callbackID: callbackID + }); +}; + +var memoryStorage = { storage: {} }; +memoryStorage. getItem = function(key ) { return memoryStorage.storage[key] }; +memoryStorage. setItem = function(key, value) { memoryStorage.storage[key] = value }; +memoryStorage.removeItem = function(key ) { delete memoryStorage.storage[key] }; + +chrome.runtime.onMessage.addListener( + function(request, sender, sendResponse) { + // all requests expect a JSON object with requestType and then the relevant + // companion information... + switch(request.requestType) { + case 'xmlhttpRequest': + var xhr = new XMLHttpRequest(); + xhr.open(request.method, request.url, true, request.user, request.password); + if (request.method === "POST") { + xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); + // xhr.setRequestHeader("Content-length", request.data.length); + // xhr.setRequestHeader("Connection", "close"); + } + Object.keys(request.headers).forEach(function(header) { xhr.setRequestHeader(header, request.headers[header]) }); + if ( typeof(request.overrideMimeType) != 'undefined' ) xhr.overrideMimeType = request.overrideMimeType; + xhr.onload = function() { + var response = {status: xhr.status, statusText: xhr.statusText, responseText: xhr.responseText, _response_headers: xhr.getAllResponseHeaders()}; + sendResponse(response); + } + xhr.onerror = function() { + var response = {status: xhr.status, statusText: xhr.statusText, responseText: xhr.responseText, _response_headers: xhr.getAllResponseHeaders(), error: true}; + sendResponse(response); + } + xhr.send(request.data); + return true; // true must be returned here to indicate successful XHR + break; + case 'createTab': + var newIndex, + focus = (request.background !== true); + + if (typeof(request.index) !== 'undefined') { + newIndex = request.index; + } else { + // If index wasn't specified, get the selected tab so we can get the index of it. + // This allows us to open our new tab as the "next" tab in order rather than at the end. + newIndex = sender.tab.index+1; + } + chrome.tabs.create({url: request.url, selected: focus, index: newIndex}); + sendResponse({status: "success"}); + break; + case 'createNotification': + if (!request.icon) { + // if no icon specified, make a single pixel empty gif so we don't get a broken image link. + request.icon = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=='; + } + var notification = window.webkitNotifications.createNotification( + request.icon, // icon url - can be relative + request.title, // notification title + request.text // notification body text + ); + notification.show(); + break; + case 'localStorage': + switch (request.operation) { + case 'getItem': + sendResponse({status: true, key: request.itemName, value: localStorage.getItem(request.itemName)}); + break; + case 'removeItem': + localStorage.removeItem(request.itemName); + sendResponse({status: true, key: request.itemName, value: null}); + break; + case 'setItem': + localStorage.setItem(request.itemName, request.itemValue); + sendResponse({status: true, key: request.itemName, value: request.itemValue}); + break; + } + break; + case 'memoryStorage': + switch (request.operation) { + case 'getItem': + sendResponse({status: true, key: request.itemName, value: memoryStorage.getItem(request.itemName)}); + break; + case 'removeItem': + memoryStorage.removeItem(request.itemName); + sendResponse({status: true, key: request.itemName, value: null}); + break; + case 'setItem': + memoryStorage.setItem(request.itemName, request.itemValue); + sendResponse({status: true, key: request.itemName, value: request.itemValue}); + break; + } + break; + case 'addURLToHistory': + chrome.history.addUrl({url: request.url}); + break; + case 'contextMenus.create': + if (typeof request.obj.onclick === 'number') { + var callbackID = request.obj.onclick; + request.obj.onclick = function(info, tab) { + contextMenuClick(info, tab, callbackID); + }; + } + // id not available on firefox but title is, use it as common id + request.obj.id = request.obj.title; + chrome.contextMenus.create(request.obj); + break; + case 'contextMenus.remove': + chrome.contextMenus.remove(request.obj.title); + break; + default: + sendResponse({status: "unrecognized request type"}); + break; + } + } +); + +// chrome.storage.local should return almost instantly, but has been seen in the wild timing out. +// We do a test request first, and use a fallback implementation if that takes too long. +// The fallback implementation always returns default values. +var storage_local_works = true, storage_start_time = new Date().getTime(); +try { + chrome.storage.local.get('', function() { + storage_local_works = new Date().getTime() - storage_start_time < 1000; + if (!storage_local_works) { + console.log( 'chrome.storage.local took too long to respond - disabing.', chrome.runtime.lastError ); + } + }); +} catch (e) { + storage_local_works = false; + console.log('chrome.storage.local disabled: ', e); + console.log('This extension will still work, but will act as if all options have the default value.'); +} + +// the simple "onMessage" interface only works when the response is sent sychronously. +// Because preferences need to respond after a delay, we have to use the full interface: +chrome.runtime.onConnect.addListener(function(port) { + console.assert(port.name == "delayedMessage"); + if (storage_local_works) { // default behaviour + port.onMessage.addListener(function(request) { + function sendResponse(response) { port.postMessage({ request: request, response: response }) } + // all requests expect a JSON object with requestType and then the relevant + // companion information... + switch(request.requestType) { + case 'preferences': + switch (request.operation) { + case 'getItem': + chrome.storage.local.get(request.itemName, function(items) { + sendResponse({status: true, key: request.itemName, value: (items||{}).hasOwnProperty(request.itemName) ? items[request.itemName] : default_preferences[request.itemName]}); + }); + break; + case 'setItem': + var toSet = {}; toSet[request.itemName] = request.itemValue; + chrome.storage.local.set(toSet, function() { + sendResponse({status: true, key: request.itemName, value: request.itemValue}); + }); + break; + } + } + }); + } else { // fallback behaviour - return default values without waiting for the storage system + port.onMessage.addListener(function(request) { + function sendResponse(response) { port.postMessage({ request: request, response: response }) } + switch(request.requestType) { + case 'preferences': + switch (request.operation) { + case 'getItem': + sendResponse({status: true, key: request.itemName, value: default_preferences[request.itemName]}); + break; + case 'setItem': + sendResponse({status: false, key: request.itemName, value: request.itemValue}); + break; + } + } + }); + } +}); + +if ( auto_reload ) { + // Chrome defines a "fast reload" as a reload within 10 seconds of the previous one. + // Five consecutive fast reloads and the extension is disabled for a little while. + // Ideally we'd allow a small burst, but that would require us to store the burst count across reloads + var reload_timeout = new Date().getTime() + 10000; + chrome.webNavigation.onBeforeNavigate.addListener(function(data) { + var time = reload_timeout - new Date().getTime() + if ( time < 0 ) { + chrome.runtime.reload(); + reload_timeout = new Date().getTime() + 10000; + } else { + console.log( 'Fast reload detected - must wait ' + (time/1000) + ' seconds before reloading the extension again' ); + } + }); +} diff --git a/build/Chrome/chrome-bootstrap.css b/build/Chrome/chrome-bootstrap.css new file mode 100644 index 0000000..c481188 --- /dev/null +++ b/build/Chrome/chrome-bootstrap.css @@ -0,0 +1,719 @@ +/* + * The MIT License + * + * Copyright (c) 2012 Chrome Bootstrap authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +.chrome-bootstrap { + font-family: 'Segoe UI', 'Chrome Droid Sans', 'Droid Sans Fallback', 'Lucida Grande', 'Tahoma', sans-serif; + font-size: 12px; + color: #303942; + cursor: default; + margin: 0; + /* Headings + ============================================== */ + /* Layout + ============================================== */ + /* Header + ============================================== */ + /* View sections + ============================================== */ + /* Control bar + ============================================== */ + /* Pagination + ============================================== */ + /* Alert + ============================================== */ + /* Tags + ============================================== */ + /* Main menu + ============================================== */ + /* Icons + ============================================== */ + /* Highlightable list + ============================================== */ + /* Input styling + ============================================== */ + /* Focused --------------------------------- */ + /* Disabled --------------------------------- */ + /* Hovering --------------------------------- */ + /* Active --------------------------------- */ + /* Modal + ============================================== */ +} +.chrome-bootstrap a { + border: none; + color: #15C; + cursor: pointer; + text-decoration: underline; + font-weight: normal; +} +.chrome-bootstrap a:hover, +.chrome-bootstrap a:focus { + outline: none; +} +.chrome-bootstrap ul, +.chrome-bootstrap ol { + padding: 0; +} +.chrome-bootstrap li { + list-style-type: none; +} +.chrome-bootstrap dl, +.chrome-bootstrap dt, +.chrome-bootstrap dd { + margin: 0; +} +.chrome-bootstrap button { + cursor: pointer; +} +.chrome-bootstrap h1, +.chrome-bootstrap h2, +.chrome-bootstrap h3, +.chrome-bootstrap h4 { + -webkit-user-select: none; + font-weight: normal; + line-height: 1; +} +.chrome-bootstrap h1 small, +.chrome-bootstrap h2 small, +.chrome-bootstrap h3 small, +.chrome-bootstrap h4 small { + font-size: 15px; + margin: 0 10px; + color: #53637D; +} +.chrome-bootstrap h1 { + -webkit-margin-after: 1em; + -webkit-margin-before: 21px; + -webkit-margin-start: 23px; + height: 18px; + font-size: 18px; +} +.chrome-bootstrap h1 a { + color: #5C6166; + text-decoration: none; +} +.chrome-bootstrap h3 { + color: black; + font-size: 1.2em; + margin-bottom: 0.8em; +} +.chrome-bootstrap h4 { + font-size: 1em; + margin-bottom: 5px; +} +.chrome-bootstrap .frame .navigation { + height: 100%; + -webkit-margin-start: 0; + position: fixed; + -webkit-margin-end: 15px; + width: 155px; + z-index: 3; +} +.chrome-bootstrap .frame .view, +.chrome-bootstrap .frame .content { + width: 738px; + overflow-x: hidden; +} +.chrome-bootstrap .frame .content { + padding-top: 55px; +} +.chrome-bootstrap .frame .content p { + text-align: justify; +} +.chrome-bootstrap .frame .with_controls .content { + padding-top: 104px; +} +.chrome-bootstrap .frame .view { + -webkit-margin-start: 155px; +} +.chrome-bootstrap .frame .view a { + font: inherit; +} +.chrome-bootstrap .frame .mainview > * { + -webkit-margin-start: -20px; + -webkit-transition: margin 100ms, opacity 100ms; + opacity: 0; + z-index: 0; + position: absolute; + top: 0; + display: block; +} +.chrome-bootstrap .frame .mainview > .selected { + -webkit-margin-start: 0; + -webkit-transition: margin 200ms, opacity 200ms; + -webkit-transition-delay: 100ms; + z-index: 1; + opacity: 1; +} +.chrome-bootstrap header { + position: fixed; + background-image: -webkit-linear-gradient(#ffffff, #ffffff 40%, rgba(255, 255, 255, 0.92)); + width: 738px; + z-index: 2; +} +.chrome-bootstrap header h1 { + padding: 21px 0 13px; + margin: 0; + border-bottom: 1px solid #EEE; +} +.chrome-bootstrap header .corner { + position: absolute; + right: 0px; + top: 21px; +} +.chrome-bootstrap header .corner input[type="text"] { + width: 210px; +} +.chrome-bootstrap header .corner.cancelable .delete { + opacity: 1; + top: 4px; + right: 5px; +} +.chrome-bootstrap section { + -webkit-padding-start: 18px; + margin-bottom: 24px; + margin-top: 8px; + max-width: 600px; +} +.chrome-bootstrap section h3 { + -webkit-margin-start: -18px; +} +.chrome-bootstrap section .row { + display: block; + margin: 0.65em 0; +} +.chrome-bootstrap .controls { + -webkit-padding-end: 3px; + -webkit-padding-start: 4px; + -webkit-transition: padding 100ms, height 100ms, opacity 100ms; + border-bottom: 1px solid #EEE; + display: -webkit-box; + overflow: hidden; + padding: 13px 0; + position: relative; +} +.chrome-bootstrap .controls .text { + display: inline-block; + margin-top: 4px; +} +.chrome-bootstrap .controls .spacer { + -webkit-box-flex: 1; +} +.chrome-bootstrap ol.pagination li { + margin: 0 2px; + display: inline-block; + line-height: 25px; +} +.chrome-bootstrap ol.pagination a { + width: 25px; + height: 24px; + text-align: center; + display: block; + background: #F0F6FE; + text-decoration: none; +} +.chrome-bootstrap ol.pagination a:hover, +.chrome-bootstrap ol.pagination a.selected { + background: #8AAAED; + color: #FFF; +} +.chrome-bootstrap .alert { + border-radius: 3px; + background: rgba(147, 184, 252, 0.2); + display: block; + position: relative; + padding: 10px 30px 10px 10px; + line-height: 17px; +} +.chrome-bootstrap .alert .delete { + top: 5px; + right: 6px; + opacity: 1; +} +.chrome-bootstrap ul.tags li { + background: #8AAAED; + color: #FFF; + border-radius: 3px; + position: relative; + display: inline-block; + padding: 2px 5px; +} +.chrome-bootstrap ul.tags li a { + color: #FFF; + text-decoration: none; +} +.chrome-bootstrap ul.tags li a:hover { + text-decoration: underline; +} +.chrome-bootstrap ul.tags li .delete { + opacity: 1; + position: relative; + display: inline-block; + width: 13px; + height: 12px; + top: 1px; + background-position-y: -1px; +} +.chrome-bootstrap ul.menu { + -webkit-margin-before: 1em; + -webkit-margin-after: 2em; + -webkit-margin-start: 0px; + -webkit-margin-end: 0px; + -webkit-padding-start: 40px; + list-style-type: none; + padding: 0; +} +.chrome-bootstrap ul.menu li { + -webkit-border-start: 6px solid transparent; + -webkit-padding-start: 18px; + -webkit-user-select: none; + display: list-item; + text-align: -webkit-match-parent; +} +.chrome-bootstrap ul.menu li.selected { + -webkit-border-start-color: #4e5764; +} +.chrome-bootstrap ul.menu li.selected a { + color: #464E5A; +} +.chrome-bootstrap ul.menu li a { + border: 0; + color: #999; + cursor: pointer; + font: inherit; + line-height: 29px; + margin: 0; + padding: 0; + text-decoration: none; + display: block; +} +.chrome-bootstrap .arrow_collapse { + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-left: 6px solid #999; + -webkit-margin-end: 4px; + top: 1px; +} +.chrome-bootstrap .arrow_expand { + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 7px solid #999; + -webkit-margin-end: 4px; +} +.chrome-bootstrap .arrow { + width: 0; + height: 0; + position: relative; + display: inline-block; +} +.chrome-bootstrap .delete { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAiElEQVR42r2RsQrDMAxEBRdl8SDcX8lQPGg1GBI6lvz/h7QyRRXV0qUULwfvwZ1tenw5PxToRPWMC52eA9+WDnlh3HFQ/xBQl86NFYJqeGflkiogrOvVlIFhqURFVho3x1moGAa3deMs+LS30CAhBN5nNxeT5hbJ1zwmji2k+aF6NENIPf/hs54f0sZFUVAMigAAAABJRU5ErkJggg=="); + background-repeat: no-repeat; + display: block; + opacity: 0; + height: 14px; + width: 14px; + -webkit-transition: 150ms opacity; + background-color: transparent; + text-indent: -5000px; + position: absolute; +} +.chrome-bootstrap .delete:hover { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAqklEQVR4XqWRMQ6DMAxF/1Fyilyj2SmIBUG5QcTCyJA5Z8jGhlBPgRi4TmoDraVmKFJlWYrlp/g5QfwRlwEVNWVa4WzfH9jK6kCkEkBjwxOhLghheMWMELUAqqwQ4OCbnE4LJnhr5IYdqQt4DJQjhe9u4vBBmnxHHNzRFkDGjHDo0VuTAqy2vAG4NkvXXDHxbGsIGlj3e835VFNtdugma/Jk0eXq0lP//5svi4PtO01oFfYAAAAASUVORK5CYII="); +} +.chrome-bootstrap .highlightable li { + position: relative; + padding: 2px 0; +} +.chrome-bootstrap .highlightable li:hover > a:not(.action), +.chrome-bootstrap .highlightable li a:not(.action):focus { + background-color: #F0F6FE; + color: #555; +} +.chrome-bootstrap .highlightable li:hover > .action { + opacity: 0.7; +} +.chrome-bootstrap .highlightable li a { + padding: 5px; + display: block; + position: relative; + z-index: 0; + text-decoration: none; +} +.chrome-bootstrap .highlightable li dt { + font-size: 105%; + margin-bottom: 3px; +} +.chrome-bootstrap .highlightable li dd { + color: #999; + overflow: hidden; + white-space: nowrap; + font-size: 10px; + margin-top: 5px; +} +.chrome-bootstrap .highlightable li .tags { + float: left; + margin-top: -1px; + font-size: 12px; +} +.chrome-bootstrap .highlightable li .tags li:last-child { + margin-right: 5px; +} +.chrome-bootstrap .highlightable li .tags li:hover > a:not(.action) { + background: #8AAAED; + color: #FFF; +} +.chrome-bootstrap .highlightable li .tags li a { + padding: 0; +} +.chrome-bootstrap .highlightable li .action { + -webkit-appearance: none; + -webkit-transition: opacity 150ms; + background: #8AAAED; + border: none; + border-radius: 2px; + color: white; + opacity: 0; + margin-top: 0; + font-size: 10px; + padding: 1px 6px; + position: absolute; + top: 8px; + right: 32px; + -webkit-transition: 150ms opacity; + cursor: pointer; +} +.chrome-bootstrap .highlightable li .action:hover { + opacity: 1; +} +.chrome-bootstrap .highlightable li .highlightable { + -webkit-margin-start: 30px; +} +.chrome-bootstrap .highlightable.editable .delete { + position: absolute; + top: 7px; + right: 5px; +} +.chrome-bootstrap .highlightable.editable li:hover > .delete { + opacity: 1; +} +.chrome-bootstrap .highlightable.draggable .handle { + width: 8px; + height: 41px; + background-image: linear-gradient(to bottom, #c1c1c1 50%, rgba(255, 255, 255, 0) 0%); + background-position: center; + background-size: 100% 17%; + background-repeat: repeat-y; + visibility: hidden; + position: absolute; + top: 4px; + left: 2px; +} +.chrome-bootstrap .highlightable.draggable .handle:hover { + cursor: move; + cursor: -webkit-grab; + display: block; +} +.chrome-bootstrap .highlightable.draggable .handle:after { + margin-left: 3px; + width: 2px; + height: 41px; + background: #F0F6FE; + content: ""; + display: block; +} +.chrome-bootstrap .highlightable.draggable li:hover .handle { + visibility: visible; + z-index: 1; +} +.chrome-bootstrap .highlightable.draggable li > .item { + padding-left: 20px; +} +.chrome-bootstrap .match { + background: #f2f37b; + display: inline-block; + margin: 0 1px; +} +.chrome-bootstrap select, +.chrome-bootstrap input[type='checkbox'], +.chrome-bootstrap input[type='radio'], +.chrome-bootstrap input[type='button'], +.chrome-bootstrap button { + -webkit-appearance: none; + -webkit-user-select: none; + background-image: -webkit-linear-gradient(#ededed, #ededed 38%, #dedede); + border: 1px solid rgba(0, 0, 0, 0.25); + border-radius: 2px; + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08), inset 0 1px 2px rgba(255, 255, 255, 0.75); + color: #444; + font: inherit; + margin: 0 1px 0 0; + text-shadow: 0 1px 0 #F0F0F0; +} +.chrome-bootstrap button.small { + padding: 1px 5px 2px; + min-height: 1em; +} +.chrome-bootstrap input[type='checkbox']:checked::before { + -webkit-user-select: none; + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9wDBhYcG79aGIsAAACbSURBVBjTjdFBCkFhFAXgj4fp24PBy0SZ2ICRXRgYGb2xlKzBSEo2YgsiKWVoZgFKMjD5X/2Ux6lb99bpnNO5lKMR5i8MsEQHkhJiEzlS9HCqfiFWMUIt3AfsC3KKLCL30Qr7HfM4Ro4h6rhiEqmusIMKuphGqo+ogSPGcbYLzh91vdkXSHDDBk+0gxussS3rNcMCs+D6E18/9gLPPhbDshfzLgAAAABJRU5ErkJggg=="); + background-size: 100% 100%; + content: ''; + display: block; + height: 100%; + width: 100%; +} +.chrome-bootstrap html[dir='rtl'] input[type='checkbox']:checked::before { + -webkit-transform: scaleX(-1); +} +.chrome-bootstrap input[type='radio']:checked::before { + background-color: #666; + border-radius: 100%; + bottom: 3px; + content: ''; + display: block; + left: 3px; + position: absolute; + right: 3px; + top: 3px; +} +.chrome-bootstrap select { + -webkit-appearance: none; + -webkit-padding-end: 20px; + -webkit-padding-start: 6px; + /* OVERRIDE */ + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAWklEQVQokWNgoAOIAuI0PDiKaJMSgYCZmfkbkPkfHYPEQfJEG/b//3+FBQsWLGRjY/uJbBCIDxIHyRNtGDYDyTYI3UA+Pr4vFBmEbODbt2+bKDYIyUBWYtQBAIRzRP/XKJ//AAAAAElFTkSuQmCC), -webkit-linear-gradient(#ededed, #ededed 38%, #dedede); + background-position: right center; + background-repeat: no-repeat; +} +.chrome-bootstrap select { + min-height: 2em; + min-width: 4em; +} +.chrome-bootstrap html[dir='rtl'] select { + background-position: center left; +} +.chrome-bootstrap input[type='checkbox'] { + bottom: 2px; + height: 13px; + position: relative; + vertical-align: middle; + width: 13px; +} +.chrome-bootstrap input[type='radio'] { + /* OVERRIDE */ + border-radius: 100%; + bottom: 3px; + height: 15px; + position: relative; + vertical-align: middle; + width: 15px; +} +.chrome-bootstrap button { + -webkit-padding-end: 10px; + -webkit-padding-start: 10px; + min-height: 2em; + min-width: 4em; +} +.chrome-bootstrap input[type='text'], +.chrome-bootstrap input[type='number'], +.chrome-bootstrap input[type='search'] { + border: 1px solid #BFBFBF; + border-radius: 2px; + box-sizing: border-box; + color: #444; + font: inherit; + margin: 0; + min-height: 2em; + padding: 3px; + padding-bottom: 4px; +} +.chrome-bootstrap .radio, +.chrome-bootstrap .checkbox { + margin: 0.65em 0; +} +.chrome-bootstrap select:focus, +.chrome-bootstrap input[type='checkbox']:focus, +.chrome-bootstrap input[type='password']:focus, +.chrome-bootstrap input[type='radio']:focus, +.chrome-bootstrap input[type='search']:focus, +.chrome-bootstrap input[type='text']:focus, +.chrome-bootstrap input[type='number']:focus, +.chrome-bootstrap button:focus { + /* OVERRIDE */ + -webkit-transition: border-color 200ms; + /* We use border color because it follows the border radius (unlike outline). + * This is particularly noticeable on mac. */ + border-color: #4d90fe; + outline: none; +} +.chrome-bootstrap button:disabled, +.chrome-bootstrap select:disabled { + background-image: -webkit-linear-gradient(#f1f1f1, #f1f1f1 38%, #e6e6e6); + border-color: rgba(80, 80, 80, 0.2); + box-shadow: 0 1px 0 rgba(80, 80, 80, 0.08), inset 0 1px 2px rgba(255, 255, 255, 0.75); + color: #aaa; + cursor: default; +} +.chrome-bootstrap select:disabled { + /* OVERRIDE */ + background-image: -webkit-image-set(url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAAXNSR0IArs4c6QAAAAd0SU1FB9sLAxYEBKriBmwAAAAGYktHRAD/AP8A/6C9p5MAAAAJcEhZcwAACxMAAAsTAQCanBgAAABLSURBVCiRY2CgA4gC4jQ8OIpokxKBoKGh4T8uDJIn2rD///8rLFiwYCE2g0DiIHkSfIndQLIMwmYgRQYhG/j27dsmig1CMpCVGHUAo8FcsHfxfXQAAAAASUVORK5CYII=") 1x), -webkit-linear-gradient(#f1f1f1, #f1f1f1 38%, #e6e6e6); +} +.chrome-bootstrap input[type='checkbox']:disabled, +.chrome-bootstrap input[type='radio']:disabled { + opacity: .75; +} +.chrome-bootstrap input[type='search']:disabled, +.chrome-bootstrap input[type='number']:disabled, +.chrome-bootstrap input[type='text']:disabled { + color: #999; +} +.chrome-bootstrap select:hover:enabled, +.chrome-bootstrap input[type='checkbox']:hover:enabled, +.chrome-bootstrap input[type='radio']:hover:enabled, +.chrome-bootstrap button:hover:enabled { + background-image: -webkit-linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0); + border-color: rgba(0, 0, 0, 0.3); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.12), inset 0 1px 2px rgba(255, 255, 255, 0.95); + color: black; +} +.chrome-bootstrap select:hover:enabled { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAWklEQVQokWNgoAOIAuI0PDiKaJMSgYCZmfkbkPkfHYPEQfJEG/b//3+FBQsWLGRjY/uJbBCIDxIHyRNtGDYDyTYI3UA+Pr4vFBmEbODbt2+bKDYIyUBWYtQBAIRzRP/XKJ//AAAAAElFTkSuQmCC"), -webkit-linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0); +} +.chrome-bootstrap select:active:enabled, +.chrome-bootstrap input[type='checkbox']:active:enabled, +.chrome-bootstrap input[type='radio']:active:enabled, +.chrome-bootstrap button:active:enabled { + background-image: -webkit-linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7); + box-shadow: none; + text-shadow: none; +} +.chrome-bootstrap select:active:enabled { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAWklEQVQokWNgoAOIAuI0PDiKaJMSgYCZmfkbkPkfHYPEQfJEG/b//3+FBQsWLGRjY/uJbBCIDxIHyRNtGDYDyTYI3UA+Pr4vFBmEbODbt2+bKDYIyUBWYtQBAIRzRP/XKJ//AAAAAElFTkSuQmCC"), -webkit-linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7); +} +.chrome-bootstrap .overlay { + -webkit-box-align: center; + -webkit-box-orient: vertical; + -webkit-box-pack: center; + -webkit-transition: opacity .2s; + background-color: rgba(255, 255, 255, 0.75); + bottom: 0; + display: -webkit-box; + left: 0; + overflow: auto; + padding: 20px; + position: fixed; + right: 0; + top: 0; + z-index: 5; + opacity: 1; +} +.chrome-bootstrap .overlay.transparent { + opacity: 0; +} +.chrome-bootstrap .overlay.transparent .page { + -webkit-transform: scale(0.99) translateY(-20px); +} +.chrome-bootstrap .overlay .page { + -webkit-border-radius: 3px; + -webkit-box-orient: vertical; + -webkit-transition: 200ms -webkit-transform; + background: white; + box-shadow: 0 4px 23px 5px rgba(0, 0, 0, 0.2), 0 2px 6px rgba(0, 0, 0, 0.15); + color: #333; + display: -webkit-box; + min-width: 400px; + padding: 0; + position: relative; + overflow: hidden; +} +@-webkit-keyframes pulse { + 0% { + -webkit-transform: scale(1); + } + 40% { + -webkit-transform: scale(1.02); + } + 60% { + -webkit-transform: scale(1.02); + } + 100% { + -webkit-transform: scale(1); + } +} +.chrome-bootstrap .overlay .page.pulse { + -webkit-animation-duration: 180ms; + -webkit-animation-iteration-count: 1; + -webkit-animation-name: pulse; + -webkit-animation-timing-function: ease-in-out; +} +.chrome-bootstrap .overlay .page h1 { + -webkit-padding-end: 24px; + -webkit-user-select: none; + color: #333; + font-size: 120%; + margin: 0; + padding: 14px 17px 14px; + text-shadow: white 0 1px 2px; +} +.chrome-bootstrap .overlay .page ul li { + padding: 5px 0; +} +.chrome-bootstrap .overlay .page ul.tags li { + padding: 2px 5px; +} +.chrome-bootstrap .overlay .page .content-area { + -webkit-box-flex: 1; + overflow: auto; + padding: 6px 17px 6px; +} +.chrome-bootstrap .overlay .page .close-button { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAQAAAC1QeVaAAAAUklEQVR4XqXPYQrAIAhAYW/gXd8NJxTopVqsGEhtf+L9/ERU2k/HSMFQpKcYJeNFI9Be0LCMij8cYyjj5EHIivGBkwLfrbX3IF8PqumVmnDpEG+eDsKibPG2JwAAAABJRU5ErkJggg=='); + background-position: center; + background-repeat: no-repeat; + height: 14px; + position: absolute; + right: 7px; + top: 7px; + width: 14px; +} +.chrome-bootstrap .overlay .page .close-button:hover { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAQAAAC1QeVaAAAAnUlEQVR4XoWQQQ6CQAxFewjkJkMCyXgJPMk7AiYczyBeZEAX6AKctGIaN+bt+trk9wtGQc/IkhnoKGxqqiWxOSZalapWFZ6VrIUDExsN0a5JRBq9LoVOR0eEQMoEhKizXhhsn0p1sCWVo7CwOf1RytPL8CPvwuBUoHL6ugeK30CVD1TqK7V/hdpe+VNChhOzV8xWny/+xosHF8578W/Hmc1OOC3wmwAAAABJRU5ErkJggg=='); +} +.chrome-bootstrap .overlay .page .action-area { + -webkit-box-align: center; + -webkit-box-orient: horizontal; + -webkit-box-pack: end; + display: -webkit-box; + padding: 14px 17px; +} +.chrome-bootstrap .overlay .page .action-area-right { + display: -webkit-box; +} +.chrome-bootstrap .overlay .page .button-strip { + -webkit-box-orient: horizontal; + display: -webkit-box; +} +.chrome-bootstrap .overlay .page .button-strip button { + -webkit-margin-start: 10px; + display: block; +} diff --git a/build/Chrome/manifest.json b/build/Chrome/manifest.json new file mode 100644 index 0000000..0bba4cd --- /dev/null +++ b/build/Chrome/manifest.json @@ -0,0 +1,38 @@ +{ + "name": "BabelExt", + "author": "honestbleeps", + "version": "0.95", + "manifest_version": 2, + "description": "An extension created with BabelExt - www.babelext.com", + "background": { + "scripts": [ + "preferences.js", + "background.js" + ] + }, + "content_scripts": [ + { + "matches": [ + "*://babelext.com/*" + ], + "js": [ + "lib/BabelExt.js", + "src/extension.js" + ], + "run_at": "document_end", + "css": [ + "src/extension.css" + ] + } + ], + "icons": {}, + "permissions": [ + "*://babelext.com/*", + "contextMenus", + "tabs", + "history", + "notifications", + "storage" + ], + "options_page": "options.html" +} diff --git a/build/Chrome/options.js b/build/Chrome/options.js new file mode 100644 index 0000000..0d4e53a --- /dev/null +++ b/build/Chrome/options.js @@ -0,0 +1,51 @@ +// based on https://developer.chrome.com/extensions/options + +function get_preferences() { + var preferences = {}; + [].slice.call(document.querySelectorAll('.pref')).forEach(function(element) { + switch ( element.nodeName ) { + case 'INPUT': + switch ( element.type ) { + case 'checkbox': + if ( element.checked ) { + preferences[element.id] = + element.hasAttribute('data-on') ? parseInt(element.getAttribute('data-on'),10) : true; + } else { + preferences[element.id] = + element.hasAttribute('data-off') ? parseInt(element.getAttribute('data-off'),10) : false; + } + break; + case 'radio' : if ( element.checked ) preferences[element.name] = element.value; break; + case 'number': preferences[element.id] = parseInt(element.value,10); break; + case 'text' : preferences[element.id] = element.value; break; + } + break; + case 'SELECT': preferences[element.id] = element.value; break; + } + }); + return preferences; +} + +document.addEventListener('DOMContentLoaded', function() { + chrome.storage.local.get(get_preferences(), function(preferences) { + [].slice.call(document.querySelectorAll('.pref')).forEach(function(element) { + switch ( element.nodeName ) { + case 'INPUT': + switch ( element.type ) { + case 'checkbox': + element.checked = + preferences[element.id] == ( element.hasAttribute('data-on') ? element.getAttribute('data-on') : true ); + break; + case 'radio' : element.checked = preferences[element.name] == element.value; break; + case 'number': element.value = preferences[element.id]; break; + case 'text' : element.value = preferences[element.id]; break; + } + break; + case 'SELECT': element.value = preferences[element.id]; break; + } + }); + }); +}); + +document.addEventListener('click', function() { chrome.storage.local.set(get_preferences(), function() {}); }); +document.addEventListener('input', function() { chrome.storage.local.set(get_preferences(), function() {}); }); diff --git a/build/Firefox/lib/main.js b/build/Firefox/lib/main.js new file mode 100644 index 0000000..5311b31 --- /dev/null +++ b/build/Firefox/lib/main.js @@ -0,0 +1,243 @@ +// Import the APIs we need. + +var pageMod = require("sdk/page-mod"); +var XMLHttpRequest = require("sdk/net/xhr").XMLHttpRequest; +var notifications = require("sdk/notifications"); +var self = require("sdk/self"); +var tabs = require("sdk/tabs"); +var ss = require("sdk/simple-storage"); +var workers = []; +var contextMenu = require("sdk/context-menu"); +var priv = require("sdk/private-browsing"); +var windows = require("sdk/windows").browserWindows; +var prefs = require("sdk/simple-prefs").prefs; + +// require chrome allows us to use XPCOM objects... +const {Cc, Ci, Cu, Cr} = require("chrome"); + +var historyService = Cc["@mozilla.org/browser/history;1"].getService(Ci.mozIAsyncHistory); + +// this function takes in a string (and optional charset, paseURI) and creates an nsURI object, which is required by historyService.addURI... +function makeURI(aURL, aOriginCharset, aBaseURI) { + var ioService = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + return ioService.newURI(aURL, aOriginCharset, aBaseURI); +} + + +function detachWorker(worker, workerArray) { + var index = workerArray.indexOf(worker); + if(index != -1) { + workerArray.splice(index, 1); + } +} +var localStorage = ss.storage; + +// these aliases are just for simplicity, so that the code here looks just like background code +// for all of the other browsers... +localStorage. getItem = function(key ) { return ss.storage[key] }; +localStorage. setItem = function(key, value) { ss.storage[key] = value }; +localStorage.removeItem = function(key ) { delete ss.storage[key] }; + +var memoryStorage = { storage: {} }; +memoryStorage. getItem = function(key ) { return memoryStorage.storage[key] }; +memoryStorage. setItem = function(key, value) { memoryStorage.storage[key] = value }; +memoryStorage.removeItem = function(key ) { delete memoryStorage.storage[key] }; + +var settings = require("./settings.js"); + +pageMod.PageMod({ + include: settings.include, + contentScriptWhen: settings.contentScriptWhen, + contentScriptFile: settings.contentScriptFile.map(function(file) { return self.data.url(file) }), + contentStyleFile: settings.contentStyleFile.map(function(file) { return self.data.url(file) }), + onAttach: function(worker) { + tabs.on('activate', function(tab) { + // run some code when a tab is activated... + }); + + workers.push(worker); + worker.on('detach', function () { + detachWorker(this, workers); + // console.log('worker detached, total now: ' + workers.length); + }); + + worker.on('message', function(data) { + var request = data; + switch(request.requestType) { + case 'xmlhttpRequest': + var responseObj = { + callbackID: request.callbackID, + name: request.requestType + }; + var xhr = new XMLHttpRequest(); + xhr.open(request.method, request.url, true, request.user, request.password); + if (request.method === "POST") { + xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); + } + Object.keys(request.headers).forEach(function(header) { xhr.setRequestHeader(header, request.headers[header]) }); + if ( typeof(request.overrideMimeType) != 'undefined' ) xhr.overrideMimeType = request.overrideMimeType; + xhr.onload = function() { + responseObj.response = {status: xhr.status, statusText: xhr.statusText, responseText: xhr.responseText, _response_headers: xhr.getAllResponseHeaders()}; + worker.postMessage(responseObj); + } + xhr.onerror = function() { + responseObj.response = {status: xhr.status, statusText: xhr.statusText, responseText: xhr.responseText, _response_headers: xhr.getAllResponseHeaders(), error: true}; + worker.postMessage(responseObj); + } + xhr.send(request.data); + break; + case 'createTab': + var focus = (request.background !== true); + tabs.open({url: request.url, inBackground: !focus }); + worker.postMessage({status: "success"}); + break; + case 'createNotification': + if (!request.icon) { + // if no icon specified, make a single pixel empty gif so we don't get a broken image link. + request.icon = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=='; + } + notifications.notify({ + title: request.title, + text: request.text, + iconURL: request.icon + }); + break; + case 'localStorage': + switch (request.operation) { + case 'getItem': + worker.postMessage({ + name: 'localStorage', + callbackID: request.callbackID, + status: true, + key: request.itemName, + value: localStorage.getItem(request.itemName) + }); + break; + case 'removeItem': + localStorage.removeItem(request.itemName); + worker.postMessage({ + name: 'localStorage', + callbackID: request.callbackID, + status: true, + value: null + }); + break; + case 'setItem': + localStorage.setItem(request.itemName, request.itemValue); + worker.postMessage({ + name: 'localStorage', + callbackID: request.callbackID, + status: true, + key: request.itemName, + value: request.itemValue + }); + break; + } + break; + case 'memoryStorage': + switch (request.operation) { + case 'getItem': + worker.postMessage({ + name: 'memoryStorage', + callbackID: request.callbackID, + status: true, + key: request.itemName, + value: memoryStorage.getItem(request.itemName) + }); + break; + case 'removeItem': + memoryStorage.removeItem(request.itemName); + worker.postMessage({ + name: 'memoryStorage', + callbackID: request.callbackID, + status: true, + value: null + }); + break; + case 'setItem': + memoryStorage.setItem(request.itemName, request.itemValue); + worker.postMessage({ + name: 'memoryStorage', + callbackID: request.callbackID, + status: true, + key: request.itemName, + value: request.itemValue + }); + break; + } + break; + case 'preferences': + switch (request.operation) { + case 'getItem': + worker.postMessage({ + name: 'preferences', + callbackID: request.callbackID, + status: true, + key: request.itemName, + value: prefs[request.itemName] + }); + break; + case 'setItem': + prefs[request.itemName] = request.itemValue; + worker.postMessage({ + name: 'preferences', + callbackID: request.callbackID, + status: true, + key: request.itemName, + value: request.itemValue + }); + break; + } + break; + case 'addURLToHistory': + var isPrivate = priv.isPrivate(windows.activeWindow); + if (isPrivate) { + // do not add to history if in private browsing mode! + return false; + } + var uri = makeURI(request.url); + historyService.updatePlaces({ + uri: uri, + visits: [{ + transitionType: Ci.nsINavHistoryService.TRANSITION_LINK, + visitDate: Date.now() * 1000 + }] + }); + break; + case 'contextMenus.create': + contextMenu.Item({ + label: request.obj.title, + context: contextMenu.PageContext(), + data: request.obj.onclick, + contentScript: 'self.on("click", function (node, data) {' + + 'self.postMessage(data);' + + '});', + onMessage: function(onclick) { + worker.postMessage({ + name: 'contextMenus.click', + callbackID: onclick + }); + } + + }); + break; + case 'contextMenus.remove': + // Run through the current context items and destroy the one with a matching name + contextItems = contextMenu.contentContextMenu.items; + var len = contextItems.length; + for(var i =0; i < len; ++i){ + if(request.obj.title == contextItems[i].label){ + contextMenu.contentContextMenu.destroy(contextItems[i]); + break; + } + } + + break; + default: + worker.postMessage({status: "unrecognized request type"}); + break; + } + + }); + } +}); diff --git a/build/Firefox/lib/settings.js b/build/Firefox/lib/settings.js new file mode 100644 index 0000000..ab3075f --- /dev/null +++ b/build/Firefox/lib/settings.js @@ -0,0 +1,4 @@ +exports.include = ["http://babelext.com/*","https://babelext.com/*"]; +exports.contentScriptWhen = "ready"; +exports.contentScriptFile = ["lib/BabelExt.js","src/extension.js"]; +exports.contentStyleFile = ["src/extension.css"]; diff --git a/build/Firefox/package.json b/build/Firefox/package.json new file mode 100644 index 0000000..94eee61 --- /dev/null +++ b/build/Firefox/package.json @@ -0,0 +1,81 @@ +{ + "description": "An extension created with BabelExt - www.babelext.com", + "license": "GPL", + "author": "honestbleeps", + "version": "0.95", + "title": "BabelExt", + "id": "abcdef01-2345-6789-9876-543210fedcba", + "name": "babelext_your_name_here", + "preferences": [ + { + "name": "myBool", + "type": "bool", + "title": "myBool preference title", + "description": "myBool short description for the preference", + "value": false + }, + { + "name": "myBoolint", + "type": "boolint", + "title": "myBoolint preference title", + "description": "myBoolint short description for the preference", + "off": "1", + "on": "2", + "value": 1 + }, + { + "name": "myInteger", + "type": "integer", + "title": "myInteger preference title", + "description": "myInteger short description for the preference", + "value": 2 + }, + { + "name": "myString", + "type": "string", + "title": "myString preference title", + "description": "myString short description for the preference", + "value": "this is the default string value" + }, + { + "name": "myMenulist", + "type": "menulist", + "title": "myMenulist preference title", + "value": 0, + "options": [ + { + "value": "0", + "label": "first label" + }, + { + "value": "1", + "label": "second label" + }, + { + "value": "2", + "label": "third label" + } + ] + }, + { + "name": "myRadio", + "type": "radio", + "title": "myRadioTitle", + "value": "a", + "options": [ + { + "value": "a", + "label": "first label" + }, + { + "value": "b", + "label": "second label" + }, + { + "value": "c", + "label": "third label" + } + ] + } + ] +} diff --git a/build/Firefox/test/test-main.js b/build/Firefox/test/test-main.js new file mode 100644 index 0000000..147f98a --- /dev/null +++ b/build/Firefox/test/test-main.js @@ -0,0 +1,12 @@ +var main = require("./main"); + +exports["test main"] = function(assert) { + assert.pass("Unit test running!"); +}; + +exports["test main async"] = function(assert, done) { + assert.pass("async Unit test running!"); + done(); +}; + +require("sdk/test").run(exports); diff --git a/build/README.txt b/build/README.txt new file mode 100644 index 0000000..f8a2c04 --- /dev/null +++ b/build/README.txt @@ -0,0 +1,3 @@ +Each web browser wants you to build your extension in a slightly different way. + +The build script will automatically build browser-specific extensions from the values you provide. diff --git a/Safari.safariextension/Info.plist b/build/Safari.safariextension/Info.plist similarity index 82% rename from Safari.safariextension/Info.plist rename to build/Safari.safariextension/Info.plist index 38cc1c4..594e6b9 100644 --- a/Safari.safariextension/Info.plist +++ b/build/Safari.safariextension/Info.plist @@ -3,13 +3,13 @@ Author - Steve Sobel + honestbleeps Builder Version - 8536.30.1 + 8537.85.12.18 CFBundleDisplayName - BabelExt Extension + BabelExt CFBundleIdentifier - com.honestbleeps.babelext-extension + com.honestbleeps.abcdefghijklmnopqabcdefghijklmnopqabcdefghij CFBundleInfoDictionaryVersion 6.0 CFBundleShortVersionString @@ -35,7 +35,7 @@ Command - + Identifier notificationButton Image @@ -57,16 +57,22 @@ End - extension.js + src/extension.js Start - BabelExt.js + lib/BabelExt.js + Stylesheets + + src/extension.css + Description An extension created with BabelExt - www.babelext.com + DeveloperIdentifier + ABCDEFGHIJ ExtensionInfoDictionaryVersion 1.0 Permissions diff --git a/Safari.safariextension/background.html b/build/Safari.safariextension/background.html similarity index 63% rename from Safari.safariextension/background.html rename to build/Safari.safariextension/background.html index b31ccf8..e29ae03 100644 --- a/Safari.safariextension/background.html +++ b/build/Safari.safariextension/background.html @@ -1,6 +1,11 @@ - - \ No newline at end of file + diff --git a/Safari.safariextension/notification.html b/build/Safari.safariextension/notification.html similarity index 100% rename from Safari.safariextension/notification.html rename to build/Safari.safariextension/notification.html diff --git a/Opera/notification.png b/build/Safari.safariextension/notification.png similarity index 100% rename from Opera/notification.png rename to build/Safari.safariextension/notification.png diff --git a/build/chrome-bootstrap/.gitignore b/build/chrome-bootstrap/.gitignore new file mode 100644 index 0000000..2752eb9 --- /dev/null +++ b/build/chrome-bootstrap/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +.DS_Store diff --git a/build/chrome-bootstrap/LICENSE b/build/chrome-bootstrap/LICENSE new file mode 100644 index 0000000..20219d5 --- /dev/null +++ b/build/chrome-bootstrap/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2012 Chrome Bootstrap authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/build/chrome-bootstrap/README.md b/build/chrome-bootstrap/README.md new file mode 100644 index 0000000..0f6bfc1 --- /dev/null +++ b/build/chrome-bootstrap/README.md @@ -0,0 +1,32 @@ +Chrome UI bootstrap +================ + +[![NPM version](https://badge.fury.io/js/chrome-bootstrap.svg)](http://badge.fury.io/js/chrome-bootstrap) +[![Bower version](https://badge.fury.io/bo/chrome-bootstrap.svg)](http://badge.fury.io/bo/chrome-bootstrap) + +[Style guide](http://roykolak.github.com/chrome-bootstrap/) + +Chrome's UI stylings look pretty nice and are advancing quickly. In order to make top quanlity extensions, apps, and other one offs that look and feel like Chrome's system apps, these styles must be exposed for easy reuse. + +This project aims at accomplishing this goal while keeping the scope extremely simple. + +Scope +---------------- + +* Reusable selectors +* CSS animations +* I18n'ed properties +* Images encoded as strings + +And that's it. Let's keep it simple! + +Setup +--------------- + +Chrome bootstrap is packaged as a npm package so run the following to install dependencies + + $ npm install + +To compile css with each less change, use the start command + + $ npm start diff --git a/build/chrome-bootstrap/bower.json b/build/chrome-bootstrap/bower.json new file mode 100644 index 0000000..3bd1f4b --- /dev/null +++ b/build/chrome-bootstrap/bower.json @@ -0,0 +1,28 @@ +{ + "name": "chrome-bootstrap", + "version": "1.4.0", + "homepage": "https://github.com/roykolak/chrome-bootstrap", + "authors": [ + "Roy Kolak " + ], + "description": "Reusable Chrome style settings UI", + "main": "chrome-bootstrap.css", + "keywords": [ + "css", + "bootstrap", + "chrome", + "extensions", + "ui", + "interface", + "less", + "google" + ], + "license": "MIT", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ] +} diff --git a/build/chrome-bootstrap/chrome-bootstrap.css b/build/chrome-bootstrap/chrome-bootstrap.css new file mode 100644 index 0000000..3ae66d5 --- /dev/null +++ b/build/chrome-bootstrap/chrome-bootstrap.css @@ -0,0 +1,696 @@ +.chrome-bootstrap { + font-family: 'Segoe UI', 'Chrome Droid Sans', 'Droid Sans Fallback', 'Lucida Grande', 'Tahoma', sans-serif; + font-size: 12px; + color: #303942; + cursor: default; + margin: 0; + /* Headings + ============================================== */ + /* Layout + ============================================== */ + /* Header + ============================================== */ + /* View sections + ============================================== */ + /* Control bar + ============================================== */ + /* Pagination + ============================================== */ + /* Alert + ============================================== */ + /* Tags + ============================================== */ + /* Main menu + ============================================== */ + /* Icons + ============================================== */ + /* Highlightable list + ============================================== */ + /* Input styling + ============================================== */ + /* Focused --------------------------------- */ + /* Disabled --------------------------------- */ + /* Hovering --------------------------------- */ + /* Active --------------------------------- */ + /* Modal + ============================================== */ +} +.chrome-bootstrap a { + border: none; + color: #15C; + cursor: pointer; + text-decoration: underline; + font-weight: normal; +} +.chrome-bootstrap a:hover, +.chrome-bootstrap a:focus { + outline: none; +} +.chrome-bootstrap ul, +.chrome-bootstrap ol { + padding: 0; +} +.chrome-bootstrap li { + list-style-type: none; +} +.chrome-bootstrap dl, +.chrome-bootstrap dt, +.chrome-bootstrap dd { + margin: 0; +} +.chrome-bootstrap button { + cursor: pointer; +} +.chrome-bootstrap h1, +.chrome-bootstrap h2, +.chrome-bootstrap h3, +.chrome-bootstrap h4 { + -webkit-user-select: none; + font-weight: normal; + line-height: 1; +} +.chrome-bootstrap h1 small, +.chrome-bootstrap h2 small, +.chrome-bootstrap h3 small, +.chrome-bootstrap h4 small { + font-size: 15px; + margin: 0 10px; + color: #53637D; +} +.chrome-bootstrap h1 { + -webkit-margin-after: 1em; + -webkit-margin-before: 21px; + -webkit-margin-start: 23px; + height: 18px; + font-size: 18px; +} +.chrome-bootstrap h1 a { + color: #5C6166; + text-decoration: none; +} +.chrome-bootstrap h3 { + color: black; + font-size: 1.2em; + margin-bottom: 0.8em; +} +.chrome-bootstrap h4 { + font-size: 1em; + margin-bottom: 5px; +} +.chrome-bootstrap .frame .navigation { + height: 100%; + -webkit-margin-start: 0; + position: fixed; + -webkit-margin-end: 15px; + width: 155px; + z-index: 3; +} +.chrome-bootstrap .frame .view, +.chrome-bootstrap .frame .content { + width: 738px; + overflow-x: hidden; +} +.chrome-bootstrap .frame .content { + padding-top: 55px; +} +.chrome-bootstrap .frame .content p { + text-align: justify; +} +.chrome-bootstrap .frame .with_controls .content { + padding-top: 104px; +} +.chrome-bootstrap .frame .view { + -webkit-margin-start: 155px; +} +.chrome-bootstrap .frame .view a { + font: inherit; +} +.chrome-bootstrap .frame .mainview > * { + -webkit-margin-start: -20px; + -webkit-transition: margin 100ms, opacity 100ms; + opacity: 0; + z-index: 0; + position: absolute; + top: 0; + display: block; +} +.chrome-bootstrap .frame .mainview > .selected { + -webkit-margin-start: 0; + -webkit-transition: margin 200ms, opacity 200ms; + -webkit-transition-delay: 100ms; + z-index: 1; + opacity: 1; +} +.chrome-bootstrap header { + position: fixed; + background-image: -webkit-linear-gradient(#ffffff, #ffffff 40%, rgba(255, 255, 255, 0.92)); + width: 738px; + z-index: 2; +} +.chrome-bootstrap header h1 { + padding: 21px 0 13px; + margin: 0; + border-bottom: 1px solid #EEE; +} +.chrome-bootstrap header .corner { + position: absolute; + right: 0px; + top: 21px; +} +.chrome-bootstrap header .corner input[type="text"] { + width: 210px; +} +.chrome-bootstrap header .corner.cancelable .delete { + opacity: 1; + top: 4px; + right: 5px; +} +.chrome-bootstrap section { + -webkit-padding-start: 18px; + margin-bottom: 24px; + margin-top: 8px; + max-width: 600px; +} +.chrome-bootstrap section h3 { + -webkit-margin-start: -18px; +} +.chrome-bootstrap section .row { + display: block; + margin: 0.65em 0; +} +.chrome-bootstrap .controls { + -webkit-padding-end: 3px; + -webkit-padding-start: 4px; + -webkit-transition: padding 100ms, height 100ms, opacity 100ms; + border-bottom: 1px solid #EEE; + display: -webkit-box; + overflow: hidden; + padding: 13px 0; + position: relative; +} +.chrome-bootstrap .controls .text { + display: inline-block; + margin-top: 4px; +} +.chrome-bootstrap .controls .spacer { + -webkit-box-flex: 1; +} +.chrome-bootstrap ol.pagination li { + margin: 0 2px; + display: inline-block; + line-height: 25px; +} +.chrome-bootstrap ol.pagination a { + width: 25px; + height: 24px; + text-align: center; + display: block; + background: #F0F6FE; + text-decoration: none; +} +.chrome-bootstrap ol.pagination a:hover, +.chrome-bootstrap ol.pagination a.selected { + background: #8AAAED; + color: #FFF; +} +.chrome-bootstrap .alert { + border-radius: 3px; + background: rgba(147, 184, 252, 0.2); + display: block; + position: relative; + padding: 10px 30px 10px 10px; + line-height: 17px; +} +.chrome-bootstrap .alert .delete { + top: 5px; + right: 6px; + opacity: 1; +} +.chrome-bootstrap ul.tags li { + background: #8AAAED; + color: #FFF; + border-radius: 3px; + position: relative; + display: inline-block; + padding: 2px 5px; +} +.chrome-bootstrap ul.tags li a { + color: #FFF; + text-decoration: none; +} +.chrome-bootstrap ul.tags li a:hover { + text-decoration: underline; +} +.chrome-bootstrap ul.tags li .delete { + opacity: 1; + position: relative; + display: inline-block; + width: 13px; + height: 12px; + top: 1px; + background-position-y: -1px; +} +.chrome-bootstrap ul.menu { + -webkit-margin-before: 1em; + -webkit-margin-after: 2em; + -webkit-margin-start: 0px; + -webkit-margin-end: 0px; + -webkit-padding-start: 40px; + list-style-type: none; + padding: 0; +} +.chrome-bootstrap ul.menu li { + -webkit-border-start: 6px solid transparent; + -webkit-padding-start: 18px; + -webkit-user-select: none; + display: list-item; + text-align: -webkit-match-parent; +} +.chrome-bootstrap ul.menu li.selected { + -webkit-border-start-color: #4e5764; +} +.chrome-bootstrap ul.menu li.selected a { + color: #464E5A; +} +.chrome-bootstrap ul.menu li a { + border: 0; + color: #999; + cursor: pointer; + font: inherit; + line-height: 29px; + margin: 0; + padding: 0; + text-decoration: none; + display: block; +} +.chrome-bootstrap .arrow_collapse { + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-left: 6px solid #999; + -webkit-margin-end: 4px; + top: 1px; +} +.chrome-bootstrap .arrow_expand { + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 7px solid #999; + -webkit-margin-end: 4px; +} +.chrome-bootstrap .arrow { + width: 0; + height: 0; + position: relative; + display: inline-block; +} +.chrome-bootstrap .delete { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAiElEQVR42r2RsQrDMAxEBRdl8SDcX8lQPGg1GBI6lvz/h7QyRRXV0qUULwfvwZ1tenw5PxToRPWMC52eA9+WDnlh3HFQ/xBQl86NFYJqeGflkiogrOvVlIFhqURFVho3x1moGAa3deMs+LS30CAhBN5nNxeT5hbJ1zwmji2k+aF6NENIPf/hs54f0sZFUVAMigAAAABJRU5ErkJggg=="); + background-repeat: no-repeat; + display: block; + opacity: 0; + height: 14px; + width: 14px; + -webkit-transition: 150ms opacity; + background-color: transparent; + text-indent: -5000px; + position: absolute; +} +.chrome-bootstrap .delete:hover { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAqklEQVR4XqWRMQ6DMAxF/1Fyilyj2SmIBUG5QcTCyJA5Z8jGhlBPgRi4TmoDraVmKFJlWYrlp/g5QfwRlwEVNWVa4WzfH9jK6kCkEkBjwxOhLghheMWMELUAqqwQ4OCbnE4LJnhr5IYdqQt4DJQjhe9u4vBBmnxHHNzRFkDGjHDo0VuTAqy2vAG4NkvXXDHxbGsIGlj3e835VFNtdugma/Jk0eXq0lP//5svi4PtO01oFfYAAAAASUVORK5CYII="); +} +.chrome-bootstrap .highlightable li { + position: relative; + padding: 2px 0; +} +.chrome-bootstrap .highlightable li:hover > a:not(.action), +.chrome-bootstrap .highlightable li a:not(.action):focus { + background-color: #F0F6FE; + color: #555; +} +.chrome-bootstrap .highlightable li:hover > .action { + opacity: 0.7; +} +.chrome-bootstrap .highlightable li a { + padding: 5px; + display: block; + position: relative; + z-index: 0; + text-decoration: none; +} +.chrome-bootstrap .highlightable li dt { + font-size: 105%; + margin-bottom: 3px; +} +.chrome-bootstrap .highlightable li dd { + color: #999; + overflow: hidden; + white-space: nowrap; + font-size: 10px; + margin-top: 5px; +} +.chrome-bootstrap .highlightable li .tags { + float: left; + margin-top: -1px; + font-size: 12px; +} +.chrome-bootstrap .highlightable li .tags li:last-child { + margin-right: 5px; +} +.chrome-bootstrap .highlightable li .tags li:hover > a:not(.action) { + background: #8AAAED; + color: #FFF; +} +.chrome-bootstrap .highlightable li .tags li a { + padding: 0; +} +.chrome-bootstrap .highlightable li .action { + -webkit-appearance: none; + -webkit-transition: opacity 150ms; + background: #8AAAED; + border: none; + border-radius: 2px; + color: white; + opacity: 0; + margin-top: 0; + font-size: 10px; + padding: 1px 6px; + position: absolute; + top: 8px; + right: 32px; + -webkit-transition: 150ms opacity; + cursor: pointer; +} +.chrome-bootstrap .highlightable li .action:hover { + opacity: 1; +} +.chrome-bootstrap .highlightable li .highlightable { + -webkit-margin-start: 30px; +} +.chrome-bootstrap .highlightable.editable .delete { + position: absolute; + top: 7px; + right: 5px; +} +.chrome-bootstrap .highlightable.editable li:hover > .delete { + opacity: 1; +} +.chrome-bootstrap .highlightable.draggable .handle { + width: 8px; + height: 41px; + background-image: linear-gradient(to bottom, #c1c1c1 50%, rgba(255, 255, 255, 0) 0%); + background-position: center; + background-size: 100% 17%; + background-repeat: repeat-y; + visibility: hidden; + position: absolute; + top: 4px; + left: 2px; +} +.chrome-bootstrap .highlightable.draggable .handle:hover { + cursor: move; + cursor: -webkit-grab; + display: block; +} +.chrome-bootstrap .highlightable.draggable .handle:after { + margin-left: 3px; + width: 2px; + height: 41px; + background: #F0F6FE; + content: ""; + display: block; +} +.chrome-bootstrap .highlightable.draggable li:hover .handle { + visibility: visible; + z-index: 1; +} +.chrome-bootstrap .highlightable.draggable li > .item { + padding-left: 20px; +} +.chrome-bootstrap .match { + background: #f2f37b; + display: inline-block; + margin: 0 1px; +} +.chrome-bootstrap select, +.chrome-bootstrap input[type='checkbox'], +.chrome-bootstrap input[type='radio'], +.chrome-bootstrap input[type='button'], +.chrome-bootstrap button { + -webkit-appearance: none; + -webkit-user-select: none; + background-image: -webkit-linear-gradient(#ededed, #ededed 38%, #dedede); + border: 1px solid rgba(0, 0, 0, 0.25); + border-radius: 2px; + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08), inset 0 1px 2px rgba(255, 255, 255, 0.75); + color: #444; + font: inherit; + margin: 0 1px 0 0; + text-shadow: 0 1px 0 #F0F0F0; +} +.chrome-bootstrap button.small { + padding: 1px 5px 2px; + min-height: 1em; +} +.chrome-bootstrap input[type='checkbox']:checked::before { + -webkit-user-select: none; + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9wDBhYcG79aGIsAAACbSURBVBjTjdFBCkFhFAXgj4fp24PBy0SZ2ICRXRgYGb2xlKzBSEo2YgsiKWVoZgFKMjD5X/2Ux6lb99bpnNO5lKMR5i8MsEQHkhJiEzlS9HCqfiFWMUIt3AfsC3KKLCL30Qr7HfM4Ro4h6rhiEqmusIMKuphGqo+ogSPGcbYLzh91vdkXSHDDBk+0gxussS3rNcMCs+D6E18/9gLPPhbDshfzLgAAAABJRU5ErkJggg=="); + background-size: 100% 100%; + content: ''; + display: block; + height: 100%; + width: 100%; +} +.chrome-bootstrap html[dir='rtl'] input[type='checkbox']:checked::before { + -webkit-transform: scaleX(-1); +} +.chrome-bootstrap input[type='radio']:checked::before { + background-color: #666; + border-radius: 100%; + bottom: 3px; + content: ''; + display: block; + left: 3px; + position: absolute; + right: 3px; + top: 3px; +} +.chrome-bootstrap select { + -webkit-appearance: none; + -webkit-padding-end: 20px; + -webkit-padding-start: 6px; + /* OVERRIDE */ + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAWklEQVQokWNgoAOIAuI0PDiKaJMSgYCZmfkbkPkfHYPEQfJEG/b//3+FBQsWLGRjY/uJbBCIDxIHyRNtGDYDyTYI3UA+Pr4vFBmEbODbt2+bKDYIyUBWYtQBAIRzRP/XKJ//AAAAAElFTkSuQmCC), -webkit-linear-gradient(#ededed, #ededed 38%, #dedede); + background-position: right center; + background-repeat: no-repeat; +} +.chrome-bootstrap select { + min-height: 2em; + min-width: 4em; +} +.chrome-bootstrap html[dir='rtl'] select { + background-position: center left; +} +.chrome-bootstrap input[type='checkbox'] { + bottom: 2px; + height: 13px; + position: relative; + vertical-align: middle; + width: 13px; +} +.chrome-bootstrap input[type='radio'] { + /* OVERRIDE */ + border-radius: 100%; + bottom: 3px; + height: 15px; + position: relative; + vertical-align: middle; + width: 15px; +} +.chrome-bootstrap button { + -webkit-padding-end: 10px; + -webkit-padding-start: 10px; + min-height: 2em; + min-width: 4em; +} +.chrome-bootstrap input[type='text'], +.chrome-bootstrap input[type='number'], +.chrome-bootstrap input[type='search'] { + border: 1px solid #BFBFBF; + border-radius: 2px; + box-sizing: border-box; + color: #444; + font: inherit; + margin: 0; + min-height: 2em; + padding: 3px; + padding-bottom: 4px; +} +.chrome-bootstrap .radio, +.chrome-bootstrap .checkbox { + margin: 0.65em 0; +} +.chrome-bootstrap select:focus, +.chrome-bootstrap input[type='checkbox']:focus, +.chrome-bootstrap input[type='password']:focus, +.chrome-bootstrap input[type='radio']:focus, +.chrome-bootstrap input[type='search']:focus, +.chrome-bootstrap input[type='text']:focus, +.chrome-bootstrap input[type='number']:focus, +.chrome-bootstrap button:focus { + /* OVERRIDE */ + -webkit-transition: border-color 200ms; + /* We use border color because it follows the border radius (unlike outline). + * This is particularly noticeable on mac. */ + border-color: #4d90fe; + outline: none; +} +.chrome-bootstrap button:disabled, +.chrome-bootstrap select:disabled { + background-image: -webkit-linear-gradient(#f1f1f1, #f1f1f1 38%, #e6e6e6); + border-color: rgba(80, 80, 80, 0.2); + box-shadow: 0 1px 0 rgba(80, 80, 80, 0.08), inset 0 1px 2px rgba(255, 255, 255, 0.75); + color: #aaa; + cursor: default; +} +.chrome-bootstrap select:disabled { + /* OVERRIDE */ + background-image: -webkit-image-set(url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAAXNSR0IArs4c6QAAAAd0SU1FB9sLAxYEBKriBmwAAAAGYktHRAD/AP8A/6C9p5MAAAAJcEhZcwAACxMAAAsTAQCanBgAAABLSURBVCiRY2CgA4gC4jQ8OIpokxKBoKGh4T8uDJIn2rD///8rLFiwYCE2g0DiIHkSfIndQLIMwmYgRQYhG/j27dsmig1CMpCVGHUAo8FcsHfxfXQAAAAASUVORK5CYII=") 1x), -webkit-linear-gradient(#f1f1f1, #f1f1f1 38%, #e6e6e6); +} +.chrome-bootstrap input[type='checkbox']:disabled, +.chrome-bootstrap input[type='radio']:disabled { + opacity: .75; +} +.chrome-bootstrap input[type='search']:disabled, +.chrome-bootstrap input[type='number']:disabled, +.chrome-bootstrap input[type='text']:disabled { + color: #999; +} +.chrome-bootstrap select:hover:enabled, +.chrome-bootstrap input[type='checkbox']:hover:enabled, +.chrome-bootstrap input[type='radio']:hover:enabled, +.chrome-bootstrap button:hover:enabled { + background-image: -webkit-linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0); + border-color: rgba(0, 0, 0, 0.3); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.12), inset 0 1px 2px rgba(255, 255, 255, 0.95); + color: black; +} +.chrome-bootstrap select:hover:enabled { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAWklEQVQokWNgoAOIAuI0PDiKaJMSgYCZmfkbkPkfHYPEQfJEG/b//3+FBQsWLGRjY/uJbBCIDxIHyRNtGDYDyTYI3UA+Pr4vFBmEbODbt2+bKDYIyUBWYtQBAIRzRP/XKJ//AAAAAElFTkSuQmCC"), -webkit-linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0); +} +.chrome-bootstrap select:active:enabled, +.chrome-bootstrap input[type='checkbox']:active:enabled, +.chrome-bootstrap input[type='radio']:active:enabled, +.chrome-bootstrap button:active:enabled { + background-image: -webkit-linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7); + box-shadow: none; + text-shadow: none; +} +.chrome-bootstrap select:active:enabled { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAWklEQVQokWNgoAOIAuI0PDiKaJMSgYCZmfkbkPkfHYPEQfJEG/b//3+FBQsWLGRjY/uJbBCIDxIHyRNtGDYDyTYI3UA+Pr4vFBmEbODbt2+bKDYIyUBWYtQBAIRzRP/XKJ//AAAAAElFTkSuQmCC"), -webkit-linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7); +} +.chrome-bootstrap .overlay { + -webkit-box-align: center; + -webkit-box-orient: vertical; + -webkit-box-pack: center; + -webkit-transition: opacity .2s; + background-color: rgba(255, 255, 255, 0.75); + bottom: 0; + display: -webkit-box; + left: 0; + overflow: auto; + padding: 20px; + position: fixed; + right: 0; + top: 0; + z-index: 5; + opacity: 1; +} +.chrome-bootstrap .overlay.transparent { + opacity: 0; +} +.chrome-bootstrap .overlay.transparent .page { + -webkit-transform: scale(0.99) translateY(-20px); +} +.chrome-bootstrap .overlay .page { + -webkit-border-radius: 3px; + -webkit-box-orient: vertical; + -webkit-transition: 200ms -webkit-transform; + background: white; + box-shadow: 0 4px 23px 5px rgba(0, 0, 0, 0.2), 0 2px 6px rgba(0, 0, 0, 0.15); + color: #333; + display: -webkit-box; + min-width: 400px; + padding: 0; + position: relative; + overflow: hidden; +} +@-webkit-keyframes pulse { + 0% { + -webkit-transform: scale(1); + } + 40% { + -webkit-transform: scale(1.02); + } + 60% { + -webkit-transform: scale(1.02); + } + 100% { + -webkit-transform: scale(1); + } +} +.chrome-bootstrap .overlay .page.pulse { + -webkit-animation-duration: 180ms; + -webkit-animation-iteration-count: 1; + -webkit-animation-name: pulse; + -webkit-animation-timing-function: ease-in-out; +} +.chrome-bootstrap .overlay .page h1 { + -webkit-padding-end: 24px; + -webkit-user-select: none; + color: #333; + font-size: 120%; + margin: 0; + padding: 14px 17px 14px; + text-shadow: white 0 1px 2px; +} +.chrome-bootstrap .overlay .page ul li { + padding: 5px 0; +} +.chrome-bootstrap .overlay .page ul.tags li { + padding: 2px 5px; +} +.chrome-bootstrap .overlay .page .content-area { + -webkit-box-flex: 1; + overflow: auto; + padding: 6px 17px 6px; +} +.chrome-bootstrap .overlay .page .close-button { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAQAAAC1QeVaAAAAUklEQVR4XqXPYQrAIAhAYW/gXd8NJxTopVqsGEhtf+L9/ERU2k/HSMFQpKcYJeNFI9Be0LCMij8cYyjj5EHIivGBkwLfrbX3IF8PqumVmnDpEG+eDsKibPG2JwAAAABJRU5ErkJggg=='); + background-position: center; + background-repeat: no-repeat; + height: 14px; + position: absolute; + right: 7px; + top: 7px; + width: 14px; +} +.chrome-bootstrap .overlay .page .close-button:hover { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAQAAAC1QeVaAAAAnUlEQVR4XoWQQQ6CQAxFewjkJkMCyXgJPMk7AiYczyBeZEAX6AKctGIaN+bt+trk9wtGQc/IkhnoKGxqqiWxOSZalapWFZ6VrIUDExsN0a5JRBq9LoVOR0eEQMoEhKizXhhsn0p1sCWVo7CwOf1RytPL8CPvwuBUoHL6ugeK30CVD1TqK7V/hdpe+VNChhOzV8xWny/+xosHF8578W/Hmc1OOC3wmwAAAABJRU5ErkJggg=='); +} +.chrome-bootstrap .overlay .page .action-area { + -webkit-box-align: center; + -webkit-box-orient: horizontal; + -webkit-box-pack: end; + display: -webkit-box; + padding: 14px 17px; +} +.chrome-bootstrap .overlay .page .action-area-right { + display: -webkit-box; +} +.chrome-bootstrap .overlay .page .button-strip { + -webkit-box-orient: horizontal; + display: -webkit-box; +} +.chrome-bootstrap .overlay .page .button-strip button { + -webkit-margin-start: 10px; + display: block; +} diff --git a/build/chrome-bootstrap/chrome-bootstrap.less b/build/chrome-bootstrap/chrome-bootstrap.less new file mode 100644 index 0000000..83d5b49 --- /dev/null +++ b/build/chrome-bootstrap/chrome-bootstrap.less @@ -0,0 +1,783 @@ +.chrome-bootstrap { + font-family: 'Segoe UI', 'Chrome Droid Sans', 'Droid Sans Fallback', 'Lucida Grande', 'Tahoma', sans-serif; + font-size: 12px; + color: #303942; + cursor: default; + margin: 0; + + a { + border: none; + color: #15C; + cursor: pointer; + text-decoration: underline; + font-weight: normal; + + &:hover, &:focus { + outline: none; + } + } + ul, ol { + padding: 0; + } + li { + list-style-type:none; + } + dl, dt, dd { + margin:0; + } + button { + cursor: pointer; + } + + + /* Headings + ============================================== */ + h1, h2, h3, h4 { + -webkit-user-select: none; + font-weight: normal; + line-height: 1; + + small { + font-size:15px; + margin:0 10px; + color: #53637D; + } + } + h1 { + -webkit-margin-after: 1em; + -webkit-margin-before: 21px; + -webkit-margin-start: 23px; + height: 18px; + font-size: 18px; + + a { + color: #5C6166; + text-decoration: none; + } + } + h3 { + color: black; + font-size: 1.2em; + margin-bottom: 0.8em; + } + h4 { + font-size: 1em; + margin-bottom: 5px; + } + + + /* Layout + ============================================== */ + .frame { + .navigation { + height: 100%; + -webkit-margin-start: 0; + position: fixed; + -webkit-margin-end: 15px; + width: 155px; + z-index: 3; + } + .view, .content { + width: 738px; + overflow-x: hidden; + } + .content { + padding-top: 55px; + p { + text-align: justify; + } + } + .with_controls .content { + padding-top: 104px; + } + .view { + -webkit-margin-start: 155px; + + a { + font: inherit; + } + } + .mainview > * { + -webkit-margin-start: -20px; + -webkit-transition: margin 100ms, opacity 100ms; + opacity: 0; + z-index: 0; + position: absolute; + top: 0; + display: block; + } + .mainview > .selected { + -webkit-margin-start: 0; + -webkit-transition: margin 200ms, opacity 200ms; + -webkit-transition-delay: 100ms; + z-index: 1; + opacity: 1; + } + } + + + /* Header + ============================================== */ + header { + position: fixed; + background-image: -webkit-linear-gradient(#FFF, #FFF 40%, rgba(255, 255, 255, 0.92)); + width: 738px; + z-index: 2; + + h1 { + padding: 21px 0 13px; + margin: 0; + border-bottom: 1px solid #EEE; + } + + .corner { + position: absolute; + right: 0px; + top: 21px; + + input[type="text"] { + width: 210px; + } + &.cancelable { + .delete { + opacity: 1; + top: 4px; + right: 5px; + } + } + + } + } + + + /* View sections + ============================================== */ + section { + -webkit-padding-start: 18px; + margin-bottom: 24px; + margin-top: 8px; + max-width: 600px; + + h3 { + -webkit-margin-start: -18px; + } + .row { + display: block; + margin: 0.65em 0; + } + } + + + /* Control bar + ============================================== */ + .controls { + -webkit-padding-end: 3px; + -webkit-padding-start: 4px; + -webkit-transition: padding 100ms, height 100ms, opacity 100ms; + border-bottom: 1px solid #EEE; + display: -webkit-box; + overflow: hidden; + padding: 13px 0; + position: relative; + + .text { + display: inline-block; + margin-top: 4px; + } + + .spacer { + -webkit-box-flex: 1; + } + } + + /* Pagination + ============================================== */ + ol.pagination { + li { + margin: 0 2px; + display: inline-block; + line-height: 25px; + } + a { + width: 25px; + height: 24px; + text-align: center; + display: block; + background: #F0F6FE; + text-decoration: none; + + &:hover, &.selected { + background: #8AAAED; + color: #FFF; + } + } + } + + /* Alert + ============================================== */ + .alert { + border-radius: 3px; + background: rgba(147, 184, 252, 0.2); + display: block; + position: relative; + padding: 10px 30px 10px 10px; + line-height: 17px; + } + .alert .delete { + top: 5px; + right: 6px; + opacity: 1; + } + + /* Tags + ============================================== */ + ul.tags { + li { + background: #8AAAED; + color: #FFF; + border-radius: 3px; + position: relative; + display: inline-block; + padding: 2px 5px; + + a { + color: #FFF; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + .delete { + opacity: 1; + position: relative; + display: inline-block; + width: 13px; + height: 12px; + top: 1px; + background-position-y: -1px; + } + } + } + + + /* Main menu + ============================================== */ + ul.menu { + -webkit-margin-before: 1em; + -webkit-margin-after: 2em; + -webkit-margin-start: 0px; + -webkit-margin-end: 0px; + -webkit-padding-start: 40px; + + list-style-type: none; + padding: 0; + + li { + -webkit-border-start: 6px solid transparent; + -webkit-padding-start: 18px; + -webkit-user-select: none; + display: list-item; + text-align: -webkit-match-parent; + + &.selected { + -webkit-border-start-color: rgb(78, 87, 100); + + a { + color: #464E5A; + } + } + + a { + border: 0; + color: #999; + cursor: pointer; + font: inherit; + line-height: 29px; + margin: 0; + padding: 0; + text-decoration: none; + display: block; + } + } + } + + + /* Icons + ============================================== */ + .arrow_collapse { + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-left: 6px solid #999; + -webkit-margin-end: 4px; + top: 1px; + } + .arrow_expand { + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 7px solid #999; + -webkit-margin-end: 4px; + } + .arrow { + width: 0; + height: 0; + position: relative; + display: inline-block; + } + .delete { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAiElEQVR42r2RsQrDMAxEBRdl8SDcX8lQPGg1GBI6lvz/h7QyRRXV0qUULwfvwZ1tenw5PxToRPWMC52eA9+WDnlh3HFQ/xBQl86NFYJqeGflkiogrOvVlIFhqURFVho3x1moGAa3deMs+LS30CAhBN5nNxeT5hbJ1zwmji2k+aF6NENIPf/hs54f0sZFUVAMigAAAABJRU5ErkJggg=="); + background-repeat: no-repeat; + display: block; + opacity: 0; + height: 14px; + width: 14px; + -webkit-transition: 150ms opacity; + background-color: transparent; + text-indent: -5000px; + position: absolute; + + &:hover { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAqklEQVR4XqWRMQ6DMAxF/1Fyilyj2SmIBUG5QcTCyJA5Z8jGhlBPgRi4TmoDraVmKFJlWYrlp/g5QfwRlwEVNWVa4WzfH9jK6kCkEkBjwxOhLghheMWMELUAqqwQ4OCbnE4LJnhr5IYdqQt4DJQjhe9u4vBBmnxHHNzRFkDGjHDo0VuTAqy2vAG4NkvXXDHxbGsIGlj3e835VFNtdugma/Jk0eXq0lP//5svi4PtO01oFfYAAAAASUVORK5CYII="); + } + } + + + /* Highlightable list + ============================================== */ + .highlightable { + li { + position: relative; + padding: 2px 0; + + &:hover > a:not(.action), + a:not(.action):focus { + background-color: #F0F6FE; + color: #555; + } + &:hover > .action { + opacity: 0.7; + } + a { + padding: 5px; + display: block; + position: relative; + z-index: 0; + text-decoration: none; + } + dt { + font-size: 105%; + margin-bottom: 3px; + } + dd { + color: #999; + overflow: hidden; + white-space: nowrap; + font-size: 10px; + margin-top: 5px; + } + .tags { + float: left; + margin-top: -1px; + font-size: 12px; + + li { + &:last-child { + margin-right: 5px; + } + &:hover > a:not(.action) { + background: #8AAAED; + color: #FFF; + } + a { + padding: 0; + } + } + + } + .action { + -webkit-appearance: none; + -webkit-transition: opacity 150ms; + background: #8AAAED; + border: none; + border-radius: 2px; + color: white; + opacity: 0; + margin-top: 0; + font-size: 10px; + padding: 1px 6px; + position: absolute; + top: 8px; + right: 32px; + -webkit-transition: 150ms opacity; + cursor: pointer; + + &:hover { + opacity: 1; + } + } + .highlightable { + -webkit-margin-start: 30px; + } + } + + &.editable { + .delete { + position: absolute; + top: 7px; + right: 5px; + } + li:hover > .delete { + opacity: 1; + } + } + + &.draggable { + .handle { + width: 8px; + height: 41px; + background-image: linear-gradient(to bottom, #c1c1c1 50%, rgba(255, 255, 255, 0) 0%); + background-position: center; + background-size: 100% 17%; + background-repeat: repeat-y; + visibility: hidden; + position: absolute; + top: 4px; + left: 2px; + + &:hover { + cursor: move; + cursor: -webkit-grab; + display: block; + } + &:after { + margin-left: 3px; + width: 2px; + height: 41px; + background: #F0F6FE; + content: ""; + display: block; + } + } + li:hover .handle { + visibility: visible; + z-index: 1; + } + li > .item { + padding-left: 20px; + } + } + } + .match { + background: #f2f37b; + display: inline-block; + margin: 0 1px; + } + + /* Input styling + ============================================== */ + select, + input[type='checkbox'], + input[type='radio'], + input[type='button'], + button { + -webkit-appearance: none; + -webkit-user-select: none; + background-image: -webkit-linear-gradient(#EDEDED, #EDEDED 38%, #DEDEDE); + border: 1px solid rgba(0, 0, 0, 0.25); + border-radius: 2px; + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08), inset 0 1px 2px rgba(255, 255, 255, 0.75); + color: #444; + font: inherit; + margin: 0 1px 0 0; + text-shadow: 0 1px 0 #F0F0F0; + } + button.small { + padding: 1px 5px 2px; + min-height: 1em; + } + + input[type='checkbox']:checked::before { + -webkit-user-select: none; + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9wDBhYcG79aGIsAAACbSURBVBjTjdFBCkFhFAXgj4fp24PBy0SZ2ICRXRgYGb2xlKzBSEo2YgsiKWVoZgFKMjD5X/2Ux6lb99bpnNO5lKMR5i8MsEQHkhJiEzlS9HCqfiFWMUIt3AfsC3KKLCL30Qr7HfM4Ro4h6rhiEqmusIMKuphGqo+ogSPGcbYLzh91vdkXSHDDBk+0gxussS3rNcMCs+D6E18/9gLPPhbDshfzLgAAAABJRU5ErkJggg=="); + background-size: 100% 100%; + content: ''; + display: block; + height: 100%; + width: 100%; + } + html[dir='rtl'] input[type='checkbox']:checked::before { + -webkit-transform: scaleX(-1); + } + input[type='radio']:checked::before { + background-color: #666; + border-radius: 100%; + bottom: 3px; + content: ''; + display: block; + left: 3px; + position: absolute; + right: 3px; + top: 3px; + } + select { + -webkit-appearance: none; + -webkit-padding-end: 20px; + -webkit-padding-start: 6px; + /* OVERRIDE */ + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAWklEQVQokWNgoAOIAuI0PDiKaJMSgYCZmfkbkPkfHYPEQfJEG/b//3+FBQsWLGRjY/uJbBCIDxIHyRNtGDYDyTYI3UA+Pr4vFBmEbODbt2+bKDYIyUBWYtQBAIRzRP/XKJ//AAAAAElFTkSuQmCC), -webkit-linear-gradient(#ededed, #ededed 38%, #dedede); + background-position: right center; + background-repeat: no-repeat; + } + select { + min-height: 2em; + min-width: 4em; + } + + html[dir='rtl'] select { + background-position: center left; + } + + input[type='checkbox'] { + bottom: 2px; + height: 13px; + position: relative; + vertical-align: middle; + width: 13px; + } + + input[type='radio'] { + /* OVERRIDE */ + border-radius: 100%; + bottom: 3px; + height: 15px; + position: relative; + vertical-align: middle; + width: 15px; + } + button { + -webkit-padding-end: 10px; + -webkit-padding-start: 10px; + min-height: 2em; + min-width: 4em; + } + input[type='text'], + input[type='number'], + input[type='search'] { + border: 1px solid #BFBFBF; + border-radius: 2px; + box-sizing: border-box; + color: #444; + font: inherit; + margin: 0; + min-height: 2em; + padding: 3px; + padding-bottom: 4px; + } + .radio, .checkbox { + margin: 0.65em 0; + } + + /* Focused --------------------------------- */ + + select, + input[type='checkbox'], + input[type='password'], + input[type='radio'], + input[type='search'], + input[type='text'], + input[type='number'], + button { + &:focus { + /* OVERRIDE */ + -webkit-transition: border-color 200ms; + /* We use border color because it follows the border radius (unlike outline). + * This is particularly noticeable on mac. */ + border-color: rgb(77, 144, 254); + outline: none; + } + } + + /* Disabled --------------------------------- */ + + button, + select { + &:disabled { + background-image: -webkit-linear-gradient(#f1f1f1, #f1f1f1 38%, #e6e6e6); + border-color: rgba(80, 80, 80, 0.2); + box-shadow: 0 1px 0 rgba(80, 80, 80, 0.08), + inset 0 1px 2px rgba(255, 255, 255, 0.75); + color: #aaa; + cursor: default; + } + } + select:disabled { + /* OVERRIDE */ + background-image: -webkit-image-set(url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAAXNSR0IArs4c6QAAAAd0SU1FB9sLAxYEBKriBmwAAAAGYktHRAD/AP8A/6C9p5MAAAAJcEhZcwAACxMAAAsTAQCanBgAAABLSURBVCiRY2CgA4gC4jQ8OIpokxKBoKGh4T8uDJIn2rD///8rLFiwYCE2g0DiIHkSfIndQLIMwmYgRQYhG/j27dsmig1CMpCVGHUAo8FcsHfxfXQAAAAASUVORK5CYII=") 1x), -webkit-linear-gradient(#f1f1f1, #f1f1f1 38%, #e6e6e6); + } + input[type='checkbox'], + input[type='radio'] { + &:disabled { + opacity: .75; + } + } + input[type='search'], + input[type='number'], + input[type='text'] { + &:disabled { + color: #999; + } + } + + /* Hovering --------------------------------- */ + + select, + input[type='checkbox'], + input[type='radio'], + button { + &:hover:enabled { + background-image: -webkit-linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0); + border-color: rgba(0, 0, 0, 0.3); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.12), + inset 0 1px 2px rgba(255, 255, 255, 0.95); + color: black; + } + } + select:hover:enabled { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAWklEQVQokWNgoAOIAuI0PDiKaJMSgYCZmfkbkPkfHYPEQfJEG/b//3+FBQsWLGRjY/uJbBCIDxIHyRNtGDYDyTYI3UA+Pr4vFBmEbODbt2+bKDYIyUBWYtQBAIRzRP/XKJ//AAAAAElFTkSuQmCC"), -webkit-linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0); + } + + /* Active --------------------------------- */ + + select, + input[type='checkbox'], + input[type='radio'], + button { + &:active:enabled { + background-image: -webkit-linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7); + box-shadow: none; + text-shadow: none; + } + } + select:active:enabled { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAWklEQVQokWNgoAOIAuI0PDiKaJMSgYCZmfkbkPkfHYPEQfJEG/b//3+FBQsWLGRjY/uJbBCIDxIHyRNtGDYDyTYI3UA+Pr4vFBmEbODbt2+bKDYIyUBWYtQBAIRzRP/XKJ//AAAAAElFTkSuQmCC"), -webkit-linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7); + } + + + /* Modal + ============================================== */ + .overlay { + -webkit-box-align: center; + -webkit-box-orient: vertical; + -webkit-box-pack: center; + -webkit-transition: opacity .2s; + background-color: rgba(255, 255, 255, 0.75); + bottom: 0; + display: -webkit-box; + left: 0; + overflow: auto; + padding: 20px; + position: fixed; + right: 0; + top: 0; + z-index: 5; + opacity: 1; + + &.transparent { + opacity: 0; + + .page { + -webkit-transform: scale(0.99) translateY(-20px); + } + } + + .page { + -webkit-border-radius: 3px; + -webkit-box-orient: vertical; + -webkit-transition: 200ms -webkit-transform; + background: white; + box-shadow: 0 4px 23px 5px rgba(0, 0, 0, 0.2), 0 2px 6px rgba(0, 0, 0, 0.15); + color: #333; + display: -webkit-box; + min-width: 400px; + padding: 0; + position: relative; + overflow: hidden; + + @-webkit-keyframes pulse { + 0% { + -webkit-transform: scale(1); + } + 40% { + -webkit-transform: scale(1.02); + } + 60% { + -webkit-transform: scale(1.02); + } + 100% { + -webkit-transform: scale(1); + } + } + + &.pulse { + -webkit-animation-duration: 180ms; + -webkit-animation-iteration-count: 1; + -webkit-animation-name: pulse; + -webkit-animation-timing-function: ease-in-out; + } + + h1 { + -webkit-padding-end: 24px; + -webkit-user-select: none; + color: #333; + font-size: 120%; + margin: 0; + padding: 14px 17px 14px; + text-shadow: white 0 1px 2px; + } + ul li { + padding: 5px 0; + } + ul.tags li { + padding: 2px 5px; + } + .content-area { + -webkit-box-flex: 1; + overflow: auto; + padding: 6px 17px 6px; + } + .close-button { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAQAAAC1QeVaAAAAUklEQVR4XqXPYQrAIAhAYW/gXd8NJxTopVqsGEhtf+L9/ERU2k/HSMFQpKcYJeNFI9Be0LCMij8cYyjj5EHIivGBkwLfrbX3IF8PqumVmnDpEG+eDsKibPG2JwAAAABJRU5ErkJggg=='); + background-position: center; + background-repeat: no-repeat; + height: 14px; + position: absolute; + right: 7px; + top: 7px; + width: 14px; + + &:hover { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAQAAAC1QeVaAAAAnUlEQVR4XoWQQQ6CQAxFewjkJkMCyXgJPMk7AiYczyBeZEAX6AKctGIaN+bt+trk9wtGQc/IkhnoKGxqqiWxOSZalapWFZ6VrIUDExsN0a5JRBq9LoVOR0eEQMoEhKizXhhsn0p1sCWVo7CwOf1RytPL8CPvwuBUoHL6ugeK30CVD1TqK7V/hdpe+VNChhOzV8xWny/+xosHF8578W/Hmc1OOC3wmwAAAABJRU5ErkJggg=='); + } + } + .action-area { + -webkit-box-align: center; + -webkit-box-orient: horizontal; + -webkit-box-pack: end; + display: -webkit-box; + padding: 14px 17px; + } + .action-area-right { + display: -webkit-box; + } + .button-strip { + -webkit-box-orient: horizontal; + display: -webkit-box; + + button { + -webkit-margin-start: 10px; + display: block; + } + } + } + } +} diff --git a/build/chrome-bootstrap/chrome-bootstrap.min.css b/build/chrome-bootstrap/chrome-bootstrap.min.css new file mode 100644 index 0000000..52bd145 --- /dev/null +++ b/build/chrome-bootstrap/chrome-bootstrap.min.css @@ -0,0 +1 @@ +.chrome-bootstrap{font-family:'Segoe UI','Chrome Droid Sans','Droid Sans Fallback','Lucida Grande','Tahoma',sans-serif;font-size:12px;color:#303942;cursor:default;margin:0}.chrome-bootstrap a{border:none;color:#15c;cursor:pointer;text-decoration:underline;font-weight:normal}.chrome-bootstrap a:hover,.chrome-bootstrap a:focus{outline:none}.chrome-bootstrap ul,.chrome-bootstrap ol{padding:0}.chrome-bootstrap li{list-style-type:none}.chrome-bootstrap dl,.chrome-bootstrap dt,.chrome-bootstrap dd{margin:0}.chrome-bootstrap button{cursor:pointer}.chrome-bootstrap h1,.chrome-bootstrap h2,.chrome-bootstrap h3,.chrome-bootstrap h4{-webkit-user-select:none;font-weight:normal;line-height:1}.chrome-bootstrap h1 small,.chrome-bootstrap h2 small,.chrome-bootstrap h3 small,.chrome-bootstrap h4 small{font-size:15px;margin:0 10px;color:#53637d}.chrome-bootstrap h1{-webkit-margin-after:1em;-webkit-margin-before:21px;-webkit-margin-start:23px;height:18px;font-size:18px}.chrome-bootstrap h1 a{color:#5c6166;text-decoration:none}.chrome-bootstrap h3{color:#000;font-size:1.2em;margin-bottom:.8em}.chrome-bootstrap h4{font-size:1em;margin-bottom:5px}.chrome-bootstrap .frame .navigation{height:100%;-webkit-margin-start:0;position:fixed;-webkit-margin-end:15px;width:155px;z-index:3}.chrome-bootstrap .frame .view,.chrome-bootstrap .frame .content{width:738px;overflow-x:hidden}.chrome-bootstrap .frame .content{padding-top:55px}.chrome-bootstrap .frame .content p{text-align:justify}.chrome-bootstrap .frame .with_controls .content{padding-top:104px}.chrome-bootstrap .frame .view{-webkit-margin-start:155px}.chrome-bootstrap .frame .view a{font:inherit}.chrome-bootstrap .frame .mainview>*{-webkit-margin-start:-20px;-webkit-transition:margin 100ms,opacity 100ms;opacity:0;z-index:0;position:absolute;top:0;display:block}.chrome-bootstrap .frame .mainview>.selected{-webkit-margin-start:0;-webkit-transition:margin 200ms,opacity 200ms;-webkit-transition-delay:100ms;z-index:1;opacity:1}.chrome-bootstrap header{position:fixed;background-image:-webkit-linear-gradient(#fff,#fff 40%,rgba(255,255,255,.92));width:738px;z-index:2}.chrome-bootstrap header h1{padding:21px 0 13px;margin:0;border-bottom:1px solid #eee}.chrome-bootstrap header .corner{position:absolute;right:0;top:21px}.chrome-bootstrap header .corner input[type="text"]{width:210px}.chrome-bootstrap header .corner.cancelable .delete{opacity:1;top:4px;right:5px}.chrome-bootstrap section{-webkit-padding-start:18px;margin-bottom:24px;margin-top:8px;max-width:600px}.chrome-bootstrap section h3{-webkit-margin-start:-18px}.chrome-bootstrap section .row{display:block;margin:.65em 0}.chrome-bootstrap .controls{-webkit-padding-end:3px;-webkit-padding-start:4px;-webkit-transition:padding 100ms,height 100ms,opacity 100ms;border-bottom:1px solid #eee;display:-webkit-box;overflow:hidden;padding:13px 0;position:relative}.chrome-bootstrap .controls .text{display:inline-block;margin-top:4px}.chrome-bootstrap .controls .spacer{-webkit-box-flex:1}.chrome-bootstrap ol.pagination li{margin:0 2px;display:inline-block;line-height:25px}.chrome-bootstrap ol.pagination a{width:25px;height:24px;text-align:center;display:block;background:#f0f6fe;text-decoration:none}.chrome-bootstrap ol.pagination a:hover,.chrome-bootstrap ol.pagination a.selected{background:#8aaaed;color:#fff}.chrome-bootstrap .alert{border-radius:3px;background:rgba(147,184,252,.2);display:block;position:relative;padding:10px 30px 10px 10px;line-height:17px}.chrome-bootstrap .alert .delete{top:5px;right:6px;opacity:1}.chrome-bootstrap ul.tags li{background:#8aaaed;color:#fff;border-radius:3px;position:relative;display:inline-block;padding:2px 5px}.chrome-bootstrap ul.tags li a{color:#fff;text-decoration:none}.chrome-bootstrap ul.tags li a:hover{text-decoration:underline}.chrome-bootstrap ul.tags li .delete{opacity:1;position:relative;display:inline-block;width:13px;height:12px;top:1px;background-position-y:-1px}.chrome-bootstrap ul.menu{-webkit-margin-before:1em;-webkit-margin-after:2em;-webkit-margin-start:0;-webkit-margin-end:0;-webkit-padding-start:40px;list-style-type:none;padding:0}.chrome-bootstrap ul.menu li{-webkit-border-start:6px solid transparent;-webkit-padding-start:18px;-webkit-user-select:none;display:list-item;text-align:-webkit-match-parent}.chrome-bootstrap ul.menu li.selected{-webkit-border-start-color:#4e5764}.chrome-bootstrap ul.menu li.selected a{color:#464e5a}.chrome-bootstrap ul.menu li a{border:0;color:#999;cursor:pointer;font:inherit;line-height:29px;margin:0;padding:0;text-decoration:none;display:block}.chrome-bootstrap .arrow_collapse{border-top:5px solid transparent;border-bottom:5px solid transparent;border-left:6px solid #999;-webkit-margin-end:4px;top:1px}.chrome-bootstrap .arrow_expand{border-left:5px solid transparent;border-right:5px solid transparent;border-top:7px solid #999;-webkit-margin-end:4px}.chrome-bootstrap .arrow{width:0;height:0;position:relative;display:inline-block}.chrome-bootstrap .delete{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAiElEQVR42r2RsQrDMAxEBRdl8SDcX8lQPGg1GBI6lvz/h7QyRRXV0qUULwfvwZ1tenw5PxToRPWMC52eA9+WDnlh3HFQ/xBQl86NFYJqeGflkiogrOvVlIFhqURFVho3x1moGAa3deMs+LS30CAhBN5nNxeT5hbJ1zwmji2k+aF6NENIPf/hs54f0sZFUVAMigAAAABJRU5ErkJggg==");background-repeat:no-repeat;display:block;opacity:0;height:14px;width:14px;-webkit-transition:150ms opacity;background-color:transparent;text-indent:-5000px;position:absolute}.chrome-bootstrap .delete:hover{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAqklEQVR4XqWRMQ6DMAxF/1Fyilyj2SmIBUG5QcTCyJA5Z8jGhlBPgRi4TmoDraVmKFJlWYrlp/g5QfwRlwEVNWVa4WzfH9jK6kCkEkBjwxOhLghheMWMELUAqqwQ4OCbnE4LJnhr5IYdqQt4DJQjhe9u4vBBmnxHHNzRFkDGjHDo0VuTAqy2vAG4NkvXXDHxbGsIGlj3e835VFNtdugma/Jk0eXq0lP//5svi4PtO01oFfYAAAAASUVORK5CYII=")}.chrome-bootstrap .highlightable li{position:relative;padding:2px 0}.chrome-bootstrap .highlightable li:hover>a:not(.action),.chrome-bootstrap .highlightable li a:not(.action):focus{background-color:#f0f6fe;color:#555}.chrome-bootstrap .highlightable li:hover>.action{opacity:.7}.chrome-bootstrap .highlightable li a{padding:5px;display:block;position:relative;z-index:0;text-decoration:none}.chrome-bootstrap .highlightable li dt{font-size:105%;margin-bottom:3px}.chrome-bootstrap .highlightable li dd{color:#999;overflow:hidden;white-space:nowrap;font-size:10px;margin-top:5px}.chrome-bootstrap .highlightable li .tags{float:left;margin-top:-1px;font-size:12px}.chrome-bootstrap .highlightable li .tags li:last-child{margin-right:5px}.chrome-bootstrap .highlightable li .tags li:hover>a:not(.action){background:#8aaaed;color:#fff}.chrome-bootstrap .highlightable li .tags li a{padding:0}.chrome-bootstrap .highlightable li .action{-webkit-appearance:none;-webkit-transition:opacity 150ms;background:#8aaaed;border:none;border-radius:2px;color:#fff;opacity:0;margin-top:0;font-size:10px;padding:1px 6px;position:absolute;top:8px;right:32px;-webkit-transition:150ms opacity;cursor:pointer}.chrome-bootstrap .highlightable li .action:hover{opacity:1}.chrome-bootstrap .highlightable li .highlightable{-webkit-margin-start:30px}.chrome-bootstrap .highlightable.editable .delete{position:absolute;top:7px;right:5px}.chrome-bootstrap .highlightable.editable li:hover>.delete{opacity:1}.chrome-bootstrap .highlightable.draggable .handle{width:8px;height:41px;background-image:linear-gradient(to bottom,#c1c1c1 50%,rgba(255,255,255,0) 0%);background-position:center;background-size:100% 17%;background-repeat:repeat-y;visibility:hidden;position:absolute;top:4px;left:2px}.chrome-bootstrap .highlightable.draggable .handle:hover{cursor:move;cursor:-webkit-grab;display:block}.chrome-bootstrap .highlightable.draggable .handle:after{margin-left:3px;width:2px;height:41px;background:#f0f6fe;content:"";display:block}.chrome-bootstrap .highlightable.draggable li:hover .handle{visibility:visible;z-index:1}.chrome-bootstrap .highlightable.draggable li>.item{padding-left:20px}.chrome-bootstrap .match{background:#f2f37b;display:inline-block;margin:0 1px}.chrome-bootstrap select,.chrome-bootstrap input[type='checkbox'],.chrome-bootstrap input[type='radio'],.chrome-bootstrap input[type='button'],.chrome-bootstrap button{-webkit-appearance:none;-webkit-user-select:none;background-image:-webkit-linear-gradient(#ededed,#ededed 38%,#dedede);border:1px solid rgba(0,0,0,.25);border-radius:2px;box-shadow:0 1px 0 rgba(0,0,0,.08),inset 0 1px 2px rgba(255,255,255,.75);color:#444;font:inherit;margin:0 1px 0 0;text-shadow:0 1px 0 #f0f0f0}.chrome-bootstrap button.small{padding:1px 5px 2px;min-height:1em}.chrome-bootstrap input[type='checkbox']:checked::before{-webkit-user-select:none;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9wDBhYcG79aGIsAAACbSURBVBjTjdFBCkFhFAXgj4fp24PBy0SZ2ICRXRgYGb2xlKzBSEo2YgsiKWVoZgFKMjD5X/2Ux6lb99bpnNO5lKMR5i8MsEQHkhJiEzlS9HCqfiFWMUIt3AfsC3KKLCL30Qr7HfM4Ro4h6rhiEqmusIMKuphGqo+ogSPGcbYLzh91vdkXSHDDBk+0gxussS3rNcMCs+D6E18/9gLPPhbDshfzLgAAAABJRU5ErkJggg==");background-size:100% 100%;content:'';display:block;height:100%;width:100%}.chrome-bootstrap html[dir='rtl'] input[type='checkbox']:checked::before{-webkit-transform:scaleX(-1)}.chrome-bootstrap input[type='radio']:checked::before{background-color:#666;border-radius:100%;bottom:3px;content:'';display:block;left:3px;position:absolute;right:3px;top:3px}.chrome-bootstrap select{-webkit-appearance:none;-webkit-padding-end:20px;-webkit-padding-start:6px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAWklEQVQokWNgoAOIAuI0PDiKaJMSgYCZmfkbkPkfHYPEQfJEG/b//3+FBQsWLGRjY/uJbBCIDxIHyRNtGDYDyTYI3UA+Pr4vFBmEbODbt2+bKDYIyUBWYtQBAIRzRP/XKJ//AAAAAElFTkSuQmCC),-webkit-linear-gradient(#ededed,#ededed 38%,#dedede);background-position:right center;background-repeat:no-repeat}.chrome-bootstrap select{min-height:2em;min-width:4em}.chrome-bootstrap html[dir='rtl'] select{background-position:center left}.chrome-bootstrap input[type='checkbox']{bottom:2px;height:13px;position:relative;vertical-align:middle;width:13px}.chrome-bootstrap input[type='radio']{border-radius:100%;bottom:3px;height:15px;position:relative;vertical-align:middle;width:15px}.chrome-bootstrap button{-webkit-padding-end:10px;-webkit-padding-start:10px;min-height:2em;min-width:4em}.chrome-bootstrap input[type='text'],.chrome-bootstrap input[type='number'],.chrome-bootstrap input[type='search']{border:1px solid #bfbfbf;border-radius:2px;box-sizing:border-box;color:#444;font:inherit;margin:0;min-height:2em;padding:3px;padding-bottom:4px}.chrome-bootstrap .radio,.chrome-bootstrap .checkbox{margin:.65em 0}.chrome-bootstrap select:focus,.chrome-bootstrap input[type='checkbox']:focus,.chrome-bootstrap input[type='password']:focus,.chrome-bootstrap input[type='radio']:focus,.chrome-bootstrap input[type='search']:focus,.chrome-bootstrap input[type='text']:focus,.chrome-bootstrap input[type='number']:focus,.chrome-bootstrap button:focus{-webkit-transition:border-color 200ms;border-color:#4d90fe;outline:none}.chrome-bootstrap button:disabled,.chrome-bootstrap select:disabled{background-image:-webkit-linear-gradient(#f1f1f1,#f1f1f1 38%,#e6e6e6);border-color:rgba(80,80,80,.2);box-shadow:0 1px 0 rgba(80,80,80,.08),inset 0 1px 2px rgba(255,255,255,.75);color:#aaa;cursor:default}.chrome-bootstrap select:disabled{background-image:-webkit-image-set(url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAAXNSR0IArs4c6QAAAAd0SU1FB9sLAxYEBKriBmwAAAAGYktHRAD/AP8A/6C9p5MAAAAJcEhZcwAACxMAAAsTAQCanBgAAABLSURBVCiRY2CgA4gC4jQ8OIpokxKBoKGh4T8uDJIn2rD///8rLFiwYCE2g0DiIHkSfIndQLIMwmYgRQYhG/j27dsmig1CMpCVGHUAo8FcsHfxfXQAAAAASUVORK5CYII=") 1x),-webkit-linear-gradient(#f1f1f1,#f1f1f1 38%,#e6e6e6)}.chrome-bootstrap input[type='checkbox']:disabled,.chrome-bootstrap input[type='radio']:disabled{opacity:.75}.chrome-bootstrap input[type='search']:disabled,.chrome-bootstrap input[type='number']:disabled,.chrome-bootstrap input[type='text']:disabled{color:#999}.chrome-bootstrap select:hover:enabled,.chrome-bootstrap input[type='checkbox']:hover:enabled,.chrome-bootstrap input[type='radio']:hover:enabled,.chrome-bootstrap button:hover:enabled{background-image:-webkit-linear-gradient(#f0f0f0,#f0f0f0 38%,#e0e0e0);border-color:rgba(0,0,0,.3);box-shadow:0 1px 0 rgba(0,0,0,.12),inset 0 1px 2px rgba(255,255,255,.95);color:#000}.chrome-bootstrap select:hover:enabled{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAWklEQVQokWNgoAOIAuI0PDiKaJMSgYCZmfkbkPkfHYPEQfJEG/b//3+FBQsWLGRjY/uJbBCIDxIHyRNtGDYDyTYI3UA+Pr4vFBmEbODbt2+bKDYIyUBWYtQBAIRzRP/XKJ//AAAAAElFTkSuQmCC"),-webkit-linear-gradient(#f0f0f0,#f0f0f0 38%,#e0e0e0)}.chrome-bootstrap select:active:enabled,.chrome-bootstrap input[type='checkbox']:active:enabled,.chrome-bootstrap input[type='radio']:active:enabled,.chrome-bootstrap button:active:enabled{background-image:-webkit-linear-gradient(#e7e7e7,#e7e7e7 38%,#d7d7d7);box-shadow:none;text-shadow:none}.chrome-bootstrap select:active:enabled{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAWklEQVQokWNgoAOIAuI0PDiKaJMSgYCZmfkbkPkfHYPEQfJEG/b//3+FBQsWLGRjY/uJbBCIDxIHyRNtGDYDyTYI3UA+Pr4vFBmEbODbt2+bKDYIyUBWYtQBAIRzRP/XKJ//AAAAAElFTkSuQmCC"),-webkit-linear-gradient(#e7e7e7,#e7e7e7 38%,#d7d7d7)}.chrome-bootstrap .overlay{-webkit-box-align:center;-webkit-box-orient:vertical;-webkit-box-pack:center;-webkit-transition:opacity .2s;background-color:rgba(255,255,255,.75);bottom:0;display:-webkit-box;left:0;overflow:auto;padding:20px;position:fixed;right:0;top:0;z-index:5;opacity:1}.chrome-bootstrap .overlay.transparent{opacity:0}.chrome-bootstrap .overlay.transparent .page{-webkit-transform:scale(.99) translateY(-20px)}.chrome-bootstrap .overlay .page{-webkit-border-radius:3px;-webkit-box-orient:vertical;-webkit-transition:200ms -webkit-transform;background:#fff;box-shadow:0 4px 23px 5px rgba(0,0,0,.2),0 2px 6px rgba(0,0,0,.15);color:#333;display:-webkit-box;min-width:400px;padding:0;position:relative;overflow:hidden}@-webkit-keyframes pulse{0%{-webkit-transform:scale(1)}40%{-webkit-transform:scale(1.02)}60%{-webkit-transform:scale(1.02)}100%{-webkit-transform:scale(1)}}.chrome-bootstrap .overlay .page.pulse{-webkit-animation-duration:180ms;-webkit-animation-iteration-count:1;-webkit-animation-name:pulse;-webkit-animation-timing-function:ease-in-out}.chrome-bootstrap .overlay .page h1{-webkit-padding-end:24px;-webkit-user-select:none;color:#333;font-size:120%;margin:0;padding:14px 17px 14px;text-shadow:white 0 1px 2px}.chrome-bootstrap .overlay .page ul li{padding:5px 0}.chrome-bootstrap .overlay .page ul.tags li{padding:2px 5px}.chrome-bootstrap .overlay .page .content-area{-webkit-box-flex:1;overflow:auto;padding:6px 17px 6px}.chrome-bootstrap .overlay .page .close-button{background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAQAAAC1QeVaAAAAUklEQVR4XqXPYQrAIAhAYW/gXd8NJxTopVqsGEhtf+L9/ERU2k/HSMFQpKcYJeNFI9Be0LCMij8cYyjj5EHIivGBkwLfrbX3IF8PqumVmnDpEG+eDsKibPG2JwAAAABJRU5ErkJggg==');background-position:center;background-repeat:no-repeat;height:14px;position:absolute;right:7px;top:7px;width:14px}.chrome-bootstrap .overlay .page .close-button:hover{background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAQAAAC1QeVaAAAAnUlEQVR4XoWQQQ6CQAxFewjkJkMCyXgJPMk7AiYczyBeZEAX6AKctGIaN+bt+trk9wtGQc/IkhnoKGxqqiWxOSZalapWFZ6VrIUDExsN0a5JRBq9LoVOR0eEQMoEhKizXhhsn0p1sCWVo7CwOf1RytPL8CPvwuBUoHL6ugeK30CVD1TqK7V/hdpe+VNChhOzV8xWny/+xosHF8578W/Hmc1OOC3wmwAAAABJRU5ErkJggg==')}.chrome-bootstrap .overlay .page .action-area{-webkit-box-align:center;-webkit-box-orient:horizontal;-webkit-box-pack:end;display:-webkit-box;padding:14px 17px}.chrome-bootstrap .overlay .page .action-area-right{display:-webkit-box}.chrome-bootstrap .overlay .page .button-strip{-webkit-box-orient:horizontal;display:-webkit-box}.chrome-bootstrap .overlay .page .button-strip button{-webkit-margin-start:10px;display:block} \ No newline at end of file diff --git a/build/chrome-bootstrap/package.json b/build/chrome-bootstrap/package.json new file mode 100644 index 0000000..87b8d7f --- /dev/null +++ b/build/chrome-bootstrap/package.json @@ -0,0 +1,16 @@ +{ + "name": "chrome-bootstrap", + "version": "1.4.0", + "description": "Reusable Chrome settings UI", + "author": "Roy Kolak ", + "repository": { + "type": "git", + "url": "http://github.com/roykolak/chrome-bootstrap.git" + }, + "scripts": { + "start": "./node_modules/less/bin/lessc chrome-bootstrap.less > chrome-bootstrap.css" + }, + "devDependencies": { + "less": "1.3.1" + } +} diff --git a/conf/local_settings.json.example b/conf/local_settings.json.example new file mode 100644 index 0000000..5a36a51 --- /dev/null +++ b/conf/local_settings.json.example @@ -0,0 +1,57 @@ +/* + * Local settings + * This contains settings that are specific to an installation, + * Rename this file to 'local_settings.json' before use + * 'localsettings.json' is in .gitignore because it should never go in version control + */ +{ + + // set this to release to addons.mozilla.org: + amo_login_info: { + username: 'user@example.org', + password: 'password123' // optional - defaults to the environment variable 'AMO_PASSWORD' + }, + + // set this to release to the Chrome store: + chrome_login_info: { + + username: 'user@example.org', + password: 'password123', // optional - defaults to the environment variable 'CHROME_PASSWORD' + + // "ID" string in chrome://extensions/: + id: "abcdefghijklmnopqrstuvwxyz012345", + + /* + * Keys for the Chrome WebStore API. + * Follow the instructions here: https://developer.chrome.com/webstore/using_webstore_api + * + * Make sure to get an access token (code) manually once, to make sure it works. + * If you get a 401 error when you try to retrieve the token, go to APIs & auth > Consent screen + * in the Google Developers Console and fill in your email address and product name. + * + * Also make sure to enable the Web Store API by going to APIs & auth > APIs, + * browsing for "Chrome Web Store API" and changing the status to "ON" + */ + "client_id" : 'abcdefghijkl-mnopqrstuvwxyz0123456789ABCDEFGH.apps.googleusercontent.com', + "client_secret": "abcdefghijklm-nopqrstuvw", + + }, + + // set this to release to the Opera extensions site: + opera_login_info: { + username: 'user@example.org', + password: 'password123', // optional - defaults to the environment variable 'OPERA_PASSWORD' + tested_on: 'Opera 25 Developer Linux' // List of versions you've tested on (see opera://about for your version) + }, + + // set this to build a signed Safari extension: + safari_login_info: { + username: 'user@example.org', + password: 'password123' // optional - defaults to the environment variable 'OPERA_PASSWORD' + }, + + // Command to get the changelog for your latest version. + // At the time of writing, this was required by Opera and hadn't yet been implemented for other browsers: + changelog_command: [ 'git', 'log', '--pretty=* %s', '@{u}..HEAD', '--reverse', '--first-parent' ], + +} \ No newline at end of file diff --git a/conf/settings.json b/conf/settings.json new file mode 100644 index 0000000..911c205 --- /dev/null +++ b/conf/settings.json @@ -0,0 +1,154 @@ +/* + * BROWSER-NEUTRAL CONFIGURATION FILE + * (copied to browser-specific files during build) + */ +{ + // In Linux, you can make a GUID by running `uuidgen` on the command line: + "id": "abcdef01-2345-6789-9876-543210fedcba", + + // General config parameters + "name": "babelext_your_name_here", + "title": "BabelExt Your Name Here", + "short_title": "Name Here", // Currently only used in Google Chrome + "description": "An extension created with BabelExt - www.babelext.com", + "license": "GPL", + "author": "honestbleeps", + "version": "0.95", + "website": "http://babelext.com", + "icons": { + /* + 16: "icons/icon-16.png", + 32: "icons/icon-32.png", + 48: "icons/icon-48.png", + 64: "icons/icon-64.png", + 128: "icons/icon-128.png" + */ + }, + + "long_description": + "This extension uses the BabelExt cross-browser development framework.\n" + + "Go to http://www.babelext.com for more information.", + + // userscript config parameters: + + /* + * contentScriptWhen can be 'early', 'middle' or 'late'. + * different browsers interpret this in different ways, but in general: + * * 'early' runs at the earliest point supported by the browser (possibly before the DOM exists) + * * 'middle' guarantees the DOM exists, but might run while the page is still loading + * * 'late' guarantees the scripts are run aft the page finishes loading + */ + "contentScriptWhen": "middle", + + "contentScriptFiles": [ "src/extension.js" ], + "contentStyleFiles": [ "src/extension.css" ], + "resources": [ "res/extension.html" ], + "match_domains": [ "babelext.com" ], // or *.babelext.com to include subdomains + // whether to match https://: + "match_secure_domain": true, + + "xhr_patterns": [ + // allowed URL patterns for cross-site/mixed content XMLHttpRequests + // See https://developer.chrome.com/extensions/match_patterns + "http://www.w3.org/*" + ], + + "maintenanceInterval": 5 * 1000, // the 'maintain' command will pause for this long between updates + + "environment_specific": { + /* + * If you set the "ENVIRONMENT" environment variable, + * variables from the relevant block will be used: + */ + "development": { + + "contentScriptFiles": [], + + /* + * Reloads unpacked extensions as frequently as possible. Currently only supports + * Chrome/Opera, which can be forced to reload extensions so long as we avoid their + * "fast reload" protection. Note that Firefox reloads unpacked extensions automatically. + * + * Valid values: + * + 'timeout' - reload as frequently as possible without triggering fast reloads + */ + "autoReload": 'timeout', + + }, + + "test": { + "contentScriptFiles": [ "src/test-helper.js" ], + }, + + "production": { + "match_domain": "babelext.com", + } + + }, + + "preferences": [ + /* + * Preferences that users can set in the browser's preferences page. + * The layout is based on Mozilla's "simple preferences" interface: + * https://developer.mozilla.org/en-US/Add-ons/SDK/High-Level_APIs/simple-prefs + * currently supported types: bool, boolint, integer, string, menulist, radio + */ + { + "name": "myBool", + "type": "bool", + "title": "myBool preference title", + "description": "myBool short description for the preference", + "value": false, // default value - true or false + }, + { + "name": "myBoolint", + "type": "boolint", + "title": "myBoolint preference title", + "description": "myBoolint short description for the preference", + "off": "1", + "on": "2", + "value": 1, // default value - 0 or 1 + }, + { + "name": "myInteger", + "type": "integer", + "title": "myInteger preference title", + "description": "myInteger short description for the preference", + "value": 2, // default value - integer number + }, + { + "name": "myString", + "type": "string", + "title": "myString preference title", + "description": "myString short description for the preference", + "value": "this is the default string value", + }, + { + "name": "myMenulist", + "type": "menulist", + "title": "myMenulist preference title", + "value": 0, + "options": [ + { "value": "0", "label": "first label" }, + { "value": "1", "label": "second label" }, + { "value": "2", "label": "third label" }, + ] + }, + { + "name": "myRadio", + "type": "radio", + "title": "myRadioTitle", + "value": "a", + "options": [ + { "value": "a", "label": "first label" }, + { "value": "b", "label": "second label" }, + { "value": "c", "label": "third label" }, + ] + }, + ], + + "firefox_max_version": '32.*', + + "safari_team_id": 'ABCDEFGHIJ' // visible at https://developer.apple.com/membercenter/index.action#accountSummary + +} diff --git a/icons/README.txt b/icons/README.txt new file mode 100644 index 0000000..17f51db --- /dev/null +++ b/icons/README.txt @@ -0,0 +1,3 @@ +Put icon files in this directory. + +Ideally, you want a 16x16, 32x32, 48x48, 64x64 and 128x128 icon. diff --git a/lib/BabelExt.js b/lib/BabelExt.js index bce9116..c190d85 100644 --- a/lib/BabelExt.js +++ b/lib/BabelExt.js @@ -5,6 +5,32 @@ var Global = typeof window !== 'undefined' ? window : this; var unsafeGlobal = typeof unsafeWindow !=='undefined' ? unsafeWindow : (Global||this); +/* + * You can't log to the normal console from within a content script. + * Console logging is extremely useful during development, + * so this script makes console logging work as expected. + */ +var console = (function() { + + function log_in_embedded_page(command, args) { + BabelExt.utils.runInEmbeddedPage('console.' + command + '.apply( console, ' + JSON.stringify(Array.prototype.slice.call( args, 0 )) + ')'); + } + + return { + + assert: function() { return log_in_embedded_page( 'assert', arguments ) }, + + log : function() { return log_in_embedded_page( 'log' , arguments ) }, + + trace : function() { return log_in_embedded_page( 'trace' , arguments ) }, + info : function() { return log_in_embedded_page( 'info' , arguments ) }, + warn : function() { return log_in_embedded_page( 'warn' , arguments ) }, + error : function() { return log_in_embedded_page( 'error' , arguments ) } + + }; + +})(); + var BabelExt = (function(Global, unsafeGlobal) { 'use strict'; // define private variables here... @@ -16,8 +42,16 @@ var BabelExt = (function(Global, unsafeGlobal) { var console = Global.console ? Global.console:{log:function(){}}; var document = Global.document; var chrome = Global.chrome; - var opera = Global.opera; var safari = Global.safari; + var delayedMessagePort; + var entityMap = { + "&": "&", + "<": "<", + ">": ">", + '"': '"', + "'": ''', + "/": '/' + }; if (typeof(self.on) !== 'undefined') { // firefox addon SDK @@ -35,6 +69,8 @@ var BabelExt = (function(Global, unsafeGlobal) { instance.callbackQueue.callbacks[msgEvent.callbackID](msgEvent.response); break; case 'localStorage': + case 'memoryStorage': + case 'preferences': instance.callbackQueue.callbacks[msgEvent.callbackID](msgEvent); break; case 'createTab': @@ -66,43 +102,19 @@ var BabelExt = (function(Global, unsafeGlobal) { } } ); - } else if (typeof(opera) !== 'undefined') { - // opera - instance.detectedBrowser = 'Opera'; - /* - * Opera compatibility hacks - * - * Note: it's not recommended you use the localStorage function since that can easily be deleted - * when the user clears history, etc. localStorage on its own is tied to the domain name - * of the site and is more likely to be cleared. You should instead use the BabelExt.storage API. - * - */ - var localStorage = Global.localStorage; - var location = Global.location; - var XMLHttpRequest = Global.XMLHttpRequest; - - // console.log = opera.postError; - // listen for messages from opera's background page... - // This is the message handler for Opera - the background page calls this function with return data... - instance.operaMessageHandler = function(msgEvent) { - var eventData = msgEvent.data; - switch (eventData.msgType) { - case 'xmlhttpRequest': - // Fire the appropriate onload function for this xmlhttprequest. - instance.callbackQueue.callbacks[eventData.callbackID](eventData.data); - break; - case 'localStorage': - instance.callbackQueue.callbacks[eventData.callbackID](eventData); - break; - case 'createTab': - // we don't need to do anything here, but we could if we wanted to add something... - break; - default: - console.log('unknown event type in operaMessageHandler'); - break; - } - }; - opera.extension.addEventListener( "message", instance.operaMessageHandler, false); + delayedMessagePort = chrome.runtime.connect({name: "delayedMessage"}); + delayedMessagePort.onMessage.addListener(function(message) { + switch(message.request.requestType) { + case "preferences": + if (message.request.callbackID) { + callbackQueue.callbacks[message.request.callbackID](message.response); + } + break; + default: + // sendResponse({status: "unrecognized request type"}); + break; + } + }); } else if (typeof(safari) !== 'undefined') { // safari instance.detectedBrowser = 'Safari'; @@ -114,6 +126,8 @@ var BabelExt = (function(Global, unsafeGlobal) { instance.callbackQueue.callbacks[msgEvent.message.callbackID](msgEvent.message); break; case 'localStorage': + case 'memoryStorage': + case 'preferences': instance.callbackQueue.callbacks[msgEvent.message.callbackID](msgEvent.message); break; case 'createTab': @@ -134,10 +148,10 @@ var BabelExt = (function(Global, unsafeGlobal) { /* * callbackQueue is a queue of callbacks to be executed on return data from the background page. - * This is necessary due to the double-layered asynchronous calls we're making. Calls to + * This is necessary due to the double-layered asynchronous calls we're making. Calls to * background pages are asynchronous as it is, so when we make an asynchronous call from the * foreground page to the background, we need to hold on to a reference to our callback that lives - * in the context of the foreground, because Opera, Safari and Firefox do not allow you to pass + * in the context of the foreground, because Safari and Firefox do not allow you to pass * a foreground callback function to the background page. * * Note that this is not necessary in Chrome, because Chrome allows you to pass @@ -149,7 +163,7 @@ var BabelExt = (function(Global, unsafeGlobal) { createId: function(prefix) { var rnd = Math.floor((Math.random()*65535)+1); if (prefix) rnd = prefix+rnd; - + if (this.callbacks.hasOwnProperty(rnd)) { return this.createId(prefix); } @@ -179,7 +193,7 @@ var BabelExt = (function(Global, unsafeGlobal) { * key: [key of item], * value: [value of item, if relevant] * } - */ + */ switch(instance.detectedBrowser) { case 'Firefox': instance.bgMessage = function(thisJSON, callback) { @@ -196,15 +210,14 @@ var BabelExt = (function(Global, unsafeGlobal) { } else { callback = function() {}; } - chrome.runtime.sendMessage(thisJSON, callback); - }; - break; - case 'Opera': - instance.bgMessage = function(thisJSON, callback) { - if (typeof(callback) === 'function') { - thisJSON.callbackID = callbackQueue.add(callback); + switch ( thisJSON.requestType ) { + case 'preferences': + delayedMessagePort.postMessage(thisJSON); + break; + default: + chrome.runtime.sendMessage(thisJSON, callback); + break; } - opera.extension.postMessage(JSON.stringify(thisJSON)); }; break; case 'Safari': @@ -221,6 +234,7 @@ var BabelExt = (function(Global, unsafeGlobal) { } /* + * XHR functionality based on the GM_xmlhttpRequest API * check the xmlHttpRequest function to see if it's cross domain, and pass to * the background page if appropriate. If it's not, run as a normal xmlHttpRequest. * xmlhttpRequest = function(obj) { @@ -228,6 +242,8 @@ var BabelExt = (function(Global, unsafeGlobal) { instance.BabelXHR = function(obj) { var crossDomain = (location) ? (obj.url.indexOf(location.hostname) === -1) : true; if (crossDomain) { + if ( obj.url.search(BabelExt._xhr_regexp) == -1 ) + throw "Please add '" + url + "' to xhr_patterns in your extension settings"; obj.requestType = 'xmlhttpRequest'; if (typeof(obj.onload) === 'undefined') { obj.onload = function() {}; // anon function that does nothing, since none was specified... @@ -264,7 +280,7 @@ var BabelExt = (function(Global, unsafeGlobal) { }; /* - * tabs - abstracted functions for creating tabs, choosing if focused, and when applicable + * tabs - abstracted functions for creating tabs, choosing if focused, and when applicable * assigning them an index (not supported in Firefox or Safari) */ instance.browserTabs = { @@ -281,7 +297,7 @@ var BabelExt = (function(Global, unsafeGlobal) { /* * notifications - abstracted functions for creating notifications, though they are not natively - * supported in all browsers, so they are "faked" in Safari and Opera + * supported in all browsers, so they are "faked" in Safari * */ instance.browserNotification = { @@ -306,15 +322,6 @@ var BabelExt = (function(Global, unsafeGlobal) { }; instance.bgMessage(thisJSON); break; - case 'Opera': - thisJSON = { - requestType: 'createNotification', - icon: icon, - title: title, - text: text - }; - instance.bgMessage(thisJSON); - break; case 'Safari': thisJSON = { requestType: 'createNotification', @@ -359,6 +366,60 @@ var BabelExt = (function(Global, unsafeGlobal) { } }; + /* + * memoryStorage - abstracted functions for get/set/remove... + */ + instance.memoryStorage = { + get: function(key, callback) { + var thisJSON = { + requestType: 'memoryStorage', + operation: 'getItem', + itemName: key + }; + instance.bgMessage(thisJSON, callback); + }, + set: function(key, value, callback) { + var thisJSON = { + requestType: 'memoryStorage', + operation: 'setItem', + itemName: key, + itemValue: value + }; + instance.bgMessage(thisJSON, callback); + }, + remove: function(key, callback) { + var thisJSON = { + requestType: 'memoryStorage', + operation: 'removeItem', + itemName: key + }; + instance.bgMessage(thisJSON, callback); + } + }; + + /* + * browserPreferences - abstracted functions for get/set/remove based on browser... + */ + instance.browserPreferences = { + get: function(key, callback) { + var thisJSON = { + requestType: 'preferences', + operation: 'getItem', + itemName: key + }; + instance.bgMessage(thisJSON, callback); + }, + set: function(key, value, callback) { + var thisJSON = { + requestType: 'preferences', + operation: 'setItem', + itemName: key, + itemValue: value + }; + instance.bgMessage(thisJSON, callback); + } + }; + /* * browserHistory - abstracted function for adding a URL to history * note: Only "add" is supported, because only Chrome supports any @@ -391,7 +452,6 @@ var BabelExt = (function(Global, unsafeGlobal) { instance.bgMessage(thisJSON); callback(url); break; - case 'Opera': case 'Safari': instance.callbackQueue.callbacks[instance.callbackQueue.count] = callback; if (!instance.browserHistory.initialized) { @@ -430,7 +490,7 @@ var BabelExt = (function(Global, unsafeGlobal) { /** * Creates a context menu with the provided parameters, and calls the * provided callback function upon click - * + * * @param {Object} obj An object that specifies type, id, title and "checked" - optional default boolean * @param {Function} callback [description] * @return {[type]} [description] @@ -468,8 +528,6 @@ var BabelExt = (function(Global, unsafeGlobal) { }; instance.bgMessage(thisJSON); // todo: add callback support - break; - case 'Opera': break; case 'Safari': // send a message to the background page to create this context menu @@ -488,9 +546,73 @@ var BabelExt = (function(Global, unsafeGlobal) { break; } + }, + + /** + * Removes a context menu matching the given id + * + * @param {Object} obj An object that specifies type, id, title and "checked" - optional default boolean + * @param {Function} callback [description] + * @return {[type]} [description] + */ + remove: function(obj, callback) { + var thisJSON, callbackID; + + switch (instance.detectedBrowser) { + case 'Chrome': + // send a message to the background page to remove this context menu + + thisJSON = { + requestType: 'contextMenus.remove', + obj: obj, + callback: callback + }; + instance.bgMessage(thisJSON); // todo: add callback support + + break; + case 'Firefox': + // send a message to the background page to create this context menu + if (typeof obj.onclick === 'function') { + callbackID = callbackQueue.add(obj.onclick); + obj.onclick = callbackID; + } + + thisJSON = { + requestType: 'contextMenus.remove', + obj: obj, + callback: callback + }; + instance.bgMessage(thisJSON); // todo: add callback support + + break; + case 'Opera': + break; + case 'Safari': + // send a message to the background page to create this context menu + if (typeof obj.onclick === 'function') { + callbackID = callbackQueue.add(obj.onclick); + obj.onclick = callbackID; + } + + thisJSON = { + requestType: 'contextMenus.remove', + obj: obj, + callback: callback + }; + instance.bgMessage(thisJSON); // todo: add callback support + + break; + } + } + }; + instance.params = {}; + location.search.substr(1).replace( /([^&=]*)=([^&]*)/g, function(match, key, value) { + instance.params[ decodeURIComponent(key) ] = decodeURIComponent(value); + }); + return { // public interface /* * utility functions - useful functions used by BabelExt to perform certain operations... @@ -499,16 +621,166 @@ var BabelExt = (function(Global, unsafeGlobal) { utils: { /* * merge: merges two objects (useful for creating defaults for functions that take a data object as a parameter) - * - + * - * */ merge: function(objA, objB) { - for (var key in objA) { + for (var key in objA) { if (key in objB) { continue; } objB[key] = objA[key]; } return objB; + }, + /* + * Escape HTML characters in a string + */ + escapeHTML: function(text) { + return String(text).replace(/[&<>"'\/]/g, function (c) { + return entityMap[c]; + }); + }, + /* For security reasons, some scripts need to be run in the context of the embedded page + * NOTE: do not use this to inject run content loaded remotely - + * the reviewers at addons.mozilla.org will reject your code because they can't check said code. + */ + runInEmbeddedPage: function(content) { + var script = document.createElement('script'); + script.textContent = '(function() {' + content + '})();'; + document.documentElement.appendChild(script); + // remove the script so developers don't see thousands of redundant tags cluttering the DOM: + setTimeout(function() { document.documentElement.removeChild(script) }, 0 ); + }, + params: instance.params, + + /* + * If your plugin needs to perform different actions on different pages, + * You can use this to dispatch page-specific actions. + */ + dispatch: function() { + + var pathname = location.pathname, + params = instance.params, + handlers = Array.prototype.slice.call( arguments, 0 ), + stash = {}; + + // check whether a string meets the specified test(s) + function check_match( string, tests ) { + if ( tests instanceof Array ) { + for ( var n=0; n!=tests.length; ++n ) + if ( check_match(string, tests[n]) ) return true; + } else if ( typeof(tests) == 'string' ) { + return string == tests; + } else if ( typeof(tests) == "number" ) { + return string == (""+tests); + } else if ( tests instanceof RegExp ) { + return tests.test(string); + } else { + throw "Only arrays, strings, numbers, RegExps and booleans are allowed in match_*"; + } + } + + function next_handler() { // check and execute the next handler + + if ( !handlers.length ) return; + var handler = handlers.shift(); + + // Check match_* to see if this handler should be executed: + if ( handler.match_pathname && !check_match(pathname, handler.match_pathname) ) + return next_handler(); + if ( handler.match_params ) { + for ( var param in handler.match_params ) + if ( handler.match_params.hasOwnProperty(param) ) + if ( + typeof(handler.match_params[param]) == 'boolean' + ? params.hasOwnProperty(param) != handler.match_params[param] + : !( params.hasOwnProperty(param) && check_match( params[param], handler.match_params[param] ) ) + ) + return next_handler(); + } + + // handler should be executed, retrieve arguments + var args = [ stash, pathname, params ]; + next_handler_elements(); + + // match, wait for and retrieve elements: + function next_handler_elements() { + if ( !handler.hasOwnProperty('match_elements') ) return next_handler_storage(); + if ( !( handler.match_elements instanceof Array ) ) handler.match_elements = [ handler.match_elements ]; + + var next_match = 0, observer; + + if ( typeof( MutationObserver) != 'undefined' ) observer = new MutationObserver(observe_mutation); + else if ( typeof(WebKitMutationObserver) != 'undefined' ) observer = new WebKitMutationObserver(observe_mutation); + else throw 'ERROR: This browser does not have a MutationObserver - cannot wait for elements to exist'; + + function observe_mutation() { + while ( next_match != handler.match_elements.length ) { + var element = document.querySelector(handler.match_elements[next_match]); + if ( !element ) return; + args.push( element ); + ++next_match; + } + observer.disconnect(); + return next_handler_storage(); + } + observe_mutation(); + if ( next_match != handler.match_elements.length ) { + observer.observe(document, { childList: true, subtree: true }); + } + + } + + function next_handler_storage() { + function get(data) { + args.push(data ? data.value : undefined); + if ( handler.pass_storage.length ) + BabelExt.storage.get(handler.pass_storage.shift(), get ); + else + next_handler_preferences(); + } + // retrieve arguments from storage: + if ( handler.hasOwnProperty('pass_storage') ) { + if ( !( handler.pass_storage instanceof Array ) ) handler.pass_storage = [ handler.pass_storage ]; + BabelExt.storage.get(handler.pass_storage.shift(), get ); + } else { + next_handler_preferences(); + } + } + + // retrieve arguments from preferences: + function next_handler_preferences() { + function get(data) { + args.push(data ? data.value : undefined); + if ( handler.pass_preferences.length ) + BabelExt.preferences.get(handler.pass_preferences.shift(), get ); + else + next_handler_handle(); + } + if ( handler.hasOwnProperty('pass_preferences') ) { + if ( !( handler.pass_preferences instanceof Array ) ) handler.pass_preferences = [ handler.pass_preferences ]; + BabelExt.preferences.get(handler.pass_preferences.shift(), get ); + } else { + next_handler_handle(); + } + } + + // execute handler: + function next_handler_handle() { + try { + if ( handler.callback.apply( document, args ) !== false ) + return next_handler(); + } catch (error) { + console.error( 'handler failed', handler, error.toString() ); + throw error; + }; + } + + } + + next_handler(); + } + }, /* @@ -520,7 +792,7 @@ var BabelExt = (function(Global, unsafeGlobal) { * - url - URL to open tab to * - focused - boolean, true = focused, false = background * - index - index to open tab at (i.e. 0th tab? nth tab?) - * note: index is not supported in Firefox or Opera, so it will be ignored + * note: index is not supported in Firefox, so it will be ignored */ create: function(url, focused, index) { instance.browserTabs.create(url, focused, index); @@ -557,7 +829,32 @@ var BabelExt = (function(Global, unsafeGlobal) { /* - * storage functions - handles opening and closing tabs, choosing if they're focused, etc. + * resources functions + */ + resources: { + /* + * resources.get - returns resource for [key] + */ + get: function(key) { + if ( !BabelExt.resources._resources.hasOwnProperty(key) ) throw "No such resource: " + key; + return BabelExt.resources._resources[key]; + }, + /* + * resources.set - sets resource for [key] to [value] + */ + set: function(key, value) { + BabelExt.resources._resources[key] = value; + }, + /* + * resources.remove - deletes resource item at [key] + */ + remove: function(key) { + delete BabelExt.resources._resources[key]; + } + }, + + /* + * storage functions */ storage: { /* @@ -565,8 +862,7 @@ var BabelExt = (function(Global, unsafeGlobal) { */ get: function(key, callback) { if (typeof(callback) !== 'function') { - console.log('ERROR: no callback provided for BabelExt.storage.get()'); - return false; + throw 'ERROR: no callback provided for BabelExt.storage.get()'; } instance.browserStorage.get(key, callback); }, @@ -574,21 +870,61 @@ var BabelExt = (function(Global, unsafeGlobal) { * storage.set - sets storage for [key] to [value], calls callback with key, value as parameters */ set: function(key, value, callback) { + instance.browserStorage.set(key, value, callback || function() {}); + }, + /* + * storage.remove - deletes storage item at [key], calls callback with key as parameter + */ + remove: function(key, callback) { + instance.browserStorage.remove(key, callback || function() {}); + } + }, + + /* + * memory storage functions + */ + memoryStorage: { + /* + * memoryStorage.get - gets storage for [key], calls callback with [return value] as parameter + */ + get: function(key, callback) { if (typeof(callback) !== 'function') { - console.log('ERROR: no callback provided for BabelExt.storage.set()'); - return false; + throw 'ERROR: no callback provided for BabelExt.memoryStorage.get()'; } - instance.browserStorage.set(key, value, callback); + instance.memoryStorage.get(key, callback); }, /* - * storage.remove - deletes storage item at [key], calls callback with key as parameter + * memoryStorage.set - sets storage for [key] to [value], calls callback with key, value as parameters + */ + set: function(key, value, callback) { + instance.memoryStorage.set(key, value, callback || function() {}); + }, + /* + * memoryStorage.remove - deletes storage item at [key], calls callback with key as parameter */ remove: function(key, callback) { + instance.memoryStorage.remove(key, callback || function() {}); + } + }, + + /* + * preference functions + */ + preferences: { + /* + * preferences.get - gets preferences for [key], calls callback with [return value] as parameter + */ + get: function(key, callback) { if (typeof(callback) !== 'function') { - console.log('ERROR: no callback provided for BabelExt.storage.remove()'); - return false; + throw 'ERROR: no callback provided for BabelExt.preferences.get()'; } - instance.browserStorage.remove(key, callback); + instance.browserPreferences.get(key, callback); + }, + /* + * preferences.set - sets preferences for [key] to [value], calls callback with key, value as parameters + */ + set: function(key, value, callback) { + instance.browserPreferences.set(key, value, callback || function() {}); } }, @@ -640,9 +976,184 @@ var BabelExt = (function(Global, unsafeGlobal) { contextMenu: { create: function(obj, callback) { instance.contextMenu.create(obj, callback); + }, + remove: function(obj, callback) { + instance.contextMenu.remove(obj, callback); } }, - detectedBrowser: instance.detectedBrowser + detectedBrowser: instance.detectedBrowser, + + /* + * XHR functionality based on the XMLHttpRequest API + * Build an XHR object that can do cross-site requests + * var xhr = new BabelExt.XMLHttpRequest(); + */ + XMLHttpRequest: function() { + this._request_headers = {}; + this.setRequestHeader = function( header, value ) { this._request_headers[header] = value }; + this.overrideMimeType = function( mime_type ) { this._mime_type = mime_type }; + this.open = function(method, url, async, user, password) { + + var xhr = this; + + if ( url == location.origin || url.search(location.origin+'/') == 0 || url.search(/^[a-z]+:/) == -1 ) { + // Single-site request - proxy everything straight through to an XMLHttpRequest object: + + this._xhr = new XMLHttpRequest( method, url, async, user, password ); + Object.keys(this._request_headers).forEach(function(header) { xhr._xhr.setRequestHeader(header, xhr._request_headers[header]) }); + if ( this.hasOwnProperty('_mime_type') ) this._xhr.overrideMimeType(this._mime_type); + + // proxy properties through to the XHR: + [ + 'onreadystatechange', 'ontimeout', 'onabort', 'onerror', 'onload', 'onloadend', 'onloadstart', 'onprogress', + 'readyState', 'responseText', 'responseType', 'status', 'statusText', 'timeout', 'withCredentials', 'response', 'responseXML' + ] + .forEach(function(property) { + if ( xhr.hasOwnProperty(property) ) { + xhr._xhr[property] = xhr[property]; + delete xhr[property]; + } + Object.defineProperty(xhr, property, { get: function() { return xhr._xhr[property] }, set: function(p) { xhr._xhr[property] = p } }); + }); + + // proxy methods through to the XHR: + [ 'abort', 'overrideMimeType', 'send', 'setRequestHeader', 'getAllResponseHeaders', 'getResponseHeader' ].forEach(function(method) { + xhr[method] = function() { return xhr._xhr[method].apply( xhr._xhr, Array.prototype.slice.call( arguments, 0 ) ) }; + }); + + xhr._xhr.open(method, url, (typeof(async)=='undefined')?true:async, user, password); + + } else { + + if ( url.search(BabelExt._xhr_regexp) == -1 ) { + console.log( "Blocked request to non-whitelisted URL '" + url + "' - please add this to xhr_patterns in your extension settings" ); + throw "Please add '" + url + "' to xhr_patterns in your extension settings"; + } + + // Communicating with the browser side is necessarily asynchronous: + if ( typeof(async) != 'undefined' && !async ) throw "Synchronous cross-site requests are not supported"; + + var aborted; + this.abort = function() { + if ( this.onabort ) this.onabort(); + aborted = true; + }; + + this._response_headers = null; + this.getAllResponseHeaders = function( ) { return this._response_headers } + this.getResponseHeader = function(h) { return ( this._response_headers || {} ).hasOwnProperty(h) ? this._response_headers[h] : null }; + + this.send = function(data) { + if ( this.onloadstart ) this.onloadstart(); + instance.bgMessage({ + requestType: 'xmlhttpRequest', + method : method, + url : url, + headers : this._request_headers, + data : data, + overrideMimeType: this._mime_type, + }, function(response) { + if ( aborted ) return; + Object.keys(response).forEach(function(p) { + xhr[p] = response[p]; + }); + xhr.readyState = 4; + xhr.responseType = 'text'; + if ( xhr.onreadystatechange ) xhr.onreadystatechange(); + if ( response.error ) { + if ( xhr.onerror ) xhr.onerror(); + } else { + if ( xhr.onload ) xhr.onload (); + } + if ( xhr.onloadend ) xhr.onloadend(); + }); + }; + + } + + } + } + }; })(Global, unsafeGlobal); + +BabelExt.debugLog = function() { + // "use strict" // NO! + // We need to show "log.caller" in the debug log, which is not available in strict mode + + var start_time = new Date(); + + var div = document.createElement('DIV'); + div.setAttribute( 'style', "width: 100%; text-align: center" ); + div.innerHTML = '

Debugging log - please send this to the maintainer if requested

'; + + var textarea = div.lastChild; + + return { + + /** + * @summary Show the debugging log at the bottom of the current page + * @description You might want to show the debugging log at page load, or only when a "severe" error occurs + */ + show: function() { + if ( document.body ) { + document.body.appendChild(div); + } else { + var interval = setInterval(function() { + if ( document.body ) { + document.body.appendChild(div); + clearInterval(interval); + } + }, 10 ); + } + this.show = function() { return this }; + return this; + }, + + /** + * @summary Add values to the debugging log + * @param {...Any} var_args variadic list of things to log + * Need to define this eoutside the main block so we can build a stack trace with "log.caller" + * ("use strict" breaks this) + */ + log: function log() { + var messages = []; + for ( var n=0; n!=arguments.length; ++n ) try { + messages.push( JSON.stringify(arguments[n]) ); + } catch (e) { + messages.push( '(cannot stringify circular data structure)' ); + } + var caller = log.caller.name || log.caller.toString(); + caller = caller.length > 110 ? caller.substr(0,100) + '...' : caller; + BabelExt.utils.runInEmbeddedPage( 'document.head.setAttribute("data-js-is-enabled", "true" );' ); + textarea.value += + '================================================================================\n' + + 'Start date: ' + start_time + " (" + ( new Date().getTime() - start_time.getTime() )/1000 + " seconds ago)\n" + + 'Extension version: ' + BabelExt.about.version + ' (built: ' + BabelExt.about.build_time + ')\n' + + 'Frame: ' + ( (window.location == window.parent.location) ? 'main document' : 'iFrame' ) + "\n" + + 'URL: ' + location.toString() + "\n" + + 'User agent: ' + navigator.userAgent + "\n" + + 'Cookies: ' + ( + ( typeof(navigator.cookieEnabled) == 'undefined' ) + ? 'unknown' + : ( navigator.cookieEnabled ? 'enabled' : 'disabled' ) + ) + "\n" + + 'Javascript: ' + ( document.head.hasAttribute('data-js-is-enabled') ? 'enabled' : 'disabled' ) + "\n" + + 'Caller: ' + caller + "\n" + + "Data: [\n " + messages.join(",\n ") + "\n]\n" + + "\n" + ; + document.head.removeAttribute('data-js-is-enabled'); + }, + + /** + * @summary Get the contents of the debug log + */ + text: function() { + return '' + textarea.value; + } + + } + +} diff --git a/lib/README.txt b/lib/README.txt new file mode 100644 index 0000000..e7a53f7 --- /dev/null +++ b/lib/README.txt @@ -0,0 +1 @@ +Add third-party libraries to this directory. diff --git a/lib/extension.js b/lib/extension.js deleted file mode 100644 index 0fffac9..0000000 --- a/lib/extension.js +++ /dev/null @@ -1,181 +0,0 @@ -// ==UserScript== -// @name BabelExt Extension -// @namespace http://babelext.com/ -// @description An extension built with BabelExt -// @copyright 2013, Steve Sobel (http://babelext.com/) -// @author honestbleeps -// @include http://babelext.com/* -// @version 0.95 -// ==/UserScript== - -/* - * extension.js - contains your code to be run on page load - * - */ -(function(u) { - // Any code that follows will run on document ready... - - // this ugly hack is because Opera seems to run userJS on iFrames regardless of @include and @exclude directives. - // unfortunately, more sites than you'd guess use iframes - which can cause unexpected behavior if Opera goes and - // runs this script on a page it's not meant to be run - if (window!=window.top) { - return false; - } - - /* - * NOTE: Opera will run this on EVERY page! It does not have a "run only on matching domains" feature - * like Firefox, Safari and Chrome all have. If you want to restrict execution to certain sites, this - * is the place to add some code to check location.href and return false if not a match. - * - */ - - /* - * GREASEMONKEY COMPATIBILITY SECTION - you can remove these items if you're not porting a GM script. - * - * WARNING: GM_setValue, GM_getValue, GM_deleteValue are NOT ideal to use! These are only present - * for the sake of easy porting/compatibility, but they will use localStorage, which can easily be erased - * by the user if he/she clears cookies or runs any "privacy" software... - * - * Ideally, you should update any Greasemonkey scripts you want to port to perform asynchronous calls to - * BabelExt.storage.get and .set instead of using localStorage, but a simple replacement won't work due - * to the asynchronous nature of extension-based localStorage (and similar) calls. - * - */ - GM_xmlhttpRequest = BabelExt.xhr; - GM_setValue = localStorage.setItem; - GM_getValue = localStorage.getItem; - GM_deleteValue = localStorage.removeItem; - GM_log = console.log; - GM_openInTab = BabelExt.tabs.create; - // NOTE: you'll want to add a render() function call at the end of your GM script for compatibility. - GM_addStyle = BabelExt.css.add; - - /* BEGIN KITCHEN SINK DEMO CODE */ - - /* - * The code below is for testing / execution on the BabelExt website at: - * http://babelext.com/demo.html - * - * You should remove this code, and replace it with your own! - * - */ - // hide the "install BabelExt" message, and show the kitchen sink demos... - var installBabelExt = document.getElementById('installBabelExt'); - var container = document.getElementById('container'); - if (installBabelExt && container) { - container.style.display = 'block'; - installBabelExt.style.display = 'none'; - } - - // show each available kitchen sink demo, let the user know the extension is out of date if there's an unrecognized demo... - var features = document.body.querySelectorAll('.featureContainer'); - var recognizedFeatures = ['save','load','tabCreate','notificationCreate','historyAdd', 'cssAdd']; - for (var i=0, len=features.length; i + + BabelExt Resources + + +

BabelExt Resources

+

BabelExt.resources allows you to store and retrieve information in flat files. You can use this for anything, but it's most useful for storing large HTML snippets that would be hard to read in JavaScript.

+ + diff --git a/script/build.js b/script/build.js new file mode 100755 index 0000000..7c68ed8 --- /dev/null +++ b/script/build.js @@ -0,0 +1,1654 @@ +#!/usr/bin/env phantomjs --ssl-protocol=any + +/* + * Phantom JS build script + * + * Usage: phantomjs + * + * + * PhantomJS is a headless web browser, which allows us to automate + * the release process even for sites without a release API. + * Using it as a build system is a bit clunky, but less so than + * adding a second dependency + * + */ + +// Required modules: +var childProcess = require('child_process'); +var fs = require('fs'); +var system = require('system'); +var webPage = require('webpage'); + +/* + * OS INTERACTION + * improve PhantomJS' ability to interact with the operating system + */ + +var chrome_command; +switch ( system.os.name ) { +case 'windows': chrome_command = 'chrome.exe' ; break; +case 'linux' : chrome_command = 'google-chrome'; break; +case 'mac' : chrome_command = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; break; +default: + console.error( "Sorry, but your operating system (" + system.os.name + ") is not supported." ); + phantom.exit(1); +} + +// script-wide debugging: +phantom.onError = function(msg, trace) { + var msgStack = ['PHANTOM ERROR: ' + msg]; + if (trace && trace.length) { + msgStack.push('TRACE:'); + trace.forEach(function(t) { + msgStack.push(' -> ' + (t.file || t.sourceURL) + ': ' + t.line + (t.function ? ' (in function ' + t.function +')' : '')); + }); + } + console.error(msgStack.join('\n')); + phantom.exit(1); +}; + +/* + * Replace PhantomJS' execFile() with something more useful: + */ +var execFile = childProcess.execFile; +childProcess.execFile = function(cmd, args, opts, cb) { + + // need to check both the callback and value of "exit": + var calls = 0, err, stdout, stderr, code, has_finished = false; + + // run the command and get stdout/stderr: + var ctx = execFile.call( childProcess, cmd, args, opts, function(_err,_stdout,_stderr) { + setTimeout(function() { + if ( !has_finished ) { + console.log( cmd + ' returned but did not exit - does the command exist?' ); + code = 100; + run_callback(); + } + }, 1000 ); + err = _err; + stdout = _stdout; + stderr = _stderr; + if ( calls++ ) run_callback(); + }); + + // also get the exit code: + ctx.on("exit", function (_code) { + code = _code; + if ( calls++ ) run_callback(); + }); + + // once we've got all the information, print STDERR and continue if there was no error: + function run_callback() { + if ( !has_finished ) { + has_finished = true; + if ( stderr != '' ) console.log(stderr.replace(/\n$/,'')); + if ( code ) program_counter.end(code); + else if ( cb ) cb(null, stdout, stderr, code); + } + } + + return ctx; +} + +/* + * Create a symbolic link from source to target + */ +function symbolicLink( source, target ) { + target.replace(/\//g, function() { source = '../' + source }); + if ( ! fs.isLink(target) ) { + if ( system.os.name == 'windows' ) { + childProcess.execFile('mklink', [target,source] ); + } else { + childProcess.execFile('ln', ["-s",source,target] ); + } + } +} + +/* + * Create a hard link from source to target + */ +function hardLink( source, target ) { + if ( fs.exists(target) ) fs.remove(target); + if ( system.os.name == 'windows' ) { + childProcess.execFile('mklink', ['/H',target,source]); + } else { + childProcess.execFile('ln' , [source,target]); + } +} + +/* + * Create a tree of directories + */ +function makeTree( directory ) { + directory = directory.split( '/' ); + for ( var n=0; n!=directory.length; ++n ) { + if ( !fs.exists( directory.slice( 0, n ).join( '/' ) ) ) + fs.makeDirectory(directory.slice( 0, n ).join( '/' )); + } +} + +// Sugar functions to make the containing directory and file link: +function makeTreeHardLink ( source, target ) { makeTree( target.replace( /[^\/]+$/, '' ) ); hardLink( source, target ); } +function makeTreeSymbolicLink( source, target ) { makeTree( target.replace( /[^\/]+$/, '' ) ); symbolicLink( source, target ); } + + +/* + * Return information about the specified files. + * Currently returns an array of { name: ..., id: ..., modified: ... } + * 'name' is the passed-in name, 'id' is the file's inode, and 'modified' is the modification time relative to the epoch. + */ +function stat( files, callback ) { + // TODO: no idea how you'd do this on Windows + files = files.filter( fs.exists ); + childProcess.execFile( 'stat', [ '--printf=%i %Y\n' ].concat(files), null, function(err,stdout,stderr) { + var lines = stdout.split("\n"); + lines.pop(); // eat trailing newline + callback( lines.map(function(line, index) { + var rows = line.split(' '); + return { name: files[index], id: rows[0], modified: rows[1] }; + }) ); + }); +} + +/* + * PAGE UTILITIES + * Functions to better interact with web pages + */ + +function _waitForEvent( test, callback ) { // low-level interface - see waitFor* below + + // originally based on http://newspaint.wordpress.com/2013/04/05/waiting-for-page-to-load-in-phantomjs/ + + var timeout = 20000; + var expiry = new Date().getTime() + timeout; + + var interval = setInterval(checkEvent,100); + + var page = this; + + function checkEvent() { + var failure_reason = test(this); + if ( !failure_reason ) { + clearInterval(interval); + interval = undefined; + if ( callback ) callback('success'); + return true; + } else if ( new Date().getTime() > expiry ) { + clearInterval(interval); + //callback('fail'); + console.log('Waited for ' + timeout + 'ms, but ' + failure_reason + ' - see fail.png and fail.html'); + fs.write( 'fail.html', page.content ); + page.render('fail.png'); + return program_counter.end(1); + } + return false; + }; + + return checkEvent(); + +} + +function _waitForElementsPresent( selectors, callback ) { // call callback when all selectors exist on the page + + if ( typeof(selectors) == "string" ) + selectors = [ selectors ]; + + var page = this; + + return this.waitForEvent( + function() { + var missing_elements = selectors.filter( + function(selector) { + return ! page.evaluate(function(selector) { return document.querySelector(selector) }, selector ); + } + ); + if ( missing_elements.length ) + return JSON.stringify(missing_elements) + ' did not appear'; + else + return null; + }, + callback + ); + +} + +function _waitForElementsNotPresent( selectors, callback ) { // call callback when no selecters exist on the page + + if ( typeof(selectors) == "string" ) + selectors = [ selectors ]; + + var page = this; + + return this.waitForEvent( + function() { + var present_elements = []; + selectors.forEach( + function(selector) { + if ( page.evaluate(function(selector) {return document.querySelector(selector)}, selector ) ) + present_elements.push(selector); + } + ); + if ( present_elements.length ) + return JSON.stringify(present_elements) + ' remained present'; + else + return null; + }, + callback + ); + +} + +function _click(selector) { // click an HTML element + var page = this; + return this.waitForElementsPresent( + [ selector ], + function () { + page.evaluate(function(selector) { + var element = document.querySelector(selector); + var event = document.createEvent("MouseEvent"); + event.initMouseEvent( "click", true, true, window, null, 0, 0, 0, 0, false, false, false, false, 0, null ); + element.dispatchEvent(event); + }, selector); + } + ); +} + +function _submit_form(submit_selector, fields, callback) { // fill in the relevant fields, then click the submit button + var page = this; + return this.waitForElementsPresent( + Object.keys(fields).concat([submit_selector]), + function() { + var file_inputs = + page.evaluate(function(fields) { + return Object.keys(fields).filter(function(key) { + var element = document.querySelector(key); + if ( element.type == 'file' ) { + return true; + } else { + element.value = fields[key]; + return false; + } + }); + }, fields); + file_inputs.forEach(function(field) { + var filename = fields[field]; + if ( filename ) { + if ( fs.exists(filename) ) { + page.uploadFile(field, filename); + } else { + console.log( "Tried to upload non-existent file: " + filename ); + phantom.exit(1); + } + } + }); + page.click(submit_selector); + if ( callback ) callback(); + } + ); +} + +function _showConsoleMessage() { // show console.log() commands from page context (enabled by default) + this.onConsoleMessage = function(message) { + if ( message.search("\n") != -1 ) + message = ( "\n" + message ).replace( /\n(.)/g, "\n\t$1" ); + system.stderr.writeLine('console: ' + message); + }; +} + +function _showResourceError() { // show errors loading resources + console.log('Started logging resorce error'); + this.onResourceError = function(resourceError) { + console.log('Unable to load resource (' + resourceError.url + ')'); + console.log('Error code: ' + resourceError.errorCode + '. Description: ' + resourceError.errorString); + }; +} +function _showResourceReceived() { // show information when resources are received from the web + console.log('Started logging resorce received'); + this.onResourceReceived = function(response) { + if ( response.stage == 'start' ) + console.log('Received ' + response.url + ': bodySize=' + response.bodySize) + }; +} + +function _hideResourceError () { console.log('Stopped logging resorce error' ); this.onResourceError = function() {} } +function _hideResourceReceived() { console.log('Stopped logging resorce received'); this.onResourceReceived = function() {} } +function _hideConsoleMessage () { this.onConsoleMessage = function() {} } + +// Initialise a page object: +function page( url, callback ) { + var page = require('webpage').create(); + page.onError = function(msg, trace) { + var msgStack = ['PAGE ERROR: ' + msg]; + if (trace && trace.length) { + msgStack.push('TRACE:'); + trace.forEach(function(t) { + msgStack.push(' -> ' + (t.file || t.sourceURL) + ': ' + t.line + (t.function ? ' (in function ' + t.function +')' : '')); + }); + } + console.error(msgStack.join('\n')); + phantom.exit(1); + }; + page.waitForEvent = _waitForEvent; + page.waitForElementsPresent = _waitForElementsPresent; + page.waitForElementsNotPresent = _waitForElementsNotPresent; + page.click = _click; + page.submit_form = _submit_form; + page.showResourceError = _showResourceError; + page.showResourceReceived = _showResourceReceived; + page.hideResourceError = _hideResourceError; + page.hideResourceReceived = _hideResourceReceived; + page.showConsoleMessage = _showConsoleMessage; + page.hideConsoleMessage = _hideConsoleMessage; + + page.showConsoleMessage(); + + page.settings.loadImages = false; + + page.openBinary = function(url, settings, callback) { + + // PhantomJS refuses to download chunked data, do it with `curl` instead (TODO: make this work in Windows): + + if ( !callback ) { + callback = settings; + settings = {}; + } + + var args = [ "--silent", url, '-L' ]; + + if ( settings.data ) args = args.concat([ '-d', settings.data ]); + if ( settings.out_file ) args = args.concat([ '-o', settings.out_file ]); + if ( settings.cookies ) args = args.concat([ '-H', 'Cookie: ' + settings.cookies ]); + + childProcess.execFile( 'curl', args, null, callback ); + } + + return page.open( url, function(status) { + if (status == 'success') { + callback(page); + } else { + console.log( "Couln't connect to " + url ); + return program_counter.end(1); + } + }); +} + +/* + * MISCELLANEOUS BABELEXT-SPECIFIC UTILITIES + */ + +/* + * Keep track of asynchronous jobs, and exit when the last one finishes: + */ + +function AsyncCounter(zero_callback) { + this.count = 0; + this.errors = 0; + this.zero_callback = zero_callback +} +AsyncCounter.prototype.begin = function( ) { ++this.count }; +AsyncCounter.prototype.end = function(errors) { this.errors += (errors||0); if ( !--this.count ) this.zero_callback(this.errors) }; + +var program_counter = new AsyncCounter(function(errors) { phantom.exit(errors||0) }); + +/* + * Build a resources.js file + */ +function build_resources() { + var about = "BabelExt.about = " + JSON.stringify({ + version : settings.version, + build_time: new Date().toString() + }) + ";\n"; + var xhr_regexp = "BabelExt._xhr_regexp = new RegExp('" + ( settings.xhr_regexp || '(?!)' ) + "');\n"; + + if ( settings.resources ) { + var resources = {}; + settings.resources.forEach(function(filename) { + resources[filename] = fs.open(filename, 'r').read(); + }); + fs.write( + 'lib/BabelExtResources.js', "BabelExt.resources._resources = " + + // prettify our JavaScript a bit, for the benefit of reviewers: + JSON.stringify(resources, null, ' ').replace( /\\n(?!")/g, "\\n\" +\n \"" ) + ";\n" + + about + xhr_regexp, + 'w' + ); + } else { + fs.write( 'lib/BabelExtResources.js', about + xhr_regexp, 'w' ); + } +} + +/* + * Load settings from conf/settings.json + */ +var settings; +function update_settings() { + + try { + settings = eval('('+fs.read('conf/settings.json')+')'); + } catch (e) { + console.error( + "Error in conf/settings.json: " + e + "\n" + + "Please make sure the file is formatted correctly and try again." + ); + phantom.exit(1); + } + if ( system.env.hasOwnProperty('ENVIRONMENT') ) { + var environment_specific = settings.environment_specific[ system.env.ENVIRONMENT ]; + if ( !environment_specific ) { + console.log( + 'Please specify one of the following build environments: ' + + Object.keys(settings.environment_specific).join(' ') + ); + phantom.exit(1); + } + Object.keys(environment_specific) + .forEach(function(property, n, properties) { + settings[ property ] = + ( Object.prototype.toString.call( settings[ property ] ) === '[object Array]' ) + ? settings[ property ].concat( environment_specific[property] ) + : environment_specific[property] + ; + }); + } else if ( settings.environment_specific ) { + console.log( + 'Please specify build environment using the ENVIRONMENT environment variable, e.g.:\n\n' + + '\texport ENVIRONMENT=' + Object.keys(settings.environment_specific)[0] + "\n\n" + + 'Alternatively, comment out the "environment_specific" section in settings.json' + ); + phantom.exit(1); + }; + settings.contentScriptFiles.unshift('lib/BabelExt.js'); + delete settings.environment_specific; + + if ( + settings.version.search(/^[0-9]+(?:\.[0-9]+){0,3}$/) || + settings.version.split('.').filter(function(number) { return number > 65535 }).length + ) { + console.log( + 'Google Chrome will not accept version number "' + settings.version + '"\n' + + 'Please specify a version number containing 1-4 dot-separated integers between 0 and 65535' + ); + phantom.exit(1); + } + + if ( settings.xhr_patterns ) { + /* + * Convert a "match pattern" to a regular expression + * Different browsers implement this differently. + * We treat Chrome's implementation as canonical: https://developer.chrome.com/extensions/match_patterns + */ + var regexps = []; + settings.xhr_patterns.forEach(function(pattern) { + if ( pattern.replace( /^(\*|https?|file|ftp):\/\/(\*|(?:\*\.)?[^\/*]*)\/(.*)$/, function( url, protocol, domain, path ) { + protocol = ( protocol == '*' ) ? 'https?' : protocol.replace( /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g , "\\$&"); + domain = domain.replace( /[\-\[\]\/\{\}\(\)\+\?\.\\\^\$\|]/g , "\\$&").replace( '*', '[^/]*' ); + path = path .replace( /[\-\[\]\/\{\}\(\)\+\?\.\\\^\$\|]/g , "\\$&").replace( '*', '.*' ); + regexps.push( protocol + '://' + domain + '/' + path ); + return url + ' '; + }) == pattern ) { + console.log( 'Ignoring invalid match pattern: ' + pattern ); + } + }); + settings.xhr_regexp = '^(?:' + regexps.join('|') + ')$'; + } + + + settings.preferences.forEach(function(preference) { + /* + * Known-but-unsupported types: + * color - not supported by Safari + * file - not supported by Safari + * directory - not supported by Safari + * control - not supported by Safari, not clear what we'd do with it anyway + */ + if ( preference.type.search(/^(bool|boolint|integer|string|menulist|radio)$/) == -1 ) { + console.log( + 'Preference type "' + preference.type + ' is not supported.\n' + + 'Please specify a valid preference type: bool, boolint, integer, string, menulist, radio\n' + ); + phantom.exit(1); + } + }); + +} +update_settings(); + +/* + * Load settings from conf/local_settings.json + */ +var local_settings; +if ( !fs.exists('conf/local_settings.json') ) { + console.error( + "Please create conf/local_settings.json (you can probably just rename conf/local_settings.json.example)" + ); + phantom.exit(1); +} +try { + local_settings = eval('('+fs.read('conf/local_settings.json')+')'); +} catch (e) { + console.error( + "Error in conf/local_settings.json: " + e + "\n" + + "Please make sure the file is formatted correctly and try again." + ); + phantom.exit(1); +} + +function get_changelog(callback) { // call the callback with the changelog text as its only argument + + if ( !local_settings.changelog_command ) + return console.log("Please specify the changelog command"); + if ( local_settings.changelog ) + return callback(local_settings.changelog); + + childProcess.execFile( + local_settings.changelog_command[0], + local_settings.changelog_command.splice(1), + null, + function(err,changelog,stderr) { + if ( changelog == '' ) { + console.log( "Error: empty changelog" ); + return program_counter.end(1); + } else { + callback( local_settings.changelog = changelog ); + } + } + ); +} + +/* + * BUILD COMMANDS + */ + +function build_safari(login_info) { + + var when_string = { + 'early' : 'Start', + 'middle': 'End', + 'late' : 'End' + }; + + var document = new DOMParser().parseFromString(fs.read('build/Safari.safariextension/Info.plist'),"text/xml"); + + function get_node( key ) { + return document + .evaluate( '//dict/key[.="' + key + '"]/following-sibling::*[1]', document, null, XPathResult.ANY_UNORDERED_NODE_TYPE, null ) + .singleNodeValue + ; + } + + function set_key( key, value ) { + get_node(key).textContent = value; + } + + /* + * PART ONE: build the Safari.safariextension directory: + */ + + // BabelExt IDs are UUIDs, but Safari IDs must be alphabetical: + var map = { + '0': 'a', + '1': 'b', + '2': 'c', + '3': 'd', + '4': 'e', + '5': 'f', + '6': 'g', + '7': 'h', + '8': 'i', + '9': 'j', + 'a': 'k', + 'b': 'l', + 'c': 'm', + 'd': 'n', + 'e': 'o', + 'f': 'p', + '-': 'q' + }; + + get_node('Author').textContent = settings.author; + + get_node('CFBundleDisplayName' ).textContent = settings.title; + get_node('CFBundleIdentifier' ).textContent = 'com.honestbleeps.' + settings.id.replace( /(.)/g, function(char) { return map[char] }); + get_node('CFBundleShortVersionString').textContent = settings.version; + get_node('CFBundleVersion' ).textContent = settings.version; + get_node('Description' ).textContent = settings.description; + get_node('Website' ).textContent = settings.website; + get_node('DeveloperIdentifier' ).textContent = settings.safari_team_id || '(not set)'; + + var match_domains = get_node('Allowed Domains'); + while (match_domains.firstChild) match_domains.removeChild(match_domains.firstChild); + settings.match_domains.forEach(function(match_domain) { + var domain = document.createElement("string"); + domain.textContent = match_domain; + match_domains.appendChild( document.createTextNode('\n\t\t\t\t') ); + match_domains.appendChild(domain); + }); + match_domains.appendChild( document.createTextNode('\n\t\t\t') ); + + var match_secure_domain = get_node('Include Secure Pages'); + match_secure_domain.parentNode.replaceChild(document.createElement((settings.match_secure_domain||false).toString()),match_secure_domain); + + var start_scripts = get_node('Start'); + var end_scripts = get_node('End'); + + while (start_scripts.firstChild) start_scripts.removeChild(start_scripts.firstChild); + while ( end_scripts.firstChild) end_scripts.removeChild( end_scripts.firstChild); + + settings.contentScriptFiles.forEach(function(file) { + + makeTreeHardLink( file, 'build/Safari.safariextension/' + file ) + + var script = document.createElement("string"); + script.textContent = file; + + if ( file == 'lib/BabelExt.js' || when_string[ settings.contentScriptWhen ] == 'Start' ) { + start_scripts.appendChild( document.createTextNode('\n\t\t\t\t') ); + start_scripts.appendChild(script); + } else { + end_scripts.appendChild( document.createTextNode('\n\t\t\t\t') ); + end_scripts.appendChild(script); + } + }); + + start_scripts.appendChild( document.createTextNode('\n\t\t\t') ); + end_scripts.appendChild( document.createTextNode('\n\t\t\t') ); + + var stylesheets = get_node('Stylesheets'); + + while (stylesheets.firstChild) stylesheets.removeChild(stylesheets.firstChild); + + settings.contentStyleFiles.forEach(function(file) { + makeTreeHardLink( file, 'build/Safari.safariextension/' + file ) + + var sheet = document.createElement("string"); + sheet.textContent = file; + + stylesheets.appendChild( document.createTextNode('\n\t\t\t') ); + stylesheets.appendChild(sheet); + }); + + stylesheets.appendChild( document.createTextNode('\n\t\t') ); + + var xml_txt = '\n' + new XMLSerializer().serializeToString(document).replace(">",">\n") + "\n"; + fs.write( 'build/Safari.safariextension/Info.plist', xml_txt ); + + if ( settings.preferences ) + function build_dict( preference, values ) { + return '\t\n\t\tDefaultValue\n\t\t' + preference.value + '\n\t\tKey\n\t\t' + preference.name + '\n\t\tTitle\n\t\t' + preference.title + '' + + Object.keys(values).map(function(value) { + if ( typeof(values[value]) == 'string' ) return '\n\t\t' + value + '\n\t\t' + values[value] + ''; + else if ( typeof(values[value]) == 'number' ) return '\n\t\t' + value + '\n\t\t' + values[value] + ''; + else if ( typeof(values[value]) == 'boolean' ) return '\n\t\t' + value + '\n\t\t<' + values[value] + '/>'; + else /* must be an array */ return '\n\t\t' + value + '\n\t\t' + values[value].map(function(v) { return '\n\t\t\t'+v+''; }).join('') + '\n\t\t' + }).join('') + + '\n\t\n' + } + fs.write( + 'build/Safari.safariextension/Settings.plist', + '\n' + + '\n' + + '\n' + + '\n' + + settings.preferences.map(function(preference) { + switch ( preference.type ) { + case 'bool' : return build_dict( preference, { Type: 'CheckBox' } ); + case 'boolint' : return build_dict( preference, { Type: 'CheckBox', FalseValue: 0, TrueValue: 1 } ); + case 'integer' : return build_dict( preference, { Type: 'Slider' } ); + case 'string' : return build_dict( preference, { Type: 'TextField', Password: false } ); + case 'menulist': return build_dict( preference, { Type: 'ListBox', Titles: preference.options.map(function(o) { return o.label }), Values: preference.options.map(function(o) { return o.value }), } ); + case 'radio' : return build_dict( preference, { Type: 'RadioButtons', Titles: preference.options.map(function(o) { return o.label }), Values: preference.options.map(function(o) { return o.value }), } ); + } + }).join('') + + '\n' + + '\n', + 'w' + ); + + + /* + * PART TWO: build a signed .safariextz file + */ + + program_counter.begin(); + + if ( fs.exists('build/safari-certs/AppleWWDRCA.cer') ) { + check_xar(); + } else { + + if ( !login_info || login_info.skip ) { + console.log( 'Please add Safari login details to local_settings.json to build a Safari package' ); + return program_counter.end(0); + } + + if ( !login_info.password ) { + if ( system.env.hasOwnProperty('APPLE_PASSWORD') ) { + login_info.password = system.env.APPLE_PASSWORD; + } else { + console.log("Please specify a password for apple.com"); + return program_counter.end(1); + } + } + + if ( !fs.exists('build/safari-certs/id.rsa') ) { + console.log( + "Please generate a private key and Certificater Signature Request.\n" + + "The private key should not have an associated password.\n" + + "Example command:\n" + + "openssl req -new -nodes -newkey rsa:2048 -keyout build/safari-certs/id.rsa -out build/safari-certs/request.csr" + ); + return program_counter.end(1); + } + + console.log( 'Generating keys...' ); + page( 'https://developer.apple.com/account/safari/certificate/certificateRequest.action', function(page) { + + var onError = page.onError; + page.onError = function(msg, trace) { + // Ignore expected error + if ( msg != "TypeError: 'undefined' is not an object (evaluating 'document.form1.submit')" ) { + onError.call( page, msg, trace ); + } + }; + + page.submit_form( + '#submitButton2', + { + '#accountname' : login_info.username, + '#accountpassword': login_info.password + }, + function() { + page.onError = onError; + page.waitForElementsPresent( + [ 'form[name="certificateRequest"]' ], + function() { + page.click('a.submit'); + page.waitForElementsPresent( + [ '#certificateSubmit' ], + function() { + + page.submit_form( + 'a.submit', + { + 'input[name="upload"]': 'build/safari-certs/request.csr', + }, + function() { + page.waitForElementsPresent( + [ '.downloadForm' ], + function() { + var download_url = page.evaluate(function() { + return document.getElementsByClassName('blue')[0].getAttribute('href') + }); + var cookies = page.cookies.map(function(cookie) { return cookie.name + '=' + cookie.value }); + page.openBinary( 'https://developer.apple.com' + download_url, { cookies: cookies.join('; '), out_file: 'build/safari-certs/local.cer' }, function() { + page.openBinary( 'https://www.apple.com/appleca/AppleIncRootCertificate.cer', { out_file: 'build/safari-certs/AppleIncRootCertificate.cer' }, function() { + page.openBinary('https://developer.apple.com/certificationauthority/AppleWWDRCA.cer', { out_file: 'build/safari-certs/AppleWWDRCA.cer' }, check_xar ); + }); + }); + }); + }); + } + ); + } + ); + } + ); + + }); + + } + + function check_xar() { + page( 'http://mackyle.github.io/xar/', function(page) { + + var xar_url = page.evaluate(function() { + return document.getElementsByClassName('down')[0].parentNode.getAttribute('href') + }); + + if ( fs.exists('build/xar-url.txt') && fs.read('build/xar-url.txt') == xar_url ) { + console.log( 'XAR is up-to-date.' ); + build_safariextz(); + } else { + console.log( 'Downloading xar archiver...' ); + page.openBinary(xar_url, { out_file: 'temporary_file.tar.gz' }, function() { + console.log( 'Unpacking xar archiver...', status ); + if ( fs.exists( 'build/xar' ) ) fs.removeTree('build/xar'); + fs.makeDirectory('build/xar'); + childProcess.execFile( 'tar', ["zxf",'temporary_file.tar.gz','-C','build/xar','--strip-components=1'], null, function(err,stdout,stderr) { + console.log( 'Building xar archiver...', status ); + if ( system.os.name == 'windows' ) { + // TODO: fill in real Windows values here (the following line is just a guess): + childProcess.execFile( 'cmd' , [ 'cd build\\xar ; ./configure ; make'], null, finalise_xar ); + } else { + childProcess.execFile( 'bash', ['-c','cd build/xar && ./configure && make'], null, finalise_xar ); + } + + function finalise_xar(err,stdout,stderr) { + fs.remove('temporary_file.tar.gz'); + fs.write( 'build/xar-url.txt', xar_url, 'w' ); + build_safariextz(); + } + + }); + }); + } + + }); + } + + function build_safariextz() { + + function run_commands(commands, then) { + function run_command(err, stdout, stderr) { + if ( commands.length ) { + var command = commands.shift(); + return childProcess.execFile( command[0], command.splice(1), null, run_command ); + } else { + return then(err, stdout, stderr) + } + } + run_command(); + } + + fs.changeWorkingDirectory('build'); + + var xar = './xar/src/xar'; + var safariextz = '../out/' + settings.name + '.safariextz'; + + run_commands([ + [ xar, '-czf', safariextz, '--distribution', 'Safari.safariextension' ], + [ xar, '-f', safariextz, '--sign', '--digestinfo-to-sign', 'safari-certs/tmp.dat', '--sig-size', 256, '--cert-loc', 'safari-certs/local.cer', '--cert-loc', 'safari-certs/AppleWWDRCA.cer', '--cert-loc', 'safari-certs/AppleIncRootCertificate.cer' ], + [ 'openssl', 'rsautl', '-sign', '-inkey', 'safari-certs/id.rsa', '-in', 'safari-certs/tmp.dat', '-out', 'safari-certs/tmp.sig' ], + [ xar, '-f', safariextz, '--inject-sig', 'safari-certs/tmp.sig' ] + ], function() { + fs.remove('safari-certs/tmp.dat'); + fs.remove('safari-certs/tmp.sig'); + fs.changeWorkingDirectory('..'); + console.log('Built ' + safariextz.substr(3)); + return program_counter.end(0); + }); + } + + +} + +function build_firefox() { + + var when_string = { + 'early' : 'start', + 'middle': 'ready', + 'late' : 'end' + }; + + // Copy scripts into place: + fs.removeTree('build/Firefox/data'); // PhantomJS won't list dangling symlinks, so we have to just delete the directory and recreate it + fs.makeDirectory('build/Firefox/data'); + + var contentFiles = settings.contentScriptFiles.concat( settings.contentStyleFiles || [] ); + + contentFiles.forEach(function(file) { + makeTreeSymbolicLink( file, 'build/Firefox/data/' + file ); + }); + + // Create settings.js: + fs.write( + 'build/Firefox/lib/settings.js', + 'exports.include = [' + + settings.match_domains.map(function(domain) { + return ( + settings.match_secure_domain + ? '"http://' + domain + '/*","https://' + domain + '/*"' + : '"http://' + domain + '/*"' + ) + }).join(',') + + '];\n' + + 'exports.contentScriptWhen = "' + when_string[settings.contentScriptWhen] + '";\n' + + 'exports.contentScriptFile = ' + JSON.stringify(settings.contentScriptFiles) + ";\n" + + 'exports.contentStyleFile = ' + JSON.stringify(settings.contentStyleFiles || []) + ";\n" + , + 'w' + ); + + // Create package.json and copy icons into place: + var pkg = { + "description": settings.description, + "license": settings.license, + "author": settings.author, + "version": settings.version, + "title": settings.title, + "id": settings.id, + "name": settings.name + }; + if (settings.icons[48] ) { pkg.icon = settings.icons[48]; makeTreeSymbolicLink( pkg.icon , 'build/Firefox/'+pkg.icon ); } + if (settings.icons[64] ) { pkg.icon_64 = settings.icons[64]; makeTreeSymbolicLink( pkg.icon_64, 'build/Firefox/'+pkg.icon_64 ); } + if (settings.preferences) { pkg.preferences = settings.preferences; } + fs.write( 'build/Firefox/package.json', JSON.stringify(pkg, null, ' ' ) + "\n", 'w' ); + + program_counter.begin(); + + // Check whether the Addon SDK is up-to-date: + var page = webPage.create(); + page.onResourceError = function(resourceError) { + console.log('Unable to load resource (' + resourceError.url + ')'); + console.log('Error code: ' + resourceError.errorCode + '. Description: ' + resourceError.errorString); + return program_counter.end(1); + }; + page.onResourceReceived = function(response) { + if ( fs.exists('build/firefox-addon-sdk-url.txt') && fs.read('build/firefox-addon-sdk-url.txt') == response.redirectURL ) { + console.log( 'Firefox Addon SDK is up-to-date.' ); + build_xpi(); + } else { + console.log( 'Downloading Firefox Addon SDK...' ); + page.openBinary( response.redirectURL, { out_file: 'temporary_file.tar.gz' }, function() { + fs.makeDirectory('build/firefox-addon-sdk'); + childProcess.execFile( 'tar', ["zxf",'temporary_file.tar.gz','-C','build/firefox-addon-sdk','--strip-components=1'], null, function(err,stdout,stderr) { + fs.remove('temporary_file.tar.gz'); + fs.write( 'build/firefox-addon-sdk-url.txt', response.redirectURL, 'w' ); + build_xpi(); + }); + }); + } + page.stop(); // TODO: check which of these two we need + page.close(); + }; + page.openUrl('https://ftp.mozilla.org/pub/mozilla.org/labs/jetpack/addon-sdk-latest.tar.gz', 'HEAD', page.settings); + + // Build the .xpi file: + function build_xpi() { + if ( system.os.name == 'windows' ) { + // TODO: fill in real Windows values here (the following line is just a guess): + childProcess.execFile( 'cmd' , [ 'cd build\firefox-addon-sdk ; bin\activate ; cd ../Firefox ; cfx xpi'], null, finalise_xpi ); + } else { + childProcess.execFile( 'bash', ['-c','cd build/firefox-addon-sdk && source bin/activate && cd ../Firefox && cfx xpi'], null, finalise_xpi ); + } + } + + // Move the .xpi into place, fix its install.rdf, and update firefox-unpacked: + function finalise_xpi(err, stdout, stderr) { + var xpi = 'out/' + settings.name + '.xpi'; + if ( fs.exists(xpi) ) fs.remove(xpi); + fs.list('build/Firefox').forEach(function(file) { if ( file.search(/\.xpi$/) != -1 ) fs.move( 'build/Firefox/' + file, xpi ); }); + fs.removeTree('build/firefox-unpacked'); + fs.makeDirectory('build/firefox-unpacked'); + childProcess.execFile( 'unzip', ['-d','build/firefox-unpacked',xpi], null, function(err,stdout,stderr) { + fs.write( + 'build/firefox-unpacked/install.rdf', + fs.read('build/firefox-unpacked/install.rdf').replace( /.*<\/em:maxVersion>/, '' + settings.firefox_max_version + '' ) + ); + contentFiles.forEach(function(file) { + fs.remove('build/firefox-unpacked/resources/'+settings.name+'/data/'+file); + makeTreeSymbolicLink( file, 'build/firefox-unpacked/resources/'+settings.name+'/data/'+file ) + }); + fs.changeWorkingDirectory('build/firefox-unpacked'); + childProcess.execFile( 'zip', ['../../'+xpi,'install.rdf'], null, function(err,stdout,stderr) { + fs.changeWorkingDirectory('../..'); + if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(1); } + console.log('Built ' + xpi + '\n\033[1mRemember to restart Firefox if you added/removed any files!\033[0m'); + return program_counter.end(0); + }); + }); + } + +} + +function build_chrome() { + + var when_string = { + 'early' : 'document_start', + 'middle': 'document_end', + 'late' : 'document_idle' + }; + + var match_urls = settings.match_domains.map(function(domain) { + return ( settings.match_secure_domain ? "*://" : "http://" ) + domain + '/*'; + }); + + var manifest = { + "short_name": settings.short_title || settings.title, + "name": settings.title, + "author": settings.author, + "version": settings.version, + "manifest_version": 2, + "description": settings.description, + "background": { + "scripts": ["background.js"] + }, + "content_scripts": [ + { + "matches": match_urls, + "js": settings.contentScriptFiles, + "run_at": when_string[settings.contentScriptWhen], + // Chrome defaults to only loading in the main frame, Firefox can only load in all frames. + // Not sure which behaviour is better, but this at least makes it standard across browsers: + "all_frames": true + } + ], + "icons": settings.icons, + "permissions": match_urls.concat([ + "contextMenus", + "tabs", + "history", + "notifications" + ]) + }; + + var contentFiles = settings.contentScriptFiles.concat( + Object.keys(settings.icons).map(function(key) { return settings.icons[key] }) + ); + if ( settings.contentStyleFiles ) { + manifest.content_scripts[0].css = settings.contentStyleFiles; + contentFiles = contentFiles.concat( settings.contentStyleFiles ); + } + + + var extra_files = []; + + if ( settings.xhr_patterns ) + manifest.permissions = settings.xhr_patterns.concat(manifest.permissions); + + if ( settings.preferences ) { + manifest.options_page = "options.html"; + manifest.permissions.push('storage'); + manifest.background.scripts.unshift('preferences.js'); + extra_files.push('build/Chrome/'+manifest.background.scripts[0]); + extra_files.push('build/Chrome/'+manifest.options_page); + + fs.list('build/Chrome').forEach(function(file) { + if ( file[0] == '.' ) return; + if ( file.search( /^(?:background\.js|chrome-bootstrap\.css|options\.js)$/ ) == 0 ) return; + if ( fs.isDirectory(file) ) + fs.removeTree('build/Chrome/' + file); + else + fs.remove ('build/Chrome/' + file); + }); + + if ( settings.autoReload ) manifest.permissions.push('webNavigation'); + + fs.write( + 'build/Chrome/' + manifest.background.scripts[0], + "var default_preferences = {" + + settings.preferences.map(function(preference) { + switch ( preference.type ) { + case 'bool' : return "'" + preference.name + "':" + (preference.value?'true':'false'); + case 'boolint': return "'" + preference.name + "':" + (preference.value?'1' :'0' ); + default : return "'" + preference.name + "':" + JSON.stringify(preference.value); + } + }).join(', ') + + "};\n" + + "var auto_reload = " + (settings.autoReload == 'timeout') + ";\n", + 'w' + ); + + fs.write( + 'build/Chrome/' + manifest.options_page, + "\n" + + "\n" + + "" + settings.title + " Options\n" + + '\n' + + '\n' + + '
\n' + + '
\n' + + '

' + settings.title + '

\n' + + '
\n' + + settings.preferences.map(function(preference) { + switch ( preference.type ) { + case 'bool' : return '
\n'; + case 'boolint': return '
\n'; + case 'integer': return '
\n'; + case 'string': return '
\n'; + case 'menulist': + return '
' + preference.title + ':
' + ; + case 'radio': + return '

' + preference.title + '

' + + preference.options.map(function(option,index) { + return '
' + }).join('') + '
'; + } + }).join('') + + + '
\n' + + '
\n' + // no buttons because we apply changes on click, but the padding makes the page look better + '
\n' + + '
\n' + + "\n" + + "\n" + + "\n", + 'w' + ); + + } + + // Create manifest.json: + fs.write( 'build/Chrome/manifest.json', JSON.stringify(manifest, null, '\t' ) + "\n", 'w' ); + + // Copy scripts and icons into place: + contentFiles.forEach(function(file) { makeTreeHardLink( file, 'build/Chrome/' + file ) }); + + program_counter.begin(); + + // Create a Chrome key: + if (fs.exists('build/Chrome.pem')) { + build_crx(); + } else { + childProcess.execFile(chrome_command, ["--pack-extension=build/Chrome"], null, build_crx ); + }; + + // Build the .crx, move it into place, and build the upload zip file: + function build_crx() { + childProcess.execFile(chrome_command, ["--pack-extension=build/Chrome","--pack-extension-key=build/Chrome.pem"], null, function (err, stdout, stderr) { + if ( stdout != 'Created the extension:\n\nbuild/Chrome.crx\n' ) console.log(stdout.replace(/\n$/,'')); + var crx = 'out/' + settings.name + '.crx'; + if ( fs.exists(crx) ) fs.remove(crx); + fs.move( 'build/Chrome.crx', crx ); + console.log('Built ' + crx); + if ( fs.exists('out/chrome-store-upload.zip') ) fs.remove('out/chrome-store-upload.zip'); + childProcess.execFile( + 'zip', + ['out/chrome-store-upload.zip','build/Chrome/background.js','build/Chrome/manifest.json'] + .concat( extra_files ) + .concat( contentFiles.map(function(file) { return 'build/Chrome/'+file }) ) + , + null, + function(err,stdout,stderr) { + console.log('Built out/chrome-store-upload.zip'); + return program_counter.end(0); + } + ); + }); + }; + +} + + +/* + * RELEASE COMMANDS + */ + +function release_amo(login_info) { + + program_counter.begin(); + if ( !login_info.password ) { + if ( system.env.hasOwnProperty('AMO_PASSWORD') ) { + login_info.password = system.env.AMO_PASSWORD; + } else { + console.log("Please specify a password for addons.mozilla.org"); + return program_counter.end(1); + } + } + + var name = settings.name.substr(0,30); + + page( 'https://addons.mozilla.org/en-US/developers/addon/' + name + '/edit', function(page) { get_changelog(function(changelog) { + + page.evaluate( function() { Tabzilla.disableEasterEgg() }); + + page.submit_form( + "#login-submit", + { + "#id_username": login_info.username, + "#id_password": login_info.password, + } + ); + + page.waitForElementsPresent( + [ '#edit-addon-basic a.button', '#edit-addon-media a.button' ], + function() { + setTimeout(function() { + + function submit_section(section, values) { + // Doing the AJAX request manually turns out less hassle than clicking the buttons: + page.evaluate(function(addon, section, values) { + $.ajax({ + async: false, + url: 'https://addons.mozilla.org/en-US/developers/addon/' + addon + '/edit_' + section + '/edit', + dataType: 'html', + success: function(html) { + var data = {}; + $(html).find('[name]').each(function() { + if ( $(this).filter(':radio,:checkbox').length ) { + if ( $(this).prop('checked') ) { + if ( !data.hasOwnProperty($(this).attr('name')) ) + data[ $(this).attr('name') ] = [ $(this).val() ]; + else + data[ $(this).attr('name') ].push( $(this).val() ); + } + } else { + data[ $(this).attr('name') ] = $(this).val(); + }; + }); + Object.keys(values).forEach(function(key) { + data[key] = values[key]; + }); + $.ajax({ + async: false, + type: "POST", + url: 'https://addons.mozilla.org/en-US/developers/addon/' + addon + '/edit_' + section + '/edit', + headers: { 'X-CSRFToken': $('meta[name=csrf]').attr('content') }, + data: data, + dataType: 'html', + //success: function(html) { console.log(html) }, + traditional: true + }); + } + }); + }, name, section, values); + } + + submit_section( 'basic', { + 'form-INITIAL_FORMS': 1, + 'form-MAX_NUM_FORMS': 1000, + 'form-TOTAL_FORMS' : 1, + 'name_en-us' : settings.title, + 'slug' : settings.name.substr(0,30), + 'summary_en-us' : settings.description, + }); + + submit_section( 'details', { + 'description_en-us': settings.long_description + }); + + var best_icon = settings.icons[64] || settings.icons[128] || settings.icons[32] || settings.icons[48] || settings.icons[16]; + if ( best_icon ) { + page.click('#edit-addon-media a.button'); + page.waitForElementsPresent( + [ '#id_icon_upload', '.edit-media-button.listing-footer button' ], + function() { + setTimeout(function() { + page.submit_form( + '#id_icon_upload', + { + '#id_icon_upload': best_icon + }, + function() { + setTimeout(function() { + page.click('.edit-media-button.listing-footer button'); + page.waitForElementsNotPresent('#id_icon_upload', function() { + release_new_version(); + }) + }, 2000); + } + ); + }, 2000 ); + } + ) + } else { + release_new_version(); + }; + + }, 1000 ); + + } + ); + + function release_new_version() { + page.open( 'https://addons.mozilla.org/en-US/developers/addon/' + name + '/versions#version-upload', function() { + page.submit_form( + '#upload-addon', + { + '#upload-addon': 'out/' + settings.name + '.xpi' + }, + function() { + page.waitForElementsPresent( + '#upload-status-results.status-pass', + function() { + page.click('#upload-file-finish'); + page.submit_form( + '.listing-footer button[type="submit"]', + { + '#id_releasenotes_0': changelog + }, + function() { + page.waitForElementsPresent( + '.notification-box.success', + function() { + console.log('Released to https://addons.mozilla.org/en-US/firefox/addon/' + name); + return program_counter.end(0); + } + ); + } + ); + } + ); + } + ); + }); + } + })}); + +} + +function release_chrome(login_info) { + + program_counter.begin(); + if ( !login_info.password ) { + if ( system.env.hasOwnProperty('CHROME_PASSWORD') ) { + login_info.password = system.env.CHROME_PASSWORD; + } else { + console.log("Please specify a password for the Chrome store"); + return program_counter.end(1); + } + } + + page( 'https://chrome.google.com/webstore/developer/edit/' + login_info.id, function(page) { + + page.hideConsoleMessage(); + + page.submit_form( + "#signIn", + { + "#Email" : login_info.username, + "#Passwd": login_info.password, + }, + function() { change_details(page) } + ); + + }); + + function change_details(page) { + + if ( settings.icons[128] ) { + page.click(".id-upload-icon-image"); + setTimeout(function() { + page.submit_form( + '.id-upload-image.cx-bold', + { + '#cx-img-uploader-input': settings.icons[128] + }, + function() { + page.waitForElementsPresent( + [ 'b#cx-error-html' ], + function() { + setTimeout( publish_details, 1000 ); + } + ); + } + ); + }, 100 ); + } else { + publish_details(); + } + + function publish_details() { + page.submit_form( + '.id-publish', + { + '#cx-dev-edit-desc': settings.description + }, + function() { + setTimeout(function() { + page.click('.id-confirm-dialog-publish-ok'); + page.waitForElementsPresent( + [ '#hist_state' ], + get_access_code + ) + }, 100 ); + } + ); + } + + }; + + function get_access_code() { + + page( + 'https://accounts.google.com/o/oauth2/auth?response_type=code&scope=https://www.googleapis.com/auth/chromewebstore&redirect_uri=urn:ietf:wg:oauth:2.0:oob&client_id=' + login_info.client_id, + function(page) { + + page.waitForElementsPresent( + '#submit_approve_access', + function() { + page.hideConsoleMessage(); + setTimeout( + function() { + page.click('#submit_approve_access'); + page.waitForElementsPresent( + '#code', + function() { + var code = page.evaluate(function() { return document.getElementById('code').value }); + var post_data = "grant_type=authorization_code&redirect_uri=urn:ietf:wg:oauth:2.0:oob&client_id=" + login_info.client_id + "&client_secret=" + login_info.client_secret + "&code=" + code; + page.openBinary( 'https://accounts.google.com/o/oauth2/token', { data: post_data }, upload_and_publish ); + } + ) + }, + 3000 + ); + } + ); + + } + ); + + function upload_and_publish(err, data) { + + data = JSON.parse(data); + + var page = webPage.create(); + page.customHeaders = { + "Authorization": "Bearer " + data.access_token, + "x-goog-api-version": 2, + }; + + page.open( + "https://www.googleapis.com/upload/chromewebstore/v1.1/items/" + login_info.id, + 'PUT', + fs.open('out/chrome-store-upload.zip', 'rb').read(), + function (status) { + if ( status == "success" ) { + var result = JSON.parse(page.plainText); + if ( result.error ) { + console.log( page.plainText ); + return program_counter.end(1); + } + page.open( + "https://www.googleapis.com/chromewebstore/v1.1/items/" + login_info.id + "/publish", + 'POST', + '', + function (status) { + if ( result.error ) { + console.log( page.plainText ); + return program_counter.end(1); + } + if ( status == "success" ) { + console.log('Released to https://chrome.google.com/webstore/detail/' + login_info.id); + return program_counter.end(0); + } else { + console.log( "Couln't upload new version" ); + return program_counter.end(1); + } + } + ); + } + } + ); + + } + + } + +} + +function release_opera(login_info) { + + program_counter.begin(); + if ( !login_info.password ) { + if ( system.env.hasOwnProperty('OPERA_PASSWORD') ) { + login_info.password = system.env.OPERA_PASSWORD; + } else { + console.log("Please specify a password for the Opera Developer site"); + return program_counter.end(1); + } + } + + get_changelog(function(changelog) { + + page( 'https://addons.opera.com/en-gb/developer/upgrade/' + settings.name, function(page) { + + page.submit_form( + 'button[type="submit"]', + { + "#login-page-username": login_info.username, + "#login-page-password": login_info.password, + } + ); + + page.submit_form( + '.submit-button', + { + '#id_package_file': 'out/' + settings.name + '.crx' + }, + function() { + page.submit_form( + '.submit-button', + { + '#id_translations-0-short_description': settings.description, + '#id_translations-0-long_description' : settings.long_description, + '#id_translations-0-changelog' : changelog, + '#id_target_platform-comment' : login_info.tested_on, + '#id_icons-0-icon' : settings.icons[64] ? settings.icons[64] : undefined, + }, + function() { + page.click('input.submit-button[type="submit"][name="approve_widget"]'); + page.waitForElementsPresent( + [ '#dev-sel-container' ], + function() { + console.log('Released to https://addons.opera.com/en-gb/extensions/details/' + settings.name); + return program_counter.end(0); + } + ); + } + ); + } + ); + }); + + }); + +} + +function release_safari() { + console.log( + 'The Safari extensions gallery just links to your actual download site.\n' + + 'This function is included only for completeness' + ); +} + +/* + * MAINTAIN COMMANDS + */ + +function maintain() { + + program_counter.begin(); + + function maintain_resources() { + update_settings(); + if ( settings.resources ) { + stat( [ 'lib/BabelExtResources.js' ].concat( settings.resources ), function(files) { + var resources_file = files.shift(); + if ( files.filter(function(file) { return file.modified > resources_file.modified } ).length ) { + console.log( 'Rebuilding lib/BabelExtResources.js' ); + build_resources(); + } + maintain_content_files(); + }); + } else { + maintain_content_files(); + } + } + + function maintain_content_files() { + var files = settings.contentScriptFiles.concat( settings.contentStyleFiles || [] ); + files = files.concat( + files.map(function(name) { return 'build/Chrome/' + name }) + ).concat( + files.map(function(name) { return 'build/Safari.safariextension/' + name }) + ); + + stat( files, function(files) { + var id_links = {}, name_links = {}; // list of file IDs that are valid hardlink targets + files.forEach(function(file) { + if ( file.name.search( '^build/' ) == -1 ) { + // source file - set hardlink target + id_links[ file.id ] = file; + name_links[ file.name ] = file; + } else if ( !id_links.hasOwnProperty(file.id) ) { + var source = name_links[ file.name.replace( /^build\/(?:[^\/]+)\//, '' ) ]; + // need to recreate + if ( file.modified > source.modified ) { + console.log( file.name + ' is newer than ' + source.name + ' - please save the built contents back to the original' ); + } else { + console.log( 'Relinking ' + file.name + ' to ' + source.name ); + fs.remove( file.name ); + hardLink( source.name, file.name ); + } + } + }); + + }); + } + + maintain_resources(); + setInterval(maintain_resources, settings.maintenanceInterval ); + +} + +/* + * MAIN SECTION + */ + +var args = system.args; + +function usage() { + console.log( + settings.name + ' v' + settings.version + ' (built on BabelExt)\n' + + 'Usage: ' + args[0] + ' \n' + + 'Commands:\n' + + ' build - builds extensions for "amo" (Firefox) "chrome" or "safari"\n' + + ' release - release extension to "amo" (addons.mozilla.org) "chrome" (Chrome store), "opera" (opera site) or "safari" (extensions gallery)\n' + + ' maintain - keep various files up-to-date' + ); + phantom.exit(1); +} + +program_counter.begin(); + +switch ( args[1] || '' ) { + +case 'build': + if ( args.length != 3 ) usage(); + build_resources(); + settings.contentScriptFiles.splice(1, 0, 'lib/BabelExtResources.js'); + switch ( args[2] ) { + case 'firefox': build_firefox(local_settings. amo_login_info); break; + case 'chrome' : build_chrome (local_settings.chrome_login_info); break; + case 'safari' : build_safari (local_settings.safari_login_info); break; + default : console.log( "Please specify 'firefox', 'chrome' or 'safari', not '" + args[2] + "'" ); break; + } + break; + +case 'release': + fs.makeDirectory('out'); + if ( args.length != 3 ) usage(); + switch ( args[2] ) { + case 'amo' : release_amo (local_settings. amo_login_info); break; + case 'chrome': release_chrome(local_settings.chrome_login_info); break; + case 'opera' : release_opera (local_settings. opera_login_info); break; + case 'safari': release_safari(local_settings.safari_login_info); break; + default : console.log( "Please specify 'amo', 'chrome', 'opera' or 'safari', not '" + args[2] + "'" ); break; + } + break; + +case 'maintain': + + maintain(); + break; + +default: + usage(); + +} +program_counter.end(0); diff --git a/script/build.sh b/script/build.sh new file mode 100755 index 0000000..52f6d70 --- /dev/null +++ b/script/build.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +get_password() { + echo -n "Password for $1: " + stty -echo + trap 'stty echo' EXIT + read "${2}_PASSWORD" + stty echo + trap - EXIT + echo +} + +if test "x$1" = "xrelease" +then + if grep -q chrome_login_info conf/local_settings.json && test -z "$CHROME_PASSWORD" + then + get_password chrome.google.com CHROME + fi + export CHROME_PASSWORD + + if grep -q amo_login_info conf/local_settings.json && test -z "$AMO_PASSWORD" + then + get_password addons.mozilla.org AMO + fi + export AMO_PASSWORD + + if grep -q opera_login_info conf/local_settings.json && test -z "$OPERA_PASSWORD" + then + get_password developer.opera.com OPERA + fi + export OPERA_PASSWORD +fi + +exec /usr/bin/env phantomjs --ssl-protocol=any ./script/build.js "$@" diff --git a/sink.html b/sink.html index ac2637c..83e990f 100644 --- a/sink.html +++ b/sink.html @@ -33,16 +33,15 @@ - +

BabelExt Kitchen Sink Demos

This demonstration requires that you have the BabelExt extension installed in your browser. You can always acquire and compile the very latest from the Github repo, but you can also download compiled versions here for each browser: @@ -79,6 +78,76 @@

After installing the extension, refresh this page to see the features that B

+
+
+ + Save a key in memory... + +
+ + +
+
+ + +
+ +
+
+
+
+ + Load a key... + +
+ + +
+
+ +
+
+
+
+ + Save a preference... + +
+ +
+
+ + +
+ +
+
+
+
+ + Load a preference... + +
+ +
+
+ +
+
@@ -137,4 +206,4 @@

After installing the extension, refresh this page to see the features that B

- \ No newline at end of file + diff --git a/src/extension.css b/src/extension.css new file mode 100644 index 0000000..f93ec48 --- /dev/null +++ b/src/extension.css @@ -0,0 +1,3 @@ +/* + * CSS used in your extension + */ \ No newline at end of file diff --git a/src/extension.js b/src/extension.js new file mode 100644 index 0000000..7cfa522 --- /dev/null +++ b/src/extension.js @@ -0,0 +1,363 @@ +// ==UserScript== +// @name BabelExt Extension +// @namespace http://babelext.com/ +// @description An extension built with BabelExt +// @copyright 2013, Steve Sobel (http://babelext.com/) +// @author honestbleeps +// @include http://babelext.com/* +// @version 0.95 +// ==/UserScript== + +/* + * extension.js - contains your code to be run on page load + * + */ +(function(u) { + // Any code that follows will run on document ready... + + /* + * GREASEMONKEY COMPATIBILITY SECTION - you can remove these items if you're not porting a GM script. + * + * WARNING: GM_setValue, GM_getValue, GM_deleteValue are NOT ideal to use! These are only present + * for the sake of easy porting/compatibility, but they will use localStorage, which can easily be erased + * by the user if he/she clears cookies or runs any "privacy" software... + * + * Ideally, you should update any Greasemonkey scripts you want to port to perform asynchronous calls to + * BabelExt.storage.get and .set instead of using localStorage, but a simple replacement won't work due + * to the asynchronous nature of extension-based localStorage (and similar) calls. + * + */ + GM_xmlhttpRequest = BabelExt.xhr; + GM_setValue = localStorage.setItem; + GM_getValue = localStorage.getItem; + GM_deleteValue = localStorage.removeItem; + GM_log = console.log; + GM_openInTab = BabelExt.tabs.create; + // NOTE: you'll want to add a render() function call at the end of your GM script for compatibility. + GM_addStyle = BabelExt.css.add; + + /* BEGIN KITCHEN SINK DEMO CODE */ + + /* + * The code below is for testing / execution on the BabelExt website at: + * http://babelext.com/demo.html + * + * You should remove this code, and replace it with your own! + * + */ + // hide the "install BabelExt" message, and show the kitchen sink demos... + var installBabelExt = document.getElementById('installBabelExt'); + var container = document.getElementById('container'); + if (installBabelExt && container) { + container.style.display = 'block'; + installBabelExt.style.display = 'none'; + } + + // show each available kitchen sink demo, let the user know the extension is out of date if there's an unrecognized demo... + var features = document.body.querySelectorAll('.featureContainer'); + var recognizedFeatures = ['save','load','savePref','loadPref','tabCreate','notificationCreate','historyAdd', 'cssAdd']; + for (var i=0, len=features.length; i + + + + Test Page + + + + +
+
+
+ + + + + diff --git a/test/tests.js b/test/tests.js new file mode 100644 index 0000000..05add0b --- /dev/null +++ b/test/tests.js @@ -0,0 +1,30 @@ +/** + * Evaluate JavaScript as ContentScript + * @param {string} js Scripnt to eval + * + * The easiest way to manipulate your contentscript + * (and sometimes the only way that doesn't involve nasty code changes) + * is to pass some JavaScript across and eval() it on the other side. + * This is nasty and insecure, which is one reason why test JS should + * be configured out before compiling your final extension. + */ +function eval_js_as_contentscript(js) { + var dispatcher = document.getElementById('rendezvous'); + dispatcher.setAttribute( 'data-args', JSON.stringify(js) ); + dispatcher.click(); +} + +/** + * Get values from BabelExt + * @return {string} + * + * By default, BabelExt is mocked by the test helper. + * Data is stored in an attribute so we can get it back. + */ +function get_stored_values() { + return document.getElementById('rendezvous').getAttribute( 'data-stored-values' ); +} + +QUnit.test( "My test", function( assert ) { + run_js_in_contentscript_context( 'my_func()' ); +});