diff --git a/README.md b/README.md index e45e619..042084f 100644 --- a/README.md +++ b/README.md @@ -1,80 +1,6 @@ # My Style -[My Style](https://chrome.google.com/webstore/detail/my-style/ljdhjpmbnkbengahefamnhmegbdifhlb) -is a Google Chrome extension that allows you to insert custom CSS into pages, -immediately see the visual results, and have that CSS persist for future -visits. -## Usage -Take TechCrunch, for example. You may dislike the site's design and want to add -your own touch--that is, your own style--to make it aesthetically pleasing. -After installing the extension, visit [TechCrunch](http://techcrunch.com) and -hit control + m in your browser. You should see a textarea to the right: +I've found the `my-style` [extension](https://github.com/karthikv/my-style) very pleasant to use, apart from a few issues. +This repo contains a modified version with my own fixes to the original extension. -![Techcrunch textarea](https://raw.github.com/karthikv/my-style/master/screenshots/techcrunch-textarea.png) - -Insert your custom CSS and see results: - -![Techcrunch restyled](https://raw.github.com/karthikv/my-style/master/screenshots/techcrunch-restyled.png) - -(Note: if you'd like this specific Techcrunch skin, it's available at -[styles/techcrunch.css](https://github.com/karthikv/my-style/blob/master/styles/techcrunch.css).) - -This CSS will be reinserted upon revisiting Techcrunch, thereby saving your -styles. Pressing control + m once more will hide the textarea, but your -contents will still be retained. - -To speed up the process of styling, if you hold alt while clicking an element, -a selector for that element will automatically be appended to the textarea -editor. With this, you can circumvent the time it takes to inspect the element -to find how to target it. - -## Installation -You may install My Style at the -[Chrome Web Store](https://chrome.google.com/webstore/detail/my-style/ljdhjpmbnkbengahefamnhmegbdifhlb). -Just click the 'Add to Chrome' button. - -## How it works -My Style is quite simple. It inserts a textarea into the page, looks for -changes to the textarea's contents, and updates a dynamically inserted style -tag appropriately. My Style employs local storage to make CSS changes -persistent. It uses simple technologies for a simple result. - -## Improvements -Sites that clear local storage can erase My Style's custom CSS. A more -persistent form of storage (e.g. the JavaScript FileSystem APIs) would be -a better option to retain CSS. - -## Contributors -### Karthik Viswanathan -- GitHub: [@karthikv](https://github.com/karthikv) -- Twitter: [@karthikvnet](https://twitter.com/karthikvnet) -- Website: [http://karthikv.net](http://karthikv.net) -- Email: me@karthikv.net - -## Safari extension -If you use Safari, consider downloading Luke Hagan's Safari port of My Style. -It can be found at [lhagan/my-style](https://github.com/lhagan/my-style). - -## License -(The MIT License) - -Copyright (c) 2013 Karthik Viswanathan <me@karthikv.net> - -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. +The main change is use of **chrome extension local storage** rather than **per-page local storage**. This means that the styles can sync with a Google account and work in incognito mode. \ No newline at end of file diff --git a/extension/manifest.json b/extension/manifest.json index 533b9a1..d275214 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -4,6 +4,7 @@ "name": "My Style", "description": "Insert custom CSS into pages, immediately see the visual results, and have that CSS persist for future visits.", "version": "0.3", + "permissions": ["storage"], "content_scripts": [{ "matches": ["http://*/*", "https://*/*"], diff --git a/extension/my-style.css b/extension/my-style.css index a005f81..542b5cc 100644 --- a/extension/my-style.css +++ b/extension/my-style.css @@ -1,31 +1,32 @@ /* Textarea for custom styles. Make all styles !important to prevent * site's stylesheet from overriding. */ #my-style-input { - /* Position on right side, taking up entire browser height. */ - position: fixed !important; - top: 0 !important; - right: 0 !important; - width: 300px !important; - height: 100% !important; + /* Position on right side, taking up entire browser height. */ + position: fixed !important; + top: 0 !important; + right: 0 !important; + width: 300px !important; + height: 100% !important; - /* Should be visible above all other elements. */ - z-index: 5000000 !important; - overflow: auto !important; - outline: none !important; - padding: 10px 20px !important; + /* Should be visible above all other elements. */ + z-index: 5000000 !important; + overflow: auto !important; + outline: none !important; + padding: 10px 20px !important; + visibility: hidden; - border-top: 0 !important; - border-bottom: 0 !important; - border-right: 0 !important; - border-left: 1px solid #ccc !important; + border-top: 0 !important; + border-bottom: 0 !important; + border-right: 0 !important; + border-left: 1px solid #ccc !important; - /* Dark code font with light background. */ - color: #222 !important; - background: #fcfcfc !important; - font: 13px "Inconsolata", "Consolas", "Menlo", "Monaco", - "Lucida Console", "Courier New", "Courier", monospace !important; - - /* Maintain left-to-right direction even on right-to-left websites. */ - direction: ltr !important; - text-align: left !important; + /* Dark code font with light background. */ + color: #222 !important; + background: #fcfcfc !important; + font: 13px "Fira Code", "Inconsolata", "Consolas", "Menlo", "Monaco", + "Lucida Console", "Courier New", "Courier", monospace !important; + + /* Maintain left-to-right direction even on right-to-left websites. */ + direction: ltr !important; + text-align: left !important; } diff --git a/extension/my-style.js b/extension/my-style.js index e9e0a4e..1239a70 100644 --- a/extension/my-style.js +++ b/extension/my-style.js @@ -1,15 +1,15 @@ // asynchronous self-invoking function to not pollute global namespace (function(window, document, undefined) { - var TAB_KEY_CODE = 9; - var M_KEY_CODE = 77; + var TAB_KEY_CODE = 9; + var M_KEY_CODE = 77; - var SOFT_TAB = ' '; - var SOFT_TAB_LENGTH = SOFT_TAB.length; + var SOFT_TAB = " "; + var SOFT_TAB_LENGTH = SOFT_TAB.length; - var ONLY_WHITESPACE_REGEX = /^\s*$/; - var WHITESPACE_SPLIT_REGEX = /\s+$/g; + var ONLY_WHITESPACE_REGEX = /^\s*$/; + var WHITESPACE_SPLIT_REGEX = /\s+$/g; - /* Throttle the given function, condensing multiple calls into one call after + /* Throttle the given function, condensing multiple calls into one call after * the given timeout period. In other words, allow at most one call to go * through per timeout period. Returns the throttled function. * @@ -17,158 +17,198 @@ * fn -- the function to throttle * timeout -- the timeout to throttle for */ - function throttle(fn, timeout) { - return function throttledFn() { - if (!throttledFn.timer) { - // keep track of the arguments to the function and the context - var args = arguments; - var that = this; - - // call the function after the provided timeout - throttledFn.timer = setTimeout(function() { - fn.apply(that, args); - - // finished calling the function; unset the timer - throttledFn.timer = undefined; - }, timeout); - } - }; - } - - /* Remove whitespace on the edges of this string. */ - String.prototype.trim = function() { - return this.replace(/(^\s+|\s+$)/g, ''); - }; - - window.addEventListener('DOMContentLoaded', function(event) { - var head = document.getElementsByTagName('head')[0]; - var body = document.body; - - var style = document.createElement('style'); - var textarea = document.createElement('textarea'); - - // hide textarea by default - textarea.style.display = 'none'; - textarea.id = 'my-style-input'; - textarea.spellcheck = false; - - head.appendChild(style); - body.appendChild(textarea); - - style.innerHTML = localStorage.myStyle || ''; - textarea.value = style.innerHTML; - textarea.placeholder = '/* Enter your styles here. */'; - - // alt + click on an element adds its selector to the textarea - body.addEventListener('click', function(event) { - // ensure textarea is actually displayed - if (textarea.style.display.indexOf('none') === -1 && - event.target.id !== textarea.id && event.altKey) { - var i = 0; - var target = event.target; - var elemClass = target.className.split(' ') || ''; - var stylesList = []; - var existingStyles = ''; - var selector = ''; - var cssStatement; - var textToAdd; - - // selector starts with the tag - selector += target.tagName.toLowerCase(); - - // include ID if there is one - if (target.id) { - selector += '#' + target.id; - } + function throttle(fn, timeout) { + return function throttledFn() { + if (!throttledFn.timer) { + // keep track of the arguments to the function and the context + var args = arguments; + var that = this; + + // call the function after the provided timeout + throttledFn.timer = setTimeout(function() { + fn.apply(that, args); + + // finished calling the function; unset the timer + throttledFn.timer = undefined; + }, timeout); + } + }; + } - // include all classes found - for (i = 0; i < elemClass.length; i++) { - if (!ONLY_WHITESPACE_REGEX.test(elemClass[i])) { - selector += '.' + elemClass[i]; - } - } + /* Convenience wrappers for chrome local storage API + * Use 'chrome.storage.local' to disable chrome syncing + */ + function localStore(key, value) { + let store = {}; + store[key] = value; + chrome.storage.sync.set(store); + } + async function localLoad(key) { + return new Promise((resolve, reject) => { + chrome.storage.sync.get(key, items => { + if (chrome.runtime.lastError) { + reject(); + } else { + resolve(items[key]); + } + }); + }); + } - // fill CSS with styles defined in the style attribute - if (target.getAttribute('style')) { - stylesList = target.getAttribute('style').split(';'); - - // keep track of CSS properties already defined in style attribute - for (i = 0; i < stylesList.length; i++) { - // condense mutliple whitespace into one space - cssStatement = stylesList[i].split(WHITESPACE_SPLIT_REGEX) - .join(' ').trim(); - - if (!ONLY_WHITESPACE_REGEX.test(cssStatement)) { - existingStyles += SOFT_TAB + cssStatement.toLowerCase() + ";\n"; - } - } - } - - // construct text to add to textarea - if (selector) { - // add existing styles in braces - if (existingStyles) { - existingStyles = "{\n" + existingStyles + "}"; - } else { - existingStyles = "{\n\n}"; - } - - textToAdd = '\n' + selector + ' ' + existingStyles; - textarea.value += textToAdd; - - // highlight added text for easy removal - textarea.focus(); - textarea.setSelectionRange(textarea.value.length - textToAdd.length, - textarea.value.length); - } + /* Remove whitespace on the edges of this string. */ + String.prototype.trim = function() { + return this.replace(/(^\s+|\s+$)/g, ""); + }; - event.preventDefault(); - } - }); + window.addEventListener("DOMContentLoaded", async function(event) { + var head = document.getElementsByTagName("head")[0]; + var body = document.body; - /* Save styles persistently in local storage. */ - var saveStyles = throttle(function() { - localStorage.myStyle = style.innerHTML; - }, 500); + var style = document.createElement("style"); + var textarea = document.createElement("textarea"); - /* Updates styles with content in textarea and saves styles. */ - function updateAndSaveStyles() { - style.innerHTML = textarea.value; - saveStyles(); - } + // hide textarea by default + textarea.style.display = "none"; + textarea.id = "my-style-input"; + textarea.spellcheck = false; - // continually update styles with textarea content - textarea.addEventListener('keyup', updateAndSaveStyles); - textarea.addEventListener('change', updateAndSaveStyles); + head.appendChild(style); + body.appendChild(textarea); - // pressing tab should insert spaces instead of focusing another element - textarea.addEventListener('keydown', function(event) { - var value = textarea.value; - var caret = textarea.selectionStart; + style.innerHTML = localStorage.myStyle || ""; + textarea.value = style.innerHTML; + textarea.placeholder = "/* Enter your styles here. */"; - // if tab is pressed, insert four spaces - if (event.keyCode === TAB_KEY_CODE) { - textarea.value = value.substring(0, caret) + SOFT_TAB + - value.substring(caret); + let loadedStyle = await localLoad(document.domain); + if (loadedStyle) { + textarea.value = loadedStyle; + style.innerHTML = textarea.value; + } - // move caret to after soft tab - textarea.setSelectionRange(caret + SOFT_TAB_LENGTH, caret + - SOFT_TAB_LENGTH); + // alt + click on an element adds its selector to the textarea + body.addEventListener("click", function(event) { + // ensure textarea is actually displayed + if ( + textarea.style.display.indexOf("none") === -1 && + event.target.id !== textarea.id && + event.altKey + ) { + var i = 0; + var target = event.target; + var elemClass = target.className.split(" ") || ""; + var stylesList = []; + var existingStyles = ""; + var selector = ""; + var cssStatement; + var textToAdd; + + // selector starts with the tag + selector += target.tagName.toLowerCase(); + + // include ID if there is one + if (target.id) { + selector += "#" + target.id; + } + + // include all classes found + for (i = 0; i < elemClass.length; i++) { + if (!ONLY_WHITESPACE_REGEX.test(elemClass[i])) { + selector += "." + elemClass[i]; + } + } + + // fill CSS with styles defined in the style attribute + if (target.getAttribute("style")) { + stylesList = target.getAttribute("style").split(";"); + + // keep track of CSS properties already defined in style attribute + for (i = 0; i < stylesList.length; i++) { + // condense mutliple whitespace into one space + cssStatement = stylesList[i] + .split(WHITESPACE_SPLIT_REGEX) + .join(" ") + .trim(); + + if (!ONLY_WHITESPACE_REGEX.test(cssStatement)) { + existingStyles += + SOFT_TAB + cssStatement.toLowerCase() + ";\n"; + } + } + } + + // construct text to add to textarea + if (selector) { + // add existing styles in braces + if (existingStyles) { + existingStyles = "{\n" + existingStyles + "}"; + } else { + existingStyles = "{\n\n}"; + } + + textToAdd = "\n" + selector + " " + existingStyles; + textarea.value += textToAdd; + + // highlight added text for easy removal + textarea.focus(); + textarea.setSelectionRange( + textarea.value.length - textToAdd.length, + textarea.value.length + ); + } + + event.preventDefault(); + } + }); - // prevent default tab action that shifts focus to the next element - event.preventDefault(); - } - }); + /* Save styles persistently in local storage. */ + var saveStyles = throttle(function() { + localStore(document.domain, style.innerHTML); + }, 500); - window.addEventListener('keydown', function(event) { - // control + m toggles text area - if (event.ctrlKey && event.keyCode === M_KEY_CODE) { - if (textarea.style.display == 'none') { - textarea.style.display = 'block'; - } else { - textarea.style.display = 'none'; + /* Updates styles with content in textarea and saves styles. */ + function updateAndSaveStyles() { + style.innerHTML = textarea.value; + saveStyles(); } - } + + // continually update styles with textarea content + textarea.addEventListener("keyup", updateAndSaveStyles); + textarea.addEventListener("change", updateAndSaveStyles); + + // pressing tab should insert spaces instead of focusing another element + textarea.addEventListener("keydown", function(event) { + var value = textarea.value; + var caret = textarea.selectionStart; + + // if tab is pressed, insert four spaces + if (event.keyCode === TAB_KEY_CODE) { + textarea.value = + value.substring(0, caret) + + SOFT_TAB + + value.substring(caret); + + // move caret to after soft tab + textarea.setSelectionRange( + caret + SOFT_TAB_LENGTH, + caret + SOFT_TAB_LENGTH + ); + + // prevent default tab action that shifts focus to the next element + event.preventDefault(); + } + }); + + window.addEventListener("keydown", function(event) { + // control + m toggles text area + if (event.ctrlKey && event.keyCode === M_KEY_CODE) { + if (textarea.style.display == "none") { + textarea.style.display = "block"; + textarea.style.visibility = "visible"; + } else { + textarea.style.display = "none"; + textarea.style.visibility = "hidden"; + } + } + }); }); - }); })(this, this.document);