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
importStyles: true
getActiveTabRealURL: true
openURL: true
onDOMready: true
getDomains: true
webSqlStorage: 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
// 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;
getCodeMirrorThemes(function(themes) {
codeMirrorThemes = themes;

View File

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

View File

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

View File

@ -1,4 +1,4 @@
healthCheck();
setTimeout(healthCheck, 0);
function healthCheck() {
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>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title i18n-text="manageTitle"></title>
<style>
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 */
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title i18n-text="manageTitle"></title>
<link href="manage.css" rel="stylesheet">
.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">
<div>
<h2 class="style-name"></h2>
<p class="applies-to"></p>
<p class="actions">
<a class="style-edit-link" href="edit.html?id="><button i18n-text="editStyleLabel"></button></a>
<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>
<template data-id="styleHomepage">
<a target="_blank" class="homepage">
<svg class="svg-icon"><use xlink:href="#svg-icon-external-link"/></svg>
<template data-id="style">
<div class="entry">
<h2 class="style-name"><a href="edit.html?id="></a></h2>
<p class="applies-to"><span></span></p>
<p class="actions">
<a class="style-edit-link" href="edit.html?id=">
<button i18n-text="editStyleLabel"></button>
</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>
<script src="health.js"></script>
<script src="storage.js"></script>
<script src="messaging.js"></script>
<script src="apply.js"></script>
<script src="manage.js"></script>
<template data-id="styleHomepage">
<a target="_blank" class="homepage">
<svg class="svg-icon"><use xlink:href="#svg-icon-external-link"/></svg>
</a>
</template>
<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>
<body id="stylus-manage" i18n-dragndrop-hint="dragDropMessage">
<div id="header">
<h1 id="manage-heading" i18n-text="manageHeading"></h1>
<fieldset>
<legend id="filters" i18n-text="manageFilters"></legend>
<div>
<input id="manage.onlyEnabled" type="checkbox">
<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 id="header">
<h1 id="manage-heading" i18n-text="manageHeading"></h1>
<fieldset>
<legend id="filters" i18n-text="manageFilters"></legend>
<div>
<input id="manage.onlyEnabled" type="checkbox">
<label id="manage.onlyEnabled-label" for="manage.onlyEnabled" i18n-text="manageOnlyEnabled"></label>
</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">
<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>
</symbol>
</svg>
<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">
<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>
</svg>
<script src="openOptions.js"></script>
<script src="backup/fileSaveLoad.js"></script>
<script src="manage.js"></script>
<script src="openOptions.js"></script>
<script src="backup/fileSaveLoad.js"></script>
</body>
</html>

819
manage.js
View File

@ -1,434 +1,471 @@
/* globals styleSectionsEqual */
var lastUpdatedStyleId = null;
var installed;
var appliesToExtraTemplate = document.createElement("span");
appliesToExtraTemplate.className = "applies-to-extra";
appliesToExtraTemplate.innerHTML = " " + t('appliesDisplayTruncatedSuffix');
const installed = $('#installed');
const TARGET_LABEL = t('appliesDisplay', '').trim();
const TARGET_TYPES = ['domains', 'urls', 'urlPrefixes', 'regexps'];
const TARGET_LIMIT = 10;
getStylesSafe({code: false}).then(showStyles);
function showStyles(styles) {
if (!installed) {
// "getStyles" message callback is invoked before document is loaded,
// postpone the action until DOMContentLoaded is fired
document.stylishStyles = styles;
return;
}
styles.sort(function(a, b) { return a.name.localeCompare(b.name)});
styles.forEach(handleUpdate);
if (history.state) {
window.scrollTo(0, history.state.scrollY);
}
getStylesSafe({code: false})
.then(showStyles)
.then(initGlobalEvents);
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
switch (msg.method) {
case 'styleUpdated':
case 'styleAdded':
handleUpdate(msg.style, msg);
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) {
var e = template.style.cloneNode(true);
e.setAttribute("class", style.enabled ? "enabled" : "disabled");
e.setAttribute("style-id", style.id);
if (style.updateUrl) {
e.setAttribute("style-update-url", style.updateUrl);
}
if (style.md5Url) {
e.setAttribute("style-md5-url", style.md5Url);
}
if (style.originalMd5) {
e.setAttribute("style-original-md5", style.originalMd5);
}
const entry = template.style.cloneNode(true);
entry.classList.add(style.enabled ? 'enabled' : 'disabled');
entry.setAttribute('style-id', style.id);
entry.styleId = style.id;
if (style.updateUrl) {
entry.setAttribute('style-update-url', style.updateUrl);
}
if (style.md5Url) {
entry.setAttribute('style-md5-url', style.md5Url);
}
if (style.originalMd5) {
entry.setAttribute('style-original-md5', style.originalMd5);
}
var styleName = e.querySelector(".style-name");
styleName.appendChild(document.createTextNode(style.name));
if (style.url) {
var homepage = template.styleHomepage.cloneNode(true)
homepage.setAttribute("href", style.url);
styleName.appendChild(document.createTextNode(" " ));
styleName.appendChild(homepage);
}
var domains = [];
var urls = [];
var urlPrefixes = [];
var regexps = [];
function add(array, property) {
style.sections.forEach(function(section) {
if (section[property]) {
section[property].filter(function(value) {
return array.indexOf(value) == -1;
}).forEach(function(value) {
array.push(value);
});;
}
});
}
add(domains, 'domains');
add(urls, 'urls');
add(urlPrefixes, 'urlPrefixes');
add(regexps, 'regexps');
var appliesToToShow = [];
if (domains)
appliesToToShow = appliesToToShow.concat(domains);
if (urls)
appliesToToShow = appliesToToShow.concat(urls);
if (urlPrefixes)
appliesToToShow = appliesToToShow.concat(urlPrefixes.map(function(u) { return u + "*"; }));
if (regexps)
appliesToToShow = appliesToToShow.concat(regexps.map(function(u) { return "/" + u + "/"; }));
var appliesToString = "";
var showAppliesToExtra = false;
if (appliesToToShow.length == "")
appliesToString = t('appliesToEverything');
else if (appliesToToShow.length <= 10)
appliesToString = appliesToToShow.join(", ");
else {
appliesToString = appliesToToShow.slice(0, 10).join(", ");
showAppliesToExtra = true;
}
e.querySelector(".applies-to").appendChild(document.createTextNode(t('appliesDisplay', [appliesToString])));
if (showAppliesToExtra) {
e.querySelector(".applies-to").appendChild(appliesToExtraTemplate.cloneNode(true));
}
var editLink = e.querySelector(".style-edit-link");
editLink.setAttribute("href", editLink.getAttribute("href") + style.id);
editLink.addEventListener("click", function(event) {
if (!event.altKey) {
var left = event.button == 0, middle = event.button == 1,
shift = event.shiftKey, ctrl = event.ctrlKey;
var openWindow = left && shift && !ctrl;
var openBackgroundTab = (middle && !shift) || (left && ctrl && !shift);
var openForegroundTab = (middle && shift) || (left && ctrl && shift);
var url = event.target.href || event.target.parentNode.href;
event.preventDefault();
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;
const styleName = $('.style-name', entry);
const styleNameEditLink = $('a', styleName);
styleNameEditLink.appendChild(document.createTextNode(style.name));
styleNameEditLink.href = styleNameEditLink.getAttribute('href') + style.id;
styleNameEditLink.onclick = EntryOnClick.edit;
if (style.url) {
const homepage = template.styleHomepage.cloneNode(true);
homepage.href = style.url;
styleName.appendChild(document.createTextNode(' '));
styleName.appendChild(homepage);
}
const targets = new Map(TARGET_TYPES.map(t => [t, new Set()]));
const decorations = {
urlPrefixesAfter: '*',
regexpsBefore: '/',
regexpsAfter: '/',
};
for (let [name, target] of targets.entries()) {
for (let section of style.sections) {
for (let targetValue of section[name] || []) {
target.add(
(decorations[name + 'Before'] || '') +
targetValue.trim() +
(decorations[name + 'After'] || ''));
}
}
}
const appliesTo = $('.applies-to', entry);
appliesTo.firstElementChild.textContent = TARGET_LABEL;
const targetsList = Array.prototype.concat.apply([],
[...targets.values()].map(set => [...set.values()]));
if (!targetsList.length) {
appliesTo.appendChild(template.appliesToEverything.cloneNode(true));
entry.classList.add('global');
} else {
let index = 0;
let container = appliesTo;
for (let target of targetsList) {
if (index > 0) {
container.appendChild(template.appliesToSeparator.cloneNode(true));
}
if (++index == TARGET_LIMIT) {
container = appliesTo.appendChild(template.extraAppliesTo.cloneNode(true));
}
const item = template.appliesToTarget.cloneNode(true);
item.textContent = target;
container.appendChild(item);
}
}
const editLink = $('.style-edit-link', entry);
editLink.href = editLink.getAttribute('href') + style.id;
editLink.onclick = EntryOnClick.edit;
$('.enable', entry).onclick = EntryOnClick.toggle;
$('.disable', entry).onclick = EntryOnClick.toggle;
$('.check-update', entry).onclick = EntryOnClick.check;
$('.update', entry).onclick = EntryOnClick.update;
$('.delete', entry).onclick = EntryOnClick.delete;
return entry;
}
function enable(event, enabled) {
var id = getId(event);
enableStyle(id, enabled);
class EntryOnClick {
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'))) {
return;
}
var id = getId(event);
deleteStyle(id);
function handleUpdate(style, {reason} = {}) {
const element = createStyleElement(style);
const oldElement = $(`[style-id="${style.id}"]`, installed);
if (!oldElement) {
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) {
var node = installed.querySelector("[style-id='" + id + "']");
if (node) {
installed.removeChild(node);
}
const node = $(`[style-id="${id}"]`, installed);
if (node) {
node.remove();
}
}
function doCheckUpdate(event) {
checkUpdate(getStyleElement(event));
}
function applyUpdateAll() {
var btnApply = document.getElementById("apply-all-updates");
btnApply.disabled = true;
setTimeout(function() {
btnApply.style.display = "none";
btnApply.disabled = false;
}, 1000);
const btnApply = $('#apply-all-updates');
btnApply.disabled = true;
setTimeout(() => {
btnApply.style.display = 'none';
btnApply.disabled = false;
}, 1000);
Array.prototype.forEach.call(document.querySelectorAll(".can-update .update"), function(button) {
button.click();
});
[...document.querySelectorAll('.can-update .update')]
.forEach(button => {
// align to the bottom of the visible area if wasn't visible
button.scrollIntoView(false);
button.click();
});
}
function checkUpdateAll() {
var btnCheck = document.getElementById("check-all-updates");
var btnApply = document.getElementById("apply-all-updates");
var noUpdates = document.getElementById("update-all-no-updates");
const btnCheck = $('#check-all-updates');
const btnApply = $('#apply-all-updates');
const noUpdates = $('#update-all-no-updates');
btnCheck.disabled = true;
btnApply.classList.add("hidden");
noUpdates.classList.add("hidden");
btnCheck.disabled = true;
btnApply.classList.add('hidden');
noUpdates.classList.add('hidden');
var elements = document.querySelectorAll("[style-update-url]");
var toCheckCount = elements.length;
var updatableCount = 0;
Array.prototype.forEach.call(elements, function(element) {
checkUpdate(element, function(success) {
if (success) {
++updatableCount;
}
if (--toCheckCount == 0) {
btnCheck.disabled = false;
if (updatableCount) {
btnApply.classList.remove("hidden");
} else {
noUpdates.classList.remove("hidden");
setTimeout(function() {
noUpdates.classList.add("hidden");
}, 10000);
}
}
});
});
// notify the automatic updater to reset the next automatic update accordingly
chrome.runtime.sendMessage({
method: 'resetInterval'
});
const elements = document.querySelectorAll('[style-update-url]');
Promise.all([...elements].map(checkUpdate))
.then(updatables => {
btnCheck.disabled = false;
if (updatables.includes(true)) {
btnApply.classList.remove('hidden');
} else {
noUpdates.classList.remove('hidden');
setTimeout(() => {
noUpdates.classList.add('hidden');
}, 10e3);
}
});
// 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) {
chrome.runtime.sendMessage({method: "getStyles", id: id}, function(styles) {
var style = styles[0];
var needsUpdate = false;
if (!forceUpdate && styleSectionsEqual(style, serverJson)) {
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 checkUpdate(element) {
$('.update-note', element).innerHTML = t('checkingForUpdate');
element.classList.remove('checking-update', 'no-update', 'can-update');
element.classList.add('checking-update');
return new Updater(element).run();
}
function checkUpdateFullCode(url, forceUpdate, successCallback, failureCallback) {
download(url, function(responseText) {
successCallback(forceUpdate, JSON.parse(responseText));
}, failureCallback);
class Updater {
constructor(element) {
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) {
if (responseText.length != 32) {
failureCallback(-1);
return;
}
successCallback(responseText != originalMd5);
}, failureCallback);
function searchStyles(immediately, bin) {
const query = $('#search').value.toLocaleLowerCase();
if (query == (searchStyles.lastQuery || '') && !bin) {
return;
}
searchStyles.lastQuery = query;
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();
xhr.onreadystatechange = function (aEvt) {
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 getClickedStyleId(event) {
return (getClickedStyleElement(event) || {}).styleId;
}
function handleNeedsUpdate(needsUpdate, id, serverJson) {
var e = document.querySelector("[style-id='" + id + "']");
e.className = e.className.replace("checking-update", "");
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 getClickedStyleElement(event) {
return event.target.closest('.entry');
}
function doUpdate(event) {
var element = getStyleElement(event);
var updatedCode = element.updatedCode;
// update everything but name
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 rememberScrollPosition() {
history.replaceState({scrollY}, document.title);
}
function searchStyles(immediately) {
var query = document.getElementById("search").value.toLocaleLowerCase();
if (query == (searchStyles.lastQuery || "")) {
return;
}
searchStyles.lastQuery = query;
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 $(selector, base = document) {
if (selector.startsWith('#') && /^#[^,\s]+$/.test(selector)) {
return document.getElementById(selector.slice(1));
} else {
return base.querySelector(selector);
}
}
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 OWN_ORIGIN = chrome.runtime.getURL('');
function notifyAllTabs(request) {
// list all tabs including chrome-extension:// which can be ours
if (request.codeIsUpdated === false && request.style) {
@ -24,6 +25,7 @@ function notifyAllTabs(request) {
}
}
function refreshAllTabs() {
return new Promise(resolve => {
// list all tabs including chrome-extension:// which can be ours
@ -47,6 +49,7 @@ function refreshAllTabs() {
});
}
function updateIcon(tab, styles) {
// 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)
@ -62,7 +65,7 @@ function updateIcon(tab, styles) {
});
return;
}
getTabRealURL(tab, url => {
getTabRealURL(tab).then(url => {
// if we have access to this, call directly
// (Chrome no longer sends messages to the page itself)
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) {
callback(tabs[0]);
function getActiveTab() {
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) {
if (tab.url != "chrome://newtab/") {
callback(tab.url);
} else {
chrome.webNavigation.getFrame({tabId: tab.id, frameId: 0, processId: -1}, function(frame) {
frame && callback(frame.url);
function openURL({url}) {
url = !url.includes('://') ? chrome.runtime.getURL(url) : url;
return new Promise(resolve => {
chrome.tabs.query({currentWindow: true, url}, tabs => {
// switch to an existing tab with the requested 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) {
return new RegExp(s.replace(/[{}()\[\]\/\\.+?^$:=*!|]/g, "\\$&"), flags);
return new RegExp(s.replace(/[{}()\[\]\/\\.+?^$:=*!|]/g, '\\$&'), flags);
}
// expands * as .*?
function wildcardAsRegExp(s, flags) {
return new RegExp(s.replace(/[{}()\[\]\/\\.+?^$:=!|]/g, "\\$&").replace(/\*/g, '.*?'), flags);
return new RegExp(s.replace(/[{}()\[\]\/\\.+?^$:=!|]/g, '\\$&').replace(/\*/g, '.*?'), flags);
}
var configureCommands = {
get url () {
return navigator.userAgent.indexOf('OPR') > -1 ?

View File

@ -20,7 +20,7 @@
</tr>
<tr>
<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>
<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('popupWidth').value = localStorage.getItem('popupWidth') || '246';
document.getElementById('updateInterval').value = bg.prefs.get('updateInterval');
enforceValueRange('popupWidth');
});
}
@ -14,7 +15,7 @@ function save () {
chrome.runtime.getBackgroundPage(bg => {
bg.prefs.set('badgeDisabled', document.getElementById('badgeDisabled').value);
bg.prefs.set('badgeNormal', document.getElementById('badgeNormal').value);
localStorage.setItem('popupWidth', document.getElementById('popupWidth').value);
localStorage.setItem('popupWidth', enforceValueRange('popupWidth'));
bg.prefs.set(
'updateInterval',
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.getElementById('save').addEventListener('click', save);

View File

@ -25,6 +25,7 @@ input[type=checkbox] {
}
a, a:visited {
color: black;
text-decoration-skip: ink;
}
.left-gutter {
@ -112,6 +113,10 @@ body:not(.blocked) #unavailable {
display: none;
}
body.blocked #unavailable {
display: flex;
}
/* Never shown, but can be enabled with a style */
.enable, .disable {
display: none;
@ -120,7 +125,7 @@ body:not(.blocked) #unavailable {
/* 'New style' links */
#write-style-for {margin-right: .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 */
#match {overflow-wrap: break-word;}
@ -154,6 +159,7 @@ body:not(.blocked) #unavailable {
.breadcrumbs > .write-style-link:focus ~ .write-style-link[subdomain] {
color: inherit;
text-decoration: underline;
text-decoration-skip: ink;
}
/* action buttons */

View File

@ -29,11 +29,16 @@
</div>
</template>
<template data-id="writeStyle">
<a class="write-style-link"></a>
</template>
<script src="localization.js"></script>
<script src="health.js"></script>
<script src="storage.js"></script>
<script src="messaging.js"></script>
<script src="apply.js"></script>
<script src="popup.js"></script>
</head>
<body id="stylus-popup">
@ -75,11 +80,10 @@
<!-- Actions -->
<div id="popup-options">
<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>
</div>
</div>
<script src="popup.js"></script>
</body>
</html>

493
popup.js
View File

@ -1,275 +1,298 @@
/* globals configureCommands */
/* globals configureCommands, openURL */
var writeStyleTemplate = document.createElement("a");
writeStyleTemplate.className = "write-style-link";
const RX_SUPPORTED_URLS = new RegExp(
`^(file|https?|ftps?):|^${OWN_ORIGIN}`);
let installed;
var installed = document.getElementById("installed");
if (!prefs.get("popup.stylesFirst")) {
document.body.insertBefore(document.querySelector("body > .actions"), installed);
getActiveTabRealURL().then(url => {
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) {
var enabledFirst = prefs.get("popup.enabledFirst");
styles.sort(function(a, b) {
if (enabledFirst && a.enabled !== b.enabled) return !(a.enabled < b.enabled) ? -1 : 1;
return a.name.localeCompare(b.name);
});
if (styles.length == 0) {
installed.innerHTML = "<div class='entry' id='no-styles'>" + t('noStylesForSite') + "</div>";
}
styles.map(createStyleElement).forEach(function(e) {
installed.appendChild(e);
});
// force Chrome to resize the popup
document.body.style.height = '10px';
document.documentElement.style.height = '10px';
if (!styles.length) {
installed.innerHTML =
`<div class="entry" id="no-styles">${t('noStylesForSite')}</div>`;
} else {
const enabledFirst = prefs.get('popup.enabledFirst');
styles.sort((a, b) =>
enabledFirst && a.enabled !== b.enabled
? !(a.enabled < b.enabled) ? -1 : 1
: a.name.localeCompare(b.name));
const fragment = document.createDocumentFragment();
for (let style of styles) {
fragment.appendChild(createStyleElement(style));
}
installed.appendChild(fragment);
}
// force Chrome to resize the popup
document.body.style.height = '10px';
document.documentElement.style.height = '10px';
}
function createStyleElement(style) {
// reuse event function references
createStyleElement.events = createStyleElement.events || {
checkboxClick() {
enableStyle(getClickedStyleId(event), this.checked);
},
styleNameClick(event) {
this.checkbox.click();
event.preventDefault();
},
toggleClick(event) {
enableStyle(getClickedStyleId(event), this.matches('.enable'));
},
deleteClick() {
doDelete(event);
// reuse event listener function references
const listeners = createStyleElement.listeners = createStyleElement.listeners || {
checkboxClick() {
enableStyle(getClickedStyleId(event), this.checked)
.then(handleUpdate);
},
styleNameClick(event) {
this.checkbox.click();
event.preventDefault();
},
toggleClick(event) {
enableStyle(getClickedStyleId(event), this.matches('.enable'))
.then(handleUpdate);
},
deleteClick(event) {
doDelete(event);
}
};
const entry = template.style.cloneNode(true);
entry.setAttribute('style-id', style.id);
Object.assign(entry, {
styleId: style.id,
className: ['entry', style.enabled ? 'enabled' : 'disabled'].join(' '),
onmousedown: openEditorOnMiddleclick,
onauxclick: openEditorOnMiddleclick,
});
};
const entry = template.style.cloneNode(true);
entry.setAttribute('style-id', style.id);
Object.assign(entry, {
styleId: style.id,
className: ['entry', style.enabled ? 'enabled' : 'disabled'].join(' '),
onmousedown: openEditorOnMiddleclick,
onauxclick: openEditorOnMiddleclick,
});
const checkbox = entry.querySelector('.checker');
Object.assign(checkbox, {
id: 'style-' + style.id,
checked: style.enabled,
onclick: createStyleElement.events.checkboxClick,
});
const checkbox = $('.checker', entry);
Object.assign(checkbox, {
id: 'style-' + style.id,
checked: style.enabled,
onclick: listeners.checkboxClick,
});
const editLink = entry.querySelector('.style-edit-link');
Object.assign(editLink, {
href: editLink.getAttribute('href') + style.id,
onclick: openLinkInTabOrWindow,
});
const editLink = $('.style-edit-link', entry);
Object.assign(editLink, {
href: editLink.getAttribute('href') + style.id,
onclick: openLinkInTabOrWindow,
});
const styleName = entry.querySelector('.style-name');
Object.assign(styleName, {
htmlFor: 'style-' + style.id,
onclick: createStyleElement.events.styleNameClick,
});
styleName.checkbox = checkbox;
styleName.appendChild(document.createTextNode(style.name));
const styleName = $('.style-name', entry);
Object.assign(styleName, {
htmlFor: 'style-' + style.id,
onclick: listeners.styleNameClick,
});
styleName.checkbox = checkbox;
styleName.appendChild(document.createTextNode(style.name));
entry.querySelector('.enable').onclick = createStyleElement.events.toggleClick;
entry.querySelector('.disable').onclick = createStyleElement.events.toggleClick;
entry.querySelector('.delete').onclick = createStyleElement.events.deleteClick;
$('.enable', entry).onclick = listeners.toggleClick;
$('.disable', entry).onclick = listeners.toggleClick;
$('.delete', entry).onclick = listeners.deleteClick;
return entry;
return entry;
}
function doDelete(event) {
document.getElementById('confirm').dataset.display = true;
const id = getClickedStyleId(event);
document.querySelector('#confirm b').textContent =
document.querySelector(`[style-id="${id}"] label`).textContent;
document.getElementById('confirm').dataset.id = id;
$('#confirm').dataset.display = true;
const id = getClickedStyleId(event);
$('#confirm b').textContent =
$(`[style-id="${id}"] label`).textContent;
$('#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) {
const entry = event.target.closest('.entry');
return entry ? entry.styleId : null;
const entry = event.target.closest('.entry');
return entry ? entry.styleId : null;
}
function openLinkInTabOrWindow(event) {
event.preventDefault();
if (prefs.get("openEditInWindow", false)) {
var options = {url: event.target.href}
var wp = prefs.get("windowPosition", {});
for (var k in wp) options[k] = wp[k];
chrome.windows.create(options);
} else {
openLink(event);
}
close();
if (!prefs.get('openEditInWindow', false)) {
openURLandHide(event);
return;
}
event.preventDefault();
chrome.windows.create(
Object.assign({
url: event.target.href
}, prefs.get('windowPosition', {}))
);
close();
}
function openEditorOnMiddleclick(event) {
if (event.button != 1) {
return;
}
// open an editor on middleclick
if (event.target.matches('.entry, .style-name, .style-edit-link')) {
this.querySelector('.style-edit-link').click();
event.preventDefault();
return;
}
// prevent the popup being opened in a background tab
// when an irrelevant link was accidentally clicked
if (event.target.closest('a')) {
event.preventDefault();
return;
}
if (event.button != 1) {
return;
}
// open an editor on middleclick
if (event.target.matches('.entry, .style-name, .style-edit-link')) {
$('.style-edit-link', this).click();
event.preventDefault();
return;
}
// prevent the popup being opened in a background tab
// when an irrelevant link was accidentally clicked
if (event.target.closest('a')) {
event.preventDefault();
return;
}
}
function openLink(event) {
event.preventDefault();
chrome.runtime.sendMessage({method: "openURL", url: event.target.href});
close();
function openURLandHide(event) {
event.preventDefault();
openURL({url: event.target.href})
.then(close);
}
function handleUpdate(style) {
var styleElement = installed.querySelector("[style-id='" + style.id + "']");
if (styleElement) {
installed.replaceChild(createStyleElement(style), styleElement);
} else {
getActiveTabRealURL(function(url) {
if (chrome.extension.getBackgroundPage().getApplicableSections(style, url).length) {
// a new style for the current url is installed
document.getElementById("unavailable").style.display = "none";
installed.appendChild(createStyleElement(style));
}
});
}
const styleElement = $(`[style-id="${style.id}"]`, installed);
if (styleElement) {
installed.replaceChild(createStyleElement(style), styleElement);
} else {
getActiveTabRealURL().then(url => {
if (getApplicableSections(style, url).length) {
// a new style for the current url is installed
$('#unavailable').style.display = 'none';
installed.appendChild(createStyleElement(style));
}
});
}
}
function handleDelete(id) {
var styleElement = installed.querySelector("[style-id='" + id + "']");
if (styleElement) {
installed.removeChild(styleElement);
}
var styleElement = $(`[style-id="${id}"]`, installed);
if (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) {
document.getElementById(id).addEventListener("click", openLink, false);
});
document.getElementById("disableAll").addEventListener("change", function(event) {
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';
function $(selector, base = document) {
if (selector.startsWith('#') && /^#[^,\s]+$/.test(selector)) {
return document.getElementById(selector.slice(1));
} else {
return base.querySelector(selector);
}
}

View File

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