diff --git a/edit/codemirror-default.js b/edit/codemirror-default.js
index e83deeca..72dace8b 100644
--- a/edit/codemirror-default.js
+++ b/edit/codemirror-default.js
@@ -315,7 +315,7 @@ CodeMirror.hint && (() => {
}
// USO vars in usercss mode editor
- const vars = editor.getStyle().usercssData.vars;
+ const vars = editor.style.usercssData.vars;
const list = vars ?
Object.keys(vars).filter(name => name.startsWith(leftPart)) : [];
return {
@@ -343,7 +343,7 @@ CodeMirror.hint && (() => {
string[start + 3] === '[' &&
string[pos - 3] === ']' &&
string[pos - 4] === ']') {
- const vars = typeof editor !== 'undefined' && (editor.getStyle().usercssData || {}).vars;
+ const vars = typeof editor !== 'undefined' && (editor.style.usercssData || {}).vars;
const name = vars && string.slice(start + 4, pos - 4);
if (vars && Object.hasOwnProperty.call(vars, name.endsWith('-rgb') ? name.slice(0, -4) : name)) {
token[0] = USO_VALID_VAR;
diff --git a/edit/edit.css b/edit/edit.css
index 54aeed44..9e385382 100644
--- a/edit/edit.css
+++ b/edit/edit.css
@@ -88,6 +88,9 @@ label {
display: flex;
align-items: center;
}
+#reset-name {
+ margin: 0 .25em 0 .5em;
+}
#url {
margin-left: 0.25rem;
}
@@ -610,7 +613,8 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high
right: 4px;
top: .5em;
}
-#help-popup input[type="search"] {
+#help-popup input[type="search"],
+#help-popup .CodeMirror {
margin: 3px;
}
@@ -813,11 +817,6 @@ body.linter-disabled .hidden-unless-compact {
margin-top: .75rem;
}
-.usercss #name {
- background-color: #eee;
- color: #888;
-}
-
.single-editor {
height: 100%;
}
diff --git a/edit/edit.js b/edit/edit.js
index c310c881..a9ffaa0e 100644
--- a/edit/edit.js
+++ b/edit/edit.js
@@ -1,15 +1,11 @@
/* global CodeMirror onDOMready prefs setupLivePrefs $ $$ $create t tHTML
createSourceEditor sessionStorageHash getOwnTab FIREFOX API tryCatch
- closeCurrentTab messageBox debounce workerUtil
- initBeautifyButton ignoreChromeError
+ closeCurrentTab messageBox debounce
+ initBeautifyButton ignoreChromeError dirtyReporter linter
moveFocus msg createSectionsEditor rerouteHotkeys CODEMIRROR_THEMES */
/* exported showCodeMirrorPopup editorWorker toggleContextMenuDelete */
'use strict';
-const editorWorker = workerUtil.createWorker({
- url: '/edit/editor-worker.js'
-});
-
let saveSizeOnClose;
// direct & reverse mapping of @-moz-document keywords and internal property names
@@ -28,48 +24,84 @@ document.addEventListener('visibilitychange', beforeUnload);
window.addEventListener('beforeunload', beforeUnload);
msg.onExtension(onRuntimeMessage);
-preinit();
+lazyInit();
-(() => {
- onDOMready().then(() => {
- prefs.subscribe(['editor.keyMap'], showHotkeyInTooltip);
- addEventListener('showHotkeyInTooltip', showHotkeyInTooltip);
- showHotkeyInTooltip();
+(async function init() {
+ const [style] = await Promise.all([
+ initStyleData(),
+ onDOMready(),
+ prefs.initializing.then(() => new Promise(resolve => {
+ const theme = prefs.get('editor.theme');
+ const el = $('#cm-theme');
+ if (theme === 'default') {
+ resolve();
+ } else {
+ // preload the theme so CodeMirror can use the correct metrics
+ el.href = `vendor/codemirror/theme/${theme}.css`;
+ el.addEventListener('load', resolve, {once: true});
+ }
+ })),
+ ]);
+ const usercss = isUsercss(style);
+ const dirty = dirtyReporter();
+ let wasDirty = false;
+ let nameTarget;
- buildThemeElement();
- buildKeymapElement();
+ prefs.subscribe(['editor.linter'], updateLinter);
+ prefs.subscribe(['editor.keyMap'], showHotkeyInTooltip);
+ addEventListener('showHotkeyInTooltip', showHotkeyInTooltip);
+ showHotkeyInTooltip();
+ buildThemeElement();
+ buildKeymapElement();
+ setupLivePrefs();
+ initNameArea();
+ initBeautifyButton($('#beautify'), () => editor.getEditors());
+ initResizeListener();
+ detectLayout();
+ updateTitle();
- setupLivePrefs();
+ $('#heading').textContent = t(style.id ? 'editStyleHeading' : 'addStyleTitle');
+ $('#preview-label').classList.toggle('hidden', !style.id);
+
+ editor = (usercss ? createSourceEditor : createSectionsEditor)({
+ style,
+ dirty,
+ updateName,
+ toggleStyle,
});
+ dirty.onChange(updateDirty);
+ await editor.ready;
- initEditor();
+ // enabling after init to prevent flash of validation failure on an empty name
+ $('#name').required = !usercss;
+ $('#save-button').onclick = editor.save;
- function getCodeMirrorThemes() {
- if (!chrome.runtime.getPackageDirectoryEntry) {
- const themes = [
- chrome.i18n.getMessage('defaultTheme'),
- ...CODEMIRROR_THEMES
- ];
- localStorage.codeMirrorThemes = themes.join(' ');
- return Promise.resolve(themes);
- }
- return new Promise(resolve => {
- chrome.runtime.getPackageDirectoryEntry(rootDir => {
- rootDir.getDirectory('vendor/codemirror/theme', {create: false}, themeDir => {
- themeDir.createReader().readEntries(entries => {
- const themes = [
- chrome.i18n.getMessage('defaultTheme')
- ].concat(
- entries.filter(entry => entry.isFile)
- .sort((a, b) => (a.name < b.name ? -1 : 1))
- .map(entry => entry.name.replace(/\.css$/, ''))
- );
- localStorage.codeMirrorThemes = themes.join(' ');
- resolve(themes);
- });
- });
- });
+ function initNameArea() {
+ const nameEl = $('#name');
+ const resetEl = $('#reset-name');
+ const isCustomName = style.updateUrl || usercss;
+ nameTarget = isCustomName ? 'customName' : 'name';
+ nameEl.placeholder = t(usercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName');
+ nameEl.title = isCustomName ? t('customNameHint') : '';
+ nameEl.addEventListener('input', () => {
+ updateName();
+ resetEl.hidden = false;
});
+ resetEl.hidden = !style.customName;
+ resetEl.onclick = () => {
+ const style = editor.style;
+ nameEl.focus();
+ nameEl.select();
+ // trying to make it undoable via Ctrl-Z
+ if (!document.execCommand('insertText', false, style.name)) {
+ nameEl.value = style.name;
+ updateName();
+ }
+ style.customName = null; // to delete it from db
+ resetEl.hidden = true;
+ };
+ const enabledEl = $('#enabled');
+ enabledEl.onchange = () => updateEnabledness(enabledEl.checked);
}
function findKeyForCommand(command, map) {
@@ -88,27 +120,10 @@ preinit();
}
function buildThemeElement() {
- const themeElement = $('#editor.theme');
- const themeList = localStorage.codeMirrorThemes;
-
- const optionsFromArray = options => {
- const fragment = document.createDocumentFragment();
- options.forEach(opt => fragment.appendChild($create('option', opt)));
- themeElement.appendChild(fragment);
- };
-
- if (themeList) {
- optionsFromArray(themeList.split(/\s+/));
- } else {
- // Chrome is starting up and shows our edit.html, but the background page isn't loaded yet
- const theme = prefs.get('editor.theme');
- optionsFromArray([theme === 'default' ? t('defaultTheme') : theme]);
- getCodeMirrorThemes().then(() => {
- const themes = (localStorage.codeMirrorThemes || '').split(/\s+/);
- optionsFromArray(themes);
- themeElement.selectedIndex = Math.max(0, themes.indexOf(theme));
- });
- }
+ CODEMIRROR_THEMES.unshift(chrome.i18n.getMessage('defaultTheme'));
+ $('#editor.theme').append(...CODEMIRROR_THEMES.map(s => $create('option', s)));
+ // move the theme after built-in CSS so that its same-specificity selectors win
+ document.head.appendChild($('#cm-theme'));
}
function buildKeymapElement() {
@@ -159,134 +174,118 @@ preinit();
}
}
- function initEditor() {
- return Promise.all([
- initStyleData(),
- onDOMready(),
- prefs.initializing,
- ])
- .then(([style]) => {
- const usercss = isUsercss(style);
- $('#heading').textContent = t(style.id ? 'editStyleHeading' : 'addStyleTitle');
- $('#name').placeholder = t(usercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName');
- $('#name').title = usercss ? t('usercssReplaceTemplateName') : '';
- $('#preview-label').classList.toggle('hidden', !style.id);
- initBeautifyButton($('#beautify'), () => editor.getEditors());
- const {onBoundsChanged} = chrome.windows || {};
- if (onBoundsChanged) {
- // * movement is reported even if the window wasn't resized
- // * fired just once when done so debounce is not needed
- onBoundsChanged.addListener(wnd => {
- // getting the current window id as it may change if the user attached/detached the tab
- chrome.windows.getCurrent(ownWnd => {
- if (wnd.id === ownWnd.id) rememberWindowSize();
- });
- });
- }
- window.addEventListener('resize', () => {
- if (!onBoundsChanged) debounce(rememberWindowSize, 100);
- detectLayout();
+ function initResizeListener() {
+ const {onBoundsChanged} = chrome.windows || {};
+ if (onBoundsChanged) {
+ // * movement is reported even if the window wasn't resized
+ // * fired just once when done so debounce is not needed
+ onBoundsChanged.addListener(wnd => {
+ // getting the current window id as it may change if the user attached/detached the tab
+ chrome.windows.getCurrent(ownWnd => {
+ if (wnd.id === ownWnd.id) rememberWindowSize();
});
- detectLayout();
- editor = (usercss ? createSourceEditor : createSectionsEditor)({
- style,
- onTitleChanged: updateTitle
- });
- editor.dirty.onChange(updateDirty);
- return Promise.resolve(editor.ready && editor.ready())
- .then(updateDirty);
});
- }
-
- function updateTitle() {
- if (editor) {
- const styleName = editor.getStyle().name;
- const isDirty = editor.dirty.isDirty();
- document.title = (isDirty ? '* ' : '') + styleName;
}
- }
-
- function updateDirty() {
- const isDirty = editor.dirty.isDirty();
- document.body.classList.toggle('dirty', isDirty);
- $('#save-button').disabled = !isDirty;
- updateTitle();
- }
-})();
-
-function preinit() {
- // preload the theme so that CodeMirror can calculate its metrics in DOMContentLoaded->setupLivePrefs()
- new MutationObserver((mutations, observer) => {
- const themeElement = $('#cm-theme');
- if (themeElement) {
- themeElement.href = prefs.get('editor.theme') === 'default' ? ''
- : 'vendor/codemirror/theme/' + prefs.get('editor.theme') + '.css';
- observer.disconnect();
- }
- }).observe(document, {subtree: true, childList: true});
-
- if (chrome.windows) {
- browser.tabs.query({currentWindow: true}).then(tabs => {
- const windowId = tabs[0].windowId;
- if (prefs.get('openEditInWindow')) {
- if (
- /true/.test(sessionStorage.saveSizeOnClose) &&
- 'left' in prefs.get('windowPosition', {}) &&
- !isWindowMaximized()
- ) {
- // window was reopened via Ctrl-Shift-T etc.
- chrome.windows.update(windowId, prefs.get('windowPosition'));
- }
- if (tabs.length === 1 && window.history.length === 1) {
- chrome.windows.getAll(windows => {
- if (windows.length > 1) {
- sessionStorageHash('saveSizeOnClose').set(windowId, true);
- saveSizeOnClose = true;
- }
- });
- } else {
- saveSizeOnClose = sessionStorageHash('saveSizeOnClose').value[windowId];
- }
- }
+ window.addEventListener('resize', () => {
+ if (!onBoundsChanged) debounce(rememberWindowSize, 100);
+ detectLayout();
});
}
- getOwnTab().then(tab => {
- const ownTabId = tab.id;
+ function toggleStyle() {
+ $('#enabled').checked = !style.enabled;
+ updateEnabledness(!style.enabled);
+ }
- // use browser history back when 'back to manage' is clicked
- if (sessionStorageHash('manageStylesHistory').value[ownTabId] === location.href) {
- onDOMready().then(() => {
- $('#cancel-button').onclick = event => {
- event.stopPropagation();
- event.preventDefault();
- history.back();
- };
- });
+ function updateDirty() {
+ const isDirty = dirty.isDirty();
+ if (wasDirty !== isDirty) {
+ wasDirty = isDirty;
+ document.body.classList.toggle('dirty', isDirty);
+ $('#save-button').disabled = !isDirty;
}
- // no windows on android
- if (!chrome.windows) {
+ updateTitle();
+ }
+
+ function updateEnabledness(enabled) {
+ dirty.modify('enabled', style.enabled, enabled);
+ style.enabled = enabled;
+ editor.updateLivePreview();
+ }
+
+ function updateName() {
+ if (!editor) return;
+ const {value} = $('#name');
+ dirty.modify('name', style[nameTarget] || style.name, value);
+ style[nameTarget] = value;
+ updateTitle({});
+ }
+
+ function updateTitle() {
+ document.title = `${dirty.isDirty() ? '* ' : ''}${style.customName || style.name}`;
+ }
+
+ function updateLinter(key, value) {
+ $('body').classList.toggle('linter-disabled', value === '');
+ linter.run();
+ }
+})();
+
+/* Stuff not needed for the main init so we can let it run at its own tempo */
+async function lazyInit() {
+ const ownTabId = (await getOwnTab()).id;
+ // use browser history back when 'back to manage' is clicked
+ if (sessionStorageHash('manageStylesHistory').value[ownTabId] === location.href) {
+ onDOMready().then(() => {
+ $('#cancel-button').onclick = event => {
+ event.stopPropagation();
+ event.preventDefault();
+ history.back();
+ };
+ });
+ }
+ // no windows on android
+ if (!chrome.windows) {
+ return;
+ }
+ const tabs = await browser.tabs.query({currentWindow: true});
+ const windowId = tabs[0].windowId;
+ if (prefs.get('openEditInWindow')) {
+ if (
+ /true/.test(sessionStorage.saveSizeOnClose) &&
+ 'left' in prefs.get('windowPosition', {}) &&
+ !isWindowMaximized()
+ ) {
+ // window was reopened via Ctrl-Shift-T etc.
+ chrome.windows.update(windowId, prefs.get('windowPosition'));
+ }
+ if (tabs.length === 1 && window.history.length === 1) {
+ chrome.windows.getAll(windows => {
+ if (windows.length > 1) {
+ sessionStorageHash('saveSizeOnClose').set(windowId, true);
+ saveSizeOnClose = true;
+ }
+ });
+ } else {
+ saveSizeOnClose = sessionStorageHash('saveSizeOnClose').value[windowId];
+ }
+ }
+ chrome.tabs.onAttached.addListener((tabId, info) => {
+ if (tabId !== ownTabId) {
return;
}
- // When an edit page gets attached or detached, remember its state
- // so we can do the same to the next one to open.
- chrome.tabs.onAttached.addListener((tabId, info) => {
- if (tabId !== ownTabId) {
- return;
+ if (info.newPosition !== 0) {
+ prefs.set('openEditInWindow', false);
+ return;
+ }
+ chrome.windows.get(info.newWindowId, {populate: true}, win => {
+ // If there's only one tab in this window, it's been dragged to new window
+ const openEditInWindow = win.tabs.length === 1;
+ if (openEditInWindow && FIREFOX) {
+ // FF-only because Chrome retardedly resets the size during dragging
+ chrome.windows.update(info.newWindowId, prefs.get('windowPosition'));
}
- if (info.newPosition !== 0) {
- prefs.set('openEditInWindow', false);
- return;
- }
- chrome.windows.get(info.newWindowId, {populate: true}, win => {
- // If there's only one tab in this window, it's been dragged to new window
- const openEditInWindow = win.tabs.length === 1;
- if (openEditInWindow && FIREFOX) {
- // FF-only because Chrome retardedly resets the size during dragging
- chrome.windows.update(info.newWindowId, prefs.get('windowPosition'));
- }
- prefs.set('openEditInWindow', openEditInWindow);
- });
+ prefs.set('openEditInWindow', openEditInWindow);
});
});
}
@@ -295,7 +294,7 @@ function onRuntimeMessage(request) {
switch (request.method) {
case 'styleUpdated':
if (
- editor.getStyleId() === request.style.id &&
+ editor.style.id === request.style.id &&
!['editPreview', 'editPreviewEnd', 'editSave', 'config']
.includes(request.reason)
) {
@@ -309,7 +308,7 @@ function onRuntimeMessage(request) {
}
break;
case 'styleDeleted':
- if (editor.getStyleId() === request.style.id) {
+ if (editor.style.id === request.style.id) {
document.removeEventListener('visibilitychange', beforeUnload);
document.removeEventListener('beforeunload', beforeUnload);
closeCurrentTab();
@@ -503,10 +502,6 @@ function rememberWindowSize() {
}
}
-prefs.subscribe(['editor.linter'], (key, value) => {
- $('body').classList.toggle('linter-disabled', value === '');
-});
-
function fixedHeader() {
const scrollPoint = $('#header').clientHeight - 40;
const linterEnabled = prefs.get('editor.linter') !== '';
diff --git a/edit/linter.js b/edit/linter.js
index a8d927fa..607eb057 100644
--- a/edit/linter.js
+++ b/edit/linter.js
@@ -1,6 +1,11 @@
-/* global prefs */
+/* global workerUtil */
'use strict';
+/* exported editorWorker */
+const editorWorker = workerUtil.createWorker({
+ url: '/edit/editor-worker.js'
+});
+
/* exported linter */
const linter = (() => {
const lintingUpdatedListeners = [];
@@ -59,8 +64,3 @@ const linter = (() => {
.then(results => [].concat(...results.filter(Boolean)));
}
})();
-
-// FIXME: this should be put inside edit.js
-prefs.subscribe(['editor.linter'], () => {
- linter.run();
-});
diff --git a/edit/sections-editor.js b/edit/sections-editor.js
index 8a04e06c..d7fde20b 100644
--- a/edit/sections-editor.js
+++ b/edit/sections-editor.js
@@ -1,41 +1,24 @@
-/* global dirtyReporter showHelp toggleContextMenuDelete createSection
+/* global showHelp toggleContextMenuDelete createSection
CodeMirror linter createLivePreview showCodeMirrorPopup
sectionsToMozFormat messageBox clipString
- rerouteHotkeys $ $$ $create t FIREFOX API
+ $ $$ $create t FIREFOX API
debounce */
/* exported createSectionsEditor */
'use strict';
-function createSectionsEditor({style, onTitleChanged}) {
+function createSectionsEditor(editorBase) {
+ const {style, dirty} = editorBase;
+
let INC_ID = 0; // an increment id that is used by various object to track the order
- const dirty = dirtyReporter();
const container = $('#sections');
const sections = [];
container.classList.add('section-editor');
-
- const nameEl = $('#name');
- nameEl.addEventListener('input', () => {
- dirty.modify('name', style.name, nameEl.value);
- style.name = nameEl.value;
- onTitleChanged();
- });
-
- const enabledEl = $('#enabled');
- enabledEl.addEventListener('change', () => {
- dirty.modify('enabled', style.enabled, enabledEl.checked);
- style.enabled = enabledEl.checked;
- updateLivePreview();
- });
-
updateHeader();
- rerouteHotkeys(true);
-
$('#to-mozilla').addEventListener('click', showMozillaFormat);
$('#to-mozilla-help').addEventListener('click', showToMozillaHelp);
$('#from-mozilla').addEventListener('click', () => showMozillaFormatImport());
- $('#save-button').addEventListener('click', saveStyle);
document.addEventListener('wheel', scrollEntirePageOnCtrlShift, {passive: false});
CodeMirror.defaults.extraKeys['Shift-Ctrl-Wheel'] = 'scrollWindow';
@@ -65,33 +48,30 @@ function createSectionsEditor({style, onTitleChanged}) {
let sectionOrder = '';
let headerOffset; // in compact mode the header is at the top so it reduces the available height
- const initializing = initSections(style.sections.slice());
+ const ready = initSections(style.sections.slice());
const livePreview = createLivePreview();
livePreview.show(Boolean(style.id));
- return {
- ready: () => initializing,
+ return Object.assign({}, editorBase, {
+ ready,
replaceStyle,
- dirty,
- getStyle: () => style,
getEditors,
scrollToEditor,
- getStyleId: () => style.id,
getEditorTitle: cm => {
const index = sections.filter(s => !s.isRemoved()).findIndex(s => s.cm === cm);
return `${t('sectionCode')} ${index + 1}`;
},
- save: saveStyle,
- toggleStyle,
+ save,
nextEditor,
prevEditor,
closestVisible,
getSearchableInputs,
- };
+ updateLivePreview,
+ });
function fitToContent(section) {
- const {cm, cm: {display: {wrapper, sizer}}} = section;
+ const {el, cm, cm: {display: {wrapper, sizer}}} = section;
if (cm.display.renderedView) {
resize();
} else {
@@ -104,7 +84,7 @@ function createSectionsEditor({style, onTitleChanged}) {
return;
}
if (headerOffset == null) {
- headerOffset = wrapper.getBoundingClientRect().top;
+ headerOffset = el.getBoundingClientRect().top;
}
contentHeight += 9; // border & resize grip
cm.off('update', resize);
@@ -115,16 +95,15 @@ function createSectionsEditor({style, onTitleChanged}) {
}
function fitToAvailableSpace() {
- const available =
- Math.floor(container.offsetHeight - sections.reduce((h, s) => h + s.el.offsetHeight, 0)) ||
- window.innerHeight - container.offsetHeight;
- if (available <= 0) {
- return;
+ const ch = container.offsetHeight;
+ let available = ch - sections[sections.length - 1].el.getBoundingClientRect().bottom + headerOffset;
+ if (available <= 1) available = window.innerHeight - ch - headerOffset;
+ const delta = Math.floor(available / sections.length);
+ if (delta > 1) {
+ sections.forEach(({cm}) => {
+ cm.setSize(null, cm.display.wrapper.offsetHeight + delta);
+ });
}
- const cmHeights = sections.map(s => s.cm.getWrapperElement().offsetHeight);
- sections.forEach((section, i) => {
- section.cm.setSize(null, cmHeights[i] + Math.floor(available / sections.length));
- });
}
function genId() {
@@ -246,14 +225,6 @@ function createSectionsEditor({style, onTitleChanged}) {
return sections.filter(s => !s.isRemoved()).map(s => s.cm);
}
- function toggleStyle() {
- const newValue = !style.enabled;
- dirty.modify('enabled', style.enabled, newValue);
- style.enabled = newValue;
- enabledEl.checked = newValue;
- updateLivePreview();
- }
-
function nextEditor(cm, cycle = true) {
if (!cycle && findLast(sections, s => !s.isRemoved()).cm === cm) {
return;
@@ -417,7 +388,7 @@ function createSectionsEditor({style, onTitleChanged}) {
}
function validate() {
- if (!nameEl.reportValidity()) {
+ if (!$('#name').reportValidity()) {
messageBox.alert(t('styleMissingName'));
return false;
}
@@ -435,7 +406,7 @@ function createSectionsEditor({style, onTitleChanged}) {
return true;
}
- function saveStyle() {
+ function save() {
if (!dirty.isDirty()) {
return;
}
@@ -464,10 +435,10 @@ function createSectionsEditor({style, onTitleChanged}) {
}
function updateHeader() {
- nameEl.value = style.name || '';
- enabledEl.checked = style.enabled !== false;
+ $('#name').value = style.customName || style.name || '';
+ $('#enabled').checked = style.enabled !== false;
$('#url').href = style.url || '';
- onTitleChanged();
+ editorBase.updateName();
}
function updateLivePreview() {
@@ -609,6 +580,7 @@ function createSectionsEditor({style, onTitleChanged}) {
}
function replaceStyle(newStyle, codeIsUpdated) {
+ dirty.clear('name');
// FIXME: avoid recreating all editors?
reinit().then(() => {
Object.assign(style, newStyle);
diff --git a/edit/source-editor.js b/edit/source-editor.js
index b3292606..af72b911 100644
--- a/edit/source-editor.js
+++ b/edit/source-editor.js
@@ -1,4 +1,4 @@
-/* global dirtyReporter
+/* global
createAppliesToLineWidget messageBox
sectionsToMozFormat
createMetaCompiler linter createLivePreview cmFactory $ $create API prefs t
@@ -6,17 +6,14 @@
/* exported createSourceEditor */
'use strict';
-function createSourceEditor({style, onTitleChanged}) {
- $('#name').disabled = true;
- $('#save-button').disabled = true;
+function createSourceEditor(editorBase) {
+ const {style, dirty} = editorBase;
+
$('#mozilla-format-container').remove();
- $('#save-button').onclick = save;
$('#header').addEventListener('wheel', headerOnScroll);
$('#sections').textContent = '';
$('#sections').appendChild($create('.single-editor'));
- const dirty = dirtyReporter();
-
// normalize style
if (!style.id) setupNewStyle(style);
@@ -28,13 +25,6 @@ function createSourceEditor({style, onTitleChanged}) {
const livePreview = createLivePreview(preprocess);
livePreview.show(Boolean(style.id));
- $('#enabled').onchange = function () {
- const value = this.checked;
- dirty.modify('enabled', style.enabled, value);
- style.enabled = value;
- updateLivePreview();
- };
-
cm.on('changes', () => {
dirty.modify('sourceGeneration', savedGeneration, cm.changeGeneration());
updateLivePreview();
@@ -162,14 +152,15 @@ function createSourceEditor({style, onTitleChanged}) {
}
function updateMeta() {
- $('#name').value = style.name;
+ $('#name').value = style.customName || style.name;
$('#enabled').checked = style.enabled;
$('#url').href = style.url;
- onTitleChanged();
+ editorBase.updateName();
return cm.setPreprocessor((style.usercssData || {}).preprocessor);
}
function replaceStyle(newStyle, codeIsUpdated) {
+ dirty.clear('name');
const sameCode = newStyle.sourceCode === cm.getValue();
if (sameCode) {
savedGeneration = cm.changeGeneration();
@@ -210,14 +201,6 @@ function createSourceEditor({style, onTitleChanged}) {
}
}
- function toggleStyle() {
- const value = !style.enabled;
- dirty.modify('enabled', style.enabled, value);
- style.enabled = value;
- updateMeta();
- $('#enabled').dispatchEvent(new Event('change', {bubbles: true}));
- }
-
function save() {
if (!dirty.isDirty()) return;
const code = cm.getValue();
@@ -226,6 +209,7 @@ function createSourceEditor({style, onTitleChanged}) {
id: style.id,
enabled: style.enabled,
sourceCode: code,
+ customName: style.customName,
}))
.then(replaceStyle)
.catch(err => {
@@ -372,19 +356,17 @@ function createSourceEditor({style, onTitleChanged}) {
(mode.helperType || '');
}
- return {
+ return Object.assign({}, editorBase, {
+ ready: Promise.resolve(),
replaceStyle,
- dirty,
- getStyle: () => style,
getEditors: () => [cm],
scrollToEditor: () => {},
- getStyleId: () => style.id,
getEditorTitle: () => '',
save,
- toggleStyle,
prevEditor: cm => nextPrevMozDocument(cm, -1),
nextEditor: cm => nextPrevMozDocument(cm, 1),
closestVisible: () => cm,
- getSearchableInputs: () => []
- };
+ getSearchableInputs: () => [],
+ updateLivePreview,
+ });
}
diff --git a/install-usercss/install-usercss.js b/install-usercss/install-usercss.js
index 41ee0ab1..8ce11002 100644
--- a/install-usercss/install-usercss.js
+++ b/install-usercss/install-usercss.js
@@ -243,7 +243,7 @@
(!dup ?
Promise.resolve(true) :
messageBox.confirm(t('styleInstallOverwrite', [
- data.name,
+ data.name + (dup.customName ? ` (${dup.customName})` : ''),
dupData.version,
data.version,
]))
diff --git a/js/msg.js b/js/msg.js
index 26dba45d..53f17bfa 100644
--- a/js/msg.js
+++ b/js/msg.js
@@ -33,7 +33,8 @@ self.msg = self.INJECTED === 1 ? self.msg : (() => {
onExtension,
off,
RX_NO_RECEIVER,
- RX_PORT_CLOSED
+ RX_PORT_CLOSED,
+ isBg,
};
function getBg() {
diff --git a/js/prefs.js b/js/prefs.js
index bb3ba650..c7fb981c 100644
--- a/js/prefs.js
+++ b/js/prefs.js
@@ -1,6 +1,8 @@
-/* global promisifyChrome */
+/* global promisifyChrome msg API */
'use strict';
+// Needs msg.js loaded first
+
self.prefs = self.INJECTED === 1 ? self.prefs : (() => {
const defaults = {
'openEditInWindow': false, // new editor opens in a own browser window
@@ -112,12 +114,11 @@ self.prefs = self.INJECTED === 1 ? self.prefs : (() => {
'storage.sync': ['get', 'set'],
});
- const initializing = browser.storage.sync.get('settings')
- .then(result => {
- if (result.settings) {
- setAll(result.settings, true);
- }
- });
+ const initializing = (
+ msg.isBg
+ ? browser.storage.sync.get('settings').then(res => res.settings)
+ : API.getPrefs()
+ ).then(res => res && setAll(res, true));
chrome.storage.onChanged.addListener((changes, area) => {
if (area !== 'sync' || !changes.settings || !changes.settings.newValue) {
diff --git a/js/script-loader.js b/js/script-loader.js
index ada07824..2517951f 100644
--- a/js/script-loader.js
+++ b/js/script-loader.js
@@ -48,73 +48,3 @@ const loadScript = (() => {
));
};
})();
-
-
-(() => {
- let subscribers, observer;
- // natively declared
-
+
diff --git a/manage/config-dialog.js b/manage/config-dialog.js
index e4126422..ed36cef6 100644
--- a/manage/config-dialog.js
+++ b/manage/config-dialog.js
@@ -23,7 +23,7 @@ function configDialog(style) {
vars.forEach(renderValueState);
return messageBox({
- title: `${style.name} v${data.version}`,
+ title: `${style.customName || style.name} v${data.version}`,
className: 'config-dialog' + (isPopup ? ' stylus-popup' : ''),
contents: [
$create('.config-heading', data.supportURL &&
diff --git a/manage/filters.js b/manage/filters.js
index 11e11feb..3d670dfe 100644
--- a/manage/filters.js
+++ b/manage/filters.js
@@ -14,7 +14,7 @@ let initialized = false;
router.watch({search: ['search']}, ([search]) => {
$('#search').value = search || '';
if (!initialized) {
- init();
+ initFilters();
initialized = true;
} else {
searchStyles();
@@ -36,7 +36,7 @@ HTMLSelectElement.prototype.adjustWidth = function () {
parent.replaceChild(this, singleSelect);
};
-function init() {
+function initFilters() {
$('#search').oninput = e => {
router.updateSearch('search', e.target.value);
};
diff --git a/manage/import-export.js b/manage/import-export.js
index 7d08fc89..15261baf 100644
--- a/manage/import-export.js
+++ b/manage/import-export.js
@@ -1,15 +1,11 @@
/* global messageBox styleSectionsEqual API onDOMready
tryJSONparse scrollElementIntoView $ $$ API $create t animateElement
- styleJSONseemsValid */
-/* exported bulkChangeQueue bulkChangeTime */
+ styleJSONseemsValid bulkChangeQueue */
'use strict';
const STYLISH_DUMP_FILE_EXT = '.txt';
const STYLUS_BACKUP_FILE_EXT = '.json';
-let bulkChangeQueue = [];
-let bulkChangeTime = 0;
-
onDOMready().then(() => {
$('#file-all-styles').onclick = () => exportToFile();
$('#unfile-all-styles').onclick = () => importFromFile({fileTypeFilter: STYLUS_BACKUP_FILE_EXT});
@@ -135,7 +131,7 @@ function importFromString(jsonString) {
}
});
bulkChangeQueue.length = 0;
- bulkChangeTime = performance.now();
+ bulkChangeQueue.time = performance.now();
return API.importManyStyles(items.map(i => i.item))
.then(styles => {
for (let i = 0; i < styles.length; i++) {
diff --git a/manage/manage.js b/manage/manage.js
index 5bd0ca55..888da2e8 100644
--- a/manage/manage.js
+++ b/manage/manage.js
@@ -4,11 +4,10 @@ global messageBox getStyleWithNoCode
checkUpdate handleUpdateInstalled
objectDiff
configDialog
- sorter msg prefs API onDOMready $ $$ $create template setupLivePrefs
+ sorter msg prefs API $ $$ $create template setupLivePrefs
t tWordBreak formatDate
getOwnTab getActiveTab openURL animateElement sessionStorageHash debounce
scrollElementIntoView CHROME VIVALDI router
- bulkChangeTime:true bulkChangeQueue
*/
'use strict';
@@ -18,6 +17,8 @@ const ENTRY_ID_PREFIX_RAW = 'style-';
const ENTRY_ID_PREFIX = '#' + ENTRY_ID_PREFIX_RAW;
const BULK_THROTTLE_MS = 100;
+const bulkChangeQueue = [];
+bulkChangeQueue.time = 0;
// define pref-mapped ids separately
const newUI = {
@@ -49,18 +50,45 @@ Promise.all([
API.getAllStyles(true),
// FIXME: integrate this into filter.js
router.getSearch('search') && API.searchDB({query: router.getSearch('search')}),
- Promise.all([
- onDOMready(),
- prefs.initializing,
- ])
- .then(() => {
- initGlobalEvents();
- if (!VIVALDI) {
- $$('#header select').forEach(el => el.adjustWidth());
- }
- }),
-]).then(args => {
- showStyles(...args);
+ waitForSelector('#installed'), // needed to avoid flicker due to an extra frame and layout shift
+ prefs.initializing
+]).then(([styles, ids, el]) => {
+ installed = el;
+ installed.onclick = handleEvent.entryClicked;
+ $('#manage-options-button').onclick = () => router.updateHash('#stylus-options');
+ $('#sync-styles').onclick = () => router.updateHash('#stylus-options');
+ $$('#header a[href^="http"]').forEach(a => (a.onclick = handleEvent.external));
+ // show date installed & last update on hover
+ installed.addEventListener('mouseover', handleEvent.lazyAddEntryTitle);
+ installed.addEventListener('mouseout', handleEvent.lazyAddEntryTitle);
+ document.addEventListener('visibilitychange', onVisibilityChange);
+ // N.B. triggers existing onchange listeners
+ setupLivePrefs();
+ sorter.init();
+ prefs.subscribe(newUI.ids.map(newUI.prefKeyForId), () => switchUI());
+ switchUI({styleOnly: true});
+ // translate CSS manually
+ document.head.appendChild($create('style', `
+ .disabled h2::after {
+ content: "${t('genericDisabledLabel')}";
+ }
+ #update-all-no-updates[data-skipped-edited="true"]::after {
+ content: " ${t('updateAllCheckSucceededSomeEdited')}";
+ }
+ body.all-styles-hidden-by-filters::after {
+ content: "${t('filteredStylesAllHidden')}";
+ }
+ `));
+ if (!VIVALDI) {
+ $$('#header select').forEach(el => el.adjustWidth());
+ }
+ if (CHROME >= 80 && CHROME <= 88) {
+ // Wrong checkboxes are randomly checked after going back in history, https://crbug.com/1138598
+ addEventListener('pagehide', () => {
+ $$('input[type=checkbox]').forEach((el, i) => (el.name = `bug${i}`));
+ });
+ }
+ showStyles(styles, ids);
});
msg.onExtension(onRuntimeMessage);
@@ -71,7 +99,7 @@ function onRuntimeMessage(msg) {
case 'styleAdded':
case 'styleDeleted':
bulkChangeQueue.push(msg);
- if (performance.now() - bulkChangeTime < BULK_THROTTLE_MS) {
+ if (performance.now() - bulkChangeQueue.time < BULK_THROTTLE_MS) {
debounce(handleBulkChange, BULK_THROTTLE_MS);
} else {
handleBulkChange();
@@ -86,61 +114,16 @@ function onRuntimeMessage(msg) {
setTimeout(sorter.updateStripes, 0, {onlyWhenColumnsChanged: true});
}
-
-function initGlobalEvents() {
- installed = $('#installed');
- installed.onclick = handleEvent.entryClicked;
- $('#manage-options-button').onclick = () => router.updateHash('#stylus-options');
- $('#sync-styles').onclick = () => router.updateHash('#stylus-options');
- $$('#header a[href^="http"]').forEach(a => (a.onclick = handleEvent.external));
- // show date installed & last update on hover
- installed.addEventListener('mouseover', handleEvent.lazyAddEntryTitle);
- installed.addEventListener('mouseout', handleEvent.lazyAddEntryTitle);
-
- document.addEventListener('visibilitychange', onVisibilityChange);
-
- $$('[data-toggle-on-click]').forEach(el => {
- // dataset on SVG doesn't work in Chrome 49-??, works in 57+
- const target = $(el.getAttribute('data-toggle-on-click'));
- el.onclick = event => {
- event.preventDefault();
- target.classList.toggle('hidden');
- if (target.classList.contains('hidden')) {
- el.removeAttribute('open');
- } else {
- el.setAttribute('open', '');
- }
- };
- });
-
- // N.B. triggers existing onchange listeners
- setupLivePrefs();
- sorter.init();
-
- prefs.subscribe(newUI.ids.map(newUI.prefKeyForId), () => switchUI());
-
- switchUI({styleOnly: true});
-
- // translate CSS manually
- document.head.appendChild($create('style', `
- .disabled h2::after {
- content: "${t('genericDisabledLabel')}";
- }
- #update-all-no-updates[data-skipped-edited="true"]::after {
- content: " ${t('updateAllCheckSucceededSomeEdited')}";
- }
- body.all-styles-hidden-by-filters::after {
- content: "${t('filteredStylesAllHidden')}";
- }
- `));
-}
-
function showStyles(styles = [], matchUrlIds) {
const sorted = sorter.sort({
- styles: styles.map(style => ({
- style,
- name: (style.name || '').toLocaleLowerCase() + '\n' + style.name,
- })),
+ styles: styles.map(style => {
+ const name = style.customName || style.name || '';
+ return {
+ style,
+ // sort case-insensitively the whole list then sort dupes like `Foo` and `foo` case-sensitively
+ name: name.toLocaleLowerCase() + '\n' + name,
+ };
+ }),
});
let index = 0;
let firstRun = true;
@@ -187,7 +170,7 @@ function showStyles(styles = [], matchUrlIds) {
}
-function createStyleElement({style, name}) {
+function createStyleElement({style, name: nameLC}) {
// query the sub-elements just once, then reuse the references
if ((createStyleElement.parts || {}).newUI !== newUI.enabled) {
const entry = template[`style${newUI.enabled ? 'Compact' : ''}`];
@@ -216,8 +199,9 @@ function createStyleElement({style, name}) {
}
const parts = createStyleElement.parts;
const configurable = style.usercssData && style.usercssData.vars && Object.keys(style.usercssData.vars).length > 0;
+ const name = style.customName || style.name;
parts.checker.checked = style.enabled;
- parts.nameLink.textContent = tWordBreak(style.name);
+ parts.nameLink.textContent = tWordBreak(name);
parts.nameLink.href = parts.editLink.href = parts.editHrefBase + style.id;
parts.homepage.href = parts.homepage.title = style.url || '';
if (!newUI.enabled) {
@@ -234,7 +218,7 @@ function createStyleElement({style, name}) {
const entry = parts.entry.cloneNode(true);
entry.id = ENTRY_ID_PREFIX_RAW + style.id;
entry.styleId = style.id;
- entry.styleNameLowerCase = name || style.name.toLocaleLowerCase();
+ entry.styleNameLowerCase = nameLC || name.toLocaleLowerCase() + '\n' + name;
entry.styleMeta = style;
entry.className = parts.entryClassBase + ' ' +
(style.enabled ? 'enabled' : 'disabled') +
@@ -437,7 +421,7 @@ Object.assign(handleEvent, {
animateElement(entry);
messageBox({
title: t('deleteStyleConfirm'),
- contents: entry.styleMeta.name,
+ contents: entry.styleMeta.customName || entry.styleMeta.name,
className: 'danger center',
buttons: [t('confirmDelete'), t('confirmCancel')],
})
@@ -533,7 +517,7 @@ function handleBulkChange() {
const {id} = msg.style;
if (msg.method === 'styleDeleted') {
handleDelete(id);
- bulkChangeTime = performance.now();
+ bulkChangeQueue.time = performance.now();
} else {
handleUpdateForId(id, msg);
}
@@ -544,7 +528,7 @@ function handleBulkChange() {
function handleUpdateForId(id, opts) {
return API.getStyle(id, true).then(style => {
handleUpdate(style, opts);
- bulkChangeTime = performance.now();
+ bulkChangeQueue.time = performance.now();
});
}
@@ -718,6 +702,20 @@ function highlightEditedStyle() {
}
}
+function waitForSelector(selector) {
+ // TODO: if used in other places, move to dom.js
+ // TODO: if used concurrently, rework to use just one observer internally
+ return new Promise(resolve => {
+ const mo = new MutationObserver(() => {
+ const el = $(selector);
+ if (el) {
+ mo.disconnect();
+ resolve(el);
+ }
+ });
+ mo.observe(document, {childList: true, subtree: true});
+ });
+}
function embedOptions() {
let options = $('#stylus-embedded-options');
diff --git a/manage/sort.js b/manage/sort.js
index 928e3733..17454424 100644
--- a/manage/sort.js
+++ b/manage/sort.js
@@ -130,7 +130,7 @@ const sorter = (() => {
const sorted = sort({
styles: current.map(entry => ({
entry,
- name: entry.styleNameLowerCase + '\n' + entry.styleMeta.name,
+ name: entry.styleNameLowerCase,
style: entry.styleMeta,
}))
});
diff --git a/popup.html b/popup.html
index 677edb81..1a39b3a4 100644
--- a/popup.html
+++ b/popup.html
@@ -180,8 +180,8 @@
-
+
diff --git a/popup/popup.js b/popup/popup.js
index 1510ce76..979fdd4c 100644
--- a/popup/popup.js
+++ b/popup/popup.js
@@ -310,7 +310,7 @@ function sortStyles(entries) {
return entries.sort(({styleMeta: a}, {styleMeta: b}) =>
Boolean(a.frameUrl) - Boolean(b.frameUrl) ||
enabledFirst && Boolean(b.enabled) - Boolean(a.enabled) ||
- a.name.localeCompare(b.name));
+ (a.customName || a.name).localeCompare(b.customName || b.name));
}
function showStyles(frameResults) {
@@ -408,7 +408,7 @@ function createStyleElement(style) {
$('.checker', entry).checked = style.enabled;
const styleName = $('.style-name', entry);
- styleName.lastChild.textContent = style.name;
+ styleName.lastChild.textContent = style.customName || style.name;
setTimeout(() => {
styleName.title = entry.styleMeta.sloppy ?
t('styleNotAppliedRegexpProblemTooltip') :