Refactor and speed up popup & manager

Popup:
* Enforce 200-800px range for the popup width option

Manage:
* faster search via cachedStyles.byId
* faster restoration of search results on history nav
* style name is clickable and opens the editor
* animated highlight of style element on update/add/save
* expandable extra applies-to targets
* remember scroll position on normal history navigation
* boz-sizing in #header, also in editor
* applies-to targets use structured markup
* get*Tab*, enableStyle and deleteStyle are promisified
This commit is contained in:
tophf 2017-03-21 04:32:38 +03:00
parent 9fd067c6e3
commit 2f4da37fdb
16 changed files with 1199 additions and 982 deletions

View File

@ -34,6 +34,8 @@ globals:
getType: true getType: true
importStyles: true importStyles: true
getActiveTabRealURL: true getActiveTabRealURL: true
openURL: true
onDOMready: true
getDomains: true getDomains: true
webSqlStorage: true webSqlStorage: true
notifyAllTabs: true notifyAllTabs: true

View File

@ -1,4 +1,4 @@
/* globals wildcardAsRegExp, KEEP_CHANNEL_OPEN */ /* globals openURL, wildcardAsRegExp, KEEP_CHANNEL_OPEN */
// This happens right away, sometimes so fast that the content script isn't even ready. That's // This happens right away, sometimes so fast that the content script isn't even ready. That's
// why the content script also asks for this stuff. // why the content script also asks for this stuff.
@ -149,21 +149,6 @@ chrome.tabs.onAttached.addListener(function(tabId, data) {
}); });
}); });
function openURL(options) {
chrome.tabs.query({currentWindow: true, url: options.url}, function(tabs) {
// switch to an existing tab with the requested url
if (tabs.length) {
chrome.tabs.highlight({windowId: tabs[0].windowId, tabs: tabs[0].index}, function (window) {});
} else {
delete options.method;
getActiveTab(function(tab) {
// re-use an active new tab page
chrome.tabs[tab.url == "chrome://newtab/" ? "update" : "create"](options);
});
}
});
}
var codeMirrorThemes; var codeMirrorThemes;
getCodeMirrorThemes(function(themes) { getCodeMirrorThemes(function(themes) {
codeMirrorThemes = themes; codeMirrorThemes = themes;

View File

@ -64,6 +64,7 @@ function importFromString(jsonString) {
}); });
} else { } else {
refreshAllTabs().then(() => { refreshAllTabs().then(() => {
scrollTo(0, 0);
setTimeout(alert, 100, numStyles + ' styles installed/updated'); setTimeout(alert, 100, numStyles + ' styles installed/updated');
resolve(numStyles); resolve(numStyles);
}); });

View File

@ -45,14 +45,15 @@
} }
/************ header ************/ /************ header ************/
#header { #header {
height: calc(100vh - 30px); width: 280px;
height: 100vh;
overflow: auto; overflow: auto;
width: 250px;
position: fixed; position: fixed;
top: 0; top: 0;
padding: 15px; padding: 15px;
border-right: 1px dashed #AAA; border-right: 1px dashed #AAA;
-webkit-box-shadow: 0 0 3rem -1.2rem black; -webkit-box-shadow: 0 0 3rem -1.2rem black;
box-sizing: border-box;
} }
#header h1 { #header h1 {
margin-top: 0; margin-top: 0;

View File

@ -415,7 +415,7 @@ chrome.tabs.query({currentWindow: true}, function(tabs) {
}); });
}); });
getActiveTab(function(tab) { getActiveTab().then(tab => {
useHistoryBack = sessionStorageHash("manageStylesHistory").value[tab.id] == location.href; useHistoryBack = sessionStorageHash("manageStylesHistory").value[tab.id] == location.href;
}); });

View File

@ -1,4 +1,4 @@
healthCheck(); setTimeout(healthCheck, 0);
function healthCheck() { function healthCheck() {
chrome.runtime.sendMessage({method: "healthCheck"}, function(ok) { chrome.runtime.sendMessage({method: "healthCheck"}, function(ok) {

284
manage.css Normal file
View File

@ -0,0 +1,284 @@
body {
margin: 0;
font: 12px arial, sans-serif;
}
a,
a:visited {
color: inherit;
opacity: .75;
-webkit-transition: opacity 0.5s;
}
a:hover,
a.homepage:hover {
opacity: .6;
}
a.homepage {
opacity: 1;
}
#header {
width: 280px;
height: 100vh;
position: fixed;
top: 0;
padding: 15px;
border-right: 1px dashed #AAA;
-webkit-box-shadow: 0 0 50px -18px black;
overflow: auto;
box-sizing: border-box;
}
#header h1 {
margin-top: 0;
}
#installed {
position: relative;
margin-left: 280px;
}
.entry {
margin: 0;
padding: 1.25em 2em 1.5em;
border-top: 1px solid #ddd;
}
.entry:first-child {
border-top: none;
}
.svg-icon {
cursor: pointer;
vertical-align: middle;
margin-left: 0.3rem;
margin-right: 0.3rem;
margin-top: -4px;
transition: opacity .5s;
width: 16px;
height: 16px;
fill: currentColor;
}
.style-name {
margin-top: .25em;
}
.style-name a, .style-edit-link {
text-decoration: none;
color: inherit;
}
.applies-to {
word-break: break-word;
}
.applies-to,
.actions {
padding-left: 15px;
margin-bottom: 0;
}
.applies-to > :first-child {
margin-right: .5ex;
}
.applies-to .target:hover {
background-color: rgba(128, 128, 128, .15);
}
.applies-to-extra {
display: inline;
}
.applies-to-extra summary {
font-weight: bold;
cursor: pointer;
list-style-type: none; /* for FF, allegedly */
}
.applies-to-extra summary::-webkit-details-marker {
display: none;
}
.disabled h2::after {
content: " (Disabled)";
}
.disabled {
opacity: 0.5;
}
.disabled .disable {
display: none;
}
.enabled .enable {
display: none;
}
/* Default, no update buttons */
.update,
.check-update {
display: none;
}
/* Check update button for things that can*/
*[style-update-url] .check-update {
display: inline;
}
/* Update check in progress */
.checking-update .check-update {
display: none;
}
/* Updates available */
.can-update .update {
display: inline;
}
.can-update .check-update {
display: none;
}
/* Updates not available */
.no-update .check-update {
display: none;
}
/* Updates done */
.update-done .check-update {
display: none;
}
.hidden {
display: none
}
fieldset {
border-width: 1px;
border-radius: 6px;
margin: 1em 0;
}
.enabled-only > .disabled,
.edited-only > [style-update-url] {
display: none;
}
#search {
width: calc(100% - 4px);
margin: 0.25rem 4px 0;
border-radius: 0.25rem;
padding-left: 0.25rem;
border-width: 1px;
}
#import ul {
margin-left: 0;
padding-left: 0;
list-style: none;
}
#import li {
margin-bottom: .5em;
}
#import pre {
background: #eee;
overflow: auto;
margin: 0 0 .5em 0;
}
/* drag-n-drop on import button */
.dropzone:after {
background-color: rgba(0, 0, 0, 0.7);
color: white;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: 1000;
position: fixed;
padding: calc(50vh - 3em) calc(50vw - 5em);
content: attr(dragndrop-hint);
text-shadow: 1px 1px 10px black;
font-size: xx-large;
text-align: center;
animation: fadein 1s cubic-bezier(.03, .67, .08, .94);
animation-fill-mode: both;
}
.fadeout.dropzone:after {
animation: fadeout .25s ease-in-out;
animation-fill-mode: both;
}
@keyframes fadein {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeout {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@media (max-width: 675px) {
#header {
height: auto;
position: static;
width: auto;
border-right: none;
border-bottom: 1px dashed #AAA;
}
#installed {
position: static;
margin-left: 0;
overflow: visible;
}
#header h1,
#header h2,
#header h3,
#backup-message {
display: none;
}
#header p,
#header fieldset div,
#backup {
display: inline-block;
}
#backup {
margin-right: 1em;
}
#backup p,
#header fieldset {
margin: 0;
}
.entry {
margin: 0;
}
}

View File

