stylus/edit/util.js

222 lines
6.3 KiB
JavaScript
Raw Normal View History

/* global $ $create getEventKeyName messageBoxProxy moveFocus */// dom.js
/* global CodeMirror */
/* global editor */
/* global prefs */
/* global t */// localization.js
2017-09-11 16:09:25 +00:00
'use strict';
const helpPopup = {
2017-09-11 16:09:25 +00:00
/**
* @param {string} title - plain text
* @param {string|Node} body - Node, html or plain text
* @param {Node} [props] - DOM props for the popup element
* @returns {Element} the popup
*/
show(title = '', body, props) {
const div = $('#help-popup');
const contents = $('.contents', div);
div.style = '';
div.className = '';
contents.textContent = '';
Object.assign(div, props);
if (body) {
contents.appendChild(typeof body === 'string' ? t.HTML(body) : body);
2017-09-11 16:09:25 +00:00
}
$('.title', div).textContent = title;
$('.dismiss', div).onclick = helpPopup.close;
window.on('keydown', helpPopup.close, true);
div.style.display = 'block';
helpPopup.originalFocus = document.activeElement;
helpPopup.div = div;
2022-02-21 20:45:42 +00:00
moveFocus(div, 0);
return div;
},
2017-09-11 16:09:25 +00:00
close(event) {
2022-02-21 20:45:42 +00:00
let el;
const canClose =
!event ||
2022-02-21 20:45:42 +00:00
event.type === 'click' ||
getEventKeyName(event) === 'Escape' && !$('.CodeMirror-hints, #message-box') && (
!(el = document.activeElement) ||
!el.closest('#search-replace-dialog')
);
const {div} = helpPopup;
if (!canClose || !div) {
return;
2017-09-11 16:09:25 +00:00
}
2022-02-21 20:45:42 +00:00
if (event && (el = div.codebox) && !el.options.readOnly && !el.isClean()) {
setTimeout(async () => {
const ok = await messageBoxProxy.confirm(t('confirmDiscardChanges'));
return ok && helpPopup.close();
});
return;
2017-09-11 16:09:25 +00:00
}
2022-02-21 20:45:42 +00:00
if (div.contains(document.activeElement) && (el = helpPopup.originalFocus)) {
el.focus();
2017-09-11 16:09:25 +00:00
}
const contents = $('.contents', div);
div.style.display = '';
contents.textContent = '';
window.off('keydown', helpPopup.close, true);
window.dispatchEvent(new Event('closeHelp'));
},
};
2017-09-11 16:09:25 +00:00
// reroute handling to nearest editor when keypress resolves to one of these commands
const rerouteHotkeys = {
commands: [
'beautify',
'colorpicker',
'find',
'findNext',
'findPrev',
'jumpToLine',
'nextEditor',
'prevEditor',
'replace',
'replaceAll',
'save',
'toggleEditorFocus',
'toggleStyle',
],
toggle(enable) {
document[enable ? 'on' : 'off']('keydown', rerouteHotkeys.handler);
},
handler(event) {
const keyName = CodeMirror.keyName(event);
if (!keyName) {
return;
}
const rerouteCommand = name => {
if (rerouteHotkeys.commands.includes(name)) {
CodeMirror.commands[name](editor.closestVisible(event.target));
return true;
}
};
if (CodeMirror.lookupKey(keyName, CodeMirror.defaults.keyMap, rerouteCommand) === 'handled' ||
CodeMirror.lookupKey(keyName, CodeMirror.defaults.extraKeys, rerouteCommand) === 'handled') {
event.preventDefault();
event.stopPropagation();
}
},
};
function clipString(str, limit = 100) {
return str.length <= limit ? str : str.substr(0, limit) + '...';
}
Rewrite linter system (#487) * Add: implement new linter system * Refactor: pull out editor worker * Switch to new linter and worker * Enable eslint cache * Fix: undefined error * Windows compatibility * Fix: refresh linter if the editor.linter changes * Add: stylelint * Add: getStylelintRules, getCsslintRules * Fix: logic to get correct linter * WIP: linter-report * Fix: toggle hidden state * Add: matain the order of lint report for section editor * Add: unhook event * Add: gotoLintIssue * Fix: shouldn't delete rule.init * Add: linter-help-dialog * Drop linterConfig * Add: linter-config-dialog, cacheFn * Add: use cacheFn * Drop lint.js * Add: refresh. Fix report order * Fix: hide empty table * Add: updateCount. Fix table caption * Switch to new linter/worker * Fix: remove unneeded comment * Fix: cacheFn -> cacheFirstCall * Fix: use cacheFirstCall * Fix: cache metaIndex * Fix: i < trs.length * Fix: drop isEmpty * Fix: expose some simple states to global * Fix: return object code style * Fix: use proxy to reflect API * Fix: eslint-disable-line -> eslint-disable-next-line * Fix: requestId -> id * Fix: one-liner * Fix: one-liner * Fix: move dom event block to top * Fix: pending -> pendingResponse * Fix: onSuccess -> onUpdated * Fix: optimize row removing when i === 0 * Fix: hook/unhook -> enableForEditor/disableForEditor * Fix: linter.refresh -> linter.run * Fix: some shadowing * Fix: simplify getAnnotations * Fix: cacheFirstCall -> memoize * Fix: table.update -> table.updateCaption * Fix: unneeded reassign * Fix: callbacks -> listeners * Fix: don't compose but extend * Refactor: replace linter modules with linter-defaults and linter-engines * Fix: implement linter fallbacks * Fix: linter.onChange -> linter.onLintingUpdated * Fix: cms -> tables * Fix: parseMozFormat is not called correctly * Move csslint-loader to background * Fix: watch config changes * Fix: switch to LINTER_DEFAULTS * Fix: csslint-loader -> parserlib-loader
2018-10-01 14:03:17 +00:00
/* exported createHotkeyInput */
function createHotkeyInput(prefId, {buttons = true, onDone}) {
const RX_ERR = new RegExp('^(' + [
/Space/,
/(Shift-)?./, // a single character
/(?=.)(Shift-?|Ctrl-?|Control-?|Alt-?|Meta-?)*(Escape|Tab|Page(Up|Down)|Arrow(Up|Down|Left|Right)|Home|End)?/,
].map(r => r.source || r).join('|') + ')$', 'i');
const initialValue = prefs.get(prefId);
const input = $create('input', {
spellcheck: false,
onpaste: e => onkeydown(e, e.clipboardData.getData('text')),
onkeydown,
});
buttons = buttons && [
['confirmOK', 'Enter'],
['undo', initialValue],
['genericResetLabel', ''],
].map(([label, val]) =>
$create('button', {onclick: e => onkeydown(e, val)}, t(label)));
const [btnOk, btnUndo, btnReset] = buttons || [];
onkeydown(null, initialValue);
return buttons
? $create('fragment', [input, $create('.buttons', buttons)])
: input;
function onkeydown(e, key) {
let newValue;
if (e && e.type === 'keydown') {
key = getEventKeyName(e);
}
switch (e && key) {
case 'Tab':
case 'Shift-Tab':
return;
case 'BackSpace':
case 'Delete':
newValue = '';
break;
case 'Enter':
if (input.checkValidity() && onDone) onDone();
break;
case 'Escape':
if (onDone) onDone();
break;
default:
newValue = key.replace(/\b.$/, c => c.toUpperCase());
}
if (newValue != null) {
const error = RX_ERR.test(newValue) ? t('genericError') : '';
if (e && !error) prefs.set(prefId, newValue);
input.setCustomValidity(error);
input.value = newValue;
input.focus();
if (buttons) {
btnOk.disabled = Boolean(error);
btnUndo.disabled = newValue === initialValue;
btnReset.disabled = !newValue;
}
}
if (e) {
e.preventDefault();
e.stopPropagation();
}
}
}
/* exported showCodeMirrorPopup */
function showCodeMirrorPopup(title, html, options) {
const popup = helpPopup.show(title, html, {className: 'big'});
let cm = popup.codebox = CodeMirror($('.contents', popup), Object.assign({
mode: 'css',
lineNumbers: true,
lineWrapping: prefs.get('editor.lineWrapping'),
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
matchBrackets: true,
styleActiveLine: true,
theme: prefs.get('editor.theme'),
keyMap: prefs.get('editor.keyMap'),
}, options));
cm.focus();
$.root.style.pointerEvents = 'none';
popup.style.pointerEvents = 'auto';
const onKeyDown = event => {
if (event.key === 'Tab' && !event.ctrlKey && !event.altKey && !event.metaKey) {
const search = $('#search-replace-dialog');
const area = search && search.contains(document.activeElement) ? search : popup;
moveFocus(area, event.shiftKey ? -1 : 1);
event.preventDefault();
}
};
window.on('keydown', onKeyDown, true);
window.on('closeHelp', () => {
window.off('keydown', onKeyDown, true);
$.root.style.removeProperty('pointer-events');
cm = popup.codebox = null;
}, {once: true});
return popup;
}
/* exported trimCommentLabel */
function trimCommentLabel(str, limit = 1000) {
// stripping /*** foo ***/ to foo
return clipString(str.replace(/^[!-/:;=\s]*|[-#$&(+,./:;<=>\s*]*$/g, ''), limit);
}