/* global CodeMirror CSSLint editors makeSectionVisible showHelp showCodeMirrorPopup */ /* global stylelintDefaultConfig onDOMscripted injectCSS require */ 'use strict'; function initLint() { document.getElementById('lint-help').addEventListener('click', showLintHelp); document.getElementById('lint').addEventListener('click', gotoLintIssue); window.addEventListener('resize', resizeLintReport); document.getElementById('stylelint-settings').addEventListener('click', openStylelintSettings); // touch devices don't have onHover events so the element we'll be toggled via clicking (touching) if ('ontouchstart' in document.body) { document.querySelector('#lint h2').addEventListener('click', toggleLintReport); } BG.chromeLocal.getValue('editorStylelintRules').then(rules => setStylelintRules(rules)); } function setStylelintRules(rules = {}) { if (Object.keys(rules).length === 0) { rules = deepCopy(stylelintDefaultConfig.rules); } BG.chromeLocal.setValue('editorStylelintRules', rules); } function getLinterConfigForCodeMirror(name) { return CodeMirror.lint && CodeMirror.lint[name] ? { getAnnotations: CodeMirror.lint[name], delay: prefs.get('editor.lintDelay') } : false; } function updateLinter(name) { function updateEditors() { const options = getLinterConfigForCodeMirror(name); CodeMirror.defaults.lint = options === 'null' ? false : options; editors.forEach(cm => { // set lint to "null" to disable cm.setOption('lint', options); cm.refresh(); // enabling/disabling linting changes the gutter width updateLintReport(cm, 200); }); } if (prefs.get('editor.linter') !== name) { prefs.set('editor.linter', name); } // load scripts loadSelectedLinter(name).then(() => { updateEditors(); }); $('#stylelint-settings').style.display = name === 'stylelint' ? 'inline-block' : 'none'; } function updateLintReport(cm, delay) { if (delay === 0) { // immediately show pending csslint/stylelint messages in onbeforeunload and save update(cm); return; } if (delay > 0) { setTimeout(cm => { cm.performLint(); update(cm); }, delay, cm); return; } // eslint-disable-next-line no-var var state = cm.state.lint; 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); state.postponeNewIssues = delay === undefined || delay === null; function update(cm) { const scope = cm ? [cm] : editors; let changed = false; let fixedOldIssues = false; scope.forEach(cm => { const linter = prefs.get('editor.linter'); const scopedState = cm.state.lint || {}; const oldMarkers = scopedState.markedLast || {}; const newMarkers = {}; const html = !scopedState.marked || scopedState.marked.length === 0 ? '' : '' + scopedState.marked.map(mark => { const info = mark.__annotation; const isActiveLine = info.from.line === cm.getCursor().line; const pos = isActiveLine ? 'cursor' : (info.from.line + ',' + info.from.ch); // stylelint rule added in parentheses at the end const rule = linter === 'stylelint' ? info.message.substring(info.message.lastIndexOf('('), info.message.length) : / at line \d.+$/; // csslint const message = escapeHtml(info.message.replace(rule, '')); if (isActiveLine || oldMarkers[pos] === message) { delete oldMarkers[pos]; } newMarkers[pos] = message; return ` ${info.severity} ${info.from.line + 1} : ${info.from.ch + 1} ${message} `; }).join('') + ''; scopedState.markedLast = newMarkers; fixedOldIssues |= scopedState.reportDisplayed && Object.keys(oldMarkers).length > 0; if (scopedState.html !== html) { scopedState.html = html; changed = true; } }); if (changed) { clearTimeout(state ? state.renderTimeout : undefined); if (!state || !state.postponeNewIssues || fixedOldIssues) { renderLintReport(true); } else { state.renderTimeout = setTimeout(() => { renderLintReport(true); }, CodeMirror.defaults.lintReportDelay); } } } function escapeHtml(html) { const chars = {'&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '/': '/'}; return html.replace(/[&<>"'/]/g, char => chars[char]); } } function renderLintReport(someBlockChanged) { 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) => { if (cm.state.lint && cm.state.lint.html) { const html = '' + label + ' ' + (index + 1) + '' + cm.state.lint.html; const newBlock = newContent.appendChild(tHTML(html, 'table')); newBlock.cm = cm; issueCount += newBlock.rows.length; const block = content.children[newContent.children.length - 1]; const blockChanged = !block || cm !== block.cm || html !== block.innerHTML; someBlockChanged |= blockChanged; cm.state.lint.reportDisplayed = blockChanged; } }); if (someBlockChanged || newContent.children.length !== content.children.length) { document.getElementById('issue-count').textContent = issueCount; container.replaceChild(newContent, content); container.style.display = newContent.children.length ? 'block' : 'none'; resizeLintReport(); } } function resizeLintReport() { const magicBuffer = 20; // subtracted value to prevent scrollbar const content = $('#lint table'); if (content) { const bounds = content.getBoundingClientRect(); const newMaxHeight = bounds.bottom <= window.innerHeight ? '' : // subtract out a bit of padding or the vertical scrollbar extends beyond the viewport (window.innerHeight - bounds.top - magicBuffer) + 'px'; if (newMaxHeight !== content.style.maxHeight) { content.parentNode.style.maxHeight = newMaxHeight; } } } function gotoLintIssue(event) { const issue = event.target.closest('tr'); if (!issue) { return; } const block = issue.closest('table'); makeSectionVisible(block.cm); block.cm.focus(); block.cm.setSelection({ line: parseInt(issue.querySelector('td[role="line"]').textContent) - 1, ch: parseInt(issue.querySelector('td[role="col"]').textContent) - 1 }); } function toggleLintReport() { document.getElementById('lint').classList.toggle('collapsed'); } function showLintHelp() { let list = ''); } function setupStylelintSettingsEvents(popup) { popup.querySelector('.save').addEventListener('click', event => { event.preventDefault(); const json = tryJSONparse(popup.codebox.getValue()); if (json && json.rules) { setStylelintRules(json.rules); // it is possible to have stylelint rules popup open & switch to csslint if (prefs.get('editor.linter') === 'stylelint') { updateLinter('stylelint'); } } else { $('#help-popup .error').classList.add('show'); clearTimeout($('#help-popup .contents').timer); $('#help-popup .contents').timer = setTimeout(() => { // popup may be closed at this point const error = $('#help-popup .error'); if (error) { error.classList.remove('show'); } }, 3000); } }); popup.querySelector('.reset').addEventListener('click', event => { event.preventDefault(); setStylelintRules(); popup.codebox.setValue(JSON.stringify({rules: stylelintDefaultConfig.rules}, null, 2)); if (prefs.get('editor.linter') === 'stylelint') { updateLinter('stylelint'); } }); } function openStylelintSettings() { BG.chromeLocal.getValue('editorStylelintRules').then((rules = stylelintDefaultConfig.rules) => { const rulesString = JSON.stringify({rules: rules}, null, 2); setupStylelintPopup(rulesString); }); } function setupStylelintPopup(rules) { function makeButton(className, text) { return $element({tag: 'button', className, type: 'button', textContent: t(text)}); } function makeLink(url, textContent) { return $element({tag: 'a', target: '_blank', href: url, textContent}); } function setJSONMode(cm) { cm.setOption('mode', 'application/json'); cm.setOption('lint', 'json'); } const popup = showCodeMirrorPopup(t('setStylelintRules'), $element({ appendChild: [ $element({ tag: 'p', appendChild: [ t('setStylelintLink') + ' ', makeLink('https://stylelint.io/demo/', 'Stylelint') ] }), makeButton('save', 'styleSaveLabel'), makeButton('reset', 'resetStylelintRules'), $element({ tag: 'span', className: 'error', appendChild: [ t('setStylelintError') + ' ', makeLink('https://jsonlint.com/', 'jsonlint') ] }) ] })); const contents = popup.querySelector('.contents'); const loadJSON = window.jsonlint ? [] : [ 'vendor/codemirror/mode/javascript/javascript.js', 'vendor/codemirror/addon/lint/json-lint.js', 'vendor/jsonlint/jsonlint.js' ]; contents.insertBefore(popup.codebox.display.wrapper, contents.firstElementChild); popup.codebox.focus(); popup.codebox.setValue(rules); onDOMscripted(loadJSON).then(() => { setJSONMode(popup.codebox); }); setupStylelintSettingsEvents(popup); } function loadSelectedLinter(name) { let scripts = []; if (name !== 'null' && !$('script[src*="css-lint.js"]')) { // inject css injectCSS('vendor/codemirror/addon/lint/lint.css'); // load CodeMirror lint code scripts = scripts.concat([ 'vendor/codemirror/addon/lint/lint.js', 'vendor-overwrites/codemirror/addon/lint/css-lint.js' ]); } if (name === 'csslint' && !window.CSSLint) { scripts.push('vendor/csslint/csslint-worker.js'); } else if (name === 'stylelint' && !window.stylelint) { scripts = scripts.concat([ 'vendor-overwrites/stylelint/stylelint-bundle.min.js', 'vendor-overwrites/codemirror/addon/lint/stylelint-config.js' ]); } return onDOMscripted(scripts); }