sections-editor.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711
  1. /* global $ $$ $create $remove messageBoxProxy */// dom.js
  2. /* global API */// msg.js
  3. /* global CodeMirror */
  4. /* global FIREFOX RX_META debounce ignoreChromeError sessionStore */// toolbox.js
  5. /* global MozDocMapper clipString helpPopup rerouteHotkeys showCodeMirrorPopup */// util.js
  6. /* global createSection */// sections-editor-section.js
  7. /* global editor */
  8. /* global linterMan */
  9. /* global prefs */
  10. /* global t */// localization.js
  11. 'use strict';
  12. /* exported SectionsEditor */
  13. function SectionsEditor() {
  14. const {style, /** @type DirtyReporter */dirty} = editor;
  15. const container = $('#sections');
  16. /** @type {EditorSection[]} */
  17. const sections = [];
  18. const xo = window.IntersectionObserver &&
  19. new IntersectionObserver(refreshOnViewListener, {rootMargin: '100%'});
  20. let INC_ID = 0; // an increment id that is used by various object to track the order
  21. let sectionOrder = '';
  22. let headerOffset; // in compact mode the header is at the top so it reduces the available height
  23. let cmExtrasHeight; // resize grip + borders
  24. updateHeader();
  25. rerouteHotkeys.toggle(true); // enabled initially because we don't always focus a CodeMirror
  26. editor.livePreview.init();
  27. container.classList.add('section-editor');
  28. $('#to-mozilla').on('click', showMozillaFormat);
  29. $('#to-mozilla-help').on('click', showToMozillaHelp);
  30. $('#from-mozilla').on('click', () => showMozillaFormatImport());
  31. document.on('wheel', scrollEntirePageOnCtrlShift, {passive: false});
  32. CodeMirror.defaults.extraKeys['Shift-Ctrl-Wheel'] = 'scrollWindow';
  33. if (!FIREFOX) {
  34. $$('input:not([type]), input[type=text], input[type=search], input[type=number]')
  35. .forEach(e => e.on('mousedown', toggleContextMenuDelete));
  36. }
  37. /** @namespace Editor */
  38. Object.assign(editor, {
  39. sections,
  40. closestVisible,
  41. updateLivePreview,
  42. getEditors() {
  43. return sections.filter(s => !s.removed).map(s => s.cm);
  44. },
  45. getEditorTitle(cm) {
  46. const index = editor.getEditors().indexOf(cm);
  47. return `${t('sectionCode')} ${index + 1}`;
  48. },
  49. getValue(asObject) {
  50. const st = getModel();
  51. return asObject ? st : MozDocMapper.styleToCss(st);
  52. },
  53. getSearchableInputs(cm) {
  54. const sec = sections.find(s => s.cm === cm);
  55. return sec ? sec.appliesTo.map(a => a.valueEl).filter(Boolean) : [];
  56. },
  57. jumpToEditor(i) {
  58. const {cm} = sections[i] || {};
  59. if (cm) {
  60. editor.scrollToEditor(cm);
  61. cm.focus();
  62. }
  63. },
  64. nextEditor(cm, cycle = true) {
  65. return cycle || cm !== findLast(sections, s => !s.removed).cm
  66. ? nextPrevEditor(cm, 1)
  67. : null;
  68. },
  69. prevEditor(cm, cycle = true) {
  70. return cycle || cm !== sections.find(s => !s.removed).cm
  71. ? nextPrevEditor(cm, -1)
  72. : null;
  73. },
  74. async replaceStyle(newStyle) {
  75. dirty.clear();
  76. // FIXME: avoid recreating all editors?
  77. await initSections(newStyle.sections, {replace: true});
  78. Object.assign(style, newStyle);
  79. editor.onStyleUpdated();
  80. updateHeader();
  81. // Go from new style URL to edit style URL
  82. if (style.id && !/[&?]id=/.test(location.search)) {
  83. history.replaceState({}, document.title, `${location.pathname}?id=${style.id}`);
  84. }
  85. updateLivePreview();
  86. },
  87. async save() {
  88. if (!dirty.isDirty()) {
  89. return;
  90. }
  91. let newStyle = getModel();
  92. if (!validate(newStyle)) {
  93. return;
  94. }
  95. newStyle = await API.styles.editSave(newStyle);
  96. destroyRemovedSections();
  97. if (!style.id) {
  98. editor.emit('styleChange', newStyle, 'new');
  99. }
  100. sessionStore.justEditedStyleId = newStyle.id;
  101. editor.replaceStyle(newStyle, false);
  102. },
  103. scrollToEditor(cm) {
  104. const {el} = sections.find(s => s.cm === cm);
  105. const r = el.getBoundingClientRect();
  106. const h = window.innerHeight;
  107. if (r.bottom > h && r.top > 0 ||
  108. r.bottom < h && r.top < 0) {
  109. window.scrollBy(0, (r.top + r.bottom - h) / 2 | 0);
  110. }
  111. },
  112. });
  113. editor.ready = initSections(style.sections);
  114. editor.on('styleToggled', newStyle => {
  115. if (!dirty.isDirty()) {
  116. Object.assign(style, newStyle);
  117. } else {
  118. editor.toggleStyle(newStyle.enabled);
  119. }
  120. updateHeader();
  121. updateLivePreview();
  122. });
  123. editor.on('styleChange', (newStyle, reason) => {
  124. if (reason === 'new') return; // nothing is new for us
  125. if (reason === 'config') {
  126. delete newStyle.sections;
  127. delete newStyle.name;
  128. delete newStyle.enabled;
  129. Object.assign(style, newStyle);
  130. updateLivePreview();
  131. return;
  132. }
  133. editor.replaceStyle(newStyle);
  134. });
  135. /** @param {EditorSection} section */
  136. function fitToContent(section) {
  137. const {cm, cm: {display: {wrapper, sizer}}} = section;
  138. if (cm.display.renderedView) {
  139. resize();
  140. } else {
  141. cm.on('update', resize);
  142. }
  143. function resize() {
  144. let contentHeight = sizer.offsetHeight;
  145. if (contentHeight < cm.defaultTextHeight()) {
  146. return;
  147. }
  148. if (headerOffset == null) {
  149. headerOffset = Math.ceil(container.getBoundingClientRect().top + scrollY);
  150. }
  151. if (cmExtrasHeight == null) {
  152. cmExtrasHeight = $('.resize-grip', wrapper).offsetHeight + // grip
  153. wrapper.offsetHeight - wrapper.clientHeight; // borders
  154. }
  155. contentHeight += cmExtrasHeight;
  156. cm.off('update', resize);
  157. const cmHeight = wrapper.offsetHeight;
  158. const appliesToHeight = Math.min(section.el.offsetHeight - cmHeight, window.innerHeight / 2);
  159. const maxHeight = Math.floor(window.innerHeight - headerOffset - appliesToHeight);
  160. const fit = Math.min(contentHeight, maxHeight);
  161. if (Math.abs(fit - cmHeight) > 1) {
  162. cm.setSize(null, fit);
  163. }
  164. }
  165. }
  166. function fitToAvailableSpace() {
  167. const lastSectionBottom = sections[sections.length - 1].el.getBoundingClientRect().bottom;
  168. const delta = Math.floor((window.innerHeight - lastSectionBottom) / sections.length);
  169. if (delta > 1) {
  170. sections.forEach(({cm}) => {
  171. cm.setSize(null, cm.display.lastWrapHeight + delta);
  172. });
  173. }
  174. }
  175. function genId() {
  176. return INC_ID++;
  177. }
  178. function setGlobalProgress(done, total) {
  179. const progressElement = $('#global-progress') ||
  180. total && document.body.appendChild($create('#global-progress'));
  181. if (total) {
  182. const progress = (done / Math.max(done, total) * 100).toFixed(1);
  183. progressElement.style.borderLeftWidth = progress + 'vw';
  184. setTimeout(() => {
  185. progressElement.title = progress + '%';
  186. });
  187. } else {
  188. $remove(progressElement);
  189. }
  190. }
  191. function showToMozillaHelp(event) {
  192. event.preventDefault();
  193. helpPopup.show(t('styleMozillaFormatHeading'), t('styleToMozillaFormatHelp'));
  194. }
  195. /**
  196. priority:
  197. 1. associated CM for applies-to element
  198. 2. last active if visible
  199. 3. first visible
  200. */
  201. function closestVisible(el) {
  202. // closest editor should have at least 2 lines visible
  203. const lineHeight = sections[0].cm.defaultTextHeight();
  204. const margin = 2 * lineHeight;
  205. const cm = el instanceof CodeMirror ? el :
  206. el instanceof Node && getAssociatedEditor(el) || getLastActivatedEditor();
  207. if (el === cm) el = document.body;
  208. if (el instanceof Node && cm) {
  209. const {wrapper} = cm.display;
  210. if (!container.contains(el) || wrapper.closest('.section').contains(el)) {
  211. const rect = wrapper.getBoundingClientRect();
  212. if (rect.top < window.innerHeight - margin && rect.bottom > margin) {
  213. return cm;
  214. }
  215. }
  216. }
  217. const scrollY = window.scrollY;
  218. const windowBottom = scrollY + window.innerHeight - margin;
  219. const allSectionsContainerTop = scrollY + container.getBoundingClientRect().top;
  220. const distances = [];
  221. const alreadyInView = cm && offscreenDistance(null, cm) === 0;
  222. return alreadyInView ? cm : findClosest();
  223. function offscreenDistance(index, cm) {
  224. if (index >= 0 && distances[index] !== undefined) {
  225. return distances[index];
  226. }
  227. const section = cm && cm.display.wrapper.closest('.section');
  228. if (!section) {
  229. return 1e9;
  230. }
  231. const top = allSectionsContainerTop + section.offsetTop;
  232. if (top < scrollY + lineHeight) {
  233. return Math.max(0, scrollY - top - lineHeight);
  234. }
  235. if (top < windowBottom) {
  236. return 0;
  237. }
  238. const distance = top - windowBottom + section.offsetHeight;
  239. if (index >= 0) {
  240. distances[index] = distance;
  241. }
  242. return distance;
  243. }
  244. function findClosest() {
  245. const editors = editor.getEditors();
  246. const last = editors.length - 1;
  247. let a = 0;
  248. let b = last;
  249. let c;
  250. let distance;
  251. while (a < b - 1) {
  252. c = (a + b) / 2 | 0;
  253. distance = offscreenDistance(c);
  254. if (!distance || !c) {
  255. break;
  256. }
  257. const distancePrev = offscreenDistance(c - 1);
  258. const distanceNext = c < last ? offscreenDistance(c + 1) : 1e20;
  259. if (distancePrev <= distance && distance <= distanceNext) {
  260. b = c;
  261. } else {
  262. a = c;
  263. }
  264. }
  265. while (b && offscreenDistance(b - 1) <= offscreenDistance(b)) {
  266. b--;
  267. }
  268. const cm = editors[b];
  269. if (distances[b] > 0) {
  270. editor.scrollToEditor(cm);
  271. }
  272. return cm;
  273. }
  274. }
  275. function getAssociatedEditor(nearbyElement) {
  276. for (let el = nearbyElement; el; el = el.parentElement) {
  277. // added by createSection
  278. if (el.CodeMirror) {
  279. return el.CodeMirror;
  280. }
  281. }
  282. }
  283. function findLast(arr, match) {
  284. for (let i = arr.length - 1; i >= 0; i--) {
  285. if (match(arr[i])) {
  286. return arr[i];
  287. }
  288. }
  289. }
  290. function nextPrevEditor(cm, direction) {
  291. const editors = editor.getEditors();
  292. cm = editors[(editors.indexOf(cm) + direction + editors.length) % editors.length];
  293. editor.scrollToEditor(cm);
  294. cm.focus();
  295. return cm;
  296. }
  297. function getLastActivatedEditor() {
  298. let result;
  299. for (const section of sections) {
  300. if (section.removed) {
  301. continue;
  302. }
  303. // .lastActive is initiated by codemirror-factory
  304. if (!result || section.cm.lastActive > result.lastActive) {
  305. result = section.cm;
  306. }
  307. }
  308. return result;
  309. }
  310. function scrollEntirePageOnCtrlShift(event) {
  311. // make Shift-Ctrl-Wheel scroll entire page even when mouse is over a code editor
  312. if (event.shiftKey && event.ctrlKey && !event.altKey && !event.metaKey) {
  313. // Chrome scrolls horizontally when Shift is pressed but on some PCs this might be different
  314. window.scrollBy(0, event.deltaX || event.deltaY);
  315. event.preventDefault();
  316. }
  317. }
  318. function showMozillaFormat() {
  319. const popup = showCodeMirrorPopup(t('styleToMozillaFormatTitle'), '', {readOnly: true});
  320. popup.codebox.setValue(editor.getValue());
  321. popup.codebox.execCommand('selectAll');
  322. }
  323. function showMozillaFormatImport(text = '') {
  324. const popup = showCodeMirrorPopup(t('styleFromMozillaFormatPrompt'),
  325. $create('.buttons', [
  326. $create('button', {
  327. name: 'import-replace',
  328. textContent: t('importReplaceLabel'),
  329. title: 'Ctrl-Shift-Enter:\n' + t('importReplaceTooltip'),
  330. onclick: () => doImport({replaceOldStyle: true}),
  331. }),
  332. $create('button', {
  333. name: 'import-append',
  334. textContent: t('importAppendLabel'),
  335. title: 'Ctrl-Enter:\n' + t('importAppendTooltip'),
  336. onclick: doImport,
  337. }),
  338. ]));
  339. const contents = $('.contents', popup);
  340. contents.insertBefore(popup.codebox.display.wrapper, contents.firstElementChild);
  341. popup.codebox.focus();
  342. popup.codebox.on('changes', cm => {
  343. popup.classList.toggle('ready', !cm.isBlank());
  344. cm.markClean();
  345. });
  346. if (text) {
  347. popup.codebox.setValue(text);
  348. popup.codebox.clearHistory();
  349. popup.codebox.markClean();
  350. }
  351. // overwrite default extraKeys as those are inapplicable in popup context
  352. popup.codebox.options.extraKeys = {
  353. 'Ctrl-Enter': doImport,
  354. 'Shift-Ctrl-Enter': () => doImport({replaceOldStyle: true}),
  355. };
  356. async function doImport({replaceOldStyle = false}) {
  357. lockPageUI(true);
  358. try {
  359. const code = popup.codebox.getValue().trim();
  360. if (!RX_META.test(code) ||
  361. !await getPreprocessor(code) ||
  362. await messageBoxProxy.confirm(
  363. t('importPreprocessor'), 'pre-line',
  364. t('importPreprocessorTitle'))
  365. ) {
  366. const {sections, errors} = await API.worker.parseMozFormat({code});
  367. if (!sections.length || errors.some(e => !e.recoverable)) {
  368. await Promise.reject(errors);
  369. }
  370. await initSections(sections, {
  371. replace: replaceOldStyle,
  372. focusOn: replaceOldStyle ? 0 : false,
  373. keepDirty: true,
  374. });
  375. helpPopup.close();
  376. }
  377. } catch (err) {
  378. showError(err);
  379. }
  380. lockPageUI(false);
  381. }
  382. async function getPreprocessor(code) {
  383. try {
  384. return (await API.usercss.buildMeta({sourceCode: code})).usercssData.preprocessor;
  385. } catch (e) {}
  386. }
  387. function lockPageUI(locked) {
  388. document.documentElement.style.pointerEvents = locked ? 'none' : '';
  389. if (popup.codebox) {
  390. popup.classList.toggle('ready', locked ? false : !popup.codebox.isBlank());
  391. popup.codebox.options.readOnly = locked;
  392. popup.codebox.display.wrapper.style.opacity = locked ? '.5' : '';
  393. }
  394. }
  395. function showError(errors) {
  396. messageBoxProxy.show({
  397. className: 'center danger',
  398. title: t('styleFromMozillaFormatError'),
  399. contents: $create('pre',
  400. (Array.isArray(errors) ? errors : [errors])
  401. .map(e => e.message || e).join('\n')),
  402. buttons: [t('confirmClose')],
  403. });
  404. }
  405. }
  406. function updateSectionOrder() {
  407. const oldOrder = sectionOrder;
  408. const validSections = sections.filter(s => !s.removed);
  409. sectionOrder = validSections.map(s => s.id).join(',');
  410. dirty.modify('sectionOrder', oldOrder, sectionOrder);
  411. container.dataset.sectionCount = validSections.length;
  412. linterMan.refreshReport();
  413. editor.updateToc();
  414. }
  415. /** @returns {StyleObj} */
  416. function getModel() {
  417. return Object.assign({}, style, {
  418. sections: sections.filter(s => !s.removed).map(s => s.getModel()),
  419. });
  420. }
  421. function validate() {
  422. if (!$('#name').reportValidity()) {
  423. messageBoxProxy.alert(t('styleMissingName'));
  424. return false;
  425. }
  426. for (const section of sections) {
  427. for (const apply of section.appliesTo) {
  428. if (apply.type !== 'regexp') {
  429. continue;
  430. }
  431. if (!apply.valueEl.reportValidity()) {
  432. messageBoxProxy.alert(t('styleBadRegexp'));
  433. return false;
  434. }
  435. }
  436. }
  437. return true;
  438. }
  439. function destroyRemovedSections() {
  440. for (let i = 0; i < sections.length;) {
  441. if (!sections[i].removed) {
  442. i++;
  443. continue;
  444. }
  445. sections[i].destroy();
  446. sections[i].el.remove();
  447. sections.splice(i, 1);
  448. }
  449. }
  450. function updateHeader() {
  451. $('#name').value = style.customName || style.name || '';
  452. $('#enabled').checked = style.enabled !== false;
  453. $('#url').href = style.url || '';
  454. editor.updateName();
  455. }
  456. function updateLivePreview() {
  457. debounce(updateLivePreviewNow, editor.previewDelay);
  458. }
  459. function updateLivePreviewNow() {
  460. editor.livePreview.update(getModel());
  461. }
  462. async function initSections(src, {
  463. focusOn = 0,
  464. replace = false,
  465. keepDirty = false, // used by import
  466. } = {}) {
  467. if (replace) {
  468. sections.forEach(s => s.remove(true));
  469. sections.length = 0;
  470. container.textContent = '';
  471. }
  472. let si = editor.scrollInfo;
  473. if (si && si.cms && si.cms.length === src.length) {
  474. si.scrollY2 = si.scrollY + window.innerHeight;
  475. container.style.height = si.scrollY2 + 'px';
  476. scrollTo(0, si.scrollY);
  477. // only restore focus if it's the first CM to avoid derpy quirks
  478. focusOn = si.cms[0].focus && 0;
  479. } else {
  480. si = null;
  481. }
  482. let forceRefresh = true;
  483. let y = 0;
  484. let tPrev;
  485. for (let i = 0; i < src.length; i++) {
  486. const t = performance.now();
  487. if (!tPrev) {
  488. tPrev = t;
  489. } else if (t - tPrev > 100) {
  490. tPrev = 0;
  491. forceRefresh = false;
  492. await new Promise(setTimeout);
  493. }
  494. if (si) forceRefresh = y < si.scrollY2 && (y += si.cms[i].parentHeight) > si.scrollY;
  495. insertSectionAfter(src[i], null, forceRefresh, si && si.cms[i]);
  496. setGlobalProgress(i, src.length);
  497. if (!keepDirty) dirty.clear();
  498. if (i === focusOn) sections[i].cm.focus();
  499. }
  500. if (!si) requestAnimationFrame(fitToAvailableSpace);
  501. container.style.removeProperty('height');
  502. setGlobalProgress();
  503. }
  504. /** @param {EditorSection} section */
  505. function removeSection(section) {
  506. if (sections.every(s => s.removed || s === section)) {
  507. // TODO: hide remove button when `#sections[data-section-count=1]`
  508. throw new Error('Cannot remove last section');
  509. }
  510. if (section.cm.isBlank()) {
  511. const index = sections.indexOf(section);
  512. sections.splice(index, 1);
  513. section.el.remove();
  514. section.remove();
  515. section.destroy();
  516. } else {
  517. const lines = [];
  518. const MAX_LINES = 10;
  519. section.cm.doc.iter(0, MAX_LINES + 1, ({text}) => lines.push(text) && false);
  520. const title = t('sectionCode') + '\n' +
  521. '-'.repeat(20) + '\n' +
  522. lines.slice(0, MAX_LINES).map(s => clipString(s, 100)).join('\n') +
  523. (lines.length > MAX_LINES ? '\n...' : '');
  524. $('.deleted-section', section.el).title = title;
  525. section.remove();
  526. }
  527. dirty.remove(section, section);
  528. updateSectionOrder();
  529. section.off(updateLivePreview);
  530. updateLivePreview();
  531. }
  532. /** @param {EditorSection} section */
  533. function restoreSection(section) {
  534. section.restore();
  535. updateSectionOrder();
  536. section.onChange(updateLivePreview);
  537. updateLivePreview();
  538. }
  539. /**
  540. * @param {StyleSection} [init]
  541. * @param {EditorSection} [base]
  542. * @param {boolean} [forceRefresh]
  543. * @param {EditorScrollInfo} [si]
  544. */
  545. function insertSectionAfter(init, base, forceRefresh, si) {
  546. if (!init) {
  547. init = {code: '', urlPrefixes: ['http://example.com']};
  548. }
  549. const section = createSection(init, genId, si);
  550. const {cm} = section;
  551. const {code} = init;
  552. const index = base ? sections.indexOf(base) + 1 : sections.length;
  553. sections.splice(index, 0, section);
  554. container.insertBefore(section.el, base ? base.el.nextSibling : null);
  555. refreshOnView(cm, {code, force: base || forceRefresh});
  556. registerEvents(section);
  557. if ((!si || !si.height) && (!base || code)) {
  558. // Fit a) during startup or b) when the clone button is clicked on a section with some code
  559. fitToContent(section);
  560. }
  561. if (base) {
  562. cm.focus();
  563. editor.scrollToEditor(cm);
  564. }
  565. updateSectionOrder();
  566. updateLivePreview();
  567. section.onChange(updateLivePreview);
  568. }
  569. /** @param {EditorSection} section */
  570. function moveSectionUp(section) {
  571. const index = sections.indexOf(section);
  572. if (index === 0) {
  573. return;
  574. }
  575. container.insertBefore(section.el, sections[index - 1].el);
  576. sections[index] = sections[index - 1];
  577. sections[index - 1] = section;
  578. updateSectionOrder();
  579. }
  580. /** @param {EditorSection} section */
  581. function moveSectionDown(section) {
  582. const index = sections.indexOf(section);
  583. if (index === sections.length - 1) {
  584. return;
  585. }
  586. container.insertBefore(sections[index + 1].el, section.el);
  587. sections[index] = sections[index + 1];
  588. sections[index + 1] = section;
  589. updateSectionOrder();
  590. }
  591. /** @param {EditorSection} section */
  592. function registerEvents(section) {
  593. const {el, cm} = section;
  594. $('.applies-to-help', el).onclick = () => helpPopup.show(t('appliesLabel'), t('appliesHelp'));
  595. $('.remove-section', el).onclick = () => removeSection(section);
  596. $('.add-section', el).onclick = () => insertSectionAfter(undefined, section);
  597. $('.clone-section', el).onclick = () => insertSectionAfter(section.getModel(), section);
  598. $('.move-section-up', el).onclick = () => moveSectionUp(section);
  599. $('.move-section-down', el).onclick = () => moveSectionDown(section);
  600. $('.restore-section', el).onclick = () => restoreSection(section);
  601. cm.on('paste', maybeImportOnPaste);
  602. if (!FIREFOX) {
  603. cm.on('mousedown', (cm, event) => toggleContextMenuDelete.call(cm, event));
  604. }
  605. }
  606. function maybeImportOnPaste(cm, event) {
  607. const text = event.clipboardData.getData('text') || '';
  608. if (/@-moz-document/i.test(text) &&
  609. /@-moz-document\s+(url|url-prefix|domain|regexp)\(/i
  610. .test(text.replace(/\/\*([^*]|\*(?!\/))*(\*\/|$)/g, ''))
  611. ) {
  612. event.preventDefault();
  613. showMozillaFormatImport(text);
  614. }
  615. }
  616. function refreshOnView(cm, {code, force} = {}) {
  617. if (code) {
  618. linterMan.enableForEditor(cm, code);
  619. }
  620. if (force || !xo) {
  621. refreshOnViewNow(cm);
  622. } else {
  623. xo.observe(cm.display.wrapper);
  624. }
  625. }
  626. /** @param {IntersectionObserverEntry[]} entries */
  627. function refreshOnViewListener(entries) {
  628. for (const e of entries) {
  629. const r = e.isIntersecting && e.intersectionRect;
  630. if (r) {
  631. xo.unobserve(e.target);
  632. const cm = e.target.CodeMirror;
  633. if (r.bottom > 0 && r.top < window.innerHeight) {
  634. refreshOnViewNow(cm);
  635. } else {
  636. setTimeout(refreshOnViewNow, 0, cm);
  637. }
  638. }
  639. }
  640. }
  641. async function refreshOnViewNow(cm) {
  642. linterMan.enableForEditor(cm);
  643. cm.refresh();
  644. }
  645. function toggleContextMenuDelete(event) {
  646. if (chrome.contextMenus && event.button === 2 && prefs.get('editor.contextDelete')) {
  647. chrome.contextMenus.update('editor.contextDelete', {
  648. enabled: Boolean(
  649. this.selectionStart !== this.selectionEnd ||
  650. this.somethingSelected && this.somethingSelected()
  651. ),
  652. }, ignoreChromeError);
  653. }
  654. }
  655. }