| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427 |
- /* global $ */// 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.ready !== true) {
- wrapper.style.height = si ? si.height : '100vh';
- }
- elLabel.after(wrapper);
- }, {
- value: originalSection.code,
- });
- el.CodeMirror = cm; // used by getAssociatedEditor
- editor.applyScrollInfo(cm, si);
- const changeListeners = new Set();
- const appliesToContainer = $('.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');
- });
- cm.display.wrapper.on('keydown', event => handleKeydown(cm, event), true);
- $('.test-regexp', el).onclick = () => updateRegexpTester(true);
- initBeautifyButton($('.beautify-section', el), [cm]);
- }
- function handleKeydown(cm, event) {
- if (event.shiftKey || event.altKey || event.metaKey) {
- return;
- }
- const {key} = event;
- const {line, ch} = cm.getCursor();
- switch (key) {
- case 'ArrowLeft':
- if (line || ch) {
- return;
- }
- // fallthrough
- case 'ArrowUp':
- cm = line === 0 && editor.prevEditor(cm, false);
- if (!cm) {
- return;
- }
- event.preventDefault();
- event.stopPropagation();
- cm.setCursor(cm.doc.size - 1, key === 'ArrowLeft' ? 1e20 : ch);
- break;
- case 'ArrowRight':
- if (line < cm.doc.size - 1 || ch < cm.getLine(line).length - 1) {
- return;
- }
- // fallthrough
- case 'ArrowDown':
- cm = line === cm.doc.size - 1 && editor.nextEditor(cm, false);
- if (!cm) {
- return;
- }
- event.preventDefault();
- event.stopPropagation();
- cm.setCursor(0, 0);
- break;
- }
- }
- async function updateRegexpTester(toggle) {
- const isLoaded = typeof regexpTester === 'object';
- if (toggle && !isLoaded) {
- await require(['/edit/regexp-tester']); /* global regexpTester */
- }
- if (toggle != null && isLoaded) {
- regexpTester.toggle(toggle);
- }
- const regexps = appliesTo.filter(a => a.type === 'regexp')
- .map(a => a.value);
- if (regexps.length) {
- el.classList.add('has-regexp');
- if (isLoaded) regexpTester.update(regexps);
- } else {
- el.classList.remove('has-regexp');
- if (isLoaded) regexpTester.toggle(false);
- }
- }
- 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);
- appliesToContainer.insertBefore(apply.el, base ? base.el.nextSibling : null);
- dirty.add(apply, apply);
- if (appliesTo.length > 1 && appliesTo[0].all) {
- removeApply(appliesTo[0]);
- }
- 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 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;
- wrapper.style.pointerEvents = 'none';
- document.body.style.cursor = 's-resize';
- 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);
- wrapper.style.pointerEvents = '';
- document.body.style.cursor = '';
- }
- };
- 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);
- }
- }
- }
- }
|