394 lines
11 KiB
JavaScript
394 lines
11 KiB
JavaScript
/* global $ toggleDataset */// dom.js
|
|
/* global MozDocMapper trimCommentLabel */// util.js
|
|
/* global cmFactory */
|
|
/* global debounce tryRegExp */// toolbox.js
|
|
/* global editor */
|
|
/* global initBeautifyButton */// beautify.js
|
|
/* global linterMan */
|
|
/* global prefs */
|
|
/* global t */// localization.js
|
|
'use strict';
|
|
|
|
/* exported createSection */
|
|
/**
|
|
* @param {StyleSection} originalSection
|
|
* @param {function():number} genId
|
|
* @param {EditorScrollInfo} [si]
|
|
* @returns {EditorSection}
|
|
*/
|
|
function createSection(originalSection, genId, si) {
|
|
const {dirty} = editor;
|
|
const sectionId = genId();
|
|
const el = t.template.section.cloneNode(true);
|
|
const elLabel = $('.code-label', el);
|
|
const cm = cmFactory.create(wrapper => {
|
|
// making it tall during initial load so IntersectionObserver sees only one adjacent CM
|
|
if (editor.loading) {
|
|
wrapper.style.height = si ? si.height : '100vh';
|
|
}
|
|
elLabel.after(wrapper);
|
|
}, {
|
|
value: originalSection.code,
|
|
});
|
|
el.CodeMirror = cm; // used by getAssociatedEditor
|
|
cm.el = el;
|
|
editor.applyScrollInfo(cm, si);
|
|
|
|
const changeListeners = new Set();
|
|
|
|
const appliesToContainer = $('.applies-to', el);
|
|
const appliesToList = $('.applies-to-list', el);
|
|
const appliesTo = [];
|
|
MozDocMapper.forEachProp(originalSection, (type, value) =>
|
|
insertApplyAfter({type, value}));
|
|
if (!appliesTo.length) {
|
|
insertApplyAfter({all: true});
|
|
}
|
|
|
|
let changeGeneration = cm.changeGeneration();
|
|
let removed = false;
|
|
|
|
registerEvents();
|
|
updateRegexpTester();
|
|
createResizeGrip(cm);
|
|
|
|
/** @namespace EditorSection */
|
|
const section = {
|
|
id: sectionId,
|
|
el,
|
|
cm,
|
|
appliesTo,
|
|
getModel() {
|
|
const items = appliesTo.map(a => !a.all && [a.type, a.value]);
|
|
return MozDocMapper.toSection(items, {code: cm.getValue()});
|
|
},
|
|
remove() {
|
|
linterMan.disableForEditor(cm);
|
|
el.classList.add('removed');
|
|
removed = true;
|
|
appliesTo.forEach(a => a.remove());
|
|
},
|
|
render() {
|
|
cm.refresh();
|
|
},
|
|
destroy() {
|
|
cmFactory.destroy(cm);
|
|
},
|
|
restore() {
|
|
linterMan.enableForEditor(cm);
|
|
el.classList.remove('removed');
|
|
removed = false;
|
|
appliesTo.forEach(a => a.restore());
|
|
cm.refresh();
|
|
},
|
|
onChange(fn) {
|
|
changeListeners.add(fn);
|
|
},
|
|
off(fn) {
|
|
changeListeners.delete(fn);
|
|
},
|
|
get removed() {
|
|
return removed;
|
|
},
|
|
tocEntry: {
|
|
label: '',
|
|
get removed() {
|
|
return removed;
|
|
},
|
|
},
|
|
};
|
|
|
|
prefs.subscribe('editor.toc.expanded', updateTocPrefToggled, {runNow: true});
|
|
|
|
return section;
|
|
|
|
function emitSectionChange(origin) {
|
|
for (const fn of changeListeners) {
|
|
fn(origin);
|
|
}
|
|
}
|
|
|
|
function registerEvents() {
|
|
cm.on('changes', () => {
|
|
const newGeneration = cm.changeGeneration();
|
|
dirty.modify(`section.${sectionId}.code`, changeGeneration, newGeneration);
|
|
changeGeneration = newGeneration;
|
|
emitSectionChange('code');
|
|
});
|
|
$('.test-regexp', el).onclick = () => updateRegexpTester(true);
|
|
initBeautifyButton($('.beautify-section', el), [cm]);
|
|
}
|
|
|
|
async function updateRegexpTester(toggle) {
|
|
const isLoaded = typeof regexpTester === 'object' ||
|
|
toggle && await require(['/edit/regexp-tester']); /* global regexpTester */
|
|
if (toggle != null) {
|
|
regexpTester.toggle(toggle);
|
|
}
|
|
const regexps = appliesTo.filter(a => a.type === 'regexp')
|
|
.map(a => a.value);
|
|
const hasRe = regexps.length > 0;
|
|
if (hasRe && isLoaded) regexpTester.update(regexps);
|
|
el.classList.toggle('has-regexp', hasRe);
|
|
}
|
|
|
|
function updateTocEntry(origin) {
|
|
const te = section.tocEntry;
|
|
let changed;
|
|
if (origin === 'code' || !origin) {
|
|
const label = getLabelFromComment();
|
|
if (te.label !== label) {
|
|
te.label = elLabel.dataset.text = label;
|
|
changed = true;
|
|
}
|
|
}
|
|
if (!te.label) {
|
|
const target = appliesTo[0].all ? null : appliesTo[0].value;
|
|
if (te.target !== target) {
|
|
te.target = target;
|
|
changed = true;
|
|
}
|
|
if (te.numTargets !== appliesTo.length) {
|
|
te.numTargets = appliesTo.length;
|
|
changed = true;
|
|
}
|
|
}
|
|
if (changed) editor.updateToc([section]);
|
|
}
|
|
|
|
function updateTocEntryLazy(...args) {
|
|
debounce(updateTocEntry, 0, ...args);
|
|
}
|
|
|
|
function updateTocFocus() {
|
|
editor.updateToc({focus: true, 0: section});
|
|
}
|
|
|
|
function updateTocPrefToggled(key, val) {
|
|
changeListeners[val ? 'add' : 'delete'](updateTocEntryLazy);
|
|
(val ? el.on : el.off).call(el, 'focusin', updateTocFocus);
|
|
if (val) {
|
|
updateTocEntry();
|
|
if (el.contains(document.activeElement)) {
|
|
updateTocFocus();
|
|
}
|
|
}
|
|
}
|
|
|
|
function getLabelFromComment() {
|
|
let cmt = '';
|
|
let inCmt;
|
|
cm.eachLine(({text}) => {
|
|
let i = 0;
|
|
if (!inCmt) {
|
|
i = text.search(/\S/);
|
|
if (i < 0) return;
|
|
inCmt = text[i] === '/' && text[i + 1] === '*';
|
|
if (!inCmt) return true;
|
|
i += 2;
|
|
}
|
|
const j = text.indexOf('*/', i);
|
|
cmt = trimCommentLabel(text.slice(i, j >= 0 ? j : text.length));
|
|
return j >= 0 || cmt;
|
|
});
|
|
return cmt;
|
|
}
|
|
|
|
function insertApplyAfter(init, base) {
|
|
const apply = createApply(init);
|
|
appliesTo.splice(base ? appliesTo.indexOf(base) + 1 : appliesTo.length, 0, apply);
|
|
appliesToList.insertBefore(apply.el, base ? base.el.nextSibling : null);
|
|
toggleDataset(appliesToContainer, 'all', init.all);
|
|
dirty.add(apply, apply);
|
|
if (appliesTo.length > 1 && appliesTo[0].all) {
|
|
removeApply(appliesTo[0]);
|
|
}
|
|
if (base) requestAnimationFrame(shrinkSectionBy1);
|
|
emitSectionChange('apply');
|
|
return apply;
|
|
}
|
|
|
|
function removeApply(apply) {
|
|
const index = appliesTo.indexOf(apply);
|
|
appliesTo.splice(index, 1);
|
|
apply.remove();
|
|
apply.el.remove();
|
|
dirty.remove(apply, apply);
|
|
if (!appliesTo.length) {
|
|
insertApplyAfter({all: true});
|
|
}
|
|
emitSectionChange('apply');
|
|
}
|
|
|
|
function createApply({type = 'url', value, all = false}) {
|
|
const applyId = genId();
|
|
const dirtyPrefix = `section.${sectionId}.apply.${applyId}`;
|
|
const el = all ? t.template.appliesToEverything.cloneNode(true) :
|
|
t.template.appliesTo.cloneNode(true);
|
|
|
|
const selectEl = !all && $('.applies-type', el);
|
|
if (selectEl) {
|
|
selectEl.value = type;
|
|
selectEl.on('change', () => {
|
|
const oldType = type;
|
|
dirty.modify(`${dirtyPrefix}.type`, type, selectEl.value);
|
|
type = selectEl.value;
|
|
if (oldType === 'regexp' || type === 'regexp') {
|
|
updateRegexpTester();
|
|
}
|
|
emitSectionChange('apply');
|
|
validate();
|
|
});
|
|
}
|
|
|
|
const valueEl = !all && $('.applies-value', el);
|
|
if (valueEl) {
|
|
valueEl.value = value;
|
|
valueEl.on('input', () => {
|
|
dirty.modify(`${dirtyPrefix}.value`, value, valueEl.value);
|
|
value = valueEl.value;
|
|
if (type === 'regexp') {
|
|
updateRegexpTester();
|
|
}
|
|
emitSectionChange('apply');
|
|
});
|
|
valueEl.on('change', validate);
|
|
}
|
|
|
|
restore();
|
|
|
|
const apply = {
|
|
id: applyId,
|
|
all,
|
|
remove,
|
|
restore,
|
|
el,
|
|
valueEl, // used by validator
|
|
get type() {
|
|
return type;
|
|
},
|
|
get value() {
|
|
return value;
|
|
},
|
|
};
|
|
|
|
const removeButton = $('.remove-applies-to', el);
|
|
if (removeButton) {
|
|
removeButton.on('click', e => {
|
|
e.preventDefault();
|
|
removeApply(apply);
|
|
});
|
|
}
|
|
$('.add-applies-to', el).on('click', e => {
|
|
e.preventDefault();
|
|
const newApply = insertApplyAfter({type, value: ''}, apply);
|
|
$('input', newApply.el).focus();
|
|
});
|
|
|
|
return apply;
|
|
|
|
function validate() {
|
|
if (type !== 'regexp' || tryRegExp(value)) {
|
|
valueEl.setCustomValidity('');
|
|
} else {
|
|
valueEl.setCustomValidity(t('styleBadRegexp'));
|
|
setTimeout(() => valueEl.reportValidity());
|
|
}
|
|
}
|
|
|
|
function remove() {
|
|
if (all) {
|
|
return;
|
|
}
|
|
dirty.remove(`${dirtyPrefix}.type`, type);
|
|
dirty.remove(`${dirtyPrefix}.value`, value);
|
|
}
|
|
|
|
function restore() {
|
|
if (all) {
|
|
return;
|
|
}
|
|
dirty.add(`${dirtyPrefix}.type`, type);
|
|
dirty.add(`${dirtyPrefix}.value`, value);
|
|
}
|
|
}
|
|
|
|
function shrinkSectionBy1() {
|
|
const cmEl = cm.display.wrapper;
|
|
const cmH = cmEl.offsetHeight;
|
|
const viewH = el.parentElement.offsetHeight;
|
|
if (el.offsetHeight > viewH && cmH > Math.min(viewH / 2, cm.display.sizer.offsetHeight + 30)) {
|
|
cmEl.style.height = (cmH - appliesToContainer.offsetHeight / (appliesTo.length || 1) | 0) + 'px';
|
|
}
|
|
}
|
|
}
|
|
|
|
function createResizeGrip(cm) {
|
|
const wrapper = cm.display.wrapper;
|
|
wrapper.classList.add('resize-grip-enabled');
|
|
const resizeGrip = t.template.resizeGrip.cloneNode(true);
|
|
wrapper.appendChild(resizeGrip);
|
|
let lastClickTime = 0;
|
|
let lastHeight;
|
|
let lastY;
|
|
resizeGrip.onmousedown = event => {
|
|
lastHeight = wrapper.offsetHeight;
|
|
lastY = event.clientY;
|
|
if (event.button !== 0) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
if (Date.now() - lastClickTime < 500) {
|
|
lastClickTime = 0;
|
|
toggleSectionHeight(cm);
|
|
return;
|
|
}
|
|
lastClickTime = Date.now();
|
|
const minHeight = cm.defaultTextHeight() +
|
|
/* .CodeMirror-lines padding */
|
|
cm.display.lineDiv.offsetParent.offsetTop +
|
|
/* borders */
|
|
wrapper.offsetHeight - wrapper.clientHeight;
|
|
document.body.classList.add('resizing-v');
|
|
document.on('mousemove', resize);
|
|
document.on('mouseup', resizeStop);
|
|
|
|
function resize(e) {
|
|
const height = Math.max(minHeight, lastHeight + e.clientY - lastY);
|
|
if (height !== lastHeight) {
|
|
cm.setSize(null, height);
|
|
lastHeight = height;
|
|
lastY = e.clientY;
|
|
}
|
|
}
|
|
|
|
function resizeStop() {
|
|
document.off('mouseup', resizeStop);
|
|
document.off('mousemove', resize);
|
|
document.body.classList.remove('resizing-v');
|
|
}
|
|
};
|
|
|
|
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 sectionEl = wrapper.parentNode;
|
|
const sectionExtrasHeight = sectionEl.clientHeight - wrapper.offsetHeight;
|
|
cm.state.toggleHeightSaved = wrapper.clientHeight;
|
|
cm.setSize(null, window.innerHeight - sectionExtrasHeight - pageExtrasHeight);
|
|
const bounds = sectionEl.getBoundingClientRect();
|
|
if (bounds.top < 0 || bounds.bottom > window.innerHeight) {
|
|
window.scrollBy(0, bounds.top);
|
|
}
|
|
}
|
|
}
|
|
}
|