stylus/edit/util.js
tophf dd38856eda
scrollable details + sticky header (#1400)
* shorten section labels in lint report
* `sectioned` class on html for sectioned editor
* fix scrollElementIntoView
2022-02-14 22:19:20 +03:00

225 lines
6.5 KiB
JavaScript

/* global $ $create getEventKeyName messageBoxProxy moveFocus */// dom.js
/* global CodeMirror */
/* global editor */
/* global prefs */
/* global t */// localization.js
'use strict';
const helpPopup = {
/**
* @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);
}
$('.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;
return div;
},
close(event) {
const canClose =
!event ||
event.type === 'click' || (
getEventKeyName(event) === 'Escape' &&
!$('.CodeMirror-hints, #message-box') && (
!document.activeElement ||
!document.activeElement.closest('#search-replace-dialog') && (
document.activeElement.tagName !== 'INPUT' ||
document.activeElement.closest('.can-close-on-esc')
)
)
);
const {div} = helpPopup;
if (!canClose || !div) {
return;
}
if (event && div.codebox && !div.codebox.options.readOnly && !div.codebox.isClean()) {
setTimeout(async () => {
const ok = await messageBoxProxy.confirm(t('confirmDiscardChanges'));
return ok && helpPopup.close();
});
return;
}
if (div.contains(document.activeElement) && helpPopup.originalFocus) {
helpPopup.originalFocus.focus();
}
const contents = $('.contents', div);
div.style.display = '';
contents.textContent = '';
window.off('keydown', helpPopup.close, true);
window.dispatchEvent(new Event('closeHelp'));
},
};
// 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) + '...';
}
/* 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);
}