@ -1,312 +1,128 @@
<html> <html id="stylus">
<head> <head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"> <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title i18n-text="manageTitle"></title> <title i18n-text="manageTitle"></title>
<style> <link href="manage.css" rel="stylesheet">
body {
margin: 0;
font: 12px arial, sans-serif;
}
a,
a:visited {
color: inherit;
opacity: .75;
-webkit-transition: opacity 0.5s;
}
a:hover,
a.homepage:hover {
opacity: .6;
}
a.homepage {
opacity: 1;
}
#header {
height: 100%;
width: 250px;
position: fixed;
top: 0;
padding: 15px;
border-right: 1px dashed #AAA;
-webkit-box-shadow: 0 0 50px -18px black;
}
#header h1 {
margin-top: 0;
}
#installed {
position: relative;
margin-left: 280px;
}
[style-id] {
margin: 10px;
padding: 0 15px;
}
[style-id] {
border-top: 2px solid gray;
}
#installed::after {
content: "";
position: absolute;
top: 0;
width: 100%;
height: 2px;
background-color: #fff;
}
.svg-icon {
cursor: pointer;
vertical-align: middle;
margin-left: 0.3rem;
margin-right: 0.3rem;
margin-top: -4px;
transition: opacity .5s;
width: 16px;
height: 16px;
fill: currentColor;
}
.style-name {
margin-top: .25em;
word-break: break-word;
}
.applies-to {
word-break: break-word;
}
.applies-to,
.actions {
padding-left: 15px;
}
.applies-to-extra {
font-weight: bold;
}
.disabled h2::after {
content: " (Disabled)";
}
.disabled {
opacity: 0.5;
}
.disabled .disable {
display: none;
}
.enabled .enable {
display: none;
}
.style-name a[target="_blank"] {
text-decoration: none;
color: inherit;
}
/* Default, no update buttons */
.update,
.check-update {
display: none;
}
/* Check update button for things that can*/
*[style-update-url] .check-update {
display: inline;
}
/* Update check in progress */
.checking-update .check-update {
display: none;
}
/* Updates available */
.can-update .update {
display: inline;
}
.can-update .check-update {
display: none;
}
/* Updates not available */
.no-update .check-update {
display: none;
}
/* Updates done */
.update-done .check-update {
display: none;
}
.hidden {
display: none
}
@media(max-width:675px) {
#header {
height: auto;
position: inherit;
width: auto;
border-right: none;
}
#installed {
margin-left: 0;
}
[style-id] {
margin: 0;
}
}
#header {
overflow: auto;
height: calc(100vh - 30px)
}
fieldset {
border-width: 1px;
border-radius: 6px;
margin: 1em 0;
}
.enabled-only > .disabled,
.edited-only > [style-update-url] {
display: none;
}
#search {
width: calc(100% - 4px);
margin: 0.25rem 4px 0;
border-radius: 0.25rem;
padding-left: 0.25rem;
border-width: 1px;
}
#import ul {
margin-left: 0;
padding-left: 0;
list-style: none;
}
#import li {
margin-bottom: .5em;
}
#import pre {
background: #eee;
overflow: auto;
margin: 0 0 .5em 0;
}
/* drag-n-drop on import button */
.dropzone:after {
background-color: rgba(0, 0, 0, 0.7);
color: white;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: 1000;
position: fixed;
padding: calc(50vh - 3em) calc(50vw - 5em);
content: attr(dragndrop-hint);
text-shadow: 1px 1px 10px black;
font-size: xx-large;
text-align: center;
animation: fadein 1s cubic-bezier(.03,.67,.08,.94);
animation-fill-mode: both;
}
.fadeout.dropzone:after {
animation: fadeout .25s ease-in-out;
animation-fill-mode: both;
}
@keyframes fadein {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fadeout {
from { opacity: 1; }
to { opacity: 0; }
}
</style>
<template data-id="style"> <template data-id="style">
<div> <div class="entry">
<h2 class="style-name"></h2> <h2 class="style-name"><a href="edit.html?id="></a></h2>
<p class="applies-to"></p> <p class="applies-to"><span></span></p>
<p class="actions"> <p class="actions">
<a class="style-edit-link" href="edit.html?id="><button i18n-text="editStyleLabel"></button></a> <a class="style-edit-link" href="edit.html?id=">
<button class="enable" i18n-text="enableStyleLabel"></button> <button i18n-text="editStyleLabel"></button>
<button class="disable" i18n-text="disableStyleLabel"></button>
<button class="delete" i18n-text="deleteStyleLabel"></button>
<button class="check-update" i18n-text="checkForUpdate"></button>
<button class="update" i18n-text="installUpdate"></button>
<span class="update-note"></span>
</p>
</div>
</template>
<template data-id="styleHomepage">
<a target="_blank" class="homepage">
<svg class="svg-icon"><use xlink:href="#svg-icon-external-link"/></svg>
</a> </a>
</template> <button class="enable" i18n-text="enableStyleLabel"></button>
<button class="disable" i18n-text="disableStyleLabel"></button>
<button class="delete" i18n-text="deleteStyleLabel"></button>
<button class="check-update" i18n-text="checkForUpdate"></button>
<button class="update" i18n-text="installUpdate"></button>
<span class="update-note"></span>
</p>
</div>
</template>
<script src="localization.js"></script> <template data-id="styleHomepage">
<script src="health.js"></script> <a target="_blank" class="homepage">
<script src="storage.js"></script> <svg class="svg-icon"><use xlink:href="#svg-icon-external-link"/></svg>
<script src="messaging.js"></script> </a>
<script src="apply.js"></script> </template>
<script src="manage.js"></script>
<template data-id="appliesToTarget">
<span class="target"></span>
</template>
<template data-id="appliesToSeparator">
<span class="sep">, </span>
</template>
<template data-id="appliesToEverything">
<span class="target" i18n-text="appliesToEverything"></span>
</template>
<template data-id="extraAppliesTo">
<details class="applies-to-extra">
<summary i18n-html="appliesDisplayTruncatedSuffix"></summary>
</details>
</template>
<script src="health.js"></script>
<script src="storage.js"></script>
<script src="messaging.js"></script>
<script src="apply.js"></script>
<script src="localization.js"></script>
</head> </head>
<body id="stylus-manage" i18n-dragndrop-hint="dragDropMessage"> <body id="stylus-manage" i18n-dragndrop-hint="dragDropMessage">
<div id="header"> <div id="header">
<h1 id="manage-heading" i18n-text="manageHeading"></h1> <h1 id="manage-heading" i18n-text="manageHeading"></h1>
<fieldset> <fieldset>
<legend id="filters" i18n-text="manageFilters"></legend> <legend id="filters" i18n-text="manageFilters"></legend>
<div> <div>
<input id="manage.onlyEnabled" type="checkbox"> <input id="manage.onlyEnabled" type="checkbox">
<label id="manage.onlyEnabled-label" for="manage.onlyEnabled" i18n-text="manageOnlyEnabled"></label> <label id="manage.onlyEnabled-label" for="manage.onlyEnabled" i18n-text="manageOnlyEnabled"></label>
</div>
<div>
<input id="manage.onlyEdited" type="checkbox">
<label id="manage.onlyEdited-label" for="manage.onlyEdited" i18n-text="manageOnlyEdited"></label>
</div>
<div>
<input id="search" type="search" i18n-placeholder="searchStyles">
</div>
</fieldset>
<p>
<button id="check-all-updates" i18n-text="checkAllUpdates"></button>
</p>
<p>
<button id="apply-all-updates" class="hidden" i18n-text="applyAllUpdates"></button>
<span id="update-all-no-updates" class="hidden" i18n-text="updateAllCheckSucceededNoUpdate"></span>
</p>
<p>
<a href="edit.html">
<button id="add-style-label" i18n-text="addStyleLabel"></button>
</a>
</p>
<div id="options">
<h2 id="options-heading" i18n-text="optionsHeading"></h2>
<div>
<input id="show-badge" type="checkbox">
<label id="show-badge-label" for="show-badge" i18n-text="prefShowBadge"></label>
</div>
<div>
<input id="popup.stylesFirst" type="checkbox">
<label id="stylesFirst-label" for="popup.stylesFirst" i18n-text="popupStylesFirst"></label>
</div>
<div id="more-options">
<h3 id="options-subheading" i18n-text="optionsSubheading"></h3>
<button id="manage-options-button" i18n-text="openOptionsManage"></button>
<button id="manage-shortcuts-button" i18n-text="openOptionsShortcuts"></button>
<p>
<button id="editor-styles-button" i18n-text="editorStylesButton"></button>
</p>
</div>
</div>
<div id="backup">
<h2 id="backup-title" i18n-text="backupButtons"></h2>
<span id="backup-message" i18n-text="backupMessage"></span>
<p>
<button id="file-all-styles" i18n-text="bckpInstStyles"></button>
<button id="unfile-all-styles" i18n-text="retrieveBckp"></button>
</p>
</div>
<p id="manage-text" i18n-html="manageText"></p>
</div> </div>
<div id="installed"></div> <div>
<input id="manage.onlyEdited" type="checkbox">
<label id="manage.onlyEdited-label" for="manage.onlyEdited" i18n-text="manageOnlyEdited"></label>
</div>
<div>
<input id="search" type="search" i18n-placeholder="searchStyles">
</div>
</fieldset>
<p>
<button id="check-all-updates" i18n-text="checkAllUpdates"></button>
</p>
<p>
<button id="apply-all-updates" class="hidden" i18n-text="applyAllUpdates"></button>
<span id="update-all-no-updates" class="hidden" i18n-text="updateAllCheckSucceededNoUpdate"></span>
</p>
<p>
<a href="edit.html">
<button id="add-style-label" i18n-text="addStyleLabel"></button>
</a>
</p>
<div id="options">
<h2 id="options-heading" i18n-text="optionsHeading"></h2>
<div>
<input id="show-badge" type="checkbox">
<label id="show-badge-label" for="show-badge" i18n-text="prefShowBadge"></label>
</div>
<div>
<input id="popup.stylesFirst" type="checkbox">
<label id="stylesFirst-label" for="popup.stylesFirst" i18n-text="popupStylesFirst"></label>
</div>
<div id="more-options">
<h3 id="options-subheading" i18n-text="optionsSubheading"></h3>
<button id="manage-options-button" i18n-text="openOptionsManage"></button>
<button id="manage-shortcuts-button" i18n-text="openOptionsShortcuts"></button>
<p>
<button id="editor-styles-button" i18n-text="editorStylesButton"></button>
</p>
</div>
</div>
<div id="backup">
<h2 id="backup-title" i18n-text="backupButtons"></h2>
<span id="backup-message" i18n-text="backupMessage"></span>
<p>
<button id="file-all-styles" i18n-text="bckpInstStyles"></button>
<button id="unfile-all-styles" i18n-text="retrieveBckp"></button>
</p>
</div>
<p id="manage-text" i18n-html="manageText"></p>
</div>
<div id="installed"></div>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none"> <svg xmlns="http://www.w3.org/2000/svg" style="display: none">
<symbol id="svg-icon-external-link" height="16" width="16" viewBox="0 0 8 8"> <symbol id="svg-icon-external-link" height="16" width="16" viewBox="0 0 8 8">
<path d="M0 0v8h8v-2h-1v1h-6v-6h1v-1h-2zm4 0l1.5 1.5-2.5 2.5 1 1 2.5-2.5 1.5 1.5v-4h-4z"></path> <path d="M0 0v8h8v-2h-1v1h-6v-6h1v-1h-2zm4 0l1.5 1.5-2.5 2.5 1 1 2.5-2.5 1.5 1.5v-4h-4z"></path>
</symbol> </symbol>
</svg> </svg>
<script src="openOptions.js"></script> <script src="manage.js"></script>
<script src="backup/fileSaveLoad.js"></script> <script src="openOptions.js"></script>
<script src="backup/fileSaveLoad.js"></script>
</body> </body>
</html> </html>

819
manage.js
View File

@ -1,434 +1,471 @@
/* globals styleSectionsEqual */ /* globals styleSectionsEqual */
var lastUpdatedStyleId = null;
var installed;
var appliesToExtraTemplate = document.createElement("span"); const installed = $('#installed');
appliesToExtraTemplate.className = "applies-to-extra"; const TARGET_LABEL = t('appliesDisplay', '').trim();
appliesToExtraTemplate.innerHTML = " " + t('appliesDisplayTruncatedSuffix'); const TARGET_TYPES = ['domains', 'urls', 'urlPrefixes', 'regexps'];
const TARGET_LIMIT = 10;
getStylesSafe({code: false}).then(showStyles);
function showStyles(styles) { getStylesSafe({code: false})
if (!installed) { .then(showStyles)
// "getStyles" message callback is invoked before document is loaded, .then(initGlobalEvents);
// postpone the action until DOMContentLoaded is fired
document.stylishStyles = styles;
return; chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
} switch (msg.method) {
styles.sort(function(a, b) { return a.name.localeCompare(b.name)}); case 'styleUpdated':
styles.forEach(handleUpdate); case 'styleAdded':
if (history.state) { handleUpdate(msg.style, msg);
window.scrollTo(0, history.state.scrollY); break;
} case 'styleDeleted':
handleDelete(msg.id);
break;
}
});
function initGlobalEvents() {
$('#check-all-updates').onclick = checkUpdateAll;
$('#apply-all-updates').onclick = applyUpdateAll;
$('#search').oninput = searchStyles;
// focus search field on / key
document.onkeypress = event => {
if (event.keyCode == 47
&& !event.altKey && !event.shiftKey && !event.ctrlKey && !event.metaKey
&& !event.target.matches('[type="text"], [type="search"]')) {
event.preventDefault();
$('#search').focus();
}
};
// remember scroll position on normal history navigation
document.addEventListener('visibilitychange', event => {
if (document.visibilityState != 'visible') {
rememberScrollPosition();
}
});
setupLivePrefs([
'manage.onlyEnabled',
'manage.onlyEdited',
'show-badge',
'popup.stylesFirst'
]);
[
['enabled-only', $('#manage.onlyEnabled')],
['edited-only', $('#manage.onlyEdited')],
]
.forEach(([className, checkbox]) => {
checkbox.onchange = () => installed.classList.toggle(className, checkbox.checked);
checkbox.onchange();
});
} }
function showStyles(styles = []) {
const sorted = styles
.map(style => ({name: style.name.toLocaleLowerCase(), style}))
.sort((a, b) => a.name < b.name ? -1 : a.name == b.name ? 0 : 1);
const shouldRenderAll = history.state && history.state.scrollY > innerHeight;
const renderBin = document.createDocumentFragment();
renderStyles(0);
// TODO: remember how many styles fit one page to display just that portion first next time
function renderStyles(index) {
const t0 = performance.now();
while (index < sorted.length && (shouldRenderAll || performance.now() - t0 < 10)) {
renderBin.appendChild(createStyleElement(sorted[index++].style));
}
if ($('#search').value) {
// re-apply filtering on history Back
searchStyles(true, renderBin);
}
installed.appendChild(renderBin);
if (index < sorted.length) {
setTimeout(renderStyles, 0, index);
}
else if (shouldRenderAll && history.state && 'scrollY' in history.state) {
setTimeout(() => scrollTo(0, history.state.scrollY));
}
}
}
function createStyleElement(style) { function createStyleElement(style) {
var e = template.style.cloneNode(true); const entry = template.style.cloneNode(true);
e.setAttribute("class", style.enabled ? "enabled" : "disabled"); entry.classList.add(style.enabled ? 'enabled' : 'disabled');
e.setAttribute("style-id", style.id); entry.setAttribute('style-id', style.id);
if (style.updateUrl) { entry.styleId = style.id;
e.setAttribute("style-update-url", style.updateUrl); if (style.updateUrl) {
} entry.setAttribute('style-update-url', style.updateUrl);
if (style.md5Url) { }
e.setAttribute("style-md5-url", style.md5Url); if (style.md5Url) {
} entry.setAttribute('style-md5-url', style.md5Url);
if (style.originalMd5) { }
e.setAttribute("style-original-md5", style.originalMd5); if (style.originalMd5) {
} entry.setAttribute('style-original-md5', style.originalMd5);
}
var styleName = e.querySelector(".style-name"); const styleName = $('.style-name', entry);
styleName.appendChild(document.createTextNode(style.name)); const styleNameEditLink = $('a', styleName);
if (style.url) { styleNameEditLink.appendChild(document.createTextNode(style.name));
var homepage = template.styleHomepage.cloneNode(true) styleNameEditLink.href = styleNameEditLink.getAttribute('href') + style.id;
homepage.setAttribute("href", style.url); styleNameEditLink.onclick = EntryOnClick.edit;
styleName.appendChild(document.createTextNode(" " )); if (style.url) {
styleName.appendChild(homepage); const homepage = template.styleHomepage.cloneNode(true);
} homepage.href = style.url;
var domains = []; styleName.appendChild(document.createTextNode(' '));
var urls = []; styleName.appendChild(homepage);
var urlPrefixes = []; }
var regexps = [];
function add(array, property) { const targets = new Map(TARGET_TYPES.map(t => [t, new Set()]));
style.sections.forEach(function(section) { const decorations = {
if (section[property]) { urlPrefixesAfter: '*',
section[property].filter(function(value) { regexpsBefore: '/',
return array.indexOf(value) == -1; regexpsAfter: '/',
}).forEach(function(value) { };
array.push(value); for (let [name, target] of targets.entries()) {
});; for (let section of style.sections) {
} for (let targetValue of section[name] || []) {
}); target.add(
} (decorations[name + 'Before'] || '') +
add(domains, 'domains'); targetValue.trim() +
add(urls, 'urls'); (decorations[name + 'After'] || ''));
add(urlPrefixes, 'urlPrefixes'); }
add(regexps, 'regexps'); }
var appliesToToShow = []; }
if (domains) const appliesTo = $('.applies-to', entry);
appliesToToShow = appliesToToShow.concat(domains); appliesTo.firstElementChild.textContent = TARGET_LABEL;
if (urls) const targetsList = Array.prototype.concat.apply([],
appliesToToShow = appliesToToShow.concat(urls); [...targets.values()].map(set => [...set.values()]));
if (urlPrefixes) if (!targetsList.length) {
appliesToToShow = appliesToToShow.concat(urlPrefixes.map(function(u) { return u + "*"; })); appliesTo.appendChild(template.appliesToEverything.cloneNode(true));
if (regexps) entry.classList.add('global');
appliesToToShow = appliesToToShow.concat(regexps.map(function(u) { return "/" + u + "/"; })); } else {
var appliesToString = ""; let index = 0;
var showAppliesToExtra = false; let container = appliesTo;
if (appliesToToShow.length == "") for (let target of targetsList) {
appliesToString = t('appliesToEverything'); if (index > 0) {
else if (appliesToToShow.length <= 10) container.appendChild(template.appliesToSeparator.cloneNode(true));
appliesToString = appliesToToShow.join(", "); }
else { if (++index == TARGET_LIMIT) {
appliesToString = appliesToToShow.slice(0, 10).join(", "); container = appliesTo.appendChild(template.extraAppliesTo.cloneNode(true));
showAppliesToExtra = true; }
} const item = template.appliesToTarget.cloneNode(true);
e.querySelector(".applies-to").appendChild(document.createTextNode(t('appliesDisplay', [appliesToString]))); item.textContent = target;
if (showAppliesToExtra) { container.appendChild(item);
e.querySelector(".applies-to").appendChild(appliesToExtraTemplate.cloneNode(true)); }
} }
var editLink = e.querySelector(".style-edit-link");
editLink.setAttribute("href", editLink.getAttribute("href") + style.id); const editLink = $('.style-edit-link', entry);
editLink.addEventListener("click", function(event) { editLink.href = editLink.getAttribute('href') + style.id;
if (!event.altKey) { editLink.onclick = EntryOnClick.edit;
var left = event.button == 0, middle = event.button == 1,
shift = event.shiftKey, ctrl = event.ctrlKey; $('.enable', entry).onclick = EntryOnClick.toggle;
var openWindow = left && shift && !ctrl; $('.disable', entry).onclick = EntryOnClick.toggle;
var openBackgroundTab = (middle && !shift) || (left && ctrl && !shift); $('.check-update', entry).onclick = EntryOnClick.check;
var openForegroundTab = (middle && shift) || (left && ctrl && shift); $('.update', entry).onclick = EntryOnClick.update;
var url = event.target.href || event.target.parentNode.href; $('.delete', entry).onclick = EntryOnClick.delete;
event.preventDefault(); return entry;
event.stopPropagation();
if (openWindow || openBackgroundTab || openForegroundTab) {
if (openWindow) {
var options = prefs.get("windowPosition");
options.url = url;
chrome.windows.create(options);
} else {
chrome.runtime.sendMessage({
method: "openURL",
url: url,
active: openForegroundTab
});
}
} else {
history.replaceState({scrollY: window.scrollY}, document.title);
getActiveTab(function(tab) {
sessionStorageHash("manageStylesHistory").set(tab.id, url);
location.href = url;
});
}
}
});
e.querySelector(".enable").addEventListener("click", function(event) { enable(event, true); }, false);
e.querySelector(".disable").addEventListener("click", function(event) { enable(event, false); }, false);
e.querySelector(".check-update").addEventListener("click", doCheckUpdate, false);
e.querySelector(".update").addEventListener("click", doUpdate, false);
e.querySelector(".delete").addEventListener("click", doDelete, false);
return e;
} }
function enable(event, enabled) { class EntryOnClick {
var id = getId(event);
enableStyle(id, enabled); static edit(event) {
if (event.altKey) {
return;
}
event.preventDefault();
event.stopPropagation();
const left = event.button == 0, middle = event.button == 1,
shift = event.shiftKey, ctrl = event.ctrlKey;
const openWindow = left && shift && !ctrl;
const openBackgroundTab = (middle && !shift) || (left && ctrl && !shift);
const openForegroundTab = (middle && shift) || (left && ctrl && shift);
const url = event.target.closest('[href]').href;
if (openWindow || openBackgroundTab || openForegroundTab) {
if (openWindow) {
chrome.windows.create(Object.assign(prefs.get('windowPosition'), {url}));
} else {
openURL({url, active: openForegroundTab});
}
} else {
rememberScrollPosition();
getActiveTab().then(tab => {
sessionStorageHash('manageStylesHistory').set(tab.id, url);
location.href = url;
});
}
}
static toggle(event) {
enableStyle(getClickedStyleId(event), this.matches('.enable'))
.then(handleUpdate);
}
static check(event) {
checkUpdate(getClickedStyleElement(event));
}
static update(event) {
const element = getClickedStyleElement(event);
const updatedCode = element.updatedCode;
// update everything but name
delete updatedCode.name;
updatedCode.id = element.styleId;
updatedCode.reason = 'update';
saveStyle(updatedCode)
.then(style => handleUpdate(style, {reason: 'update'}));
}
static delete(event) {
if (confirm(t('deleteStyleConfirm'))) {
deleteStyle(getClickedStyleId(event))
.then(handleDelete);
}
}
} }
function doDelete(event) {
if (!confirm(t('deleteStyleConfirm'))) { function handleUpdate(style, {reason} = {}) {
return; const element = createStyleElement(style);
} const oldElement = $(`[style-id="${style.id}"]`, installed);
var id = getId(event); if (!oldElement) {
deleteStyle(id); installed.appendChild(element);
} else {
installed.replaceChild(element, oldElement);
if (reason == 'update') {
element.classList.add('update-done');
$('.update-note', element).innerHTML = t('updateCompleted');
}
}
// align to the bottom of the visible area if wasn't visible
element.scrollIntoView(false);
} }
function getId(event) {
return getStyleElement(event).getAttribute("style-id");
}
function getStyleElement(event) {
var e = event.target;
while (e) {
if (e.hasAttribute("style-id")) {
return e;
}
e = e.parentNode;
}
return null;
}
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
switch (request.method) {
case "styleUpdated":
case "styleAdded":
handleUpdate(request.style);
break;
case "styleDeleted":
handleDelete(request.id);
break;
}
});
function handleUpdate(style) {
var element = createStyleElement(style);
var oldElement = installed.querySelector(`[style-id="${style.id}"]`);
if (!oldElement) {
installed.appendChild(element);
return;
}
installed.replaceChild(element, oldElement);
if (style.id == lastUpdatedStyleId) {
lastUpdatedStyleId = null;
element.className = element.className += ' update-done';
element.querySelector('.update-note').innerHTML = t('updateCompleted');
}
}
function handleDelete(id) { function handleDelete(id) {
var node = installed.querySelector("[style-id='" + id + "']"); const node = $(`[style-id="${id}"]`, installed);
if (node) { if (node) {
installed.removeChild(node); node.remove();
} }
} }
function doCheckUpdate(event) {
checkUpdate(getStyleElement(event));
}
function applyUpdateAll() { function applyUpdateAll() {
var btnApply = document.getElementById("apply-all-updates"); const btnApply = $('#apply-all-updates');
btnApply.disabled = true; btnApply.disabled = true;
setTimeout(function() { setTimeout(() => {
btnApply.style.display = "none"; btnApply.style.display = 'none';
btnApply.disabled = false; btnApply.disabled = false;
}, 1000); }, 1000);
Array.prototype.forEach.call(document.querySelectorAll(".can-update .update"), function(button) { [...document.querySelectorAll('.can-update .update')]
button.click(); .forEach(button => {
}); // align to the bottom of the visible area if wasn't visible
button.scrollIntoView(false);
button.click();
});
} }
function checkUpdateAll() { function checkUpdateAll() {
var btnCheck = document.getElementById("check-all-updates"); const btnCheck = $('#check-all-updates');
var btnApply = document.getElementById("apply-all-updates"); const btnApply = $('#apply-all-updates');
var noUpdates = document.getElementById("update-all-no-updates"); const noUpdates = $('#update-all-no-updates');
btnCheck.disabled = true; btnCheck.disabled = true;
btnApply.classList.add("hidden"); btnApply.classList.add('hidden');
noUpdates.classList.add("hidden"); noUpdates.classList.add('hidden');
var elements = document.querySelectorAll("[style-update-url]"); const elements = document.querySelectorAll('[style-update-url]');
var toCheckCount = elements.length; Promise.all([...elements].map(checkUpdate))
var updatableCount = 0; .then(updatables => {
Array.prototype.forEach.call(elements, function(element) { btnCheck.disabled = false;
checkUpdate(element, function(success) { if (updatables.includes(true)) {
if (success) { btnApply.classList.remove('hidden');
++updatableCount; } else {
} noUpdates.classList.remove('hidden');
if (--toCheckCount == 0) { setTimeout(() => {
btnCheck.disabled = false; noUpdates.classList.add('hidden');
if (updatableCount) { }, 10e3);
btnApply.classList.remove("hidden"); }
} else { });
noUpdates.classList.remove("hidden");
setTimeout(function() { // notify the automatic updater to reset the next automatic update accordingly
noUpdates.classList.add("hidden"); chrome.runtime.sendMessage({
}, 10000); method: 'resetInterval'
} });
}
});
});
// notify the automatic updater to reset the next automatic update accordingly
chrome.runtime.sendMessage({
method: 'resetInterval'
});
} }
function checkUpdate(element, callback) {
element.querySelector(".update-note").innerHTML = t('checkingForUpdate');
element.className = element.className.replace("checking-update", "").replace("no-update", "").replace("can-update", "") + " checking-update";
var id = element.getAttribute("style-id");
var url = element.getAttribute("style-update-url");
var md5Url = element.getAttribute("style-md5-url");
var originalMd5 = element.getAttribute("style-original-md5");
function handleSuccess(forceUpdate, serverJson) { function checkUpdate(element) {
chrome.runtime.sendMessage({method: "getStyles", id: id}, function(styles) { $('.update-note', element).innerHTML = t('checkingForUpdate');
var style = styles[0]; element.classList.remove('checking-update', 'no-update', 'can-update');
var needsUpdate = false; element.classList.add('checking-update');
if (!forceUpdate && styleSectionsEqual(style, serverJson)) { return new Updater(element).run();
handleNeedsUpdate("no", id, serverJson);
} else {
handleNeedsUpdate("yes", id, serverJson);
needsUpdate = true;
}
if (callback) {
callback(needsUpdate);
}
});
}
function handleFailure(status) {
if (status == 0) {
handleNeedsUpdate(t('updateCheckFailServerUnreachable'), id, null);
} else {
handleNeedsUpdate(t('updateCheckFailBadResponseCode', [status]), id, null);
}
if (callback) {
callback(false);
}
}
if (!md5Url || !originalMd5) {
checkUpdateFullCode(url, false, handleSuccess, handleFailure)
} else {
checkUpdateMd5(originalMd5, md5Url, function(needsUpdate) {
if (needsUpdate) {
// If the md5 shows a change we will update regardless of whether the code looks different
checkUpdateFullCode(url, true, handleSuccess, handleFailure);
} else {
handleNeedsUpdate("no", id, null);
if (callback) {
callback(false);
}
}
}, handleFailure);
}
} }
function checkUpdateFullCode(url, forceUpdate, successCallback, failureCallback) {
download(url, function(responseText) { class Updater {
successCallback(forceUpdate, JSON.parse(responseText)); constructor(element) {
}, failureCallback); Object.assign(this, {
element,
id: element.getAttribute('style-id'),
url: element.getAttribute('style-update-url'),
md5Url: element.getAttribute('style-md5-url'),
md5: element.getAttribute('style-original-md5'),
});
}
run() {
return this.md5Url && this.md5
? this.checkMd5()
: this.checkFullCode();
}
checkMd5() {
return this.download(this.md5Url).then(
md5 => md5.length == 32
? this.decideOnMd5(md5 != this.md5)
: this.onFailure(-1),
this.onFailure);
}
decideOnMd5(md5changed) {
if (md5changed) {
return this.checkFullCode({forceUpdate: true});
}
this.display();
}
checkFullCode({forceUpdate = false} = {}) {
return this.download(this.url).then(
text => this.handleJson(forceUpdate, JSON.parse(text)),
this.onFailure);
}
handleJson(forceUpdate, json) {
return getStylesSafe({id: this.id}).then(([style]) => {
const needsUpdate = forceUpdate || !styleSectionsEqual(style, json);
this.display({json: needsUpdate && json});
return needsUpdate;
});
}
onFailure(status) {
this.display({
message: status == 0
? t('updateCheckFailServerUnreachable')
: t('updateCheckFailBadResponseCode', [status]),
});
}
display({json, message} = {}) {
// json on success
// message on failure
// none on update not needed
this.element.classList.remove('checking-update');
if (json) {
this.element.classList.add('can-update');
this.element.updatedCode = json;
$('.update-note', this.element).innerHTML = '';
} else {
this.element.classList.add('no-update');
$('.update-note', this.element).innerHTML = message || t('updateCheckSucceededNoUpdate');
}
}
download(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onloadend = () => xhr.status == 200
? resolve(xhr.responseText)
: reject(xhr.status);
if (url.length > 2000) {
const [mainUrl, query] = url.split('?');
xhr.open('POST', mainUrl, true);
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr.send(query);
} else {
xhr.open('GET', url);
xhr.send();
}
});
}
} }
function checkUpdateMd5(originalMd5, md5Url, successCallback, failureCallback) {
download(md5Url, function(responseText) { function searchStyles(immediately, bin) {
if (responseText.length != 32) { const query = $('#search').value.toLocaleLowerCase();
failureCallback(-1); if (query == (searchStyles.lastQuery || '') && !bin) {
return; return;
} }
successCallback(responseText != originalMd5); searchStyles.lastQuery = query;
}, failureCallback); if (!immediately) {
clearTimeout(searchStyles.timeout);
searchStyles.timeout = setTimeout(doSearch, 200, true);
return;
}
for (let element of (bin || installed).children) {
const {style} = cachedStyles.byId.get(element.styleId) || {};
if (style) {
const isMatching = !query || isMatchingText(style.name) || isMatchingStyle(style);
element.style.display = isMatching ? '' : 'none';
}
}
function isMatchingStyle(style) {
for (let section of style.sections) {
for (let prop in section) {
const value = section[prop];
switch (typeof value) {
case 'string':
if (isMatchingText(value)) {
return true;
}
break;
case 'object':
for (let str of value) {
if (isMatchingText(str)) {
return true;
}
}
break;
}
}
}
}
function isMatchingText(text) {
return text.toLocaleLowerCase().indexOf(query) >= 0;
}
} }
function download(url, successCallback, failureCallback) {
var xhr = new XMLHttpRequest(); function getClickedStyleId(event) {
xhr.onreadystatechange = function (aEvt) { return (getClickedStyleElement(event) || {}).styleId;
if (xhr.readyState == 4) {
if (xhr.status == 200) {
successCallback(xhr.responseText)
} else {
failureCallback(xhr.status);
}
}
}
if (url.length > 2000) {
var parts = url.split("?");
xhr.open("POST", parts[0], true);
xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded");
xhr.send(parts[1]);
} else {
xhr.open("GET", url, true);
xhr.send();
}
} }
function handleNeedsUpdate(needsUpdate, id, serverJson) {
var e = document.querySelector("[style-id='" + id + "']"); function getClickedStyleElement(event) {
e.className = e.className.replace("checking-update", ""); return event.target.closest('.entry');
switch (needsUpdate) {
case "yes":
e.className += " can-update";
e.updatedCode = serverJson;
e.querySelector(".update-note").innerHTML = '';
break;
case "no":
e.className += " no-update";
e.querySelector(".update-note").innerHTML = t('updateCheckSucceededNoUpdate');
break;
default:
e.className += " no-update";
e.querySelector(".update-note").innerHTML = needsUpdate;
}
} }
function doUpdate(event) {
var element = getStyleElement(event);
var updatedCode = element.updatedCode; function rememberScrollPosition() {
// update everything but name history.replaceState({scrollY}, document.title);
delete updatedCode.name;
updatedCode.id = element.getAttribute('style-id');
updatedCode.method = "saveStyle";
// updating the UI will be handled by the general update listener
lastUpdatedStyleId = updatedCode.id;
chrome.runtime.sendMessage(updatedCode, function () {});
} }
function searchStyles(immediately) {
var query = document.getElementById("search").value.toLocaleLowerCase(); function $(selector, base = document) {
if (query == (searchStyles.lastQuery || "")) { if (selector.startsWith('#') && /^#[^,\s]+$/.test(selector)) {
return; return document.getElementById(selector.slice(1));
} } else {
searchStyles.lastQuery = query; return base.querySelector(selector);
if (immediately) { }
doSearch();
} else {
clearTimeout(searchStyles.timeout);
searchStyles.timeout = setTimeout(doSearch, 100);
}
function doSearch() {
chrome.runtime.sendMessage({method: "getStyles"}, function(styles) {
styles.forEach(function(style) {
var el = document.querySelector("[style-id='" + style.id + "']");
if (el) {
el.style.display = !query || isMatchingText(style.name) || isMatchingStyle(style) ? "" : "none";
}
});
});
}
function isMatchingStyle(style) {
return style.sections.some(function(section) {
return Object.keys(section).some(function(key) {
var value = section[key];
switch (typeof value) {
case "string": return isMatchingText(value);
case "object": return value.some(isMatchingText);
}
});
});
}
function isMatchingText(text) {
return text.toLocaleLowerCase().indexOf(query) >= 0;
}
} }
function onFilterChange (className, event) {
installed.classList.toggle(className, event.target.checked);
}
function initFilter(className, node) {
node.addEventListener("change", onFilterChange.bind(undefined, className), false);
onFilterChange(className, {target: node});
}
document.addEventListener("DOMContentLoaded", function() {
installed = document.getElementById("installed");
if (document.stylishStyles) {
showStyles(document.stylishStyles);
delete document.stylishStyles;
}
document.getElementById("check-all-updates").addEventListener("click", checkUpdateAll);
document.getElementById("apply-all-updates").addEventListener("click", applyUpdateAll);
document.getElementById("search").addEventListener("input", searchStyles);
searchStyles(true); // re-apply filtering on history Back
setupLivePrefs([
"manage.onlyEnabled",
"manage.onlyEdited",
"show-badge",
"popup.stylesFirst"
]);
initFilter("enabled-only", document.getElementById("manage.onlyEnabled"));
initFilter("edited-only", document.getElementById("manage.onlyEdited"));
});

