/* eslint brace-style: 0, operator-linebreak: 0 */ /* global CodeMirror exports css_beautify parserlib CSSLint initLint setLinter updateLintReport renderLintReport updateLinter */ 'use strict'; 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 // direct & reverse mapping of @-moz-document keywords and internal property names 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 => { NodeList.prototype[method] = Array.prototype[method]; }); // Chrome pre-34 Element.prototype.matches = Element.prototype.matches || Element.prototype.webkitMatchesSelector; // Chrome pre-41 polyfill Element.prototype.closest = Element.prototype.closest || function (selector) { let e; // eslint-disable-next-line no-empty for (e = this; e && !e.matches(selector); e = e.parentElement) {} return e; }; // eslint-disable-next-line no-extend-native Array.prototype.rotate = function (amount) { // negative amount == rotate left const r = this.slice(-amount, this.length); Array.prototype.push.apply(r, this.slice(0, this.length - r.length)); return r; }; // 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) => { const themeElement = document.getElementById('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}); getCodeMirrorThemes(); // reroute handling to nearest editor when keypress resolves to one of these commands const hotkeyRerouter = { commands: { save: true, jumpToLine: true, nextEditor: true, prevEditor: true, find: true, findNext: true, findPrev: true, replace: true, replaceAll: true, toggleStyle: true, }, setState: enable => { setTimeout(() => { document[(enable ? 'add' : 'remove') + 'EventListener']('keydown', hotkeyRerouter.eventHandler); }, 0); }, eventHandler: event => { const keyName = CodeMirror.keyName(event); if ( CodeMirror.lookupKey(keyName, CodeMirror.getOption('keyMap'), handleCommand) === 'handled' || CodeMirror.lookupKey(keyName, CodeMirror.defaults.extraKeys, handleCommand) === 'handled' ) { event.preventDefault(); event.stopPropagation(); } function handleCommand(command) { if (hotkeyRerouter.commands[command] === true) { CodeMirror.commands[command](getEditorInSight(event.target)); return true; } } } }; function onChange(event) { const node = event.target; if ('savedValue' in node) { const currentValue = node.type === 'checkbox' ? node.checked : node.value; setCleanItem(node, node.savedValue === currentValue); } else { // the manually added section's applies-to is dirty only when the value is non-empty setCleanItem(node, node.localName !== 'input' || !node.value.trim()); delete node.savedValue; // only valid when actually saved } updateTitle(); } // Set .dirty on stylesheet contributors that have changed function setDirtyClass(node, isDirty) { node.classList.toggle('dirty', isDirty); } function setCleanItem(node, isClean) { 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 { node.savedValue = node.type === 'checkbox' ? node.checked : node.value; } } else { dirty[node.id] = true; } setDirtyClass(node, !isClean); } function isCleanGlobal() { const clean = Object.keys(dirty).length === 0; setDirtyClass(document.body, !clean); // let saveBtn = document.getElementById('save-button') // if (clean){ // //saveBtn.removeAttribute('disabled'); // }else{ // //saveBtn.setAttribute('disabled', true); // } return clean; } function setCleanGlobal() { document.querySelectorAll('#header, #sections > div').forEach(setCleanSection); 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); }); // #header section has no codemirror const cm = section.CodeMirror; if (cm) { section.savedValue = cm.changeGeneration(); indicateCodeChange(cm); } } function initCodeMirror() { const CM = CodeMirror; const isWindowsOS = navigator.appVersion.indexOf('Windows') > 0; // CodeMirror miserably fails on keyMap='' so let's ensure it's not if (!prefs.get('editor.keyMap')) { prefs.reset('editor.keyMap'); } // default option values Object.assign(CM.defaults, { mode: 'css', lineNumbers: true, lineWrapping: true, foldGutter: true, gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'], matchBrackets: true, highlightSelectionMatches: {showToken: /[#.\-\w]/, annotateScrollbar: true}, hintOptions: {}, lint: setLinter(prefs.get('editor.linter')), lintReportDelay: prefs.get('editor.lintReportDelay'), styleActiveLine: true, theme: 'default', keyMap: prefs.get('editor.keyMap'), extraKeys: { // independent of current keyMap 'Alt-Enter': 'toggleStyle', 'Alt-PageDown': 'nextEditor', 'Alt-PageUp': 'prevEditor' } }, prefs.get('editor.options')); // additional commands CM.commands.jumpToLine = jumpToLine; CM.commands.nextEditor = cm => { nextPrevEditor(cm, 1); }; CM.commands.prevEditor = cm => { nextPrevEditor(cm, -1); }; CM.commands.save = save; CM.commands.blockComment = cm => { cm.blockComment(cm.getCursor('from'), cm.getCursor('to'), {fullLines: false}); }; CM.commands.toggleStyle = toggleStyle; // 'basic' keymap only has basic keys by design, so we skip it const extraKeysCommands = {}; Object.keys(CM.defaults.extraKeys).forEach(key => { extraKeysCommands[CM.defaults.extraKeys[key]] = true; }); if (!extraKeysCommands.jumpToLine) { CM.keyMap.sublime['Ctrl-G'] = 'jumpToLine'; CM.keyMap.emacsy['Ctrl-G'] = 'jumpToLine'; CM.keyMap.pcDefault['Ctrl-J'] = 'jumpToLine'; CM.keyMap.macDefault['Cmd-J'] = 'jumpToLine'; } if (!extraKeysCommands.autocomplete) { 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 } if (!extraKeysCommands.blockComment) { CM.keyMap.sublime['Shift-Ctrl-/'] = 'blockComment'; } if (isWindowsOS) { // 'pcDefault' keymap on Windows should have F3/Shift-F3 if (!extraKeysCommands.findNext) { CM.keyMap.pcDefault['F3'] = 'findNext'; } if (!extraKeysCommands.findPrev) { CM.keyMap.pcDefault['Shift-F3'] = 'findPrev'; } // try to remap non-interceptable Ctrl-(Shift-)N/T/W hotkeys ['N', 'T', 'W'].forEach(char => { [{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 => { const oldKey = remap.from + char; Object.keys(CM.keyMap).forEach(keyMapName => { const keyMap = CM.keyMap[keyMapName]; const command = keyMap[oldKey]; if (!command) { return; } remap.to.some(newMod => { const newKey = newMod + char; 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) => { CodeMirror.defaults[o] = v; editors.forEach(editor => { editor.setOption(o, v); }); }; CM.prototype.getSection = function () { return this.display.wrapper.parentNode; }; // initialize global editor controls function optionsFromArray(parent, options) { const fragment = document.createDocumentFragment(); for (const opt of options) { fragment.appendChild($element({tag: 'option', textContent: opt})); } parent.appendChild(fragment); } const themeControl = document.getElementById('editor.theme'); const themeList = localStorage.codeMirrorThemes; if (themeList) { optionsFromArray(themeControl, 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(themeControl, [theme === 'default' ? t('defaultTheme') : theme]); getCodeMirrorThemes().then(() => { const themes = (localStorage.codeMirrorThemes || '').split(/\s+/); optionsFromArray(themeControl, themes); themeControl.selectedIndex = Math.max(0, themes.indexOf(theme)); }); } optionsFromArray($('#editor.keyMap'), Object.keys(CM.keyMap).sort()); document.getElementById('options').addEventListener('change', acmeEventListener, false); setupLivePrefs(); hotkeyRerouter.setState(true); } function acmeEventListener(event) { const el = event.target; const option = el.id.replace(/^editor\./, ''); //console.log('acmeEventListener heard %s on %s', event.type, el.id); if (!option) { console.error('acmeEventListener: no "cm_option" %O', el); return; } let value = el.type === 'checkbox' ? el.checked : el.value; switch (option) { case 'tabSize': CodeMirror.setOption('indentUnit', Number(value)); break; case 'theme': { const themeLink = document.getElementById('cm-theme'); // use non-localized 'default' internally if (!value || value === 'default' || value === t('defaultTheme')) { value = 'default'; if (prefs.get(el.id) !== value) { prefs.set(el.id, value); } themeLink.href = ''; el.selectedIndex = 0; break; } const url = chrome.runtime.getURL('vendor/codemirror/theme/' + value + '.css'); if (themeLink.href === url) { // preloaded in initCodeMirror() 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(); $('#cm-theme2').id = 'cm-theme'; }, 100); return; } case 'autocompleteOnTyping': editors.forEach(cm => { const onOff = el.checked ? 'on' : 'off'; cm[onOff]('change', autocompleteOnTyping); cm[onOff]('pick', autocompletePicked); }); return; case 'matchHighlight': switch (value) { case 'token': case 'selection': document.body.dataset[option] = value; value = {showToken: value === 'token' && /[#.\-\w]/, annotateScrollbar: true}; break; default: value = null; } break; case 'linter': if (value !== null && editors.length) { updateLinter(value); } break; } CodeMirror.setOption(option, value); } // replace given textarea with the CodeMirror editor function setupCodeMirror(textarea, index) { 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)); } let lastClickTime = 0; const resizeGrip = wrapper.appendChild(template.resizeGrip.cloneNode(true)); resizeGrip.onmousedown = event => { if (event.button !== 0) { return; } event.preventDefault(); if (Date.now() - lastClickTime < 500) { lastClickTime = 0; toggleSectionHeight(cm); return; } lastClickTime = Date.now(); const minHeight = cm.defaultTextHeight() + cm.display.lineDiv.offsetParent.offsetTop + /* .CodeMirror-lines padding */ wrapper.offsetHeight - wrapper.clientHeight; /* borders */ 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); if (height !== wrapper.clientHeight) { 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) { const section = cm.getSection(); setCleanItem(section, cm.isClean(section.savedValue)); updateTitle(); updateLintReport(cm); } function getSectionForChild(e) { return e.closest('#sections > div'); } function getSections() { return document.querySelectorAll('#sections > div'); } // 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', () => { 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 => { 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 => { 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.onRemoved.addListener((tabId, info) => { sessionStorageHash('manageStylesHistory').unset(tabId); if (info.windowId === windowId && info.isWindowClosing) { sessionStorageHash('saveSizeOnClose').unset(windowId); } }); }); getOwnTab().then(tab => { const ownTabId = tab.id; useHistoryBack = sessionStorageHash('manageStylesHistory').value[ownTabId] === location.href; // 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')); } prefs.set('openEditInWindow', openEditInWindow); }); }); }); function goBackToManage(event) { if (useHistoryBack) { event.stopPropagation(); event.preventDefault(); history.back(); } } function isWindowMaximized() { return ( window.screenX <= 0 && window.screenY <= 0 && window.outerWidth >= screen.availWidth && window.outerHeight >= screen.availHeight && window.screenX > -10 && window.screenY > -10 && window.outerWidth < screen.availWidth + 10 && window.outerHeight < screen.availHeight + 10 ); } function rememberWindowSize() { if ( document.visibilityState === 'visible' && prefs.get('openEditInWindow') && !isWindowMaximized() ) { prefs.set('windowPosition', { left: window.screenX, top: window.screenY, width: window.outerWidth, height: window.outerHeight, }); } } window.onbeforeunload = () => { if (saveSizeOnClose) { rememberWindowSize(); } document.activeElement.blur(); if (isCleanGlobal()) { return; } updateLintReport(null, 0); return confirm(t('styleChangesNotSaved')); }; function addAppliesTo(list, name, value) { const showingEverything = list.querySelector('.applies-to-everything') !== null; // blow away 'Everything' if it's there if (showingEverything) { list.removeChild(list.firstChild); } let e; if (name && value) { e = template.appliesTo.cloneNode(true); e.querySelector('[name=applies-type]').value = name; e.querySelector('[name=applies-value]').value = value; e.querySelector('.remove-applies-to').addEventListener('click', removeAppliesTo, false); } else if (showingEverything || list.hasChildNodes()) { e = template.appliesTo.cloneNode(true); if (list.hasChildNodes()) { e.querySelector('[name=applies-type]').value = list.querySelector('li:last-child [name="applies-type"]').value; } e.querySelector('.remove-applies-to').addEventListener('click', removeAppliesTo, false); } else { e = template.appliesToEverything.cloneNode(true); } e.querySelector('.add-applies-to').addEventListener('click', function () { addAppliesTo(this.parentNode.parentNode); }, false); list.appendChild(e); } function addSection(event, section) { 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); const codeElement = div.querySelector('.code'); const appliesTo = div.querySelector('.applies-to-list'); let appliesToAdded = false; if (section) { codeElement.value = section.code; for (const i in propertyToCss) { if (section[i]) { section[i].forEach(url => { addAppliesTo(appliesTo, propertyToCss[i], url); appliesToAdded = true; }); } } } if (!appliesToAdded) { addAppliesTo(appliesTo); } appliesTo.addEventListener('change', onChange); appliesTo.addEventListener('input', onChange); toggleTestRegExpVisibility(); appliesTo.addEventListener('change', toggleTestRegExpVisibility); div.querySelector('.test-regexp').onclick = showRegExpTester; function toggleTestRegExpVisibility() { const show = [...appliesTo.children].some(item => !item.matches('.applies-to-everything') && item.querySelector('.applies-type').value === 'regexp' && item.querySelector('.applies-value').value.trim()); div.classList.toggle('has-regexp', show); appliesTo.oninput = appliesTo.oninput || show && (event => { if ( event.target.matches('.applies-value') && event.target.parentElement.querySelector('.applies-type').value === 'regexp' ) { showRegExpTester(null, div); } }); } const sections = document.getElementById('sections'); let cm; if (event) { const clickedSection = getSectionForChild(event.target); sections.insertBefore(div, clickedSection.nextElementSibling); const newIndex = getSections().indexOf(clickedSection) + 1; cm = setupCodeMirror(codeElement, newIndex); makeSectionVisible(cm); cm.focus(); renderLintReport(); } else { sections.appendChild(div); cm = setupCodeMirror(codeElement); } div.CodeMirror = cm; setCleanSection(div); return div; } function removeAppliesTo(event) { const appliesTo = event.target.parentNode; const appliesToList = appliesTo.parentNode; removeAreaAndSetDirty(appliesTo); if (!appliesToList.hasChildNodes()) { addAppliesTo(appliesToList); } } function removeSection(event) { const section = getSectionForChild(event.target); const cm = section.CodeMirror; removeAreaAndSetDirty(section); editors.splice(editors.indexOf(cm), 1); renderLintReport(); } function removeAreaAndSetDirty(area) { const contributors = area.querySelectorAll('.style-contributor'); if (!contributors.length) { setCleanItem(area, false); } contributors.some(node => { 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); } function makeSectionVisible(cm) { const section = cm.getSection(); const bounds = section.getBoundingClientRect(); if ( (bounds.bottom > window.innerHeight && bounds.top > 0) || (bounds.top < 0 && bounds.bottom < window.innerHeight) ) { if (bounds.top < 0) { window.scrollBy(0, bounds.top - 1); } else { window.scrollBy(0, bounds.bottom - window.innerHeight + 1); } } } function setupGlobalSearch() { const originalCommand = { find: CodeMirror.commands.find, findNext: CodeMirror.commands.findNext, findPrev: CodeMirror.commands.findPrev, replace: CodeMirror.commands.replace }; const originalOpenDialog = CodeMirror.prototype.openDialog; const originalOpenConfirm = CodeMirror.prototype.openConfirm; let curState; // cm.state.search for last used 'find' function shouldIgnoreCase(query) { // treat all-lowercase non-regexp queries as case-insensitive return typeof query === 'string' && query === query.toLowerCase(); } 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)) }; cm.addOverlay(newState.overlay); return cm.state.search; } // overrides the original openDialog with a clone of the provided template function customizeOpenDialog(cm, template, callback) { cm.openDialog = (tmpl, cb, opt) => { // invoke 'callback' and bind 'this' to the original callback originalOpenDialog.call(cm, template.cloneNode(true), callback.bind(cb), opt); }; setTimeout(() => { cm.openDialog = originalOpenDialog; }, 0); refocusMinidialog(cm); } function focusClosestCM(activeCM) { editors.lastActive = activeCM; const cm = getEditorInSight(); if (cm !== activeCM) { cm.focus(); } return cm; } function find(activeCM) { activeCM = focusClosestCM(activeCM); customizeOpenDialog(activeCM, template.find, function (query) { this(query); curState = activeCM.state.search; if (editors.length === 1 || !curState.query) { return; } editors.forEach(cm => { if (cm !== activeCM) { cm.execCommand('clearSearch'); updateState(cm, curState); } }); if (CodeMirror.cmpPos(curState.posFrom, curState.posTo) === 0) { findNext(activeCM); } }); originalCommand.find(activeCM); } function findNext(activeCM, reverse) { let state = updateState(activeCM); if (!state || !state.query) { find(activeCM); return; } let pos = activeCM.getCursor(reverse ? 'from' : 'to'); activeCM.setSelection(activeCM.getCursor()); // clear the selection, don't move the cursor const rxQuery = typeof state.query === 'object' ? state.query : stringAsRegExp(state.query, shouldIgnoreCase(state.query) ? 'i' : ''); if ( document.activeElement && document.activeElement.name === 'applies-value' && searchAppliesTo(activeCM) ) { return; } let cm = activeCM; for (let i = 0; i < editors.length; i++) { state = updateState(cm); if (!cm.hasFocus()) { pos = reverse ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(0, 0); } const searchCursor = cm.getSearchCursor(state.query, pos, shouldIgnoreCase(state.query)); 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); originalCommand[reverse ? 'findPrev' : 'findNext'](cm); 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 originalCommand[reverse ? 'findPrev' : 'findNext'](activeCM); function searchAppliesTo(cm) { let inputs = [].slice.call(cm.getSection().querySelectorAll('.applies-value')); if (reverse) { inputs = inputs.reverse(); } inputs.splice(0, inputs.indexOf(document.activeElement) + 1); return inputs.some(input => { const match = rxQuery.exec(input.value); if (match) { input.focus(); const end = match.index + match[0].length; // scroll selected part into view in long inputs, // works only outside of current event handlers chain, hence timeout=0 setTimeout(() => { input.setSelectionRange(end, end); input.setSelectionRange(match.index, end); }, 0); return true; } }); } } function findPrev(cm) { findNext(cm, true); } function replace(activeCM, all) { let queue; let query; let replacement; activeCM = focusClosestCM(activeCM); customizeOpenDialog(activeCM, template[all ? 'replaceAll' : 'replace'], txt => { query = txt; customizeOpenDialog(activeCM, template.replaceWith, txt => { replacement = txt; queue = editors.rotate(-editors.indexOf(activeCM)); if (all) { editors.forEach(doReplace); } else { doReplace(); } }); this(query); }); originalCommand.replace(activeCM, all); function doReplace() { const cm = queue.shift(); if (!cm) { if (!all) { editors.lastActive.focus(); } return; } // hide the first two dialogs (replace, replaceWith) cm.openDialog = (tmpl, callback) => { cm.openDialog = (tmpl, callback) => { cm.openDialog = originalOpenDialog; if (all) { callback(replacement); } else { doConfirm(cm); callback(replacement); if (!cm.getWrapperElement().querySelector('.CodeMirror-dialog')) { // no dialog == nothing found in the current CM, move to the next doReplace(); } } }; callback(query); }; originalCommand.replace(cm, all); } function doConfirm(cm) { let wrapAround = false; const origPos = cm.getCursor(); 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'); if (!dlg || cmp === 0 || wrapAround && CodeMirror.cmpPos(cm.getCursor(), origPos) >= 0) { if (dlg) { dlg.remove(); } doReplace(); } }); originalOpenConfirm.call(cm, template.replaceConfirm.cloneNode(true), ovrCallbacks, opt); }; } } 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) { const cur = cm.getCursor(); refocusMinidialog(cm); cm.openDialog(template.jumpToLine.cloneNode(true), str => { const m = str.match(/^\s*(\d+)(?:\s*:\s*(\d+))?\s*$/); if (m) { cm.setCursor(m[1] - 1, m[2] ? m[2] - 1 : cur.ch); } }, {value: cur.line + 1}); } function toggleStyle() { $('#enabled').checked = !$('#enabled').checked; save(); } function toggleSectionHeight(cm) { 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); } } } function autocompleteOnTyping(cm, info, debounced) { if ( cm.state.completionActive || info.origin && !info.origin.includes('input') || !info.text.last ) { 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; }); } } function autocompletePicked(cm) { cm.state.autocompletePicked = true; } function refocusMinidialog(cm) { const section = cm.getSection(); if (!section.querySelector('.CodeMirror-dialog')) { return; } // close the currently opened minidialog cm.focus(); // make sure to focus the input in newly opened minidialog setTimeout(() => { section.querySelector('.CodeMirror-dialog').focus(); }, 0); } function nextPrevEditor(cm, direction) { cm = editors[(editors.indexOf(cm) + direction + editors.length) % editors.length]; makeSectionVisible(cm); cm.focus(); } function getEditorInSight(nearbyElement) { // priority: 1. associated CM for applies-to element 2. last active if visible 3. first visible let cm; if (nearbyElement && nearbyElement.className.indexOf('applies-') >= 0) { cm = getSectionForChild(nearbyElement).CodeMirror; } else { cm = editors.lastActive; } if (!cm || offscreenDistance(cm) > 0) { const sorted = editors .map((cm, index) => ({cm: cm, distance: offscreenDistance(cm), index: index})) .sort((a, b) => a.distance - b.distance || a.index - b.index); cm = sorted[0].cm; if (sorted[0].distance > 0) { makeSectionVisible(cm); } } return cm; function offscreenDistance(cm) { const LINES_VISIBLE = 2; // closest editor should have at least # lines visible const bounds = cm.getSection().getBoundingClientRect(); 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 beautify(event) { const script = document.head.appendChild(document.createElement('script')); script.src = 'vendor-overwrites/beautify/beautify-css-mod.js'; script.onload = doBeautify; function doBeautify() { 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'), '
' + ''); const undoButton = document.querySelector('#help-popup button[role="undo"]'); undoButton.textContent = t(scope.length === 1 ? 'undo' : 'undoGlobal'); undoButton.addEventListener('click', () => { let undoable = false; scope.forEach(cm => { 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(() => { const pos = options.translate_positions = [].concat.apply([], cm.doc.sel.ranges.map(r => [Object.assign({}, r.anchor), Object.assign({}, r.head)])); const text = cm.getValue(); const newText = css_beautify(text, options); if (newText !== text) { 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}) => { const value = target.type === 'checkbox' ? target.checked : target.selectedIndex > 0; 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) { const value = options[optionName]; return '' + ' | |
---|---|
' + value.key + ' | ' + value.cmd + ' |