├── .gitignore ├── AUTHORS ├── CONTRIBUTING ├── LICENSE ├── README ├── api.js ├── background.js ├── content.js ├── manifest.json ├── margin_slider.html ├── margin_slider.js ├── metrics ├── fontSize.js ├── margin.js └── misc.js ├── misc.js ├── options.js ├── options_page.html ├── options_page.js └── persistent_set.js /.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Refer to the Chromium project. 2 | https://dev.chromium.org/developers/contributing-code 3 | -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | Refer to the Chromium project. 2 | https://dev.chromium.org/developers/contributing-code 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Chromium Authors. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are 5 | // met: 6 | // 7 | // * Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer. 9 | // * Redistributions in binary form must reproduce the above 10 | // copyright notice, this list of conditions and the following disclaimer 11 | // in the documentation and/or other materials provided with the 12 | // distribution. 13 | // * Neither the name of Google Inc. nor the names of its 14 | // contributors may be used to endorse or promote products derived from 15 | // this software without specific prior written permission. 16 | // 17 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Browser Auto Zoom is an experimental feature for Chrome which automatically 2 | zooms web pages based on their content. 3 | 4 | Many sites can be difficult to read without zooming. They may not consider 5 | differences in monitor size and individual users may have different preferences 6 | or needs regarding the size of the content of the page (e.g. differences in 7 | visual acuity or viewing distance). Currently users have the option of changing 8 | the default zoom factor, but this does not account for differences between 9 | web pages. Users then have to manually zoom or reposition themselves to see 10 | better. 11 | 12 | This extension analyses the style of the page to compute and set an appropriate 13 | zoom factor automatically. 14 | -------------------------------------------------------------------------------- /api.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | 'use strict'; 6 | 7 | /** 8 | * @fileoverview Wrap chrome API calls in Promises. 9 | */ 10 | 11 | 12 | /** 13 | * Make a function which is a Promise based version of a chrome API method. 14 | * It takes the same arguments as the original method except the callback 15 | * and it returns a Promise for the result (i.e. the args passed to the 16 | * original method's callback). 17 | * @param {!Object} api The chrome API with the method to wrap. 18 | * @param {string} method The name of the method to wrap. 19 | * @return {function(...?): Promise} A Promise based API method. 20 | */ 21 | function wrapInPromise(api, method) { 22 | return function() { 23 | let args = new Array(arguments.length); 24 | for (let i = 0; i < args.length; ++i) { 25 | args[i] = arguments[i]; 26 | } 27 | 28 | return new Promise(function(resolve, reject) { 29 | args.push(resolveWithArgsOrReject.bind(null, resolve, reject)); 30 | 31 | api[method].apply(api, args); 32 | }); 33 | } 34 | } 35 | 36 | 37 | /** 38 | * Use as an API method callback for API calls within a Promise. 39 | * To use, bind the resolve and reject functions of the Promise 40 | * and use that as the callback. 41 | * If there is an error, reject with the error message. 42 | * Otherwise, resolve with arguments from the API call. 43 | * @param {Function} resolve A Promise's resolve function. 44 | * @param {Function} reject A Promise's reject function. 45 | */ 46 | function resolveWithArgsOrReject(resolve, reject) { 47 | if (chrome.runtime.lastError) { 48 | reject(chrome.runtime.lastError.message); 49 | } else { 50 | // Offset 2 into the arguments to get just the args from the API, 51 | // not the resolve and reject functions. 52 | let args = new Array(arguments.length - 2); 53 | for (let i = 0; i < args.length; ++i) { 54 | args[i] = arguments[i + 2]; 55 | } 56 | resolve.apply(null, args); 57 | } 58 | } 59 | 60 | 61 | var doGetTab = wrapInPromise(chrome.tabs, 'get'); 62 | var doGetZoom = wrapInPromise(chrome.tabs, 'getZoom'); 63 | var doGetZoomSettings = wrapInPromise(chrome.tabs, 'getZoomSettings'); 64 | var doSetZoom = wrapInPromise(chrome.tabs, 'setZoom'); 65 | var doSetZoomSettings = wrapInPromise(chrome.tabs, 'setZoomSettings'); 66 | var doExecuteScript = wrapInPromise(chrome.tabs, 'executeScript'); 67 | var doStorageLocalGet = wrapInPromise(chrome.storage.local, 'get'); 68 | var doStorageLocalSet = wrapInPromise(chrome.storage.local, 'set'); 69 | var doStorageLocalRemove = wrapInPromise(chrome.storage.local, 'remove'); 70 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | 'use strict'; 6 | 7 | /** 8 | * @fileoverview Manage zoom levels of tabs. 9 | */ 10 | 11 | // The tabs whose zoom events we're interested in. 12 | var activeListeners = new PersistentSet('activeListeners'); 13 | 14 | // The origins whose zoom factors have been overridden by the user. 15 | var overriddenOrigins = new PersistentSet('overriddenOrigins'); 16 | 17 | 18 | /** 19 | * Compute the weighted average of zoom factors from the metric results. 20 | * @param {Array} metricResults An array of metric results. 21 | * @return {number} The resulting zoom factor. 22 | */ 23 | function metricsWeightedAverage(metricResults) { 24 | let totalWeight = 0.0; 25 | for (let i = 0, len = metricResults.length; i < len; ++i) { 26 | totalWeight += metricResults[i].weight * metricResults[i].confidence; 27 | } 28 | 29 | let zoomFactor = 0.0; 30 | for (let i = 0, len = metricResults.length; i < len; ++i) { 31 | zoomFactor += metricResults[i].zoom * 32 | metricResults[i].weight * 33 | metricResults[i].confidence; 34 | } 35 | zoomFactor /= totalWeight; 36 | 37 | return zoomFactor; 38 | } 39 | 40 | 41 | /** 42 | * Returns a promise that resolves to the computed zoom factor 43 | * based on the content of the page. 44 | * @param {Tab} tab The tab for which we're computing the zoom factor. 45 | * @param {!Object} pageInfo Information about the page content. 46 | * @param {number} currentZoom The current zoom factor. 47 | * @return {Promise} The computed zoom factor. 48 | */ 49 | function computeZoom(tab, pageInfo, currentZoom) { 50 | return Promise.all([ 51 | doGetZoomSettings(tab.id), 52 | doGetOptions(['idealFontSize', 'idealPageWidth', 'metricWeights']) 53 | ]).then(function(values) { 54 | let zoomSettings = values[0]; 55 | let items = values[1]; 56 | 57 | // Prior belief: the page is fine at the default zoom factor 58 | let defaultMetric = { 59 | zoom: zoomSettings.defaultZoomFactor, 60 | confidence: 1, 61 | weight: 1 62 | }; 63 | 64 | let fontSizeMetric = metrics.fontSize.compute(pageInfo.fontSizeDistribution, 65 | items.idealFontSize, pageInfo.textArea, pageInfo.objectArea); 66 | fontSizeMetric.weight = items.metricWeights.fontSize; 67 | 68 | let marginMetric = metrics.margin.compute(pageInfo.contentDimensions, 69 | pageInfo.centeredContainers, items.idealPageWidth, currentZoom); 70 | marginMetric.weight = items.metricWeights.margin; 71 | 72 | let metricResults = [ 73 | defaultMetric, 74 | fontSizeMetric, 75 | marginMetric, 76 | ]; 77 | 78 | return metricsWeightedAverage(metricResults); 79 | }); 80 | } 81 | 82 | 83 | /** 84 | * Returns a promise that, when resolved, indicates whether 85 | * we may auto zoom the page 86 | * @param {Tab} tab The tab which we determine if we can auto zoom. 87 | * @return {Promise} A promise that will resolve to whether 88 | * we may auto zoom. 89 | */ 90 | function determineAllowedToAutoZoom(tab) { 91 | // Check the url is one we're permitted to access. 92 | // See manifest.json. 93 | let protocol = urlToProtocol(tab.url); 94 | if (protocol !== 'http:' && protocol !== 'https:') { 95 | return Promise.resolve(false); 96 | } 97 | 98 | return Promise.all([ 99 | doGetZoom(tab.id), 100 | doGetZoomSettings(tab.id), 101 | overriddenOrigins.has(urlToHostname(tab.url)), 102 | doGetOptions('ignoreOverrides') 103 | ]).then(function(values) { 104 | let currentZoom = values[0]; 105 | let zoomSettings = values[1]; 106 | let overridden = values[2]; 107 | let options = values[3]; 108 | 109 | return zoomSettings.mode === 'automatic' && 110 | zoomSettings.scope === 'per-origin' && 111 | (options.ignoreOverrides || 112 | (!overridden && 113 | zoomValuesEqual(currentZoom, zoomSettings.defaultZoomFactor))); 114 | }).catch(function() { 115 | return false; 116 | }); 117 | } 118 | 119 | // Stay informed of zoom changes to tabs we've auto zoomed. 120 | chrome.tabs.onZoomChange.addListener(function(zoomChangeInfo) { 121 | activeListeners.has(zoomChangeInfo.tabId).then(function(listening) { 122 | if (!listening) { 123 | return; 124 | } 125 | 126 | doGetZoomSettings(zoomChangeInfo.tabId).then(function(zoomSettings) { 127 | if (zoomChangeInfo.zoomSettings.scope === 'per-origin') { 128 | // Ignoring reset. 129 | return; 130 | } 131 | 132 | if (zoomChangeInfo.zoomSettings.scope !== 'per-tab' || 133 | zoomChangeInfo.zoomSettings.mode !== 'automatic') { 134 | // Something changed the zoom settings. 135 | // We won't try and control this tab's zoom anymore. 136 | activeListeners.delete(zoomChangeInfo.tabId); 137 | return; 138 | } 139 | 140 | if (zoomValuesEqual(zoomChangeInfo.newZoomFactor, 141 | zoomChangeInfo.oldZoomFactor)) { 142 | // Ignoring spurious zoom change. 143 | return; 144 | } 145 | 146 | // User has explicitly zoomed. 147 | // TODO(mcnee) Record why user overrode our choice. 148 | activeListeners.delete(zoomChangeInfo.tabId).then(function() { 149 | doGetOptions('ignoreOverrides').then(function(options) { 150 | /** 151 | * If we ignore overrides, then there's no point in resetting the 152 | * scope to per-origin, as we are going to ignore any changes 153 | * the next time we auto zoom. 154 | * Also, if we ignored a zoom level exception when we did our last 155 | * auto zoom, resetting to per-origin would case the zoom level to 156 | * jump to that of the exception, which may be jarring to the user. 157 | */ 158 | if (!options.ignoreOverrides) { 159 | doSetZoomSettings(zoomChangeInfo.tabId, {scope: 'per-origin'}); 160 | } 161 | }); 162 | 163 | doGetTab(zoomChangeInfo.tabId).then(function(tab) { 164 | overriddenOrigins.add(urlToHostname(tab.url)); 165 | }); 166 | }); 167 | }); 168 | }); 169 | }); 170 | 171 | // Handle messages containing page information from content scripts. 172 | chrome.runtime.onMessage.addListener(function(request, sender) { 173 | if (sender.tab) { 174 | activeListeners.delete(sender.tab.id).then(function() { 175 | return doSetZoomSettings(sender.tab.id, {scope: 'per-tab'}); 176 | }).then(function() { 177 | return doGetZoom(sender.tab.id); 178 | }).then(function(currentZoom) { 179 | return computeZoom( 180 | sender.tab, request, currentZoom).then(function(newZoom) { 181 | if (!zoomValuesEqual(newZoom, currentZoom)) { 182 | return doSetZoom(sender.tab.id, newZoom); 183 | } 184 | }); 185 | }).then(function() { 186 | return activeListeners.add(sender.tab.id); 187 | }); 188 | } 189 | }); 190 | 191 | 192 | /** 193 | * Insert content script into the tab, if we are allowed to. 194 | * @param {Tab} tab The tab into which we intend to insert the content script. 195 | * @return {Promise} A promise that will resolve when the script has 196 | * been inserted or once we determine that we are not allowed. 197 | */ 198 | function insertContentScript(tab) { 199 | return determineAllowedToAutoZoom(tab).then(function(allowed) { 200 | // TODO(mcnee) Handle iframes. 201 | if (allowed) { 202 | let deps = [ 203 | 'metrics/misc.js', 204 | 'metrics/fontSize.js', 205 | 'metrics/margin.js', 206 | ]; 207 | let loadDeps = new Array(deps.length); 208 | for (let i = 0, len = deps.length; i < len; ++i) { 209 | loadDeps[i] = doExecuteScript( 210 | tab.id, {file: deps[i], runAt: 'document_end'}); 211 | } 212 | 213 | return Promise.all(loadDeps).then(function() { 214 | return doExecuteScript( 215 | tab.id, {file: 'content.js', runAt: 'document_end'}); 216 | }); 217 | } 218 | }); 219 | } 220 | 221 | 222 | chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) { 223 | if (changeInfo.status === 'loading') { 224 | insertContentScript(tab); 225 | } 226 | }); 227 | 228 | chrome.tabs.onReplaced.addListener(function(addedTabId, removedTabId) { 229 | activeListeners.delete(removedTabId); 230 | 231 | doGetTab(addedTabId).then(function(addedTab) { 232 | return insertContentScript(addedTab); 233 | }); 234 | }); 235 | 236 | chrome.tabs.onRemoved.addListener(function(tabId, removeInfo) { 237 | activeListeners.delete(tabId); 238 | }); 239 | 240 | chrome.runtime.onStartup.addListener(function() { 241 | activeListeners.clear(); 242 | }); 243 | -------------------------------------------------------------------------------- /content.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | 'use strict'; 6 | 7 | 8 | /** 9 | * Gather information about the page and send it to the extension. 10 | */ 11 | function analysePage() { 12 | let message = { 13 | // Map font sizes to number of characters of that size found in page. 14 | fontSizeDistribution: {}, 15 | // Area of page covered in text content. 16 | textArea: 0.0, 17 | // Area of page covered in non-text objects (e.g. videos, images). 18 | objectArea: 0.0, 19 | // Dimensions of the content of either the body or documentElement. 20 | contentDimensions: { 21 | height: 0.0, 22 | width: 0.0 23 | }, 24 | // The dimensions of each of the containers centered in the page. 25 | centeredContainers: [], 26 | }; 27 | 28 | let textNodeIterator = document.createNodeIterator( 29 | document.body, 30 | NodeFilter.SHOW_TEXT, 31 | function(node) { 32 | let el = node.parentElement; 33 | return (!el || 34 | node.textContent.trim() === '' || 35 | el.tagName === 'SCRIPT' || 36 | el.tagName === 'STYLE') ? 37 | NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT; 38 | }); 39 | 40 | // Button text is not defined in a text node 41 | // so we have to check this separately. 42 | let buttonIterator = document.createNodeIterator( 43 | document.body, 44 | NodeFilter.SHOW_ELEMENT, 45 | function(el) { 46 | return el.tagName === 'INPUT' && 47 | (el.type === 'button' || 48 | el.type === 'submit' || 49 | el.type === 'reset') ? 50 | NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; 51 | }); 52 | 53 | let objectIterator = document.createNodeIterator( 54 | document.body, 55 | NodeFilter.SHOW_ELEMENT, 56 | function(el) { 57 | return (el.tagName === 'AUDIO' || 58 | el.tagName === 'CANVAS' || 59 | el.tagName === 'EMBED' || 60 | el.tagName === 'IFRAME' || 61 | el.tagName === 'IMG' || 62 | el.tagName === 'OBJECT' || 63 | el.tagName === 'VIDEO') ? 64 | NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; 65 | }); 66 | 67 | /** 68 | * TODO(mcnee) Don't hog the main thread when analysing the DOM. 69 | * We could divide up the tasks and occasionally yield like in this example: 70 | * https://developers.google.com/web/fundamentals/performance/rendering/optimize-javascript-execution?hl=en#reduce-complexity-or-use-web-workers 71 | */ 72 | 73 | let currentNode; 74 | 75 | while (currentNode = textNodeIterator.nextNode()) { 76 | metrics.fontSize.processTextNode(message, currentNode); 77 | } 78 | 79 | while (currentNode = buttonIterator.nextNode()) { 80 | metrics.fontSize.processButtonElement(message, currentNode); 81 | } 82 | 83 | while (currentNode = objectIterator.nextNode()) { 84 | metrics.fontSize.processObject(message, currentNode); 85 | } 86 | 87 | metrics.margin.findMargins(message); 88 | 89 | chrome.runtime.sendMessage(message); 90 | } 91 | 92 | analysePage(); 93 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | 4 | "name": "Auto Zoom", 5 | "description": "This extension automatically zooms pages", 6 | "version": "0.1", 7 | 8 | "minimum_chrome_version": "43", 9 | 10 | "options_ui": { 11 | "page": "options_page.html", 12 | "chrome_style": true 13 | }, 14 | 15 | "background": { 16 | "scripts": [ 17 | "metrics/misc.js", 18 | "metrics/fontSize.js", 19 | "metrics/margin.js", 20 | "misc.js", 21 | "api.js", 22 | "persistent_set.js", 23 | "options.js", 24 | "background.js" 25 | ], 26 | "persistent": false 27 | }, 28 | 29 | "permissions": [ 30 | "tabs", 31 | "storage", 32 | "http://*/*", 33 | "https://*/*" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /margin_slider.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Margin Slider 5 | 42 | 43 | 44 |
45 |
46 |

