stylus/edit/edit.js

2152 lines
70 KiB
JavaScript
Raw Normal View History

2017-07-12 20:44:59 +00:00
/* eslint brace-style: 0, operator-linebreak: 0 */
/* global CodeMirror exports parserlib CSSLint */
'use strict';
2017-07-12 20:44:59 +00:00
let styleId = null;
let dirty = {}; // only the actually dirty items here
const editors = []; // array of all CodeMirror instances
let saveSizeOnClose;
let useHistoryBack; // use browser history back when 'back to manage' is clicked
2015-01-30 17:05:06 +00:00
// direct & reverse mapping of @-moz-document keywords and internal property names
2017-07-12 20:44:59 +00:00
const propertyToCss = {urls: 'url', urlPrefixes: 'url-prefix', domains: 'domain', regexps: 'regexp'};
const CssToProperty = {'url': 'urls', 'url-prefix': 'urlPrefixes', 'domain': 'domains', 'regexp': 'regexps'};
// if background page hasn't been loaded yet, increase the chances it has before DOMContentLoaded
onBackgroundReady();
// make querySelectorAll enumeration code readable
['forEach', 'some', 'indexOf', 'map'].forEach(method => {
2017-07-12 20:44:59 +00:00
NodeList.prototype[method] = Array.prototype[method];
});
2015-07-26 02:24:18 +00:00
// Chrome pre-34
Element.prototype.matches = Element.prototype.matches || Element.prototype.webkitMatchesSelector;
// Chrome pre-41 polyfill
Element.prototype.closest = Element.prototype.closest || function (selector) {
2017-07-12 20:44:59 +00:00
let e;
// eslint-disable-next-line no-empty
for (e = this; e && !e.matches(selector); e = e.parentElement) {}
2017-07-12 19:50:13 +00:00
return e;
};
2017-07-12 20:44:59 +00:00
// eslint-disable-next-line no-extend-native
Array.prototype.rotate = function (amount) { // negative amount == rotate left
2017-07-12 20:44:59 +00:00
const r = this.slice(-amount, this.length);
2017-07-12 19:50:13 +00:00
Array.prototype.push.apply(r, this.slice(0, this.length - r.length));
return r;
};
2017-07-12 20:44:59 +00:00
// eslint-disable-next-line no-extend-native
Object.defineProperty(Array.prototype, 'last', {get: function () { return this[this.length - 1]; }});
// preload the theme so that CodeMirror can calculate its metrics in DOMContentLoaded->setupLivePrefs()
new MutationObserver((mutations, observer) => {
2017-07-12 20:44:59 +00:00
const themeElement = document.getElementById('cm-theme');
2017-07-12 19:50:13 +00:00
if (themeElement) {
2017-07-16 18:02:00 +00:00
themeElement.href = prefs.get('editor.theme') === 'default' ? ''
2017-07-12 20:44:59 +00:00
: 'vendor/codemirror/theme/' + prefs.get('editor.theme') + '.css';
2017-07-12 19:50:13 +00:00
observer.disconnect();
}
}).observe(document, {subtree: true, childList: true});
getCodeMirrorThemes();
// reroute handling to nearest editor when keypress resolves to one of these commands
2017-07-12 20:44:59 +00:00
const hotkeyRerouter = {
2017-07-12 19:50:13 +00:00
commands: {
save: true, jumpToLine: true, nextEditor: true, prevEditor: true,
find: true, findNext: true, findPrev: true, replace: true, replaceAll: true,
toggleStyle: true,
},
setState: enable => {
setTimeout(() => {
2017-07-12 20:44:59 +00:00
document[(enable ? 'add' : 'remove') + 'EventListener']('keydown', hotkeyRerouter.eventHandler);
2017-07-12 19:50:13 +00:00
}, 0);
},
eventHandler: event => {
2017-07-12 20:44:59 +00:00
const keyName = CodeMirror.keyName(event);
if (
2017-07-16 18:02:00 +00:00
CodeMirror.lookupKey(keyName, CodeMirror.getOption('keyMap'), handleCommand) === 'handled' ||
CodeMirror.lookupKey(keyName, CodeMirror.defaults.extraKeys, handleCommand) === 'handled'
2017-07-12 20:44:59 +00:00
) {
2017-07-12 19:50:13 +00:00
event.preventDefault();
event.stopPropagation();
}
function handleCommand(command) {
if (hotkeyRerouter.commands[command] === true) {
CodeMirror.commands[command](getEditorInSight(event.target));
return true;
}
}
}
};
function onChange(event) {
2017-07-12 20:44:59 +00:00
const node = event.target;
if ('savedValue' in node) {
const currentValue = node.type === 'checkbox' ? node.checked : node.value;
2017-07-12 19:50:13 +00:00
setCleanItem(node, node.savedValue === currentValue);
} else {
// the manually added section's applies-to is dirty only when the value is non-empty
2017-07-16 18:02:00 +00:00
setCleanItem(node, node.localName !== 'input' || !node.value.trim());
2017-07-12 19:50:13 +00:00
delete node.savedValue; // only valid when actually saved
}
updateTitle();
}
// Set .dirty on stylesheet contributors that have changed
function setDirtyClass(node, isDirty) {
2017-07-12 20:44:59 +00:00
node.classList.toggle('dirty', isDirty);
}
function setCleanItem(node, isClean) {
2017-07-12 19:50:13 +00:00
if (!node.id) {
node.id = Date.now().toString(32).substr(-6);
}
if (isClean) {
delete dirty[node.id];
// code sections have .CodeMirror property
if (node.CodeMirror) {
node.savedValue = node.CodeMirror.changeGeneration();
} else {
2017-07-12 20:44:59 +00:00
node.savedValue = node.type === 'checkbox' ? node.checked : node.value;
2017-07-12 19:50:13 +00:00
}
} else {
dirty[node.id] = true;
}
setDirtyClass(node, !isClean);
}
function isCleanGlobal() {
2017-07-16 18:02:00 +00:00
const clean = Object.keys(dirty).length === 0;
2017-07-12 19:50:13 +00:00
setDirtyClass(document.body, !clean);
2017-07-12 20:44:59 +00:00
// let saveBtn = document.getElementById('save-button')
// if (clean){
// //saveBtn.removeAttribute('disabled');
// }else{
// //saveBtn.setAttribute('disabled', true);
// }
2017-07-12 19:50:13 +00:00
return clean;
}
function setCleanGlobal() {
2017-07-12 20:44:59 +00:00
document.querySelectorAll('#header, #sections > div').forEach(setCleanSection);
2017-07-12 19:50:13 +00:00
dirty = {}; // forget the dirty applies-to ids from a deleted section after the style was saved
}
function setCleanSection(section) {
section.querySelectorAll('.style-contributor').forEach(node => { setCleanItem(node, true); });
2017-07-12 19:50:13 +00:00
// #header section has no codemirror
2017-07-12 20:44:59 +00:00
const cm = section.CodeMirror;
2017-07-12 19:50:13 +00:00
if (cm) {
section.savedValue = cm.changeGeneration();
indicateCodeChange(cm);
}
}
function initCodeMirror() {
2017-07-12 20:44:59 +00:00
const CM = CodeMirror;
const isWindowsOS = navigator.appVersion.indexOf('Windows') > 0;
2017-07-12 19:50:13 +00:00
2017-07-12 20:44:59 +00:00
// CodeMirror miserably fails on keyMap='' so let's ensure it's not
2017-07-12 19:50:13 +00:00
if (!prefs.get('editor.keyMap')) {
prefs.reset('editor.keyMap');
}
// default option values
Object.assign(CM.defaults, {
mode: 'css',
lineNumbers: true,
lineWrapping: true,
foldGutter: true,
2017-07-12 20:44:59 +00:00
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
2017-07-12 19:50:13 +00:00
matchBrackets: true,
highlightSelectionMatches: {showToken: /[#.\-\w]/, annotateScrollbar: true},
hintOptions: {},
2017-07-12 20:44:59 +00:00
lint: {getAnnotations: CodeMirror.lint.css, delay: prefs.get('editor.lintDelay')},
lintReportDelay: prefs.get('editor.lintReportDelay'),
2017-07-12 19:50:13 +00:00
styleActiveLine: true,
2017-07-12 20:44:59 +00:00
theme: 'default',
keyMap: prefs.get('editor.keyMap'),
2017-07-12 19:50:13 +00:00
extraKeys: { // independent of current keyMap
2017-07-12 20:44:59 +00:00
'Alt-Enter': 'toggleStyle',
'Alt-PageDown': 'nextEditor',
'Alt-PageUp': 'prevEditor'
2017-07-12 19:50:13 +00:00
}
2017-07-12 20:44:59 +00:00
}, prefs.get('editor.options'));
2017-07-12 19:50:13 +00:00
// additional commands
CM.commands.jumpToLine = jumpToLine;
CM.commands.nextEditor = cm => { nextPrevEditor(cm, 1); };
CM.commands.prevEditor = cm => { nextPrevEditor(cm, -1); };
2017-07-12 19:50:13 +00:00
CM.commands.save = save;
CM.commands.blockComment = cm => {
2017-07-12 20:44:59 +00:00
cm.blockComment(cm.getCursor('from'), cm.getCursor('to'), {fullLines: false});
2017-07-12 19:50:13 +00:00
};
CM.commands.toggleStyle = toggleStyle;
2017-07-12 20:44:59 +00:00
// 'basic' keymap only has basic keys by design, so we skip it
2017-07-12 19:50:13 +00:00
2017-07-12 20:44:59 +00:00
const extraKeysCommands = {};
Object.keys(CM.defaults.extraKeys).forEach(key => {
2017-07-12 19:50:13 +00:00
extraKeysCommands[CM.defaults.extraKeys[key]] = true;
});
if (!extraKeysCommands.jumpToLine) {
2017-07-12 20:44:59 +00:00
CM.keyMap.sublime['Ctrl-G'] = 'jumpToLine';
CM.keyMap.emacsy['Ctrl-G'] = 'jumpToLine';
CM.keyMap.pcDefault['Ctrl-J'] = 'jumpToLine';
CM.keyMap.macDefault['Cmd-J'] = 'jumpToLine';
2017-07-12 19:50:13 +00:00
}
if (!extraKeysCommands.autocomplete) {
2017-07-12 20:44:59 +00:00
CM.keyMap.pcDefault['Ctrl-Space'] = 'autocomplete'; // will be used by 'sublime' on PC via fallthrough
CM.keyMap.macDefault['Alt-Space'] = 'autocomplete'; // OSX uses Ctrl-Space and Cmd-Space for something else
CM.keyMap.emacsy['Alt-/'] = 'autocomplete'; // copied from 'emacs' keymap
// 'vim' and 'emacs' define their own autocomplete hotkeys
2017-07-12 19:50:13 +00:00
}
if (!extraKeysCommands.blockComment) {
2017-07-12 20:44:59 +00:00
CM.keyMap.sublime['Shift-Ctrl-/'] = 'blockComment';
2017-07-12 19:50:13 +00:00
}
if (isWindowsOS) {
2017-07-12 20:44:59 +00:00
// 'pcDefault' keymap on Windows should have F3/Shift-F3
2017-07-12 19:50:13 +00:00
if (!extraKeysCommands.findNext) {
2017-07-12 20:44:59 +00:00
CM.keyMap.pcDefault['F3'] = 'findNext';
2017-07-12 19:50:13 +00:00
}
if (!extraKeysCommands.findPrev) {
2017-07-12 20:44:59 +00:00
CM.keyMap.pcDefault['Shift-F3'] = 'findPrev';
2017-07-12 19:50:13 +00:00
}
// try to remap non-interceptable Ctrl-(Shift-)N/T/W hotkeys
['N', 'T', 'W'].forEach(char => {
2017-07-12 20:44:59 +00:00
[{from: 'Ctrl-', to: ['Alt-', 'Ctrl-Alt-']},
{from: 'Shift-Ctrl-', to: ['Ctrl-Alt-', 'Shift-Ctrl-Alt-']} // Note: modifier order in CM is S-C-A
].forEach(remap => {
2017-07-12 20:44:59 +00:00
const oldKey = remap.from + char;
Object.keys(CM.keyMap).forEach(keyMapName => {
2017-07-12 20:44:59 +00:00
const keyMap = CM.keyMap[keyMapName];
const command = keyMap[oldKey];
2017-07-12 19:50:13 +00:00
if (!command) {
return;
}
remap.to.some(newMod => {
2017-07-12 20:44:59 +00:00
const newKey = newMod + char;
2017-07-12 19:50:13 +00:00
if (!(newKey in keyMap)) {
delete keyMap[oldKey];
keyMap[newKey] = command;
return true;
}
});
});
});
});
}
// user option values
CM.getOption = o => CodeMirror.defaults[o];
CM.setOption = (o, v) => {
2017-07-12 19:50:13 +00:00
CodeMirror.defaults[o] = v;
editors.forEach(editor => {
2017-07-12 19:50:13 +00:00
editor.setOption(o, v);
});
};
CM.prototype.getSection = function () {
2017-07-12 19:50:13 +00:00
return this.display.wrapper.parentNode;
};
// initialize global editor controls
function optionsFromArray(parent, options) {
2017-07-19 16:18:34 +00:00
const fragment = document.createDocumentFragment();
for (const opt of options) {
fragment.appendChild($element({tag: 'option', textContent: opt}));
}
parent.appendChild(fragment);
2017-07-12 19:50:13 +00:00
}
2017-07-12 20:44:59 +00:00
const themeControl = document.getElementById('editor.theme');
2017-07-12 19:50:13 +00:00
const themeList = localStorage.codeMirrorThemes;
if (themeList) {
optionsFromArray(themeControl, themeList.split(/\s+/));
2017-07-12 19:50:13 +00:00
} else {
// Chrome is starting up and shows our edit.html, but the background page isn't loaded yet
2017-07-12 20:44:59 +00:00
const theme = prefs.get('editor.theme');
optionsFromArray(themeControl, [theme === 'default' ? t('defaultTheme') : theme]);
2017-07-12 19:50:13 +00:00
getCodeMirrorThemes().then(() => {
const themes = (localStorage.codeMirrorThemes || '').split(/\s+/);
optionsFromArray(themeControl, themes);
2017-07-12 19:50:13 +00:00
themeControl.selectedIndex = Math.max(0, themes.indexOf(theme));
});
}
optionsFromArray($('#editor.keyMap'), Object.keys(CM.keyMap).sort());
2017-07-12 20:44:59 +00:00
document.getElementById('options').addEventListener('change', acmeEventListener, false);
2017-07-12 19:50:13 +00:00
setupLivePrefs();
hotkeyRerouter.setState(true);
}
function acmeEventListener(event) {
2017-07-12 20:44:59 +00:00
const el = event.target;
const option = el.id.replace(/^editor\./, '');
//console.log('acmeEventListener heard %s on %s', event.type, el.id);
2017-07-12 19:50:13 +00:00
if (!option) {
2017-07-12 20:44:59 +00:00
console.error('acmeEventListener: no "cm_option" %O', el);
2017-07-12 19:50:13 +00:00
return;
}
2017-07-16 18:02:00 +00:00
let value = el.type === 'checkbox' ? el.checked : el.value;
2017-07-12 19:50:13 +00:00
switch (option) {
2017-07-12 20:44:59 +00:00
case 'tabSize':
CodeMirror.setOption('indentUnit', Number(value));
2017-07-12 19:50:13 +00:00
break;
case 'theme': {
2017-07-14 08:33:59 +00:00
const themeLink = document.getElementById('cm-theme');
2017-07-12 20:44:59 +00:00
// use non-localized 'default' internally
2017-07-16 18:02:00 +00:00
if (!value || value === 'default' || value === t('defaultTheme')) {
2017-07-12 20:44:59 +00:00
value = 'default';
2017-07-16 18:02:00 +00:00
if (prefs.get(el.id) !== value) {
2017-07-12 19:50:13 +00:00
prefs.set(el.id, value);
}
2017-07-12 20:44:59 +00:00
themeLink.href = '';
2017-07-12 19:50:13 +00:00
el.selectedIndex = 0;
break;
}
2017-07-14 08:33:59 +00:00
const url = chrome.runtime.getURL('vendor/codemirror/theme/' + value + '.css');
2017-07-16 18:02:00 +00:00
if (themeLink.href === url) { // preloaded in initCodeMirror()
2017-07-12 19:50:13 +00:00
break;
}
// avoid flicker: wait for the second stylesheet to load, then apply the theme
document.head.appendChild($element({
tag: 'link',
id: 'cm-theme2',
rel: 'stylesheet',
href: url
}));
setTimeout(() => {
CodeMirror.setOption(option, value);
themeLink.remove();
2017-07-19 16:18:34 +00:00
$('#cm-theme2').id = 'cm-theme';
}, 100);
2017-07-12 19:50:13 +00:00
return;
}
2017-07-12 19:50:13 +00:00
case 'autocompleteOnTyping':
editors.forEach(cm => {
const onOff = el.checked ? 'on' : 'off';
cm[onOff]('change', autocompleteOnTyping);
cm[onOff]('pick', autocompletePicked);
});
return;
2017-07-12 20:44:59 +00:00
case 'matchHighlight':
2017-07-12 19:50:13 +00:00
switch (value) {
case 'token':
case 'selection':
document.body.dataset[option] = value;
2017-07-16 18:02:00 +00:00
value = {showToken: value === 'token' && /[#.\-\w]/, annotateScrollbar: true};
2017-07-12 19:50:13 +00:00
break;
default:
value = null;
}
}
CodeMirror.setOption(option, value);
}
// replace given textarea with the CodeMirror editor
function setupCodeMirror(textarea, index) {
2017-07-12 19:50:13 +00:00
const cm = CodeMirror.fromTextArea(textarea, {lint: null});
const wrapper = cm.display.wrapper;
cm.on('change', indicateCodeChange);
if (prefs.get('editor.autocompleteOnTyping')) {
cm.on('change', autocompleteOnTyping);
cm.on('pick', autocompletePicked);
}
cm.on('blur', () => {
editors.lastActive = cm;
hotkeyRerouter.setState(true);
setTimeout(() => {
wrapper.classList.toggle('CodeMirror-active', wrapper.contains(document.activeElement));
});
});
cm.on('focus', () => {
hotkeyRerouter.setState(false);
wrapper.classList.add('CodeMirror-active');
});
if (!FIREFOX) {
cm.on('mousedown', (cm, event) => toggleContextMenuDelete.call(cm, event));
}
2017-07-12 19:50:13 +00:00
let lastClickTime = 0;
const resizeGrip = wrapper.appendChild(template.resizeGrip.cloneNode(true));
resizeGrip.onmousedown = event => {
2017-07-16 18:02:00 +00:00
if (event.button !== 0) {
2017-07-12 19:50:13 +00:00
return;
}
event.preventDefault();
if (Date.now() - lastClickTime < 500) {
lastClickTime = 0;
toggleSectionHeight(cm);
return;
}
lastClickTime = Date.now();
2017-07-12 20:44:59 +00:00
const minHeight = cm.defaultTextHeight() +
cm.display.lineDiv.offsetParent.offsetTop + /* .CodeMirror-lines padding */
wrapper.offsetHeight - wrapper.clientHeight; /* borders */
2017-07-12 19:50:13 +00:00
wrapper.style.pointerEvents = 'none';
document.body.style.cursor = 's-resize';
function resize(e) {
const cmPageY = wrapper.getBoundingClientRect().top + window.scrollY;
const height = Math.max(minHeight, e.pageY - cmPageY);
2017-07-16 18:02:00 +00:00
if (height !== wrapper.clientHeight) {
2017-07-12 19:50:13 +00:00
cm.setSize(null, height);
}
}
document.addEventListener('mousemove', resize);
document.addEventListener('mouseup', function resizeStop() {
document.removeEventListener('mouseup', resizeStop);
document.removeEventListener('mousemove', resize);
wrapper.style.pointerEvents = '';
document.body.style.cursor = '';
});
};
editors.splice(index || editors.length, 0, cm);
return cm;
}
function indicateCodeChange(cm) {
2017-07-12 20:44:59 +00:00
const section = cm.getSection();
2017-07-12 19:50:13 +00:00
setCleanItem(section, cm.isClean(section.savedValue));
updateTitle();
updateLintReport(cm);
2015-01-30 17:05:06 +00:00
}
function getSectionForChild(e) {
2017-07-12 20:44:59 +00:00
return e.closest('#sections > div');
}
function getSections() {
2017-07-12 20:44:59 +00:00
return document.querySelectorAll('#sections > div');
2015-04-08 18:24:26 +00:00
}
// remind Chrome to repaint a previously invisible editor box by toggling any element's transform
// this bug is present in some versions of Chrome (v37-40 or something)
document.addEventListener('scroll', () => {
2017-07-12 20:44:59 +00:00
const style = document.getElementById('name').style;
style.webkitTransform = style.webkitTransform ? '' : 'scale(1)';
});
// Shift-Ctrl-Wheel scrolls entire page even when mouse is over a code editor
document.addEventListener('wheel', event => {
2017-07-12 19:50:13 +00:00
if (event.shiftKey && event.ctrlKey && !event.altKey && !event.metaKey) {
// Chrome scrolls horizontally when Shift is pressed but on some PCs this might be different
window.scrollBy(0, event.deltaX || event.deltaY);
event.preventDefault();
}
});
queryTabs({currentWindow: true}).then(tabs => {
2017-07-12 20:44:59 +00:00
const windowId = tabs[0].windowId;
if (prefs.get('openEditInWindow')) {
2017-07-16 17:01:39 +00:00
if (
/true/.test(sessionStorage.saveSizeOnClose) &&
2017-07-16 17:01:39 +00:00
'left' in prefs.get('windowPosition', {}) &&
!isWindowMaximized()
) {
2017-07-12 19:50:13 +00:00
// window was reopened via Ctrl-Shift-T etc.
chrome.windows.update(windowId, prefs.get('windowPosition'));
}
2017-07-16 18:02:00 +00:00
if (tabs.length === 1 && window.history.length === 1) {
chrome.windows.getAll(windows => {
2017-07-12 19:50:13 +00:00
if (windows.length > 1) {
2017-07-12 20:44:59 +00:00
sessionStorageHash('saveSizeOnClose').set(windowId, true);
2017-07-12 19:50:13 +00:00
saveSizeOnClose = true;
}
});
} else {
2017-07-12 20:44:59 +00:00
saveSizeOnClose = sessionStorageHash('saveSizeOnClose').value[windowId];
2017-07-12 19:50:13 +00:00
}
}
chrome.tabs.onRemoved.addListener((tabId, info) => {
2017-07-12 20:44:59 +00:00
sessionStorageHash('manageStylesHistory').unset(tabId);
2017-07-16 18:02:00 +00:00
if (info.windowId === windowId && info.isWindowClosing) {
2017-07-12 20:44:59 +00:00
sessionStorageHash('saveSizeOnClose').unset(windowId);
2017-07-12 19:50:13 +00:00
}
});
});
getActiveTab().then(tab => {
2017-07-16 18:02:00 +00:00
useHistoryBack = sessionStorageHash('manageStylesHistory').value[tab.id] === location.href;
});
function goBackToManage(event) {
2017-07-12 19:50:13 +00:00
if (useHistoryBack) {
event.stopPropagation();
event.preventDefault();
history.back();
}
}
function isWindowMaximized() {
2017-07-16 18:02:00 +00:00
return window.screenLeft === 0 &&
window.screenTop === 0 &&
window.outerWidth === screen.availWidth &&
window.outerHeight === screen.availHeight;
}
window.onbeforeunload = () => {
2017-07-12 20:44:59 +00:00
if (saveSizeOnClose && !isWindowMaximized()) {
prefs.set('windowPosition', {
2017-07-12 19:50:13 +00:00
left: screenLeft,
top: screenTop,
width: outerWidth,
height: outerHeight
});
}
document.activeElement.blur();
if (isCleanGlobal()) {
return;
}
updateLintReport(null, 0);
return confirm(t('styleChangesNotSaved'));
2017-01-30 19:29:12 +00:00
};
2015-01-30 17:05:06 +00:00
function addAppliesTo(list, name, value) {
2017-07-12 20:44:59 +00:00
const showingEverything = list.querySelector('.applies-to-everything') !== null;
// blow away 'Everything' if it's there
2017-07-12 19:50:13 +00:00
if (showingEverything) {
list.removeChild(list.firstChild);
}
2017-07-12 20:44:59 +00:00
let e;
2017-07-12 19:50:13 +00:00
if (name && value) {
e = template.appliesTo.cloneNode(true);
2017-07-12 20:44:59 +00:00
e.querySelector('[name=applies-type]').value = name;
e.querySelector('[name=applies-value]').value = value;
e.querySelector('.remove-applies-to').addEventListener('click', removeAppliesTo, false);
2017-07-12 19:50:13 +00:00
} else if (showingEverything || list.hasChildNodes()) {
e = template.appliesTo.cloneNode(true);
if (list.hasChildNodes()) {
2017-07-12 20:44:59 +00:00
e.querySelector('[name=applies-type]').value = list.querySelector('li:last-child [name="applies-type"]').value;
2017-07-12 19:50:13 +00:00
}
2017-07-12 20:44:59 +00:00
e.querySelector('.remove-applies-to').addEventListener('click', removeAppliesTo, false);
2017-07-12 19:50:13 +00:00
} else {
e = template.appliesToEverything.cloneNode(true);
}
e.querySelector('.add-applies-to').addEventListener('click', function () {
2017-07-12 20:44:59 +00:00
addAppliesTo(this.parentNode.parentNode);
}, false);
2017-07-12 19:50:13 +00:00
list.appendChild(e);
2015-01-30 17:05:06 +00:00
}
function addSection(event, section) {
2017-07-12 20:44:59 +00:00
const div = template.section.cloneNode(true);
div.querySelector('.applies-to-help').addEventListener('click', showAppliesToHelp, false);
div.querySelector('.remove-section').addEventListener('click', removeSection, false);
div.querySelector('.add-section').addEventListener('click', addSection, false);
div.querySelector('.beautify-section').addEventListener('click', beautify);
2017-07-12 19:50:13 +00:00
2017-07-12 20:44:59 +00:00
const codeElement = div.querySelector('.code');
const appliesTo = div.querySelector('.applies-to-list');
let appliesToAdded = false;
2017-07-12 19:50:13 +00:00
if (section) {
codeElement.value = section.code;
2017-07-14 09:02:04 +00:00
for (const i in propertyToCss) {
2017-07-12 19:50:13 +00:00
if (section[i]) {
section[i].forEach(url => {
2017-07-12 19:50:13 +00:00
addAppliesTo(appliesTo, propertyToCss[i], url);
appliesToAdded = true;
});
}
}
}
if (!appliesToAdded) {
addAppliesTo(appliesTo);
}
2017-07-12 20:44:59 +00:00
appliesTo.addEventListener('change', onChange);
appliesTo.addEventListener('input', onChange);
2017-07-12 19:50:13 +00:00
toggleTestRegExpVisibility();
appliesTo.addEventListener('change', toggleTestRegExpVisibility);
div.querySelector('.test-regexp').onclick = showRegExpTester;
function toggleTestRegExpVisibility() {
const show = [...appliesTo.children].some(item =>
!item.matches('.applies-to-everything') &&
2017-07-16 18:02:00 +00:00
item.querySelector('.applies-type').value === 'regexp' &&
2017-07-12 19:50:13 +00:00
item.querySelector('.applies-value').value.trim());
div.classList.toggle('has-regexp', show);
appliesTo.oninput = appliesTo.oninput || show && (event => {
2017-07-16 17:01:39 +00:00
if (
event.target.matches('.applies-value') &&
2017-07-16 18:02:00 +00:00
event.target.parentElement.querySelector('.applies-type').value === 'regexp'
2017-07-16 17:01:39 +00:00
) {
2017-07-12 19:50:13 +00:00
showRegExpTester(null, div);
}
});
}
2017-07-12 20:44:59 +00:00
const sections = document.getElementById('sections');
let cm;
2017-07-12 19:50:13 +00:00
if (event) {
2017-07-12 20:44:59 +00:00
const clickedSection = getSectionForChild(event.target);
2017-07-12 19:50:13 +00:00
sections.insertBefore(div, clickedSection.nextElementSibling);
2017-07-12 20:44:59 +00:00
const newIndex = getSections().indexOf(clickedSection) + 1;
cm = setupCodeMirror(codeElement, newIndex);
2017-07-12 19:50:13 +00:00
makeSectionVisible(cm);
2017-07-12 20:44:59 +00:00
cm.focus();
2017-07-12 19:50:13 +00:00
renderLintReport();
} else {
sections.appendChild(div);
2017-07-12 20:44:59 +00:00
cm = setupCodeMirror(codeElement);
2017-07-12 19:50:13 +00:00
}
div.CodeMirror = cm;
setCleanSection(div);
return div;
2015-01-30 17:05:06 +00:00
}
function removeAppliesTo(event) {
2017-07-14 08:46:10 +00:00
const appliesTo = event.target.parentNode;
const appliesToList = appliesTo.parentNode;
2017-07-12 19:50:13 +00:00
removeAreaAndSetDirty(appliesTo);
if (!appliesToList.hasChildNodes()) {
addAppliesTo(appliesToList);
}
2015-01-30 17:05:06 +00:00
}
function removeSection(event) {
2017-07-12 20:44:59 +00:00
const section = getSectionForChild(event.target);
const cm = section.CodeMirror;
2017-07-12 19:50:13 +00:00
removeAreaAndSetDirty(section);
editors.splice(editors.indexOf(cm), 1);
renderLintReport();
}
function removeAreaAndSetDirty(area) {
2017-07-12 20:44:59 +00:00
const contributors = area.querySelectorAll('.style-contributor');
if (!contributors.length) {
2017-07-12 19:50:13 +00:00
setCleanItem(area, false);
}
contributors.some(node => {
2017-07-12 19:50:13 +00:00
if (node.savedValue) {
// it's a saved section, so make it dirty and stop the enumeration
setCleanItem(area, false);
return true;
} else {
// it's an empty section, so undirty the applies-to items,
// otherwise orphaned ids would keep the style dirty
setCleanItem(node, true);
}
});
updateTitle();
area.parentNode.removeChild(area);
2015-01-30 17:05:06 +00:00
}
function makeSectionVisible(cm) {
2017-07-12 20:44:59 +00:00
const section = cm.getSection();
const bounds = section.getBoundingClientRect();
if (
(bounds.bottom > window.innerHeight && bounds.top > 0) ||
(bounds.top < 0 && bounds.bottom < window.innerHeight)
) {
2017-07-12 19:50:13 +00:00
if (bounds.top < 0) {
window.scrollBy(0, bounds.top - 1);
} else {
window.scrollBy(0, bounds.bottom - window.innerHeight + 1);
}
}
}
function setupGlobalSearch() {
2017-07-12 20:44:59 +00:00
const originalCommand = {
2017-07-12 19:50:13 +00:00
find: CodeMirror.commands.find,
findNext: CodeMirror.commands.findNext,
findPrev: CodeMirror.commands.findPrev,
replace: CodeMirror.commands.replace
};
2017-07-12 20:44:59 +00:00
const originalOpenDialog = CodeMirror.prototype.openDialog;
const originalOpenConfirm = CodeMirror.prototype.openConfirm;
2017-07-12 19:50:13 +00:00
2017-07-12 20:44:59 +00:00
let curState; // cm.state.search for last used 'find'
2017-07-12 19:50:13 +00:00
function shouldIgnoreCase(query) { // treat all-lowercase non-regexp queries as case-insensitive
2017-07-16 18:02:00 +00:00
return typeof query === 'string' && query === query.toLowerCase();
2017-07-12 19:50:13 +00:00
}
function updateState(cm, newState) {
if (!newState) {
if (cm.state.search) {
return cm.state.search;
}
if (!curState) {
return null;
}
newState = curState;
}
cm.state.search = {
query: newState.query,
overlay: newState.overlay,
annotate: cm.showMatchesOnScrollbar(newState.query, shouldIgnoreCase(newState.query))
2017-07-12 20:44:59 +00:00
};
2017-07-12 19:50:13 +00:00
cm.addOverlay(newState.overlay);
return cm.state.search;
}
// overrides the original openDialog with a clone of the provided template
2017-07-12 19:50:13 +00:00
function customizeOpenDialog(cm, template, callback) {
cm.openDialog = (tmpl, cb, opt) => {
2017-07-12 19:50:13 +00:00
// invoke 'callback' and bind 'this' to the original callback
originalOpenDialog.call(cm, template.cloneNode(true), callback.bind(cb), opt);
2017-07-12 19:50:13 +00:00
};
setTimeout(() => { cm.openDialog = originalOpenDialog; }, 0);
2017-07-12 19:50:13 +00:00
refocusMinidialog(cm);
}
function focusClosestCM(activeCM) {
editors.lastActive = activeCM;
2017-07-12 20:44:59 +00:00
const cm = getEditorInSight();
2017-07-16 18:02:00 +00:00
if (cm !== activeCM) {
2017-07-12 19:50:13 +00:00
cm.focus();
}
return cm;
}
function find(activeCM) {
activeCM = focusClosestCM(activeCM);
customizeOpenDialog(activeCM, template.find, function (query) {
2017-07-12 19:50:13 +00:00
this(query);
curState = activeCM.state.search;
2017-07-16 18:02:00 +00:00
if (editors.length === 1 || !curState.query) {
2017-07-12 19:50:13 +00:00
return;
}
editors.forEach(cm => {
2017-07-16 18:02:00 +00:00
if (cm !== activeCM) {
2017-07-12 20:44:59 +00:00
cm.execCommand('clearSearch');
2017-07-12 19:50:13 +00:00
updateState(cm, curState);
}
});
2017-07-16 18:02:00 +00:00
if (CodeMirror.cmpPos(curState.posFrom, curState.posTo) === 0) {
2017-07-12 19:50:13 +00:00
findNext(activeCM);
}
});
originalCommand.find(activeCM);
}
function findNext(activeCM, reverse) {
2017-07-12 20:44:59 +00:00
let state = updateState(activeCM);
2017-07-12 19:50:13 +00:00
if (!state || !state.query) {
find(activeCM);
return;
}
2017-07-12 20:44:59 +00:00
let pos = activeCM.getCursor(reverse ? 'from' : 'to');
2017-07-12 19:50:13 +00:00
activeCM.setSelection(activeCM.getCursor()); // clear the selection, don't move the cursor
2017-07-16 18:02:00 +00:00
const rxQuery = typeof state.query === 'object'
2017-07-12 20:44:59 +00:00
? state.query : stringAsRegExp(state.query, shouldIgnoreCase(state.query) ? 'i' : '');
2017-07-12 19:50:13 +00:00
2017-07-16 17:01:39 +00:00
if (
document.activeElement &&
2017-07-16 18:02:00 +00:00
document.activeElement.name === 'applies-value' &&
2017-07-16 17:01:39 +00:00
searchAppliesTo(activeCM)
) {
2017-07-12 19:50:13 +00:00
return;
}
2017-07-14 08:46:10 +00:00
let cm = activeCM;
for (let i = 0; i < editors.length; i++) {
2017-07-12 19:50:13 +00:00
state = updateState(cm);
if (!cm.hasFocus()) {
pos = reverse ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(0, 0);
}
2017-07-12 20:44:59 +00:00
const searchCursor = cm.getSearchCursor(state.query, pos, shouldIgnoreCase(state.query));
2017-07-12 19:50:13 +00:00
if (searchCursor.find(reverse)) {
if (editors.length > 1) {
makeSectionVisible(cm);
cm.focus();
}
// speedup the original findNext
state.posFrom = reverse ? searchCursor.to() : searchCursor.from();
state.posTo = CodeMirror.Pos(state.posFrom.line, state.posFrom.ch);
2017-07-12 20:44:59 +00:00
originalCommand[reverse ? 'findPrev' : 'findNext'](cm);
2017-07-12 19:50:13 +00:00
return;
} else if (!reverse && searchAppliesTo(cm)) {
return;
}
cm = editors[(editors.indexOf(cm) + (reverse ? -1 + editors.length : 1)) % editors.length];
if (reverse && searchAppliesTo(cm)) {
return;
}
}
// nothing found so far, so call the original search with wrap-around
2017-07-12 20:44:59 +00:00
originalCommand[reverse ? 'findPrev' : 'findNext'](activeCM);
2017-07-12 19:50:13 +00:00
function searchAppliesTo(cm) {
2017-07-12 20:44:59 +00:00
let inputs = [].slice.call(cm.getSection().querySelectorAll('.applies-value'));
2017-07-12 19:50:13 +00:00
if (reverse) {
inputs = inputs.reverse();
}
inputs.splice(0, inputs.indexOf(document.activeElement) + 1);
return inputs.some(input => {
2017-07-12 20:44:59 +00:00
const match = rxQuery.exec(input.value);
2017-07-12 19:50:13 +00:00
if (match) {
input.focus();
2017-07-12 20:44:59 +00:00
const end = match.index + match[0].length;
2017-07-12 19:50:13 +00:00
// scroll selected part into view in long inputs,
// works only outside of current event handlers chain, hence timeout=0
setTimeout(() => {
2017-07-12 19:50:13 +00:00
input.setSelectionRange(end, end);
2017-07-12 20:44:59 +00:00
input.setSelectionRange(match.index, end);
2017-07-12 19:50:13 +00:00
}, 0);
return true;
}
});
}
}
function findPrev(cm) {
findNext(cm, true);
}
function replace(activeCM, all) {
2017-07-14 08:46:10 +00:00
let queue;
let query;
let replacement;
2017-07-12 19:50:13 +00:00
activeCM = focusClosestCM(activeCM);
customizeOpenDialog(activeCM, template[all ? 'replaceAll' : 'replace'], txt => {
2017-07-12 19:50:13 +00:00
query = txt;
customizeOpenDialog(activeCM, template.replaceWith, txt => {
2017-07-12 19:50:13 +00:00
replacement = txt;
queue = editors.rotate(-editors.indexOf(activeCM));
2017-07-12 20:44:59 +00:00
if (all) {
editors.forEach(doReplace);
} else {
doReplace();
}
2017-07-12 19:50:13 +00:00
});
this(query);
});
originalCommand.replace(activeCM, all);
function doReplace() {
2017-07-12 20:44:59 +00:00
const cm = queue.shift();
2017-07-12 19:50:13 +00:00
if (!cm) {
if (!all) {
editors.lastActive.focus();
}
return;
}
// hide the first two dialogs (replace, replaceWith)
2017-07-16 19:40:13 +00:00
cm.openDialog = (tmpl, callback) => {
cm.openDialog = (tmpl, callback) => {
2017-07-12 19:50:13 +00:00
cm.openDialog = originalOpenDialog;
if (all) {
callback(replacement);
} else {
doConfirm(cm);
callback(replacement);
2017-07-12 20:44:59 +00:00
if (!cm.getWrapperElement().querySelector('.CodeMirror-dialog')) {
2017-07-12 19:50:13 +00:00
// no dialog == nothing found in the current CM, move to the next
doReplace();
}
}
};
callback(query);
};
originalCommand.replace(cm, all);
}
function doConfirm(cm) {
2017-07-12 20:44:59 +00:00
let wrapAround = false;
const origPos = cm.getCursor();
2017-07-12 19:50:13 +00:00
cm.openConfirm = function overrideConfirm(tmpl, callbacks, opt) {
const ovrCallbacks = callbacks.map(callback => () => {
makeSectionVisible(cm);
cm.openConfirm = overrideConfirm;
setTimeout(() => { cm.openConfirm = originalOpenConfirm; }, 0);
const pos = cm.getCursor();
callback();
const cmp = CodeMirror.cmpPos(cm.getCursor(), pos);
wrapAround |= cmp <= 0;
const dlg = cm.getWrapperElement().querySelector('.CodeMirror-dialog');
2017-07-16 18:02:00 +00:00
if (!dlg || cmp === 0 || wrapAround && CodeMirror.cmpPos(cm.getCursor(), origPos) >= 0) {
if (dlg) {
dlg.remove();
2017-07-12 19:50:13 +00:00
}
doReplace();
}
2017-07-12 19:50:13 +00:00
});
originalOpenConfirm.call(cm, template.replaceConfirm.cloneNode(true), ovrCallbacks, opt);
2017-07-12 19:50:13 +00:00
};
}
}
function replaceAll(cm) {
replace(cm, true);
}
CodeMirror.commands.find = find;
CodeMirror.commands.findNext = findNext;
CodeMirror.commands.findPrev = findPrev;
CodeMirror.commands.replace = replace;
CodeMirror.commands.replaceAll = replaceAll;
}
function jumpToLine(cm) {
2017-07-12 20:44:59 +00:00
const cur = cm.getCursor();
2017-07-12 19:50:13 +00:00
refocusMinidialog(cm);
cm.openDialog(template.jumpToLine.cloneNode(true), str => {
2017-07-12 20:44:59 +00:00
const m = str.match(/^\s*(\d+)(?:\s*:\s*(\d+))?\s*$/);
2017-07-12 19:50:13 +00:00
if (m) {
cm.setCursor(m[1] - 1, m[2] ? m[2] - 1 : cur.ch);
}
2017-07-12 20:44:59 +00:00
}, {value: cur.line + 1});
}
function toggleStyle() {
2017-07-12 19:50:13 +00:00
$('#enabled').checked = !$('#enabled').checked;
save();
}
function toggleSectionHeight(cm) {
2017-07-12 19:50:13 +00:00
if (cm.state.toggleHeightSaved) {
// restore previous size
cm.setSize(null, cm.state.toggleHeightSaved);
cm.state.toggleHeightSaved = 0;
} else {
// maximize
const wrapper = cm.display.wrapper;
const allBounds = $('#sections').getBoundingClientRect();
const pageExtrasHeight = allBounds.top + window.scrollY +
parseFloat(getComputedStyle($('#sections')).paddingBottom);
const sectionExtrasHeight = cm.getSection().clientHeight - wrapper.offsetHeight;
cm.state.toggleHeightSaved = wrapper.clientHeight;
cm.setSize(null, window.innerHeight - sectionExtrasHeight - pageExtrasHeight);
const bounds = cm.getSection().getBoundingClientRect();
if (bounds.top < 0 || bounds.bottom > window.innerHeight) {
window.scrollBy(0, bounds.top);
}
}
}
2017-06-18 12:20:48 +00:00
function autocompleteOnTyping(cm, info, debounced) {
2017-07-12 20:44:59 +00:00
if (
cm.state.completionActive ||
info.origin && !info.origin.includes('input') ||
!info.text.last
) {
2017-07-12 19:50:13 +00:00
return;
}
if (cm.state.autocompletePicked) {
cm.state.autocompletePicked = false;
return;
}
if (!debounced) {
debounce(autocompleteOnTyping, 100, cm, info, true);
return;
}
if (info.text.last.match(/[-\w!]+$/)) {
cm.state.autocompletePicked = false;
cm.options.hintOptions.completeSingle = false;
cm.execCommand('autocomplete');
setTimeout(() => {
cm.options.hintOptions.completeSingle = true;
});
}
2017-06-18 12:20:48 +00:00
}
function autocompletePicked(cm) {
2017-07-12 19:50:13 +00:00
cm.state.autocompletePicked = true;
2017-06-18 12:20:48 +00:00
}
function refocusMinidialog(cm) {
2017-07-12 20:44:59 +00:00
const section = cm.getSection();
if (!section.querySelector('.CodeMirror-dialog')) {
2017-07-12 19:50:13 +00:00
return;
}
// close the currently opened minidialog
cm.focus();
// make sure to focus the input in newly opened minidialog
setTimeout(() => {
2017-07-12 20:44:59 +00:00
section.querySelector('.CodeMirror-dialog').focus();
2017-07-12 19:50:13 +00:00
}, 0);
}
function nextPrevEditor(cm, direction) {
2017-07-12 19:50:13 +00:00
cm = editors[(editors.indexOf(cm) + direction + editors.length) % editors.length];
makeSectionVisible(cm);
cm.focus();
}
function getEditorInSight(nearbyElement) {
2017-07-12 19:50:13 +00:00
// priority: 1. associated CM for applies-to element 2. last active if visible 3. first visible
2017-07-12 20:44:59 +00:00
let cm;
if (nearbyElement && nearbyElement.className.indexOf('applies-') >= 0) {
2017-07-12 19:50:13 +00:00
cm = getSectionForChild(nearbyElement).CodeMirror;
} else {
cm = editors.lastActive;
}
if (!cm || offscreenDistance(cm) > 0) {
2017-07-12 20:44:59 +00:00
const sorted = editors
.map((cm, index) => ({cm: cm, distance: offscreenDistance(cm), index: index}))
.sort((a, b) => a.distance - b.distance || a.index - b.index);
2017-07-12 19:50:13 +00:00
cm = sorted[0].cm;
if (sorted[0].distance > 0) {
2017-07-12 20:44:59 +00:00
makeSectionVisible(cm);
2017-07-12 19:50:13 +00:00
}
}
return cm;
function offscreenDistance(cm) {
2017-07-12 20:44:59 +00:00
const LINES_VISIBLE = 2; // closest editor should have at least # lines visible
const bounds = cm.getSection().getBoundingClientRect();
2017-07-12 19:50:13 +00:00
if (bounds.top < 0) {
return -bounds.top;
} else if (bounds.top < window.innerHeight - cm.defaultTextHeight() * LINES_VISIBLE) {
return 0;
} else {
return bounds.top - bounds.height;
}
}
}
function updateLintReport(cm, delay) {
2017-07-16 18:02:00 +00:00
if (delay === 0) {
2017-07-12 19:50:13 +00:00
// immediately show pending csslint messages in onbeforeunload and save
update(cm);
return;
}
if (delay > 0) {
2017-07-12 20:44:59 +00:00
setTimeout(cm => { cm.performLint(); update(cm); }, delay, cm);
2017-07-12 19:50:13 +00:00
return;
}
2017-07-14 08:57:28 +00:00
// eslint-disable-next-line no-var
var state = cm.state.lint;
2017-07-12 19:50:13 +00:00
if (!state) {
return;
}
// user is editing right now: postpone updating the report for the new issues (default: 500ms lint + 4500ms)
// or update it as soon as possible (default: 500ms lint + 100ms) in case an existing issue was just fixed
clearTimeout(state.reportTimeout);
state.reportTimeout = setTimeout(update, state.options.delay + 100, cm);
2017-07-16 18:02:00 +00:00
state.postponeNewIssues = delay === undefined || delay === null;
2017-07-12 19:50:13 +00:00
function update(cm) {
2017-07-12 20:44:59 +00:00
const scope = cm ? [cm] : editors;
let changed = false;
let fixedOldIssues = false;
scope.forEach(cm => {
2017-07-14 08:57:28 +00:00
const scopedState = cm.state.lint || {};
const oldMarkers = scopedState.markedLast || {};
2017-07-12 20:44:59 +00:00
const newMarkers = {};
2017-07-16 18:02:00 +00:00
const html = !scopedState.marked || scopedState.marked.length === 0 ? '' : '<tbody>' +
scopedState.marked.map(mark => {
2017-07-12 20:44:59 +00:00
const info = mark.__annotation;
2017-07-16 18:02:00 +00:00
const isActiveLine = info.from.line === cm.getCursor().line;
2017-07-12 20:44:59 +00:00
const pos = isActiveLine ? 'cursor' : (info.from.line + ',' + info.from.ch);
let message = escapeHtml(info.message.replace(/ at line \d.+$/, ''));
2017-07-12 19:50:13 +00:00
if (message.length > 100) {
2017-07-12 20:44:59 +00:00
message = message.substr(0, 100) + '...';
2017-07-12 19:50:13 +00:00
}
2017-07-16 18:02:00 +00:00
if (isActiveLine || oldMarkers[pos] === message) {
2017-07-12 19:50:13 +00:00
delete oldMarkers[pos];
}
newMarkers[pos] = message;
2017-07-12 20:44:59 +00:00
return '<tr class="' + info.severity + '">' +
'<td role="severity" class="CodeMirror-lint-marker-' + info.severity + '">' +
info.severity + '</td>' +
'<td role="line">' + (info.from.line + 1) + '</td>' +
'<td role="sep">:</td>' +
'<td role="col">' + (info.from.ch + 1) + '</td>' +
'<td role="message">' + message + '</td></tr>';
}).join('') + '</tbody>';
2017-07-14 08:57:28 +00:00
scopedState.markedLast = newMarkers;
fixedOldIssues |= scopedState.reportDisplayed && Object.keys(oldMarkers).length > 0;
2017-07-16 18:02:00 +00:00
if (scopedState.html !== html) {
2017-07-14 08:57:28 +00:00
scopedState.html = html;
2017-07-12 19:50:13 +00:00
changed = true;
}
});
if (changed) {
clearTimeout(state ? state.renderTimeout : undefined);
if (!state || !state.postponeNewIssues || fixedOldIssues) {
renderLintReport(true);
} else {
state.renderTimeout = setTimeout(() => {
2017-07-12 19:50:13 +00:00
renderLintReport(true);
}, CodeMirror.defaults.lintReportDelay);
}
}
}
function escapeHtml(html) {
2017-07-14 08:59:13 +00:00
const chars = {'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;', '/': '&#x2F;'};
return html.replace(/[&<>"'/]/g, char => chars[char]);
2017-07-12 19:50:13 +00:00
}
}
function renderLintReport(someBlockChanged) {
2017-07-12 20:44:59 +00:00
const container = document.getElementById('lint');
const content = container.children[1];
const label = t('sectionCode');
const newContent = content.cloneNode(false);
let issueCount = 0;
editors.forEach((cm, index) => {
2017-07-12 19:50:13 +00:00
if (cm.state.lint && cm.state.lint.html) {
2017-07-12 20:44:59 +00:00
const html = '<caption>' + label + ' ' + (index + 1) + '</caption>' + cm.state.lint.html;
2017-07-19 11:34:31 +00:00
const newBlock = newContent.appendChild(tHTML(html, 'table'));
2017-07-12 19:50:13 +00:00
newBlock.cm = cm;
issueCount += newBlock.rows.length;
2017-07-12 20:44:59 +00:00
const block = content.children[newContent.children.length - 1];
2017-07-16 18:02:00 +00:00
const blockChanged = !block || cm !== block.cm || html !== block.innerHTML;
2017-07-12 19:50:13 +00:00
someBlockChanged |= blockChanged;
cm.state.lint.reportDisplayed = blockChanged;
}
});
2017-07-16 18:02:00 +00:00
if (someBlockChanged || newContent.children.length !== content.children.length) {
2017-07-12 19:50:13 +00:00
document.getElementById('issue-count').textContent = issueCount;
container.replaceChild(newContent, content);
2017-07-12 20:44:59 +00:00
container.style.display = newContent.children.length ? 'block' : 'none';
2017-07-12 19:50:13 +00:00
resizeLintReport(null, newContent);
}
}
function resizeLintReport(event, content) {
2017-07-12 20:44:59 +00:00
content = content || document.getElementById('lint').children[1];
2017-07-12 19:50:13 +00:00
if (content.children.length) {
2017-07-12 20:44:59 +00:00
const bounds = content.getBoundingClientRect();
const newMaxHeight = bounds.bottom <= innerHeight ? '' : (innerHeight - bounds.top) + 'px';
2017-07-16 18:02:00 +00:00
if (newMaxHeight !== content.style.maxHeight) {
2017-07-12 19:50:13 +00:00
content.style.maxHeight = newMaxHeight;
}
}
}
function gotoLintIssue(event) {
2017-07-12 20:44:59 +00:00
const issue = event.target.closest('tr');
2017-07-12 19:50:13 +00:00
if (!issue) {
return;
}
2017-07-12 20:44:59 +00:00
const block = issue.closest('table');
2017-07-12 19:50:13 +00:00
makeSectionVisible(block.cm);
block.cm.focus();
block.cm.setSelection({
2017-07-12 20:44:59 +00:00
line: parseInt(issue.querySelector('td[role="line"]').textContent) - 1,
ch: parseInt(issue.querySelector('td[role="col"]').textContent) - 1
2017-07-12 19:50:13 +00:00
});
}
function toggleLintReport() {
2017-07-12 20:44:59 +00:00
document.getElementById('lint').classList.toggle('collapsed');
}
2015-06-23 16:24:53 +00:00
function beautify(event) {
2017-07-12 19:50:13 +00:00
if (exports.css_beautify) { // thanks to csslint's definition of 'exports'
doBeautify();
} else {
2017-07-12 20:44:59 +00:00
const script = document.head.appendChild(document.createElement('script'));
script.src = 'vendor-overwrites/beautify/beautify-css-mod.js';
2017-07-12 19:50:13 +00:00
script.onload = doBeautify;
}
function doBeautify() {
2017-07-12 20:44:59 +00:00
const tabs = prefs.get('editor.indentWithTabs');
const options = prefs.get('editor.beautify');
options.indent_size = tabs ? 1 : prefs.get('editor.tabSize');
options.indent_char = tabs ? '\t' : ' ';
const section = getSectionForChild(event.target);
const scope = section ? [section.CodeMirror] : editors;
showHelp(t('styleBeautify'), '<div class="beautify-options">' +
optionHtml('.selector1,', 'selector_separator_newline') +
optionHtml('.selector2,', 'newline_before_open_brace') +
optionHtml('{', 'newline_after_open_brace') +
optionHtml('border: none;', 'newline_between_properties', true) +
optionHtml('display: block;', 'newline_before_close_brace', true) +
optionHtml('}', 'newline_between_rules') +
2017-07-12 19:50:13 +00:00
`<label style="display: block; clear: both;"><input data-option="indent_conditional" type="checkbox"
${options.indent_conditional !== false ? 'checked' : ''}>` +
t('styleBeautifyIndentConditional') + '</label>' +
2017-07-12 20:44:59 +00:00
'</div>' +
'<div><button role="undo"></button></div>');
2017-07-12 19:50:13 +00:00
2017-07-12 20:44:59 +00:00
const undoButton = document.querySelector('#help-popup button[role="undo"]');
2017-07-16 18:02:00 +00:00
undoButton.textContent = t(scope.length === 1 ? 'undo' : 'undoGlobal');
undoButton.addEventListener('click', () => {
2017-07-12 20:44:59 +00:00
let undoable = false;
scope.forEach(cm => {
2017-07-12 19:50:13 +00:00
if (cm.beautifyChange && cm.beautifyChange[cm.changeGeneration()]) {
delete cm.beautifyChange[cm.changeGeneration()];
cm.undo();
cm.scrollIntoView(cm.getCursor());
undoable |= cm.beautifyChange[cm.changeGeneration()];
}
});
undoButton.disabled = !undoable;
});
scope.forEach(cm => {
setTimeout(() => {
2017-07-12 19:50:13 +00:00
const pos = options.translate_positions =
[].concat.apply([], cm.doc.sel.ranges.map(r =>
[Object.assign({}, r.anchor), Object.assign({}, r.head)]));
2017-07-12 20:44:59 +00:00
const text = cm.getValue();
const newText = exports.css_beautify(text, options);
2017-07-16 18:02:00 +00:00
if (newText !== text) {
2017-07-12 19:50:13 +00:00
if (!cm.beautifyChange || !cm.beautifyChange[cm.changeGeneration()]) {
// clear the list if last change wasn't a css-beautify
cm.beautifyChange = {};
}
cm.setValue(newText);
const selections = [];
for (let i = 0; i < pos.length; i += 2) {
selections.push({anchor: pos[i], head: pos[i + 1]});
}
cm.setSelections(selections);
cm.beautifyChange[cm.changeGeneration()] = true;
undoButton.disabled = false;
}
}, 0);
});
document.querySelector('.beautify-options').onchange = ({target}) => {
2017-07-16 18:02:00 +00:00
const value = target.type === 'checkbox' ? target.checked : target.selectedIndex > 0;
2017-07-12 19:50:13 +00:00
prefs.set('editor.beautify', Object.assign(options, {[target.dataset.option]: value}));
if (target.parentNode.hasAttribute('newline')) {
target.parentNode.setAttribute('newline', value.toString());
}
doBeautify();
};
function optionHtml(label, optionName, indent) {
2017-07-12 20:44:59 +00:00
const value = options[optionName];
return '<div newline="' + value.toString() + '">' +
'<span' + (indent ? ' indent' : '') + '>' + label + '</span>' +
'<select data-option="' + optionName + '">' +
'<option' + (value ? '' : ' selected') + '>&nbsp;</option>' +
'<option' + (value ? ' selected' : '') + '>\\n</option>' +
'</select></div>';
2017-07-12 19:50:13 +00:00
}
}
2015-06-23 16:24:53 +00:00
}
2017-07-12 20:44:59 +00:00
document.addEventListener('DOMContentLoaded', init);
2015-01-30 17:05:06 +00:00
function init() {
2017-07-12 19:50:13 +00:00
initCodeMirror();
2017-07-12 20:44:59 +00:00
const params = getParams();
2017-07-12 19:50:13 +00:00
if (!params.id) { // match should be 2 - one for the whole thing, one for the parentheses
// This is an add
2017-07-19 12:38:27 +00:00
$('#heading').textContent = t('addStyleTitle');
2017-07-12 20:44:59 +00:00
const section = {code: ''};
2017-07-14 09:02:04 +00:00
for (const i in CssToProperty) {
2017-07-12 19:50:13 +00:00
if (params[i]) {
section[CssToProperty[i]] = [params[i]];
}
}
window.onload = () => {
window.onload = null;
addSection(null, section);
editors[0].setOption('lint', CodeMirror.defaults.lint);
// default to enabled
2017-07-12 20:44:59 +00:00
document.getElementById('enabled').checked = true;
2017-07-12 19:50:13 +00:00
initHooks();
};
return;
}
// This is an edit
2017-07-19 12:38:27 +00:00
$('#heading').textContent = t('editStyleHeading');
2017-07-12 19:50:13 +00:00
getStylesSafe({id: params.id}).then(styles => {
let style = styles[0];
if (!style) {
style = {id: null, sections: []};
history.replaceState({}, document.title, location.pathname);
}
styleId = style.id;
sessionStorage.justEditedStyleId = styleId;
2017-07-12 19:50:13 +00:00
setStyleMeta(style);
window.onload = () => {
window.onload = null;
initWithStyle({style});
};
2017-07-16 18:02:00 +00:00
if (document.readyState !== 'loading') {
2017-07-12 19:50:13 +00:00
window.onload();
}
});
2015-01-30 17:05:06 +00:00
}
function setStyleMeta(style) {
2017-07-12 20:44:59 +00:00
document.getElementById('name').value = style.name;
document.getElementById('enabled').checked = style.enabled;
document.getElementById('url').href = style.url;
}
function initWithStyle({style, codeIsUpdated}) {
2017-07-12 19:50:13 +00:00
setStyleMeta(style);
if (codeIsUpdated === false) {
setCleanGlobal();
updateTitle();
return;
}
// if this was done in response to an update, we need to clear existing sections
getSections().forEach(div => { div.remove(); });
2017-07-12 20:44:59 +00:00
const queue = style.sections.length ? style.sections.slice() : [{code: ''}];
const queueStart = new Date().getTime();
2017-07-12 19:50:13 +00:00
// after 100ms the sections will be added asynchronously
while (new Date().getTime() - queueStart <= 100 && queue.length) {
add();
}
(function processQueue() {
if (queue.length) {
add();
setTimeout(processQueue, 0);
}
})();
initHooks();
function add() {
2017-07-12 20:44:59 +00:00
const sectionDiv = addSection(null, queue.shift());
2017-07-12 19:50:13 +00:00
maximizeCodeHeight(sectionDiv, !queue.length);
const cm = sectionDiv.CodeMirror;
setTimeout(() => {
cm.setOption('lint', CodeMirror.defaults.lint);
updateLintReport(cm, 0);
2017-07-12 20:44:59 +00:00
}, prefs.get('editor.lintDelay'));
2017-07-12 19:50:13 +00:00
}
}
function initHooks() {
document.querySelectorAll('#header .style-contributor').forEach(node => {
2017-07-12 20:44:59 +00:00
node.addEventListener('change', onChange);
node.addEventListener('input', onChange);
2017-07-12 19:50:13 +00:00
});
2017-07-12 20:44:59 +00:00
document.getElementById('toggle-style-help').addEventListener('click', showToggleStyleHelp);
document.getElementById('to-mozilla').addEventListener('click', showMozillaFormat, false);
document.getElementById('to-mozilla-help').addEventListener('click', showToMozillaHelp, false);
document.getElementById('from-mozilla').addEventListener('click', fromMozillaFormat);
document.getElementById('beautify').addEventListener('click', beautify);
document.getElementById('save-button').addEventListener('click', save, false);
document.getElementById('sections-help').addEventListener('click', showSectionHelp, false);
document.getElementById('keyMap-help').addEventListener('click', showKeyMapHelp, false);
document.getElementById('cancel-button').addEventListener('click', goBackToManage);
document.getElementById('lint-help').addEventListener('click', showLintHelp);
document.getElementById('lint').addEventListener('click', gotoLintIssue);
window.addEventListener('resize', resizeLintReport);
2017-07-12 19:50:13 +00:00
// touch devices don't have onHover events so the element we'll be toggled via clicking (touching)
2017-07-12 20:44:59 +00:00
if ('ontouchstart' in document.body) {
document.querySelector('#lint h2').addEventListener('click', toggleLintReport);
2017-07-12 19:50:13 +00:00
}
if (!FIREFOX) {
$$([
'input:not([type])',
'input[type="text"]',
'input[type="search"]',
'input[type="number"]',
].join(',')
).forEach(e => e.addEventListener('mousedown', toggleContextMenuDelete));
}
2017-07-12 19:50:13 +00:00
setupGlobalSearch();
setCleanGlobal();
updateTitle();
2015-01-30 17:05:06 +00:00
}
function toggleContextMenuDelete(event) {
2017-07-16 18:02:00 +00:00
if (event.button === 2 && prefs.get('editor.contextDelete')) {
2017-07-12 19:50:13 +00:00
chrome.contextMenus.update('editor.contextDelete', {
enabled: Boolean(
2017-07-16 18:02:00 +00:00
this.selectionStart !== this.selectionEnd ||
2017-07-12 19:50:13 +00:00
this.somethingSelected && this.somethingSelected()
),
}, ignoreChromeError);
}
}
function maximizeCodeHeight(sectionDiv, isLast) {
2017-07-12 20:44:59 +00:00
const cm = sectionDiv.CodeMirror;
const stats = maximizeCodeHeight.stats = maximizeCodeHeight.stats || {totalHeight: 0, deltas: []};
2017-07-12 19:50:13 +00:00
if (!stats.cmActualHeight) {
stats.cmActualHeight = getComputedHeight(cm.display.wrapper);
}
if (!stats.sectionMarginTop) {
stats.sectionMarginTop = parseFloat(getComputedStyle(sectionDiv).marginTop);
}
2017-07-12 20:44:59 +00:00
const sectionTop = sectionDiv.getBoundingClientRect().top - stats.sectionMarginTop;
2017-07-12 19:50:13 +00:00
if (!stats.firstSectionTop) {
stats.firstSectionTop = sectionTop;
}
2017-07-12 20:44:59 +00:00
const extrasHeight = getComputedHeight(sectionDiv) - stats.cmActualHeight;
const cmMaxHeight = window.innerHeight - extrasHeight - sectionTop - stats.sectionMarginTop;
const cmDesiredHeight = cm.display.sizer.clientHeight + 2 * cm.defaultTextHeight();
const cmGrantableHeight = Math.max(stats.cmActualHeight, Math.min(cmMaxHeight, cmDesiredHeight));
2017-07-12 19:50:13 +00:00
stats.deltas.push(cmGrantableHeight - stats.cmActualHeight);
stats.totalHeight += cmGrantableHeight + extrasHeight;
if (!isLast) {
return;
}
stats.totalHeight += stats.firstSectionTop;
if (stats.totalHeight <= window.innerHeight) {
editors.forEach((cm, index) => {
2017-07-12 19:50:13 +00:00
cm.setSize(null, stats.deltas[index] + stats.cmActualHeight);
});
return;
}
// scale heights to fill the gap between last section and bottom edge of the window
2017-07-12 20:44:59 +00:00
const sections = document.getElementById('sections');
const available = window.innerHeight - sections.getBoundingClientRect().bottom -
2017-07-12 19:50:13 +00:00
parseFloat(getComputedStyle(sections).marginBottom);
if (available <= 0) {
return;
}
const totalDelta = stats.deltas.reduce((sum, d) => sum + d, 0);
2017-07-12 20:44:59 +00:00
const q = available / totalDelta;
const baseHeight = stats.cmActualHeight - stats.sectionMarginTop;
stats.deltas.forEach((delta, index) => {
2017-07-12 19:50:13 +00:00
editors[index].setSize(null, baseHeight + Math.floor(q * delta));
});
}
function updateTitle() {
2017-07-12 20:44:59 +00:00
const DIRTY_TITLE = '* $';
2017-07-12 20:44:59 +00:00
const name = document.getElementById('name').savedValue;
const clean = isCleanGlobal();
const title = styleId === null ? t('addStyleTitle') : t('editStyleTitle', [name]);
document.title = clean ? title : DIRTY_TITLE.replace('$', title);
2015-01-30 17:05:06 +00:00
}
function validate() {
2017-07-12 20:44:59 +00:00
const name = document.getElementById('name').value;
2017-07-16 18:02:00 +00:00
if (name === '') {
2017-07-12 20:44:59 +00:00
return t('styleMissingName');
2017-07-12 19:50:13 +00:00
}
// validate the regexps
if (document.querySelectorAll('.applies-to-list').some(list => {
list.childNodes.some(li => {
2017-07-16 18:02:00 +00:00
if (li.className === template.appliesToEverything.className) {
2017-07-12 19:50:13 +00:00
return false;
}
2017-07-12 20:44:59 +00:00
const valueElement = li.querySelector('[name=applies-value]');
const type = li.querySelector('[name=applies-type]').value;
const value = valueElement.value;
2017-07-12 19:50:13 +00:00
if (type && value) {
2017-07-16 18:02:00 +00:00
if (type === 'regexp') {
2017-07-12 19:50:13 +00:00
try {
new RegExp(value);
} catch (ex) {
valueElement.focus();
return true;
}
}
}
return false;
});
})) {
2017-07-12 20:44:59 +00:00
return t('styleBadRegexp');
2017-07-12 19:50:13 +00:00
}
return null;
2015-01-30 17:05:06 +00:00
}
function save() {
2017-07-12 19:50:13 +00:00
updateLintReport(null, 0);
// save the contents of the CodeMirror editors back into the textareas
2017-07-12 20:44:59 +00:00
for (let i = 0; i < editors.length; i++) {
2017-07-12 19:50:13 +00:00
editors[i].save();
}
2017-07-12 20:44:59 +00:00
const error = validate();
2017-07-12 19:50:13 +00:00
if (error) {
alert(error);
return;
}
2017-07-12 20:44:59 +00:00
const name = document.getElementById('name').value;
const enabled = document.getElementById('enabled').checked;
2017-07-12 19:50:13 +00:00
saveStyleSafe({
id: styleId,
name: name,
enabled: enabled,
reason: 'editSave',
sections: getSectionsHashes()
})
.then(saveComplete);
2015-01-30 17:05:06 +00:00
}
function getSectionsHashes() {
2017-07-12 20:44:59 +00:00
const sections = [];
getSections().forEach(div => {
2017-07-12 20:44:59 +00:00
const meta = getMeta(div);
const code = div.CodeMirror.getValue();
2017-07-16 18:02:00 +00:00
if (/^\s*$/.test(code) && Object.keys(meta).length === 0) {
2017-07-12 19:50:13 +00:00
return;
}
meta.code = code;
sections.push(meta);
});
return sections;
2015-01-30 17:05:06 +00:00
}
function getMeta(e) {
2017-07-12 20:44:59 +00:00
const meta = {urls: [], urlPrefixes: [], domains: [], regexps: []};
e.querySelector('.applies-to-list').childNodes.forEach(li => {
2017-07-16 18:02:00 +00:00
if (li.className === template.appliesToEverything.className) {
2017-07-12 19:50:13 +00:00
return;
}
2017-07-12 20:44:59 +00:00
const type = li.querySelector('[name=applies-type]').value;
const value = li.querySelector('[name=applies-value]').value;
2017-07-12 19:50:13 +00:00
if (type && value) {
2017-07-12 20:44:59 +00:00
const property = CssToProperty[type];
2017-07-12 19:50:13 +00:00
meta[property].push(value);
}
});
return meta;
2015-01-30 17:05:06 +00:00
}
function saveComplete(style) {
2017-07-12 19:50:13 +00:00
styleId = style.id;
sessionStorage.justEditedStyleId = styleId;
2017-07-12 19:50:13 +00:00
setCleanGlobal();
// Go from new style URL to edit style URL
2017-07-16 18:02:00 +00:00
if (location.href.indexOf('id=') === -1) {
2017-07-12 20:44:59 +00:00
history.replaceState({}, document.title, 'edit.html?id=' + style.id);
2017-07-19 12:38:27 +00:00
$('#heading').textContent = t('editStyleHeading');
2017-07-12 19:50:13 +00:00
}
updateTitle();
2015-01-30 17:05:06 +00:00
}
function showMozillaFormat() {
2017-07-12 20:44:59 +00:00
const popup = showCodeMirrorPopup(t('styleToMozillaFormatTitle'), '', {readOnly: true});
2017-07-12 19:50:13 +00:00
popup.codebox.setValue(toMozillaFormat());
2017-07-12 20:44:59 +00:00
popup.codebox.execCommand('selectAll');
2015-01-30 17:05:06 +00:00
}
function toMozillaFormat() {
return getSectionsHashes().map(section => {
2017-07-12 20:44:59 +00:00
let cssMds = [];
2017-07-14 09:02:04 +00:00
for (const i in propertyToCss) {
2017-07-12 19:50:13 +00:00
if (section[i]) {
cssMds = cssMds.concat(section[i].map(v =>
propertyToCss[i] + '("' + v.replace(/\\/g, '\\\\') + '")'
));
2017-07-12 19:50:13 +00:00
}
}
2017-07-12 20:44:59 +00:00
return cssMds.length ? '@-moz-document ' + cssMds.join(', ') + ' {\n' + section.code + '\n}' : section.code;
}).join('\n\n');
2015-01-30 17:05:06 +00:00
}
function fromMozillaFormat() {
2017-07-12 20:44:59 +00:00
const popup = showCodeMirrorPopup(t('styleFromMozillaFormatPrompt'), tHTML(`<div>
<button name="import-append" i18n-text="importAppendLabel" i18n-title="importAppendTooltip"></button>
<button name="import-replace" i18n-text="importReplaceLabel" i18n-title="importReplaceTooltip"></button>
</div>`
2017-07-19 11:34:31 +00:00
));
2017-07-12 19:50:13 +00:00
2017-07-12 20:44:59 +00:00
const contents = popup.querySelector('.contents');
2017-07-12 19:50:13 +00:00
contents.insertBefore(popup.codebox.display.wrapper, contents.firstElementChild);
popup.codebox.focus();
2017-07-12 20:44:59 +00:00
popup.querySelector('[name="import-append"]').addEventListener('click', doImport);
popup.querySelector('[name="import-replace"]').addEventListener('click', doImport);
2017-07-12 19:50:13 +00:00
popup.codebox.on('change', () => {
2017-07-12 19:50:13 +00:00
clearTimeout(popup.mozillaTimeout);
popup.mozillaTimeout = setTimeout(() => {
2017-07-12 20:44:59 +00:00
popup.classList.toggle('ready', trimNewLines(popup.codebox.getValue()));
2017-07-12 19:50:13 +00:00
}, 100);
});
function doImport() {
2017-07-16 18:02:00 +00:00
const replaceOldStyle = this.name === 'import-replace';
2017-07-12 20:44:59 +00:00
popup.querySelector('.dismiss').onclick();
const mozStyle = trimNewLines(popup.codebox.getValue());
const parser = new parserlib.css.Parser();
const lines = mozStyle.split('\n');
const sectionStack = [{code: '', start: {line: 1, col: 1}}];
const errors = [];
2017-07-12 20:44:59 +00:00
// let oldSectionCount = editors.length;
let firstAddedCM;
parser.addListener('startdocument', function (e) {
2017-07-12 20:44:59 +00:00
let outerText = getRange(sectionStack.last.start, (--e.col, e));
const gapComment = outerText.match(/(\/\*[\s\S]*?\*\/)[\s\n]*$/);
const section = {code: '', start: backtrackTo(this, parserlib.css.Tokens.LBRACE, 'end')};
2017-07-12 19:50:13 +00:00
// move last comment before @-moz-document inside the section
if (gapComment && !gapComment[1].match(/\/\*\s*AGENT_SHEET\s*\*\//)) {
2017-07-12 20:44:59 +00:00
section.code = gapComment[1] + '\n';
2017-07-12 19:50:13 +00:00
outerText = trimNewLines(outerText.substring(0, gapComment.index));
}
if (outerText.trim()) {
sectionStack.last.code = outerText;
doAddSection(sectionStack.last);
2017-07-12 20:44:59 +00:00
sectionStack.last.code = '';
2017-07-12 19:50:13 +00:00
}
for (const f of e.functions) {
const m = f && f.match(/^([\w-]*)\((['"]?)(.+?)\2?\)$/);
if (!m || !/^(url|url-prefix|domain|regexp)$/.test(m[1])) {
errors.push(`${e.line}:${e.col + 1} invalid function "${m ? m[1] : f || ''}"`);
continue;
}
2017-07-12 20:44:59 +00:00
const aType = CssToProperty[m[1]];
2017-07-16 18:02:00 +00:00
const aValue = aType !== 'regexps' ? m[3] : m[3].replace(/\\\\/g, '\\');
2017-07-12 19:50:13 +00:00
(section[aType] = section[aType] || []).push(aValue);
}
2017-07-12 19:50:13 +00:00
sectionStack.push(section);
});
parser.addListener('enddocument', function () {
2017-07-12 20:44:59 +00:00
const end = backtrackTo(this, parserlib.css.Tokens.RBRACE, 'start');
const section = sectionStack.pop();
2017-07-12 19:50:13 +00:00
section.code += getRange(section.start, end);
sectionStack.last.start = (++end.col, end);
doAddSection(section);
});
parser.addListener('endstylesheet', () => {
2017-07-12 19:50:13 +00:00
// add nonclosed outer sections (either broken or the last global one)
2017-07-12 20:44:59 +00:00
const endOfText = {line: lines.length, col: lines.last.length + 1};
2017-07-12 19:50:13 +00:00
sectionStack.last.code += getRange(sectionStack.last.start, endOfText);
sectionStack.forEach(doAddSection);
delete maximizeCodeHeight.stats;
editors.forEach(cm => {
2017-07-16 18:02:00 +00:00
maximizeCodeHeight(cm.getSection(), cm === editors.last);
2017-07-12 19:50:13 +00:00
});
makeSectionVisible(firstAddedCM);
firstAddedCM.focus();
if (errors.length) {
showHelp(t('issues'), $element({
tag: 'pre',
textContent: errors.join('\n'),
}));
2017-07-12 19:50:13 +00:00
}
});
parser.addListener('error', e => {
errors.push(e.line + ':' + e.col + ' ' +
e.message.replace(/ at line \d.+$/, ''));
2017-07-12 19:50:13 +00:00
});
parser.parse(mozStyle);
2017-07-12 20:44:59 +00:00
function getRange(start, end) {
const L1 = start.line - 1;
const C1 = start.col - 1;
const L2 = end.line - 1;
const C2 = end.col - 1;
2017-07-16 18:02:00 +00:00
if (L1 === L2) {
2017-07-12 19:50:13 +00:00
return lines[L1].substr(C1, C2 - C1 + 1);
} else {
const middle = lines.slice(L1 + 1, L2).join('\n');
return lines[L1].substr(C1) + '\n' + middle +
(L2 >= lines.length ? '' : ((middle ? '\n' : '') + lines[L2].substring(0, C2)));
}
}
function doAddSection(section) {
section.code = section.code.trim();
// don't add empty sections
2017-07-12 20:44:59 +00:00
if (
!section.code &&
!section.urls &&
!section.urlPrefixes &&
!section.domains &&
!section.regexps
) {
2017-07-12 19:50:13 +00:00
return;
}
if (!firstAddedCM) {
if (!initFirstSection(section)) {
return;
}
}
setCleanItem(addSection(null, section), false);
firstAddedCM = firstAddedCM || editors.last;
}
// do onetime housekeeping as the imported text is confirmed to be a valid style
function initFirstSection(section) {
// skip adding the first global section when there's no code/comments
2017-07-12 20:44:59 +00:00
if (!section.code.replace('@namespace url(http://www.w3.org/1999/xhtml);', '') /* ignore boilerplate NS */
.replace(/[\s\n]/g, '')) { /* ignore all whitespace including new lines */
2017-07-12 19:50:13 +00:00
return false;
}
if (replaceOldStyle) {
editors.slice(0).reverse().forEach(cm => {
2017-07-12 19:50:13 +00:00
removeSection({target: cm.getSection().firstElementChild});
});
} else if (!editors.last.getValue()) {
// nuke the last blank section
2017-07-12 20:44:59 +00:00
if (editors.last.getSection().querySelector('.applies-to-everything')) {
2017-07-12 19:50:13 +00:00
removeSection({target: editors.last.getSection()});
}
}
return true;
}
}
function backtrackTo(parser, tokenType, startEnd) {
2017-07-12 20:44:59 +00:00
const tokens = parser._tokenStream._lt;
for (let i = parser._tokenStream._ltIndex - 1; i >= 0; --i) {
2017-07-16 18:02:00 +00:00
if (tokens[i].type === tokenType) {
2017-07-12 20:44:59 +00:00
return {line: tokens[i][startEnd + 'Line'], col: tokens[i][startEnd + 'Col']};
2017-07-12 19:50:13 +00:00
}
}
}
function trimNewLines(s) {
2017-07-12 20:44:59 +00:00
return s.replace(/^[\s\n]+/, '').replace(/[\s\n]+$/, '');
2017-07-12 19:50:13 +00:00
}
}
2015-01-30 17:05:06 +00:00
function showSectionHelp() {
2017-07-12 20:44:59 +00:00
showHelp(t('styleSectionsTitle'), t('sectionHelp'));
2015-01-30 17:05:06 +00:00
}
function showAppliesToHelp() {
2017-07-12 20:44:59 +00:00
showHelp(t('appliesLabel'), t('appliesHelp'));
2015-01-30 17:05:06 +00:00
}
function showToMozillaHelp() {
2017-07-12 20:44:59 +00:00
showHelp(t('styleMozillaFormatHeading'), t('styleToMozillaFormatHelp'));
}
function showToggleStyleHelp() {
2017-07-12 20:44:59 +00:00
showHelp(t('helpAlt'), t('styleEnabledToggleHint'));
}
function showKeyMapHelp() {
2017-07-12 20:44:59 +00:00
const keyMap = mergeKeyMaps({}, prefs.get('editor.keyMap'), CodeMirror.defaults.extraKeys);
const keyMapSorted = Object.keys(keyMap)
.map(key => ({key: key, cmd: keyMap[key]}))
2017-07-12 20:44:59 +00:00
.concat([{key: 'Shift-Ctrl-Wheel', cmd: 'scrollWindow'}])
2017-07-16 18:02:00 +00:00
.sort((a, b) => (a.cmd < b.cmd || (a.cmd === b.cmd && a.key < b.key) ? -1 : 1));
2017-07-12 20:44:59 +00:00
showHelp(t('cm_keyMap') + ': ' + prefs.get('editor.keyMap'),
2017-07-12 19:50:13 +00:00
'<table class="keymap-list">' +
2017-07-12 20:44:59 +00:00
'<thead><tr><th><input placeholder="' + t('helpKeyMapHotkey') + '" type="search"></th>' +
'<th><input placeholder="' + t('helpKeyMapCommand') + '" type="search"></th></tr></thead>' +
'<tbody>' + keyMapSorted.map(value =>
'<tr><td>' + value.key + '</td><td>' + value.cmd + '</td></tr>'
).join('') +
2017-07-12 20:44:59 +00:00
'</tbody>' +
'</table>');
const table = document.querySelector('#help-popup table');
table.addEventListener('input', filterTable);
const inputs = table.querySelectorAll('input');
inputs[0].addEventListener('keydown', hotkeyHandler);
2017-07-12 19:50:13 +00:00
inputs[1].focus();
function hotkeyHandler(event) {
2017-07-12 20:44:59 +00:00
const keyName = CodeMirror.keyName(event);
2017-07-16 18:02:00 +00:00
if (keyName === 'Esc' || keyName === 'Tab' || keyName === 'Shift-Tab') {
2017-07-12 19:50:13 +00:00
return;
}
event.preventDefault();
event.stopPropagation();
// normalize order of modifiers,
2017-07-12 20:44:59 +00:00
// for modifier-only keys ('Ctrl-Shift') a dummy main key has to be temporarily added
const keyMap = {};
keyMap[keyName.replace(/(Shift|Ctrl|Alt|Cmd)$/, '$&-dummy')] = '';
const normalizedKey = Object.keys(CodeMirror.normalizeKeyMap(keyMap))[0];
this.value = normalizedKey.replace('-dummy', '');
2017-07-12 19:50:13 +00:00
filterTable(event);
}
2017-07-12 19:50:13 +00:00
function filterTable(event) {
2017-07-12 20:44:59 +00:00
const input = event.target;
const col = input.parentNode.cellIndex;
inputs[1 - col].value = '';
table.tBodies[0].childNodes.forEach(row => {
2017-07-19 11:34:31 +00:00
const cell = row.children[col];
2017-07-19 16:18:34 +00:00
const text = cell.textContent;
const query = stringAsRegExp(input.value, 'gi');
const test = query.test(text);
row.style.display = input.value && test === false ? 'none' : '';
if (input.value && test) {
cell.textContent = '';
let offset = 0;
text.replace(query, (match, index) => {
if (index > offset) {
cell.appendChild(document.createTextNode(text.substring(offset, index)));
}
cell.appendChild($element({tag: 'mark', textContent: match}));
offset = index + match.length;
});
if (offset + 1 !== text.length) {
cell.appendChild(document.createTextNode(text.substring(offset)));
}
}
else {
cell.textContent = text;
2017-07-19 11:34:31 +00:00
}
2017-07-12 19:50:13 +00:00
// clear highlight from the other column
2017-07-19 16:18:34 +00:00
const otherCell = row.children[1 - col];
if (otherCell.children.length) {
const text = otherCell.textContent;
otherCell.textContent = text;
}
2017-07-12 19:50:13 +00:00
});
}
function mergeKeyMaps(merged, ...more) {
more.forEach(keyMap => {
2017-07-16 18:02:00 +00:00
if (typeof keyMap === 'string') {
2017-07-12 19:50:13 +00:00
keyMap = CodeMirror.keyMap[keyMap];
}
Object.keys(keyMap).forEach(key => {
2017-07-12 20:44:59 +00:00
let cmd = keyMap[key];
2017-07-12 19:50:13 +00:00
// filter out '...', 'attach', etc. (hotkeys start with an uppercase letter)
2017-07-16 18:02:00 +00:00
if (!merged[key] && !key.match(/^[a-z]/) && cmd !== '...') {
if (typeof cmd === 'function') {
2017-07-12 19:50:13 +00:00
// for 'emacs' keymap: provide at least something meaningful (hotkeys and the function body)
// for 'vim*' keymaps: almost nothing as it doesn't rely on CM keymap mechanism
2017-07-12 20:44:59 +00:00
cmd = cmd.toString().replace(/^function.*?\{[\s\r\n]*([\s\S]+?)[\s\r\n]*\}$/, '$1');
merged[key] = cmd.length <= 200 ? cmd : cmd.substr(0, 200) + '...';
2017-07-12 19:50:13 +00:00
} else {
merged[key] = cmd;
}
}
});
if (keyMap.fallthrough) {
merged = mergeKeyMaps(merged, keyMap.fallthrough);
}
});
return merged;
}
2015-01-30 17:05:06 +00:00
}
function showLintHelp() {
2017-07-12 20:44:59 +00:00
showHelp(t('issues'), t('issuesHelp') + '<ul>' +
CSSLint.getRules().map(rule =>
'<li><b>' + rule.name + '</b><br>' + rule.desc + '</li>'
).join('') + '</ul>'
2017-07-12 19:50:13 +00:00
);
}
2017-03-26 21:37:45 +00:00
function showRegExpTester(event, section = getSectionForChild(this)) {
2017-07-12 19:50:13 +00:00
const GET_FAVICON_URL = 'https://www.google.com/s2/favicons?domain=';
const OWN_ICON = chrome.runtime.getManifest().icons['16'];
const cachedRegexps = showRegExpTester.cachedRegexps =
showRegExpTester.cachedRegexps || new Map();
const regexps = [...section.querySelector('.applies-to-list').children]
.map(item =>
!item.matches('.applies-to-everything') &&
2017-07-16 18:02:00 +00:00
item.querySelector('.applies-type').value === 'regexp' &&
2017-07-12 19:50:13 +00:00
item.querySelector('.applies-value').value.trim())
.filter(item => item)
.map(text => {
const rxData = Object.assign({text}, cachedRegexps.get(text));
if (!rxData.urls) {
cachedRegexps.set(text, Object.assign(rxData, {
rx: tryRegExp(text),
urls: new Map(),
}));
}
return rxData;
});
chrome.tabs.onUpdated.addListener(function _(tabId, info) {
if (document.querySelector('.regexp-report')) {
if (info.url) {
showRegExpTester(event, section);
}
} else {
chrome.tabs.onUpdated.removeListener(_);
}
});
queryTabs().then(tabs => {
const supported = tabs.map(tab => tab.url)
.filter(url => URLS.supported(url));
2017-07-12 19:50:13 +00:00
const unique = [...new Set(supported).values()];
for (const rxData of regexps) {
const {rx, urls} = rxData;
if (rx) {
const urlsNow = new Map();
for (const url of unique) {
const match = urls.get(url) || (url.match(rx) || [])[0];
if (match) {
urlsNow.set(url, match);
}
}
rxData.urls = urlsNow;
}
}
const stats = {
full: {data: [], label: t('styleRegexpTestFull')},
partial: {data: [], label: [
t('styleRegexpTestPartial'),
template.regexpTestPartial.cloneNode(true),
]},
2017-07-12 19:50:13 +00:00
none: {data: [], label: t('styleRegexpTestNone')},
invalid: {data: [], label: t('styleRegexpTestInvalid')},
};
// collect stats
2017-07-12 19:50:13 +00:00
for (const {text, rx, urls} of regexps) {
if (!rx) {
stats.invalid.data.push({text});
continue;
}
if (!urls.size) {
stats.none.data.push({text});
continue;
}
const full = [];
const partial = [];
for (const [url, match] of urls.entries()) {
const faviconUrl = url.startsWith(URLS.ownOrigin)
? OWN_ICON
: GET_FAVICON_URL + new URL(url).hostname;
const icon = $element({tag: 'img', src: faviconUrl});
2017-07-16 18:02:00 +00:00
if (match.length === url.length) {
full.push($element({appendChild: [
icon,
url,
]}));
2017-07-12 19:50:13 +00:00
} else {
partial.push($element({appendChild: [
icon,
$element({tag: 'mark', textContent: match}),
url.substr(match.length),
]}));
2017-07-12 19:50:13 +00:00
}
}
if (full.length) {
stats.full.data.push({text, urls: full});
}
if (partial.length) {
stats.partial.data.push({text, urls: partial});
}
}
// render stats
const report = $element({className: 'regexp-report'});
const br = $element({tag: 'br'});
for (const type in stats) {
// top level groups: full, partial, none, invalid
const {label, data} = stats[type];
if (!data.length) {
continue;
}
const block = report.appendChild($element({
tag: 'details',
open: true,
dataset: {type},
appendChild: $element({tag: 'summary', appendChild: label}),
}));
// 2nd level: regexp text
for (const {text, urls} of data) {
if (urls) {
// type is partial or full
block.appendChild($element({
tag: 'details',
open: true,
appendChild: [
$element({tag: 'summary', textContent: text}),
// 3rd level: tab urls
...urls,
],
}));
} else {
// type is none or invalid
block.appendChild(document.createTextNode(text));
block.appendChild(br.cloneNode());
}
}
}
showHelp(t('styleRegexpTestTitle'), report);
2017-07-12 19:50:13 +00:00
document.querySelector('.regexp-report').onclick = event => {
const target = event.target.closest('a, .regexp-report div');
if (target) {
openURL({url: target.href || target.textContent});
event.preventDefault();
}
};
});
2017-03-26 21:37:45 +00:00
}
function showHelp(title, body) {
const div = $('#help-popup');
2017-07-12 20:44:59 +00:00
div.classList.remove('big');
$('.contents', div).textContent = '';
2017-07-19 11:34:31 +00:00
$('.contents', div).appendChild(typeof body === 'string' ? tHTML(body) : body);
$('.title', div).textContent = title;
2017-07-12 19:50:13 +00:00
2017-07-16 18:02:00 +00:00
if (getComputedStyle(div).display === 'none') {
2017-07-12 20:44:59 +00:00
document.addEventListener('keydown', closeHelp);
div.querySelector('.dismiss').onclick = closeHelp; // avoid chaining on multiple showHelp() calls
2017-07-12 19:50:13 +00:00
}
2017-07-12 20:44:59 +00:00
div.style.display = 'block';
2017-07-12 19:50:13 +00:00
return div;
function closeHelp(e) {
2017-07-12 20:44:59 +00:00
if (
!e ||
2017-07-16 18:02:00 +00:00
e.type === 'click' ||
((e.keyCode || e.which) === 27 && !e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey)
2017-07-12 20:44:59 +00:00
) {
div.style.display = '';
2017-07-19 08:19:15 +00:00
document.querySelector('.contents').textContent = '';
2017-07-12 20:44:59 +00:00
document.removeEventListener('keydown', closeHelp);
2017-07-12 19:50:13 +00:00
}
}
2015-01-30 17:05:06 +00:00
}
function showCodeMirrorPopup(title, html, options) {
2017-07-12 20:44:59 +00:00
const popup = showHelp(title, html);
popup.classList.add('big');
2017-07-12 19:50:13 +00:00
2017-07-12 20:44:59 +00:00
popup.codebox = CodeMirror(popup.querySelector('.contents'), Object.assign({
mode: 'css',
2017-07-12 19:50:13 +00:00
lineNumbers: true,
lineWrapping: true,
foldGutter: true,
2017-07-12 20:44:59 +00:00
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
2017-07-12 19:50:13 +00:00
matchBrackets: true,
lint: {getAnnotations: CodeMirror.lint.css, delay: 0},
styleActiveLine: true,
2017-07-12 20:44:59 +00:00
theme: prefs.get('editor.theme'),
keyMap: prefs.get('editor.keyMap')
2017-07-12 19:50:13 +00:00
}, options));
popup.codebox.focus();
popup.codebox.on('focus', () => { hotkeyRerouter.setState(false); });
popup.codebox.on('blur', () => { hotkeyRerouter.setState(true); });
2017-07-12 19:50:13 +00:00
return popup;
}
function getParams() {
2017-07-12 20:44:59 +00:00
const params = {};
const urlParts = location.href.split('?', 2);
2017-07-16 18:02:00 +00:00
if (urlParts.length === 1) {
2017-07-12 19:50:13 +00:00
return params;
}
urlParts[1].split('&').forEach(keyValue => {
2017-07-12 20:44:59 +00:00
const splitKeyValue = keyValue.split('=', 2);
2017-07-12 19:50:13 +00:00
params[decodeURIComponent(splitKeyValue[0])] = decodeURIComponent(splitKeyValue[1]);
});
return params;
}
chrome.runtime.onMessage.addListener(onRuntimeMessage);
function onRuntimeMessage(request) {
2017-07-12 19:50:13 +00:00
switch (request.method) {
2017-07-12 20:44:59 +00:00
case 'styleUpdated':
2017-07-16 18:02:00 +00:00
if (styleId && styleId === request.style.id && request.reason !== 'editSave') {
2017-07-12 19:50:13 +00:00
if ((request.style.sections[0] || {}).code === null) {
// the code-less style came from notifyAllTabs
onBackgroundReady().then(() => {
request.style = BG.cachedStyles.byId.get(request.style.id);
initWithStyle(request);
});
} else {
initWithStyle(request);
}
}
break;
2017-07-12 20:44:59 +00:00
case 'styleDeleted':
2017-07-16 18:02:00 +00:00
if (styleId && styleId === request.id) {
window.onbeforeunload = () => {};
2017-07-12 19:50:13 +00:00
window.close();
break;
}
break;
2017-07-12 20:44:59 +00:00
case 'prefChanged':
2017-07-12 19:50:13 +00:00
if ('editor.smartIndent' in request.prefs) {
CodeMirror.setOption('smartIndent', request.prefs['editor.smartIndent']);
}
break;
case 'editDeleteText':
document.execCommand('delete');
break;
}
}
function getComputedHeight(el) {
2017-07-12 20:44:59 +00:00
const compStyle = getComputedStyle(el);
2017-07-12 19:50:13 +00:00
return el.getBoundingClientRect().height +
parseFloat(compStyle.marginTop) + parseFloat(compStyle.marginBottom);
}
function getCodeMirrorThemes() {
2017-07-12 19:50:13 +00:00
if (!chrome.runtime.getPackageDirectoryEntry) {
const themes = [
chrome.i18n.getMessage('defaultTheme'),
'3024-day',
'3024-night',
'abcdef',
'ambiance',
'ambiance-mobile',
'base16-dark',
'base16-light',
'bespin',
'blackboard',
'cobalt',
'colorforth',
'dracula',
'duotone-dark',
'duotone-light',
'eclipse',
'elegant',
'erlang-dark',
'hopscotch',
'icecoder',
'isotope',
'lesser-dark',
'liquibyte',
'material',
'mbo',
'mdn-like',
'midnight',
'monokai',
'neat',
'neo',
'night',
'panda-syntax',
'paraiso-dark',
'paraiso-light',
'pastel-on-dark',
'railscasts',
'rubyblue',
'seti',
'solarized',
'the-matrix',
'tomorrow-night-bright',
'tomorrow-night-eighties',
'ttcn',
'twilight',
'vibrant-ink',
'xq-dark',
'xq-light',
'yeti',
'zenburn',
];
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);
});
});
});
});
}