View File

@ -2,6 +2,7 @@
const KEEP_CHANNEL_OPEN = true; const KEEP_CHANNEL_OPEN = true;
const OWN_ORIGIN = chrome.runtime.getURL(''); const OWN_ORIGIN = chrome.runtime.getURL('');
function notifyAllTabs(request) { function notifyAllTabs(request) {
// list all tabs including chrome-extension:// which can be ours // list all tabs including chrome-extension:// which can be ours
if (request.codeIsUpdated === false && request.style) { if (request.codeIsUpdated === false && request.style) {
@ -24,6 +25,7 @@ function notifyAllTabs(request) {
} }
} }
function refreshAllTabs() { function refreshAllTabs() {
return new Promise(resolve => { return new Promise(resolve => {
// list all tabs including chrome-extension:// which can be ours // list all tabs including chrome-extension:// which can be ours
@ -47,6 +49,7 @@ function refreshAllTabs() {
}); });
} }
function updateIcon(tab, styles) { function updateIcon(tab, styles) {
// while NTP is still loading only process the request for its main frame with a real url // while NTP is still loading only process the request for its main frame with a real url
// (but when it's loaded we should process style toggle requests from popups, for example) // (but when it's loaded we should process style toggle requests from popups, for example)
@ -62,7 +65,7 @@ function updateIcon(tab, styles) {
}); });
return; return;
} }
getTabRealURL(tab, url => { getTabRealURL(tab).then(url => {
// if we have access to this, call directly // if we have access to this, call directly
// (Chrome no longer sends messages to the page itself) // (Chrome no longer sends messages to the page itself)
const options = {method: 'getStyles', matchUrl: url, enabled: true, asHash: true}; const options = {method: 'getStyles', matchUrl: url, enabled: true, asHash: true};
@ -106,37 +109,80 @@ function updateIcon(tab, styles) {
} }
} }
function getActiveTab(callback) {
chrome.tabs.query({currentWindow: true, active: true}, function(tabs) { function getActiveTab() {
callback(tabs[0]); return new Promise(resolve =>
chrome.tabs.query({currentWindow: true, active: true}, tabs =>
resolve(tabs[0])));
}
function getActiveTabRealURL() {
return getActiveTab()
.then(getTabRealURL);
}
function getTabRealURL(tab) {
return new Promise(resolve => {
if (tab.url != 'chrome://newtab/') {
resolve(tab.url);
} else {
chrome.webNavigation.getFrame({tabId: tab.id, frameId: 0, processId: -1}, frame => {
frame && resolve(frame.url);
});
}
}); });
} }
function getActiveTabRealURL(callback) {
getActiveTab(function(tab) {
getTabRealURL(tab, callback);
});
}
function getTabRealURL(tab, callback) { function openURL({url}) {
if (tab.url != "chrome://newtab/") { url = !url.includes('://') ? chrome.runtime.getURL(url) : url;
callback(tab.url); return new Promise(resolve => {
} else { chrome.tabs.query({currentWindow: true, url}, tabs => {
chrome.webNavigation.getFrame({tabId: tab.id, frameId: 0, processId: -1}, function(frame) { // switch to an existing tab with the requested url
frame && callback(frame.url); if (tabs.length) {
chrome.tabs.highlight({
windowId: tabs[0].windowId,
tabs: tabs[0].index,
}, resolve);
} else {
// re-use an active new tab page
getActiveTab().then(tab =>
tab && tab.url == 'chrome://newtab/'
? chrome.tabs.update({url}, resolve)
: chrome.tabs.create({url}, resolve)
);
}
}); });
} });
} }
function onDOMready() {
if (document.readyState != 'loading') {
return Promise.resolve();
}
return new Promise(resolve => {
document.addEventListener('DOMContentLoaded', function _() {
document.removeEventListener('DOMContentLoaded', _);
resolve();
});
});
}
function stringAsRegExp(s, flags) { function stringAsRegExp(s, flags) {
return new RegExp(s.replace(/[{}()\[\]\/\\.+?^$:=*!|]/g, "\\$&"), flags); return new RegExp(s.replace(/[{}()\[\]\/\\.+?^$:=*!|]/g, '\\$&'), flags);
} }
// expands * as .*? // expands * as .*?
function wildcardAsRegExp(s, flags) { function wildcardAsRegExp(s, flags) {
return new RegExp(s.replace(/[{}()\[\]\/\\.+?^$:=!|]/g, "\\$&").replace(/\*/g, '.*?'), flags); return new RegExp(s.replace(/[{}()\[\]\/\\.+?^$:=!|]/g, '\\$&').replace(/\*/g, '.*?'), flags);
} }
var configureCommands = { var configureCommands = {
get url () { get url () {
return navigator.userAgent.indexOf('OPR') > -1 ? return navigator.userAgent.indexOf('OPR') > -1 ?

View File

@ -20,7 +20,7 @@
</tr> </tr>
<tr> <tr>
<td i18n-text="optionsPopupWidth"></td> <td i18n-text="optionsPopupWidth"></td>
<td><input type="number" id="popupWidth" min="200"></td> <td><input type="number" id="popupWidth" min="200" max="800"></td>
</tr> </tr>
<tr> <tr>
<td i18n-text="optionsUpdateInterval"><sup>1</sup></td> <td i18n-text="optionsUpdateInterval"><sup>1</sup></td>

View File

@ -7,6 +7,7 @@ function restore () {
document.getElementById('badgeNormal').value = bg.prefs.get('badgeNormal'); document.getElementById('badgeNormal').value = bg.prefs.get('badgeNormal');
document.getElementById('popupWidth').value = localStorage.getItem('popupWidth') || '246'; document.getElementById('popupWidth').value = localStorage.getItem('popupWidth') || '246';
document.getElementById('updateInterval').value = bg.prefs.get('updateInterval'); document.getElementById('updateInterval').value = bg.prefs.get('updateInterval');
enforceValueRange('popupWidth');
}); });
} }
@ -14,7 +15,7 @@ function save () {
chrome.runtime.getBackgroundPage(bg => { chrome.runtime.getBackgroundPage(bg => {
bg.prefs.set('badgeDisabled', document.getElementById('badgeDisabled').value); bg.prefs.set('badgeDisabled', document.getElementById('badgeDisabled').value);
bg.prefs.set('badgeNormal', document.getElementById('badgeNormal').value); bg.prefs.set('badgeNormal', document.getElementById('badgeNormal').value);
localStorage.setItem('popupWidth', document.getElementById('popupWidth').value); localStorage.setItem('popupWidth', enforceValueRange('popupWidth'));
bg.prefs.set( bg.prefs.set(
'updateInterval', 'updateInterval',
Math.max(0, +document.getElementById('updateInterval').value) Math.max(0, +document.getElementById('updateInterval').value)
@ -26,6 +27,19 @@ function save () {
}); });
} }
function enforceValueRange(id) {
let element = document.getElementById(id);
let value = Number(element.value);
const min = Number(element.min);
const max = Number(element.max);
if (value < min || value > max) {
value = Math.max(min, Math.min(max, value));
element.value = value;
}
element.onchange = element.onchange || (() => enforceValueRange(id));
return value;
}
document.addEventListener('DOMContentLoaded', restore); document.addEventListener('DOMContentLoaded', restore);
document.getElementById('save').addEventListener('click', save); document.getElementById('save').addEventListener('click', save);

View File

@ -25,6 +25,7 @@ input[type=checkbox] {
} }
a, a:visited { a, a:visited {
color: black; color: black;
text-decoration-skip: ink;
} }
.left-gutter { .left-gutter {
@ -59,7 +60,7 @@ body.blocked > DIV {
} }
#installed { #installed {
padding-top: 2px; padding-top: 2px;
max-height: 434px; max-height: 434px;
overflow-y: auto; overflow-y: auto;
} }
#installed.disabled .style-name { #installed.disabled .style-name {
@ -112,6 +113,10 @@ body:not(.blocked) #unavailable {
display: none; display: none;
} }
body.blocked #unavailable {
display: flex;
}
/* Never shown, but can be enabled with a style */ /* Never shown, but can be enabled with a style */
.enable, .disable { .enable, .disable {
display: none; display: none;
@ -120,7 +125,7 @@ body:not(.blocked) #unavailable {
/* 'New style' links */ /* 'New style' links */
#write-style-for {margin-right: .6ex} #write-style-for {margin-right: .6ex}
.write-style-link {margin-left: .6ex} .write-style-link {margin-left: .6ex}
.write-style-link::before, .write-style-link::after {font-size: x-small} .write-style-link::before, .write-style-link::after {font-size: 12px}
.write-style-link::before {content: "\00ad"} /* "soft" hyphen */ .write-style-link::before {content: "\00ad"} /* "soft" hyphen */
#match {overflow-wrap: break-word;} #match {overflow-wrap: break-word;}
@ -154,6 +159,7 @@ body:not(.blocked) #unavailable {
.breadcrumbs > .write-style-link:focus ~ .write-style-link[subdomain] { .breadcrumbs > .write-style-link:focus ~ .write-style-link[subdomain] {
color: inherit; color: inherit;
text-decoration: underline; text-decoration: underline;
text-decoration-skip: ink;
} }
/* action buttons */ /* action buttons */

View File

@ -29,11 +29,16 @@
</div> </div>
</template> </template>
<template data-id="writeStyle">
<a class="write-style-link"></a>
</template>
<script src="localization.js"></script> <script src="localization.js"></script>
<script src="health.js"></script> <script src="health.js"></script>
<script src="storage.js"></script> <script src="storage.js"></script>
<script src="messaging.js"></script> <script src="messaging.js"></script>
<script src="apply.js"></script> <script src="apply.js"></script>
<script src="popup.js"></script>
</head> </head>
<body id="stylus-popup"> <body id="stylus-popup">
@ -75,11 +80,10 @@
<!-- Actions --> <!-- Actions -->
<div id="popup-options"> <div id="popup-options">
<button id="popup-manage-button" i18n-text="openManage"></button> <button id="popup-manage-button" i18n-text="openManage"></button>
<button id="popup-options-button" i18n-text="openOptionsPopup"> <button id="popup-options-button" i18n-text="openOptionsPopup"></button>
<button id="popup-shortcuts-button" i18n-text="openShortcutsPopup"></button> <button id="popup-shortcuts-button" i18n-text="openShortcutsPopup"></button>
</div> </div>
</div> </div>
<script src="popup.js"></script>
</body> </body>
</html> </html>

493
popup.js
View File

@ -1,275 +1,298 @@
/* globals configureCommands */ /* globals configureCommands, openURL */
var writeStyleTemplate = document.createElement("a"); const RX_SUPPORTED_URLS = new RegExp(
writeStyleTemplate.className = "write-style-link"; `^(file|https?|ftps?):|^${OWN_ORIGIN}`);
let installed;
var installed = document.getElementById("installed");
if (!prefs.get("popup.stylesFirst")) { getActiveTabRealURL().then(url => {
document.body.insertBefore(document.querySelector("body > .actions"), installed); const isUrlSupported = RX_SUPPORTED_URLS.test(url);
Promise.all([
isUrlSupported ? getStylesSafe({matchUrl: url}) : null,
onDOMready().then(() => initPopup(isUrlSupported ? url : '')),
])
.then(([styles]) => styles && showStyles(styles));
});
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.method == 'updatePopup') {
switch (msg.reason) {
case 'styleAdded':
case 'styleUpdated':
handleUpdate(msg.style);
break;
case 'styleDeleted':
handleDelete(msg.id);
break;
}
}
});
function initPopup(url) {
installed = $('#installed');
// popup width
document.body.style.width =
Math.max(200, Math.min(800, Number(localStorage.popupWidth) || 246)) + 'px';
// confirm dialog
$('#confirm').onclick = e => {
const cmd = e.target.dataset.cmd;
if (cmd === 'ok') {
deleteStyle($('#confirm').dataset.id).then(() => {
// update view with 'No styles installed for this site' message
if ($('#installed').children.length === 0) {
showStyles([]);
}
});
}
//
if (cmd) {
$('#confirm').dataset.display = false;
}
};
// action buttons
$('#disableAll').onchange = () =>
installed.classList.toggle('disabled', prefs.get('disableAll'));
setupLivePrefs(['disableAll']);
$('#find-styles-link').onclick = openURLandHide;
$('#popup-manage-button').href = 'manage.html';
$('#popup-manage-button').onclick = openURLandHide;
$('#popup-options-button').onclick = () => chrome.runtime.openOptionsPage();
$('#popup-shortcuts-button').onclick = configureCommands.open;
// styles first?
if (!prefs.get('popup.stylesFirst')) {
document.body.insertBefore(
$('body > .actions'),
installed);
}
// find styles link
$('#find-styles a').href =
'https://userstyles.org/styles/browse/all/' +
encodeURIComponent(url.startsWith('file:') ? 'file:' : url);
if (!url) {
document.body.classList.add('blocked');
return;
}
// Write new style links
const writeStyle = $('#write-style');
const matchTargets = document.createElement('span');
matchTargets.id = 'match';
// For this URL
const urlLink = template.writeStyle.cloneNode(true);
Object.assign(urlLink, {
href: 'edit.html?url-prefix=' + encodeURIComponent(url),
title: `url-prefix("${url}")`,
textContent: prefs.get('popup.breadcrumbs.usePath')
? new URL(url).pathname.slice(1)
: t('writeStyleForURL').replace(/ /g, '\u00a0'), // this&nbsp;URL
onclick: openLinkInTabOrWindow,
});
if (prefs.get('popup.breadcrumbs')) {
urlLink.onmouseenter =
urlLink.onfocus = () => urlLink.parentNode.classList.add('url()');
urlLink.onmouseleave =
urlLink.onblur = () => urlLink.parentNode.classList.remove('url()');
}
matchTargets.appendChild(urlLink);
// For domain
const domains = getDomains(url);
for (let domain of domains) {
// Don't include TLD
if (domains.length > 1 && !domain.includes('.')) {
continue;
}
const domainLink = template.writeStyle.cloneNode(true);
Object.assign(domainLink, {
href: 'edit.html?domain=' + encodeURIComponent(domain),
textContent: domain,
title: `domain("${domain}")`,
onclick: openLinkInTabOrWindow,
});
domainLink.setAttribute('subdomain', domain.substring(0, domain.indexOf('.')));
matchTargets.appendChild(domainLink);
}
if (prefs.get('popup.breadcrumbs')) {
matchTargets.classList.add('breadcrumbs');
matchTargets.appendChild(matchTargets.removeChild(matchTargets.firstElementChild));
}
writeStyle.appendChild(matchTargets);
} }
getActiveTabRealURL(updatePopUp);
function updatePopUp(url) {
var urlWillWork = /^(file|http|https|ftps?|chrome\-extension):/.exec(url);
if (!urlWillWork) {
document.body.classList.add("blocked");
document.getElementById("unavailable").style.display = "flex";
return;
}
getStylesSafe({matchUrl: url}).then(showStyles);
document.querySelector("#find-styles a").href = "https://userstyles.org/styles/browse/all/" + encodeURIComponent("file" === urlWillWork[1] ? "file:" : url);
// Write new style links
var writeStyleLinks = [],
container = document.createElement('span');
container.id = "match";
// For this URL
var urlLink = writeStyleTemplate.cloneNode(true);
urlLink.href = "edit.html?url-prefix=" + encodeURIComponent(url);
urlLink.appendChild(document.createTextNode( // switchable; default="this&nbsp;URL"
!prefs.get("popup.breadcrumbs.usePath")
? t("writeStyleForURL").replace(/ /g, "\u00a0")
: /\/\/[^/]+\/(.*)/.exec(url)[1]
));
urlLink.title = "url-prefix(\"$\")".replace("$", url);
writeStyleLinks.push(urlLink);
document.querySelector("#write-style").appendChild(urlLink)
if (prefs.get("popup.breadcrumbs")) { // switchable; default=enabled
urlLink.addEventListener("mouseenter", function(event) { this.parentNode.classList.add("url()") }, false);
urlLink.addEventListener("focus", function(event) { this.parentNode.classList.add("url()") }, false);
urlLink.addEventListener("mouseleave", function(event) { this.parentNode.classList.remove("url()") }, false);
urlLink.addEventListener("blur", function(event) { this.parentNode.classList.remove("url()") }, false);
}
// For domain
var domains = getDomains(url)
domains.forEach(function(domain) {
// Don't include TLD
if (domains.length > 1 && domain.indexOf(".") == -1) {
return;
}
var domainLink = writeStyleTemplate.cloneNode(true);
domainLink.href = "edit.html?domain=" + encodeURIComponent(domain);
domainLink.appendChild(document.createTextNode(domain));
domainLink.title = "domain(\"$\")".replace("$", domain);
domainLink.setAttribute("subdomain", domain.substring(0, domain.indexOf(".")));
writeStyleLinks.push(domainLink);
});
var writeStyle = document.querySelector("#write-style");
writeStyleLinks.forEach(function(link, index) {
link.addEventListener("click", openLinkInTabOrWindow, false);
container.appendChild(link);
});
if (prefs.get("popup.breadcrumbs")) {
container.classList.add("breadcrumbs");
container.appendChild(container.removeChild(container.firstChild));
}
writeStyle.appendChild(container);
}
function showStyles(styles) { function showStyles(styles) {
var enabledFirst = prefs.get("popup.enabledFirst"); if (!styles.length) {
styles.sort(function(a, b) { installed.innerHTML =
if (enabledFirst && a.enabled !== b.enabled) return !(a.enabled < b.enabled) ? -1 : 1; `<div class="entry" id="no-styles">${t('noStylesForSite')}</div>`;
return a.name.localeCompare(b.name); } else {
}); const enabledFirst = prefs.get('popup.enabledFirst');
if (styles.length == 0) { styles.sort((a, b) =>
installed.innerHTML = "<div class='entry' id='no-styles'>" + t('noStylesForSite') + "</div>"; enabledFirst && a.enabled !== b.enabled
} ? !(a.enabled < b.enabled) ? -1 : 1
styles.map(createStyleElement).forEach(function(e) { : a.name.localeCompare(b.name));
installed.appendChild(e); const fragment = document.createDocumentFragment();
}); for (let style of styles) {
// force Chrome to resize the popup fragment.appendChild(createStyleElement(style));
document.body.style.height = '10px'; }
document.documentElement.style.height = '10px'; installed.appendChild(fragment);
}
// force Chrome to resize the popup
document.body.style.height = '10px';
document.documentElement.style.height = '10px';
} }
function createStyleElement(style) { function createStyleElement(style) {
// reuse event function references // reuse event listener function references
createStyleElement.events = createStyleElement.events || { const listeners = createStyleElement.listeners = createStyleElement.listeners || {
checkboxClick() { checkboxClick() {
enableStyle(getClickedStyleId(event), this.checked); enableStyle(getClickedStyleId(event), this.checked)
}, .then(handleUpdate);
styleNameClick(event) { },
this.checkbox.click(); styleNameClick(event) {
event.preventDefault(); this.checkbox.click();
}, event.preventDefault();
toggleClick(event) { },
enableStyle(getClickedStyleId(event), this.matches('.enable')); toggleClick(event) {
}, enableStyle(getClickedStyleId(event), this.matches('.enable'))
deleteClick() { .then(handleUpdate);
doDelete(event); },
deleteClick(event) {
doDelete(event);
} }
}; };
const entry = template.style.cloneNode(true); const entry = template.style.cloneNode(true);
entry.setAttribute('style-id', style.id); entry.setAttribute('style-id', style.id);
Object.assign(entry, { Object.assign(entry, {
styleId: style.id, styleId: style.id,
className: ['entry', style.enabled ? 'enabled' : 'disabled'].join(' '), className: ['entry', style.enabled ? 'enabled' : 'disabled'].join(' '),
onmousedown: openEditorOnMiddleclick, onmousedown: openEditorOnMiddleclick,
onauxclick: openEditorOnMiddleclick, onauxclick: openEditorOnMiddleclick,
}); });
const checkbox = entry.querySelector('.checker'); const checkbox = $('.checker', entry);
Object.assign(checkbox, { Object.assign(checkbox, {
id: 'style-' + style.id, id: 'style-' + style.id,
checked: style.enabled, checked: style.enabled,
onclick: createStyleElement.events.checkboxClick, onclick: listeners.checkboxClick,
}); });
const editLink = entry.querySelector('.style-edit-link'); const editLink = $('.style-edit-link', entry);
Object.assign(editLink, { Object.assign(editLink, {
href: editLink.getAttribute('href') + style.id, href: editLink.getAttribute('href') + style.id,
onclick: openLinkInTabOrWindow, onclick: openLinkInTabOrWindow,
}); });
const styleName = entry.querySelector('.style-name'); const styleName = $('.style-name', entry);
Object.assign(styleName, { Object.assign(styleName, {
htmlFor: 'style-' + style.id, htmlFor: 'style-' + style.id,
onclick: createStyleElement.events.styleNameClick, onclick: listeners.styleNameClick,
}); });
styleName.checkbox = checkbox; styleName.checkbox = checkbox;
styleName.appendChild(document.createTextNode(style.name)); styleName.appendChild(document.createTextNode(style.name));
entry.querySelector('.enable').onclick = createStyleElement.events.toggleClick; $('.enable', entry).onclick = listeners.toggleClick;
entry.querySelector('.disable').onclick = createStyleElement.events.toggleClick; $('.disable', entry).onclick = listeners.toggleClick;
entry.querySelector('.delete').onclick = createStyleElement.events.deleteClick; $('.delete', entry).onclick = listeners.deleteClick;
return entry; return entry;
} }
function doDelete(event) { function doDelete(event) {
document.getElementById('confirm').dataset.display = true; $('#confirm').dataset.display = true;
const id = getClickedStyleId(event); const id = getClickedStyleId(event);
document.querySelector('#confirm b').textContent = $('#confirm b').textContent =
document.querySelector(`[style-id="${id}"] label`).textContent; $(`[style-id="${id}"] label`).textContent;
document.getElementById('confirm').dataset.id = id; $('#confirm').dataset.id = id;
} }
document.getElementById('confirm').addEventListener('click', e => {
let cmd = e.target.dataset.cmd;
if (cmd === 'ok') {
deleteStyle(document.getElementById('confirm').dataset.id, () => {
// update view with 'No styles installed for this site' message
if (document.getElementById('installed').children.length === 0) {
showStyles([]);
}
});
}
//
if (cmd) {
document.getElementById('confirm').dataset.display = false;
}
});
function getClickedStyleId(event) { function getClickedStyleId(event) {
const entry = event.target.closest('.entry'); const entry = event.target.closest('.entry');
return entry ? entry.styleId : null; return entry ? entry.styleId : null;
} }
function openLinkInTabOrWindow(event) { function openLinkInTabOrWindow(event) {
event.preventDefault(); if (!prefs.get('openEditInWindow', false)) {
if (prefs.get("openEditInWindow", false)) { openURLandHide(event);
var options = {url: event.target.href} return;
var wp = prefs.get("windowPosition", {}); }
for (var k in wp) options[k] = wp[k]; event.preventDefault();
chrome.windows.create(options); chrome.windows.create(
} else { Object.assign({
openLink(event); url: event.target.href
} }, prefs.get('windowPosition', {}))
close(); );
close();
} }
function openEditorOnMiddleclick(event) { function openEditorOnMiddleclick(event) {
if (event.button != 1) { if (event.button != 1) {
return; return;
} }
// open an editor on middleclick // open an editor on middleclick
if (event.target.matches('.entry, .style-name, .style-edit-link')) { if (event.target.matches('.entry, .style-name, .style-edit-link')) {
this.querySelector('.style-edit-link').click(); $('.style-edit-link', this).click();
event.preventDefault(); event.preventDefault();
return; return;
} }
// prevent the popup being opened in a background tab // prevent the popup being opened in a background tab
// when an irrelevant link was accidentally clicked // when an irrelevant link was accidentally clicked
if (event.target.closest('a')) { if (event.target.closest('a')) {
event.preventDefault(); event.preventDefault();
return; return;
} }
} }
function openLink(event) {
event.preventDefault(); function openURLandHide(event) {
chrome.runtime.sendMessage({method: "openURL", url: event.target.href}); event.preventDefault();
close(); openURL({url: event.target.href})
.then(close);
} }
function handleUpdate(style) { function handleUpdate(style) {
var styleElement = installed.querySelector("[style-id='" + style.id + "']"); const styleElement = $(`[style-id="${style.id}"]`, installed);
if (styleElement) { if (styleElement) {
installed.replaceChild(createStyleElement(style), styleElement); installed.replaceChild(createStyleElement(style), styleElement);
} else { } else {
getActiveTabRealURL(function(url) { getActiveTabRealURL().then(url => {
if (chrome.extension.getBackgroundPage().getApplicableSections(style, url).length) { if (getApplicableSections(style, url).length) {
// a new style for the current url is installed // a new style for the current url is installed
document.getElementById("unavailable").style.display = "none"; $('#unavailable').style.display = 'none';
installed.appendChild(createStyleElement(style)); installed.appendChild(createStyleElement(style));
} }
}); });
} }
} }
function handleDelete(id) { function handleDelete(id) {
var styleElement = installed.querySelector("[style-id='" + id + "']"); var styleElement = $(`[style-id="${id}"]`, installed);
if (styleElement) { if (styleElement) {
installed.removeChild(styleElement); installed.removeChild(styleElement);
} }
} }
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
if (request.method == "updatePopup") {
switch (request.reason) {
case "styleAdded":
case "styleUpdated":
handleUpdate(request.style);
break;
case "styleDeleted":
handleDelete(request.id);
break;
}
}
});
["find-styles-link"].forEach(function(id) { function $(selector, base = document) {
document.getElementById(id).addEventListener("click", openLink, false); if (selector.startsWith('#') && /^#[^,\s]+$/.test(selector)) {
}); return document.getElementById(selector.slice(1));
} else {
document.getElementById("disableAll").addEventListener("change", function(event) { return base.querySelector(selector);
installed.classList.toggle("disabled", prefs.get("disableAll")); }
}); }
setupLivePrefs(["disableAll"]);
document.querySelector('#popup-manage-button').addEventListener("click", function() {
window.open(chrome.runtime.getURL('manage.html'));
});
document.querySelector('#popup-options-button').addEventListener("click", function() {
if (chrome.runtime.openOptionsPage) {
// Supported (Chrome 42+)
chrome.runtime.openOptionsPage();
} else {
// Fallback
window.open(chrome.runtime.getURL('options/index.html'));
}
});
document.querySelector('#popup-shortcuts-button').addEventListener("click", configureCommands.open);
// popup width
document.body.style.width = (localStorage.getItem('popupWidth') || '246') + 'px';

View File

@ -277,23 +277,21 @@ function addMissingStyleTargets(style) {
function enableStyle(id, enabled) { function enableStyle(id, enabled) {
saveStyle({id, enabled}) return saveStyle({id, enabled});
.then(handleUpdate);
} }
function deleteStyle(id, callback = function (){}) { function deleteStyle(id) {
getDatabase(function(db) { return new Promise(resolve =>
var tx = db.transaction(["styles"], "readwrite"); getDatabase(db => {
var os = tx.objectStore("styles"); const tx = db.transaction(['styles'], 'readwrite');
var request = os.delete(Number(id)); const os = tx.objectStore('styles');
request.onsuccess = function(event) { os.delete(Number(id)).onsuccess = event => {
handleDelete(id); invalidateCache(true, {deletedId: id});
invalidateCache(true, {deletedId: id}); notifyAllTabs({method: 'styleDeleted', id});
notifyAllTabs({method: "styleDeleted", id}); resolve(id);
callback(); };
}; }));
});
} }