stylus/edit/util.js
tophf 5e5fecbcfe
editor: section labels, TOC, tweaks (#1086)
* section labels, TOC, speedups and fixes

* show section numbers in widgets
* debounce livePreview in usercss editor
* better fixed header and compact layout compatibility
* fix section sizing for compact layout + layout speedup
* DocFuncMapper + cosmetics + fix Clone button
* don't run linter during initSections
* remove unused/unnecessary DOM polyfills
* report invalid @document function as parser error
* rewrite section finder
* simplify focusedViaClick
* simplify setPreprocessor and make it synchronous
* throttle offscreen line widgets in usercss with lots of sections
* add on, off aliases for add/removeEventListener + onOff
* use on/off aliases in changed files
* use getters in more places
2020-11-08 11:12:42 +03:00

220 lines
5.4 KiB
JavaScript

/* global
$create
CodeMirror
prefs
*/
'use strict';
/* exported DirtyReporter */
class DirtyReporter {
constructor() {
this._dirty = new Map();
this._onchange = new Set();
}
add(obj, value) {
const wasDirty = this.isDirty();
const saved = this._dirty.get(obj);
if (!saved) {
this._dirty.set(obj, {type: 'add', newValue: value});
} else if (saved.type === 'remove') {
if (saved.savedValue === value) {
this._dirty.delete(obj);
} else {
saved.newValue = value;
saved.type = 'modify';
}
}
this.notifyChange(wasDirty);
}
remove(obj, value) {
const wasDirty = this.isDirty();
const saved = this._dirty.get(obj);
if (!saved) {
this._dirty.set(obj, {type: 'remove', savedValue: value});
} else if (saved.type === 'add') {
this._dirty.delete(obj);
} else if (saved.type === 'modify') {
saved.type = 'remove';
}
this.notifyChange(wasDirty);
}
modify(obj, oldValue, newValue) {
const wasDirty = this.isDirty();
const saved = this._dirty.get(obj);
if (!saved) {
if (oldValue !== newValue) {
this._dirty.set(obj, {type: 'modify', savedValue: oldValue, newValue});
}
} else if (saved.type === 'modify') {
if (saved.savedValue === newValue) {
this._dirty.delete(obj);
} else {
saved.newValue = newValue;
}
} else if (saved.type === 'add') {
saved.newValue = newValue;
}
this.notifyChange(wasDirty);
}
clear(obj) {
const wasDirty = this.isDirty();
if (obj === undefined) {
this._dirty.clear();
} else {
this._dirty.delete(obj);
}
this.notifyChange(wasDirty);
}
isDirty() {
return this._dirty.size > 0;
}
onChange(cb, add = true) {
this._onchange[add ? 'add' : 'delete'](cb);
}
notifyChange(wasDirty) {
if (wasDirty !== this.isDirty()) {
this._onchange.forEach(cb => cb());
}
}
has(key) {
return this._dirty.has(key);
}
}
/* exported DocFuncMapper */
const DocFuncMapper = {
TO_CSS: {
urls: 'url',
urlPrefixes: 'url-prefix',
domains: 'domain',
regexps: 'regexp',
},
FROM_CSS: {
'url': 'urls',
'url-prefix': 'urlPrefixes',
'domain': 'domains',
'regexp': 'regexps',
},
/**
* @param {Object} section
* @param {function(func:string, value:string)} fn
*/
forEachProp(section, fn) {
for (const [propName, func] of Object.entries(DocFuncMapper.TO_CSS)) {
const props = section[propName];
if (props) props.forEach(value => fn(func, value));
}
},
/**
* @param {Array<?[type,value]>} funcItems
* @param {?Object} [section]
* @returns {Object} section
*/
toSection(funcItems, section = {}) {
for (const item of funcItems) {
const [func, value] = item || [];
const propName = DocFuncMapper.FROM_CSS[func];
if (propName) {
const props = section[propName] || (section[propName] = []);
if (Array.isArray(value)) props.push(...value);
else props.push(value);
}
}
return section;
},
};
/* exported sectionsToMozFormat */
function sectionsToMozFormat(style) {
return style.sections.map(section => {
const cssFuncs = [];
DocFuncMapper.forEachProp(section, (type, value) =>
cssFuncs.push(`${type}("${value.replace(/\\/g, '\\\\')}")`));
return cssFuncs.length ?
`@-moz-document ${cssFuncs.join(', ')} {\n${section.code}\n}` :
section.code;
}).join('\n\n');
}
/* exported trimCommentLabel */
function trimCommentLabel(str, limit = 1000) {
// stripping /*** foo ***/ to foo
return clipString(str.replace(/^[!-/:;=\s]*|[-#$&(+,./:;<=>\s*]*$/g, ''), limit);
}
/* exported clipString */
function clipString(str, limit = 100) {
return str.length <= limit ? str : str.substr(0, limit) + '...';
}
/* exported memoize */
function memoize(fn) {
let cached = false;
let result;
return (...args) => {
if (!cached) {
result = fn(...args);
cached = true;
}
return result;
};
}
/* exported createHotkeyInput */
/**
* @param {!string} prefId
* @param {?function(isEnter:boolean)} onDone
*/
function createHotkeyInput(prefId, onDone = () => {}) {
return $create('input', {
type: 'search',
spellcheck: false,
value: prefs.get(prefId),
onkeydown(event) {
const key = CodeMirror.keyName(event);
if (key === 'Tab' || key === 'Shift-Tab') {
return;
}
event.preventDefault();
event.stopPropagation();
switch (key) {
case 'Enter':
if (this.checkValidity()) onDone(true);
return;
case 'Esc':
onDone(false);
return;
default:
// disallow: [Shift?] characters, modifiers-only, [modifiers?] + Esc, Tab, nav keys
if (!key || new RegExp('^(' + [
'(Back)?Space',
'(Shift-)?.', // a single character
'(Shift-?|Ctrl-?|Alt-?|Cmd-?){0,2}(|Esc|Tab|(Page)?(Up|Down)|Left|Right|Home|End|Insert|Delete)',
].join('|') + ')$', 'i').test(key)) {
this.value = key || this.value;
this.setCustomValidity('Not allowed');
return;
}
}
this.value = key;
this.setCustomValidity('');
prefs.set(prefId, key);
},
oninput() {
// fired on pressing "x" to clear the field
prefs.set(prefId, '');
},
onpaste(event) {
event.preventDefault();
}
});
}