Preferred Page Width

47 |

Please adjust this slider until the amount of space taken up by the margins looks good.

48 |
49 | 50 | Increase Margin 51 | Reduce Margin 52 |
53 |

Some web pages center their content, leaving margins at the sides. We can adjust the zoom to make better use of this space. This will depend on the size of your monitor and your personal preferences.

54 |

With increased margins, more of a web page's content will fit on screen at once. With reduced margins, the content will take up more of the screen.

55 |
56 |
57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /margin_slider.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | 'use strict'; 6 | 7 | 8 | /** 9 | * Alias for document.getElementById. 10 | * @param {string} id The ID of the element to find. 11 | * @return {HTMLElement} The found element or null if not found. 12 | */ 13 | function $(id) { 14 | return document.getElementById(id); 15 | } 16 | 17 | 18 | /** 19 | * Saves the option in storage. 20 | */ 21 | function saveOption() { 22 | let idealPageWidth = parseFloat($('idealPageWidth').value) / 100; 23 | 24 | doSetOptions({idealPageWidth: idealPageWidth}); 25 | } 26 | 27 | 28 | /** 29 | * Restores the option from storage. 30 | */ 31 | function restoreOption() { 32 | doGetOptions('idealPageWidth').then(function(items) { 33 | $('idealPageWidth').value = items.idealPageWidth * 100; 34 | }).then(adjustContainerWidth); 35 | } 36 | 37 | 38 | /** 39 | * Change the width of the container to reflect the value of the option. 40 | */ 41 | function adjustContainerWidth() { 42 | $('container').style.width = $('idealPageWidth').value + '%'; 43 | } 44 | 45 | 46 | $('idealPageWidth').addEventListener('input', adjustContainerWidth); 47 | $('idealPageWidth').addEventListener('input', saveOption); 48 | 49 | document.addEventListener('DOMContentLoaded', restoreOption); 50 | -------------------------------------------------------------------------------- /metrics/fontSize.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | 'use strict'; 6 | 7 | 8 | var metrics = metrics || {}; 9 | 10 | 11 | /** 12 | * A metric based on the distribution of font sizes in a page. 13 | */ 14 | metrics.fontSize = { 15 | 16 | processTextNode: function(message, textNode) { 17 | let el = textNode.parentElement; 18 | let fontSize = parseInt(window.getComputedStyle(el).fontSize, 10); 19 | 20 | addToDistribution( 21 | message.fontSizeDistribution, fontSize, textNode.textContent.length); 22 | 23 | message.textArea += elementArea(el); 24 | }, 25 | 26 | processButtonElement: function(message, el) { 27 | let fontSize = parseInt(window.getComputedStyle(el).fontSize, 10); 28 | 29 | // If value is not provided for submit or reset types, then the text shown 30 | // is Submit and Reset respectively. 31 | let length = el.value ? el.value.length : el.type.length; 32 | 33 | addToDistribution(message.fontSizeDistribution, fontSize, length); 34 | 35 | message.textArea += elementArea(el); 36 | }, 37 | 38 | processObject: function(message, el) { 39 | message.objectArea += elementArea(el); 40 | }, 41 | 42 | /** 43 | * Return the ideal zoom factor given font size information. 44 | * @param {Object} fontSizeDistribution The font size distribution. 45 | * @param {number} idealFontSize The target font size. 46 | * @param {number} textArea The area of text content on the page. 47 | * @param {number} objectArea The area of non-text content on the page. 48 | * @return {Object} The computed zoom factor and confidence. 49 | */ 50 | compute: function(fontSizeDistribution, idealFontSize, textArea, objectArea) { 51 | /** 52 | * We pick a representative font of the page and pick a zoom factor 53 | * such that the representative font will appear to be the same size 54 | * as the ideal font size. 55 | */ 56 | let firstQuartile = fontSizeFirstQuartile(fontSizeDistribution); 57 | let percentTextual = textArea / (textArea + objectArea); 58 | let confidence = percentTextual > 0.5 ? 1.0 : percentTextual; 59 | if (firstQuartile && confidence) { 60 | return { 61 | zoom: idealFontSize / firstQuartile, 62 | confidence: confidence 63 | }; 64 | } else { 65 | return {zoom: 0, confidence: 0}; 66 | } 67 | } 68 | }; 69 | 70 | 71 | /** 72 | * Return the smallest font size that is above the first 73 | * quartile of font sizes in the given distribution. 74 | * @param {Object} fontSizeDistribution The font size distribution. 75 | * @return {number} The smallest font size above the first quartile. 76 | */ 77 | function fontSizeFirstQuartile(fontSizeDistribution) { 78 | let total = 0; 79 | for (let fontSize in fontSizeDistribution) { 80 | total += fontSizeDistribution[fontSize]; 81 | } 82 | 83 | // We don't care about averaging if position is not an integer 84 | // as there's no need to be that accurate. 85 | let position = Math.floor((total + 1) / 4); 86 | 87 | let sizesInOrder = Object.keys(fontSizeDistribution).sort(function(a, b) { 88 | return a - b; 89 | }); 90 | 91 | for (let i = 0, len = sizesInOrder.length; i < len; ++i) { 92 | let fontSize = sizesInOrder[i]; 93 | 94 | if (position < fontSizeDistribution[fontSize]) { 95 | return fontSize; 96 | } else { 97 | position -= fontSizeDistribution[fontSize]; 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /metrics/margin.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | 'use strict'; 6 | 7 | var metrics = metrics || {}; 8 | 9 | 10 | /** 11 | * A metric based on the empty space in the margins of a page. 12 | */ 13 | metrics.margin = { 14 | 15 | /** 16 | * Find the outermost containers which are centered in the page. 17 | * @param {Object} message The message that we fill with margin information. 18 | */ 19 | findMargins: function(message) { 20 | let bodyComputedStyle = window.getComputedStyle(document.body); 21 | let bodyContentHeight = elementContentHeight( 22 | document.body, bodyComputedStyle); 23 | let bodyContentWidth = elementContentWidth( 24 | document.body, bodyComputedStyle); 25 | 26 | // The elements in the body are centered with respect to the body. 27 | message.contentDimensions.height = bodyContentHeight; 28 | message.contentDimensions.width = bodyContentWidth; 29 | 30 | let widthDeclarations = getWidthDeclarations(); 31 | 32 | /** 33 | * Determine if the element is centered with margins. 34 | * @param {Object} el The element. 35 | * @param {Object} computedStyle The result of window.getComputedStyle(el). 36 | * @param {Object} parentComputedStyle The result of window.getComputedStyle 37 | * of el's parent. 38 | * @param {boolean} parentCenters Whether the parent element causes the 39 | * element to be centered. 40 | * @return {boolean} Whether the element is a centered container. 41 | */ 42 | function isCenteredContainer( 43 | el, computedStyle, parentComputedStyle, parentCenters) { 44 | let marginLeft = parseFloat(computedStyle.marginLeft); 45 | let marginRight = parseFloat(computedStyle.marginRight); 46 | 47 | /** 48 | * Since the CSS rule specifying width may have been filtered out, 49 | * use the element not taking up the full width of its parent as 50 | * a fallback. 51 | */ 52 | let widthSpecified = isWidthSpecified(el, widthDeclarations) || 53 | el.offsetWidth < elementContentWidth(el.parentElement, 54 | parentComputedStyle); 55 | let centered = (Math.abs(marginLeft - marginRight) <= 1) || parentCenters; 56 | 57 | return widthSpecified && centered; 58 | } 59 | 60 | /** 61 | * Recurse to find the outermost containers which are centered in the page. 62 | * @param {Object} el The element. 63 | * @param {Object} parentComputedStyle The result of window.getComputedStyle 64 | * of el's parent. 65 | * @return {boolean} Whether the element or one of its descendants is 66 | * a centered container. 67 | */ 68 | function traverse(el, parentComputedStyle) { 69 | let computedStyle = window.getComputedStyle(el); 70 | let totalWidth = elementTotalWidth(el, computedStyle); 71 | 72 | /** 73 | * The parent element may cause the child to be centered (e.g. using a 74 | *
tag). The child element may not have information about its 75 | * own margins, so we need to check if the parent is doing this. 76 | */ 77 | let parentCenters = el.parentElement.align === 'center' || 78 | parentComputedStyle.textAlign === 'center' || 79 | parentComputedStyle.textAlign === '-webkit-center'; 80 | 81 | let spansPage = (Math.abs(totalWidth - bodyContentWidth) <= 1) || 82 | (parentCenters && (Math.abs(elementTotalWidth(el.parentElement, 83 | parentComputedStyle) - 84 | bodyContentWidth) <= 1)); 85 | if (!spansPage) { 86 | /** 87 | * Early out if the element does not cover the width of the page. 88 | * Technically, a descendant could span the width of the page even if 89 | * its ancestor does not. But I've never encountered a site that does 90 | * this and checking for this case would require us to traverse the 91 | * entire DOM for sites that don't have margins. 92 | */ 93 | return false; 94 | } 95 | 96 | if (isCenteredContainer(el, computedStyle, parentComputedStyle, 97 | parentCenters)) { 98 | if (el.offsetWidth > 0 && el.offsetHeight > 0) { 99 | let totalHeight = elementTotalHeight(el, computedStyle); 100 | let cssWidth = parseFloat(computedStyle.width); 101 | let relative = isWidthRelativeToViewport( 102 | el, widthDeclarations, cssWidth, bodyContentWidth); 103 | 104 | message.centeredContainers.push({ 105 | width: el.offsetWidth, 106 | height: totalHeight, 107 | relative: relative, 108 | }); 109 | 110 | return true; 111 | } 112 | } else { 113 | let found = false; 114 | let children = el.children; 115 | for (let i = 0, len = children.length; i < len; ++i) { 116 | found = traverse(children[i], computedStyle) || found; 117 | } 118 | return found; 119 | } 120 | } 121 | 122 | let found = false; 123 | let children = document.body.children; 124 | for (let i = 0, len = children.length; i < len; ++i) { 125 | found = traverse(children[i], bodyComputedStyle) || found; 126 | } 127 | 128 | /** 129 | * The body can also be a centered container. 130 | * In fact, it is by default. 131 | * So we check the body last so that the body is ignored 132 | * if there are explicit centered containers in the body. 133 | */ 134 | if (!found) { 135 | let documentComputedStyle = 136 | window.getComputedStyle(document.documentElement); 137 | 138 | if (isCenteredContainer(document.body, bodyComputedStyle, 139 | documentComputedStyle, false)) { 140 | if (document.body.offsetWidth > 0 && document.body.offsetHeight > 0) { 141 | let bodyTotalHeight = elementTotalHeight( 142 | document.body, bodyComputedStyle); 143 | let documentContentHeight = elementContentHeight( 144 | document.documentElement, documentComputedStyle); 145 | let documentContentWidth = elementContentWidth( 146 | document.documentElement, documentComputedStyle); 147 | 148 | let bodyCssWidth = parseFloat(bodyComputedStyle.width); 149 | let relative = isWidthRelativeToViewport( 150 | document.body, widthDeclarations, 151 | bodyCssWidth, 152 | documentContentWidth); 153 | 154 | message.centeredContainers.push({ 155 | width: document.body.offsetWidth, 156 | height: bodyTotalHeight, 157 | relative: relative, 158 | }); 159 | 160 | // The body is centered with respect to the document. 161 | message.contentDimensions.height = documentContentHeight; 162 | message.contentDimensions.width = documentContentWidth; 163 | } 164 | } 165 | } 166 | }, 167 | 168 | /** 169 | * Return the ideal zoom factor given margin information. 170 | * @param {Object} contentDimensions The dimensions of the body content. 171 | * @param {Array} centeredContainers The sizes of each of 172 | * the centered containers. 173 | * @param {number} idealPageWidth The percentage of the screen we want to 174 | * be take up by the content. 175 | * @param {number} currentZoom The current zoom factor. 176 | * @return {Object} The computed zoom factor and confidence. 177 | */ 178 | compute: function(contentDimensions, centeredContainers, 179 | idealPageWidth, currentZoom) { 180 | /** 181 | * TODO(mcnee) Zooming when the width is defined relative to the viewport 182 | * does not affect the size of the margins. If we had access to the page 183 | * scale factor, we could address this problem by scaling the page instead. 184 | * It is currently not available to extensions, so for now we ignore 185 | * containers with relative widths. 186 | */ 187 | let maxContainerWidth; 188 | let totalHeight = 0.0; 189 | for (let i = 0, len = centeredContainers.length; i < len; ++i) { 190 | if (!centeredContainers[i].relative) { 191 | let width = centeredContainers[i].width; 192 | width /= currentZoom; 193 | 194 | if (!maxContainerWidth || width > maxContainerWidth) { 195 | maxContainerWidth = width; 196 | } 197 | 198 | totalHeight += centeredContainers[i].height; 199 | } 200 | } 201 | 202 | if (!maxContainerWidth || !contentDimensions.height) { 203 | return {zoom: 0, confidence: 0}; 204 | } 205 | 206 | return { 207 | zoom: idealPageWidth * contentDimensions.width / maxContainerWidth, 208 | confidence: totalHeight / contentDimensions.height 209 | }; 210 | } 211 | }; 212 | 213 | 214 | /** 215 | * Return whether there is a CSS declaration which sets the element's width. 216 | * @param {Object} el The element. 217 | * @param {Object>} widthDeclarations The width 218 | * declarations for each selector. 219 | * @return {boolean} Whether the width is specified. 220 | */ 221 | function isWidthSpecified(el, widthDeclarations) { 222 | function isSpecified(declaration) { 223 | return !isNaN(parseFloat(declaration)) && declaration !== '100%'; 224 | } 225 | 226 | return checkWidthDeclarations_(el, widthDeclarations, isSpecified); 227 | } 228 | 229 | 230 | /** 231 | * Return whether there is a CSS declaration which sets the element's width 232 | * as a percentage of the viewport. 233 | * @param {Object} el The element. 234 | * @param {Object>} widthDeclarations The width 235 | * declarations for each selector. 236 | * @param {number} cssWidth The computed style for el's width. 237 | * @param {number} viewportContentWidth The content width of the body 238 | * or if el is the body, then the content width of the document. 239 | * @return {boolean} Whether the width is specified. 240 | */ 241 | function isWidthRelativeToViewport( 242 | el, widthDeclarations, cssWidth, viewportContentWidth) { 243 | function isRelative(declaration) { 244 | let isPercent = declaration.indexOf('%') !== -1; 245 | let percentWidth = parseFloat(declaration); 246 | return (isPercent && 247 | !isNaN(percentWidth) && 248 | (Math.abs(viewportContentWidth * (percentWidth / 100) - 249 | cssWidth) <= 1)); 250 | } 251 | 252 | return checkWidthDeclarations_(el, widthDeclarations, isRelative); 253 | } 254 | 255 | 256 | /** 257 | * Return whether there is a CSS declaration which satisfies predicate. 258 | * @param {Object} el The element. 259 | * @param {Object>} widthDeclarations The width 260 | * declarations for each selector. 261 | * @param {function(string): boolean} predicate The function which 262 | * checks each declaration. 263 | * @return {boolean} Whether the width is specified. 264 | */ 265 | function checkWidthDeclarations_(el, widthDeclarations, predicate) { 266 | for (let selector in widthDeclarations) { 267 | if (el.matches(selector)) { 268 | let declarations = widthDeclarations[selector]; 269 | for (let i = 0, len = declarations.length; i < len; ++i) { 270 | if (predicate(declarations[i])) { 271 | return true; 272 | } 273 | } 274 | } 275 | } 276 | 277 | // Also check any widths defined on the element. 278 | if ((el.style.width && predicate(el.style.width)) || 279 | (el.width && predicate(el.width.toString()))) { 280 | return true; 281 | } 282 | 283 | return false; 284 | } 285 | 286 | 287 | /** 288 | * Return a mapping of selectors to the CSS width declarations that 289 | * apply to them. 290 | * Note that the style sheets we can access via document.styleSheets 291 | * are filtered (e.g. no cross origin rules), so this not reliable. 292 | * @return {Object>} The width declarations 293 | * for each selector. 294 | */ 295 | function getWidthDeclarations() { 296 | let declarations = {}; 297 | 298 | function processRuleList(rules) { 299 | if (!rules) { 300 | return; 301 | } 302 | 303 | for (let j = 0, rulesLen = rules.length; j < rulesLen; ++j) { 304 | if (rules[j].type === CSSRule.STYLE_RULE) { 305 | let selectorText = rules[j].selectorText; 306 | let width = rules[j].style.getPropertyValue('width'); 307 | let maxWidth = rules[j].style.getPropertyValue('max-width'); 308 | 309 | if ((width || maxWidth) && !declarations.hasOwnProperty(selectorText)) { 310 | declarations[selectorText] = []; 311 | } 312 | if (width) { 313 | declarations[selectorText].push(width); 314 | } 315 | if (maxWidth) { 316 | declarations[selectorText].push(maxWidth); 317 | } 318 | } else if (rules[j].type === CSSRule.MEDIA_RULE && 319 | window.matchMedia(rules[j].media.mediaText).matches) { 320 | processRuleList(rules[j].cssRules); 321 | } 322 | } 323 | } 324 | 325 | let sheets = document.styleSheets; 326 | for (let i = 0, sheetsLen = sheets.length; i < sheetsLen; ++i) { 327 | processRuleList(sheets[i].cssRules); 328 | } 329 | 330 | return declarations; 331 | } 332 | -------------------------------------------------------------------------------- /metrics/misc.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | 'use strict'; 6 | 7 | 8 | /** 9 | * Add the value to distribution[key]. 10 | * Initialize if the key doesn't already exist. 11 | * @param {Object} distribution The distribution. 12 | * @param {number} key The key. 13 | * @param {number} value The value. 14 | */ 15 | function addToDistribution(distribution, key, value) { 16 | if (!distribution.hasOwnProperty(key)) { 17 | distribution[key] = 0; 18 | } 19 | 20 | distribution[key] += value; 21 | } 22 | 23 | 24 | /** 25 | * Compute the area of an element. 26 | * @param {Object} el The element. 27 | * @return {number} The computed area. 28 | */ 29 | function elementArea(el) { 30 | let boundingRect = el.getBoundingClientRect(); 31 | return boundingRect.height * boundingRect.width; 32 | } 33 | 34 | 35 | /** 36 | * Compute the width of the content of an element. 37 | * @param {Object} el The element. 38 | * @param {Object} computedStyle The result of window.getComputedStyle(el). 39 | * @return {number} The content width. 40 | */ 41 | function elementContentWidth(el, computedStyle) { 42 | let width = parseFloat(computedStyle.width); 43 | 44 | if (isNaN(width)) { 45 | // Fallback to clientWidth, if width is not computed. 46 | width = el.clientWidth; 47 | width -= parseFloat(computedStyle.paddingLeft); 48 | width -= parseFloat(computedStyle.paddingRight); 49 | } else { 50 | if (computedStyle.boxSizing === 'border-box') { 51 | width -= parseFloat(computedStyle.borderLeftWidth); 52 | width -= parseFloat(computedStyle.paddingLeft); 53 | width -= parseFloat(computedStyle.paddingRight); 54 | width -= parseFloat(computedStyle.borderRightWidth); 55 | } 56 | } 57 | 58 | return width; 59 | } 60 | 61 | 62 | /** 63 | * Compute the total width of an element. 64 | * @param {Object} el The element. 65 | * @param {Object} computedStyle The result of window.getComputedStyle(el). 66 | * @return {number} The total width. 67 | */ 68 | function elementTotalWidth(el, computedStyle) { 69 | let totalWidth = parseFloat(computedStyle.width); 70 | 71 | if (isNaN(totalWidth)) { 72 | // Fallback to offsetWidth, if width is not computed. 73 | totalWidth = el.offsetWidth; 74 | totalWidth += parseFloat(computedStyle.marginLeft); 75 | totalWidth += parseFloat(computedStyle.marginRight); 76 | } else { 77 | if (computedStyle.boxSizing === 'content-box') { 78 | totalWidth += parseFloat(computedStyle.paddingLeft); 79 | totalWidth += parseFloat(computedStyle.paddingRight); 80 | totalWidth += parseFloat(computedStyle.borderLeftWidth); 81 | totalWidth += parseFloat(computedStyle.borderRightWidth); 82 | } 83 | 84 | totalWidth += parseFloat(computedStyle.marginLeft); 85 | totalWidth += parseFloat(computedStyle.marginRight); 86 | } 87 | 88 | return totalWidth; 89 | } 90 | 91 | 92 | /** 93 | * Compute the height of the content of an element. 94 | * @param {Object} el The element. 95 | * @param {Object} computedStyle The result of window.getComputedStyle(el). 96 | * @return {number} The content height. 97 | */ 98 | function elementContentHeight(el, computedStyle) { 99 | let height = parseFloat(computedStyle.height); 100 | 101 | if (isNaN(height)) { 102 | // Fallback to clientHeight, if height is not computed. 103 | height = el.clientHeight; 104 | height -= parseFloat(computedStyle.paddingTop); 105 | height -= parseFloat(computedStyle.paddingBottom); 106 | } else { 107 | if (computedStyle.boxSizing === 'border-box') { 108 | height -= parseFloat(computedStyle.borderTopWidth); 109 | height -= parseFloat(computedStyle.paddingTop); 110 | height -= parseFloat(computedStyle.paddingBottom); 111 | height -= parseFloat(computedStyle.borderBottomWidth); 112 | } 113 | } 114 | 115 | return height; 116 | } 117 | 118 | 119 | /** 120 | * Compute the total height of an element. 121 | * @param {Object} el The element. 122 | * @param {Object} computedStyle The result of window.getComputedStyle(el). 123 | * @return {number} The total height. 124 | */ 125 | function elementTotalHeight(el, computedStyle) { 126 | let totalHeight = parseFloat(computedStyle.height); 127 | 128 | if (isNaN(totalHeight)) { 129 | // Fallback to offsetHeight, if height is not computed. 130 | totalHeight = el.offsetHeight; 131 | totalHeight += parseFloat(computedStyle.marginTop); 132 | totalHeight += parseFloat(computedStyle.marginBottom); 133 | } else { 134 | if (computedStyle.boxSizing === 'content-box') { 135 | totalHeight += parseFloat(computedStyle.paddingTop); 136 | totalHeight += parseFloat(computedStyle.paddingBottom); 137 | totalHeight += parseFloat(computedStyle.borderTopWidth); 138 | totalHeight += parseFloat(computedStyle.borderBottomWidth); 139 | } 140 | 141 | totalHeight += parseFloat(computedStyle.marginTop); 142 | totalHeight += parseFloat(computedStyle.marginBottom); 143 | } 144 | 145 | return totalHeight; 146 | } 147 | -------------------------------------------------------------------------------- /misc.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | 'use strict'; 6 | 7 | 8 | /** 9 | * Get the hostname part of a URL. 10 | * @param {string} url The URL to parse. 11 | * @return {string} The hostname. 12 | */ 13 | function urlToHostname(url) { 14 | // TODO(mcnee) okay to use (new URL(url)).hostname ? 15 | // Use a temporary anchor object to have it parse 16 | // the url and get the resulting hostname. 17 | let anchor = document.createElement('a'); 18 | anchor.href = url; 19 | return anchor.hostname; 20 | } 21 | 22 | 23 | /** 24 | * Get the protocol scheme part of a URL. 25 | * @param {string} url The URL to parse. 26 | * @return {string} The protocol. 27 | */ 28 | function urlToProtocol(url) { 29 | // TODO(mcnee) okay to use (new URL(url)).protocol ? 30 | // Use a temporary anchor object to have it parse 31 | // the url and get the resulting protocol. 32 | let anchor = document.createElement('a'); 33 | anchor.href = url; 34 | return anchor.protocol; 35 | } 36 | 37 | 38 | /** 39 | * Returns whether two zoom values are approximately equal. 40 | * @param {number} a The first zoom value. 41 | * @param {number} b The second zoom value. 42 | * @return {boolean} Whether the values are approximately equal. 43 | */ 44 | function zoomValuesEqual(a, b) { 45 | let ZOOM_EPSILON = 0.01; 46 | return Math.abs(a - b) <= ZOOM_EPSILON; 47 | } 48 | -------------------------------------------------------------------------------- /options.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | 'use strict'; 6 | 7 | 8 | /** 9 | * The default values for the options. 10 | */ 11 | var OPTION_DEFAULTS = { 12 | ignoreOverrides: false, 13 | idealFontSize: 16, 14 | idealPageWidth: 0.8, 15 | metricWeights: { 16 | fontSize: 8, 17 | margin: 4, 18 | }, 19 | }; 20 | 21 | 22 | /** 23 | * Gets the options. 24 | * @param {string|Array} optionNames The names of the options. 25 | * @return {Promise} A promise that will resolve to the options. 26 | */ 27 | function doGetOptions(optionNames) { 28 | if (typeof optionNames === 'string') { 29 | optionNames = [optionNames]; 30 | } 31 | 32 | let data = {}; 33 | for (let i = 0, len = optionNames.length; i < len; ++i) { 34 | data[optionNames[i]] = OPTION_DEFAULTS[optionNames[i]]; 35 | } 36 | 37 | return doStorageLocalGet(data); 38 | } 39 | 40 | 41 | /** 42 | * Sets the options. 43 | * @param {Object} items The options to store. 44 | * @return {Promise} A promise that will resolve when the settings are stored. 45 | */ 46 | function doSetOptions(items) { 47 | return doStorageLocalSet(items); 48 | } 49 | -------------------------------------------------------------------------------- /options_page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Auto Zoom Options 5 | 52 | 53 | 54 |
55 | 60 |
61 | 64 | 65 |
66 | 67 | 91 | Show advanced settings... 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /options_page.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | 'use strict'; 6 | 7 | 8 | /** 9 | * Alias for document.getElementById. 10 | * @param {string} id The ID of the element to find. 11 | * @return {HTMLElement} The found element or null if not found. 12 | */ 13 | function $(id) { 14 | return document.getElementById(id); 15 | } 16 | 17 | 18 | /** 19 | * Saves the options in storage. 20 | */ 21 | function saveOptions() { 22 | let ignoreOverrides = $('ignoreOverrides').checked; 23 | let idealFontSize = parseInt($('idealFontSize').value, 10); 24 | 25 | // Validate font size. 26 | if (isNaN(idealFontSize) || idealFontSize < 6 || idealFontSize > 99) { 27 | idealFontSize = undefined; 28 | } 29 | 30 | let fontSizeWeight = parseInt($('fontSizeWeight').value, 10); 31 | let marginWeight = parseInt($('marginWeight').value, 10); 32 | 33 | doGetZoom(undefined).then(function(currentZoom) { 34 | return doSetOptions({ 35 | ignoreOverrides: ignoreOverrides, 36 | idealFontSize: idealFontSize, 37 | metricWeights: { 38 | fontSize: fontSizeWeight, 39 | margin: marginWeight, 40 | }, 41 | }); 42 | }); 43 | } 44 | 45 | 46 | /** 47 | * Restores the options from storage. 48 | */ 49 | function restoreOptions() { 50 | Promise.all([ 51 | doGetOptions([ 52 | 'ignoreOverrides', 53 | 'idealFontSize', 54 | 'metricWeights', 55 | ]), 56 | doGetZoom(undefined) 57 | ]).then(function(values) { 58 | let items = values[0]; 59 | let currentZoom = values[1]; 60 | 61 | $('ignoreOverrides').checked = items.ignoreOverrides; 62 | $('idealFontSize').value = items.idealFontSize; 63 | 64 | $('fontSizeWeight').value = items.metricWeights.fontSize; 65 | $('marginWeight').value = items.metricWeights.margin; 66 | }).then(setSampleTextSize); 67 | } 68 | 69 | 70 | /** 71 | * Set the font size of the sample text for the ideal font size option. 72 | */ 73 | function setSampleTextSize() { 74 | doGetZoom(undefined).then(function(currentZoom) { 75 | // Set the font size to how the idealFontSize would look at 100% zoom. 76 | $('sampleText').style.fontSize = 77 | ($('idealFontSize').value / currentZoom) + 'px'; 78 | }); 79 | } 80 | 81 | 82 | /** 83 | * Toggle whether the advanced settings are visible. 84 | */ 85 | function toggleAdvancedSettings() { 86 | let wasVisible = $('advancedSettings').classList.toggle('hidden'); 87 | $('advancedSettingsToggle').textContent = 88 | (wasVisible ? 'Show' : 'Hide') + ' advanced settings...'; 89 | } 90 | 91 | 92 | chrome.tabs.onZoomChange.addListener(setSampleTextSize); 93 | $('idealFontSize').addEventListener('change', setSampleTextSize); 94 | 95 | $('ignoreOverrides').addEventListener('change', saveOptions); 96 | $('idealFontSize').addEventListener('change', saveOptions); 97 | $('fontSizeWeight').addEventListener('change', saveOptions); 98 | $('marginWeight').addEventListener('change', saveOptions); 99 | 100 | $('advancedSettingsToggle').addEventListener('click', toggleAdvancedSettings); 101 | 102 | document.addEventListener('DOMContentLoaded', restoreOptions); 103 | -------------------------------------------------------------------------------- /persistent_set.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | 'use strict'; 6 | 7 | 8 | /** 9 | * A set of values that are kept in persistent storage. 10 | */ 11 | class PersistentSet { 12 | /** 13 | * @param {string} storageKey The key to use to store the set. 14 | */ 15 | constructor(storageKey) { 16 | this.storageKey_ = storageKey; 17 | } 18 | 19 | /** 20 | * Adds a value to the set. 21 | * @param {*} value The value to add. 22 | * @return {Promise} A promise that will resolve when the value is added. 23 | */ 24 | add(value) { 25 | return this.getStorage_().then(function(persistentSet) { 26 | persistentSet.add(value); 27 | return this.setStorage_(persistentSet); 28 | }.bind(this)); 29 | } 30 | 31 | /** 32 | * Deletes a value from the set. 33 | * @param {*} value The value to delete. 34 | * @return {Promise} A promise that will resolve when the value is deleted. 35 | */ 36 | delete(value) { 37 | return this.getStorage_().then(function(persistentSet) { 38 | if (persistentSet.has(value)) { 39 | persistentSet.delete(value); 40 | return this.setStorage_(persistentSet); 41 | } 42 | }.bind(this)); 43 | } 44 | 45 | /** 46 | * Determine if a value is in the set. 47 | * @param {*} value The value to check. 48 | * @return {Promise} A promise that will resolve 49 | * to whether the value is in the set. 50 | */ 51 | has(value) { 52 | return this.getStorage_().then(function(persistentSet) { 53 | return persistentSet.has(value); 54 | }); 55 | } 56 | 57 | /** 58 | * Deletes all elements from the set. 59 | * @return {Promise} A promise that will resolve when the set is cleared. 60 | */ 61 | clear() { 62 | return doStorageLocalRemove(this.storageKey_); 63 | } 64 | 65 | /** 66 | * Gets the set from persistent storage. 67 | * @private 68 | * @return {Promise} A promise that will resolve to a Set with the 69 | * persisted values. 70 | */ 71 | getStorage_() { 72 | return doStorageLocalGet(this.storageKey_).then(function(data) { 73 | return new Set(data[this.storageKey_] || []); 74 | }.bind(this)); 75 | } 76 | 77 | /** 78 | * Store the set into persistent storage. 79 | * @private 80 | * @param {Set} persistentSet The Set to store in persistent storage. 81 | * @return {Promise} A promise that will resolve when the set is stored. 82 | */ 83 | setStorage_(persistentSet) { 84 | let data = {}; 85 | data[this.storageKey_] = Array.from(persistentSet); 86 | 87 | return doStorageLocalSet(data); 88 | } 89 | }; 90 | --------------------------------------------------------------------------------