edit.js 63 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090
  1. /* eslint brace-style: 0, operator-linebreak: 0 */
  2. /* global CodeMirror parserlib */
  3. /* global loadScript */
  4. /* global css_beautify */
  5. /* global CSSLint initLint linterConfig updateLintReport renderLintReport updateLinter */
  6. /* global mozParser createSourceEditor */
  7. /* global closeCurrentTab regExpTester messageBox */
  8. /* global initColorpicker */
  9. 'use strict';
  10. onDOMready()
  11. .then(() => Promise.all([
  12. initColorpicker(),
  13. initCollapsibles(),
  14. ]))
  15. .then(init);
  16. let styleId = null;
  17. // only the actually dirty items here
  18. let dirty = {};
  19. // array of all CodeMirror instances
  20. const editors = [];
  21. let saveSizeOnClose;
  22. // use browser history back when 'back to manage' is clicked
  23. let useHistoryBack;
  24. // direct & reverse mapping of @-moz-document keywords and internal property names
  25. const propertyToCss = {urls: 'url', urlPrefixes: 'url-prefix', domains: 'domain', regexps: 'regexp'};
  26. const CssToProperty = {'url': 'urls', 'url-prefix': 'urlPrefixes', 'domain': 'domains', 'regexp': 'regexps'};
  27. let editor;
  28. // if background page hasn't been loaded yet, increase the chances it has before DOMContentLoaded
  29. onBackgroundReady();
  30. // make querySelectorAll enumeration code readable
  31. ['forEach', 'some', 'indexOf', 'map'].forEach(method => {
  32. NodeList.prototype[method] = Array.prototype[method];
  33. });
  34. // Chrome pre-34
  35. Element.prototype.matches = Element.prototype.matches || Element.prototype.webkitMatchesSelector;
  36. // Chrome pre-41 polyfill
  37. Element.prototype.closest = Element.prototype.closest || function (selector) {
  38. let e;
  39. // eslint-disable-next-line no-empty
  40. for (e = this; e && !e.matches(selector); e = e.parentElement) {}
  41. return e;
  42. };
  43. // eslint-disable-next-line no-extend-native
  44. Array.prototype.rotate = function (amount) {
  45. // negative amount == rotate left
  46. const r = this.slice(-amount, this.length);
  47. Array.prototype.push.apply(r, this.slice(0, this.length - r.length));
  48. return r;
  49. };
  50. // eslint-disable-next-line no-extend-native
  51. Object.defineProperty(Array.prototype, 'last', {get: function () { return this[this.length - 1]; }});
  52. // preload the theme so that CodeMirror can calculate its metrics in DOMContentLoaded->setupLivePrefs()
  53. new MutationObserver((mutations, observer) => {
  54. const themeElement = $('#cm-theme');
  55. if (themeElement) {
  56. themeElement.href = prefs.get('editor.theme') === 'default' ? ''
  57. : 'vendor/codemirror/theme/' + prefs.get('editor.theme') + '.css';
  58. observer.disconnect();
  59. }
  60. }).observe(document, {subtree: true, childList: true});
  61. getCodeMirrorThemes();
  62. // reroute handling to nearest editor when keypress resolves to one of these commands
  63. const hotkeyRerouter = {
  64. commands: {
  65. save: true, jumpToLine: true, nextEditor: true, prevEditor: true,
  66. find: true, findNext: true, findPrev: true, replace: true, replaceAll: true,
  67. toggleStyle: true,
  68. colorpicker: true,
  69. },
  70. setState: enable => {
  71. setTimeout(() => {
  72. document[(enable ? 'add' : 'remove') + 'EventListener']('keydown', hotkeyRerouter.eventHandler);
  73. }, 0);
  74. },
  75. eventHandler: event => {
  76. const keyName = CodeMirror.keyName(event);
  77. if (
  78. CodeMirror.lookupKey(keyName, CodeMirror.getOption('keyMap'), handleCommand) === 'handled' ||
  79. CodeMirror.lookupKey(keyName, CodeMirror.defaults.extraKeys, handleCommand) === 'handled'
  80. ) {
  81. event.preventDefault();
  82. event.stopPropagation();
  83. }
  84. function handleCommand(command) {
  85. if (hotkeyRerouter.commands[command] === true) {
  86. CodeMirror.commands[command](getEditorInSight(event.target));
  87. return true;
  88. }
  89. }
  90. }
  91. };
  92. function onChange(event) {
  93. const node = event.target;
  94. if ('savedValue' in node) {
  95. const currentValue = node.type === 'checkbox' ? node.checked : node.value;
  96. setCleanItem(node, node.savedValue === currentValue);
  97. } else {
  98. // the manually added section's applies-to is dirty only when the value is non-empty
  99. setCleanItem(node, node.localName !== 'input' || !node.value.trim());
  100. // only valid when actually saved
  101. delete node.savedValue;
  102. }
  103. updateTitle();
  104. }
  105. // Set .dirty on stylesheet contributors that have changed
  106. function setDirtyClass(node, isDirty) {
  107. node.classList.toggle('dirty', isDirty);
  108. }
  109. function setCleanItem(node, isClean) {
  110. if (!node.id) {
  111. node.id = Date.now().toString(32).substr(-6);
  112. }
  113. if (isClean) {
  114. delete dirty[node.id];
  115. // code sections have .CodeMirror property
  116. if (node.CodeMirror) {
  117. node.savedValue = node.CodeMirror.changeGeneration();
  118. } else {
  119. node.savedValue = node.type === 'checkbox' ? node.checked : node.value;
  120. }
  121. } else {
  122. dirty[node.id] = true;
  123. }
  124. setDirtyClass(node, !isClean);
  125. }
  126. function isCleanGlobal() {
  127. const clean = Object.keys(dirty).length === 0;
  128. setDirtyClass(document.body, !clean);
  129. // let saveBtn = $('#save-button')
  130. // if (clean){
  131. // //saveBtn.removeAttribute('disabled');
  132. // }else{
  133. // //saveBtn.setAttribute('disabled', true);
  134. // }
  135. return clean;
  136. }
  137. function setCleanGlobal() {
  138. $$('#header, #sections > div').forEach(setCleanSection);
  139. // forget the dirty applies-to ids from a deleted section after the style was saved
  140. dirty = {};
  141. }
  142. function setCleanSection(section) {
  143. $$('.style-contributor', section).forEach(node => { setCleanItem(node, true); });
  144. // #header section has no codemirror
  145. const cm = section.CodeMirror;
  146. if (cm) {
  147. section.savedValue = cm.changeGeneration();
  148. updateTitle();
  149. }
  150. }
  151. function initCodeMirror() {
  152. const CM = CodeMirror;
  153. // lint.js is not loaded initially
  154. CM.defaults.lint = linterConfig.getForCodeMirror();
  155. // additional commands
  156. CM.commands.jumpToLine = jumpToLine;
  157. CM.commands.nextEditor = cm => nextPrevEditor(cm, 1);
  158. CM.commands.prevEditor = cm => nextPrevEditor(cm, -1);
  159. CM.commands.save = save;
  160. CM.commands.toggleStyle = toggleStyle;
  161. // user option values
  162. CM.getOption = o => CodeMirror.defaults[o];
  163. CM.setOption = (o, v) => {
  164. CodeMirror.defaults[o] = v;
  165. if (editors.length > 4 && (o === 'theme' || o === 'lineWrapping')) {
  166. throttleSetOption({key: o, value: v, index: 0});
  167. return;
  168. }
  169. editors.forEach(editor => {
  170. editor.setOption(o, v);
  171. });
  172. };
  173. CM.prototype.getSection = function () {
  174. return this.display.wrapper.parentNode;
  175. };
  176. // initialize global editor controls
  177. function optionsFromArray(parent, options) {
  178. const fragment = document.createDocumentFragment();
  179. for (const opt of options) {
  180. fragment.appendChild($element({tag: 'option', textContent: opt}));
  181. }
  182. parent.appendChild(fragment);
  183. }
  184. // no need to escape the period in the id
  185. const themeControl = $('#editor.theme');
  186. const themeList = localStorage.codeMirrorThemes;
  187. if (themeList) {
  188. optionsFromArray(themeControl, themeList.split(/\s+/));
  189. } else {
  190. // Chrome is starting up and shows our edit.html, but the background page isn't loaded yet
  191. const theme = prefs.get('editor.theme');
  192. optionsFromArray(themeControl, [theme === 'default' ? t('defaultTheme') : theme]);
  193. getCodeMirrorThemes().then(() => {
  194. const themes = (localStorage.codeMirrorThemes || '').split(/\s+/);
  195. optionsFromArray(themeControl, themes);
  196. themeControl.selectedIndex = Math.max(0, themes.indexOf(theme));
  197. });
  198. }
  199. optionsFromArray($('#editor.keyMap'), Object.keys(CM.keyMap).sort());
  200. $('#options').addEventListener('change', acmeEventListener, false);
  201. setupLivePrefs();
  202. hotkeyRerouter.setState(true);
  203. const THROTTLE_AFTER_MS = 100;
  204. const THROTTLE_SHOW_PROGRESS_AFTER_MS = 100;
  205. function throttleSetOption({
  206. key,
  207. value,
  208. index,
  209. timeStart = performance.now(),
  210. cmStart = editors.lastActive || editors[0],
  211. editorsCopy = editors.slice(),
  212. progress,
  213. }) {
  214. if (index === 0) {
  215. if (!cmStart) {
  216. return;
  217. }
  218. cmStart.setOption(key, value);
  219. }
  220. const t0 = performance.now();
  221. const total = editorsCopy.length;
  222. while (index < total) {
  223. const cm = editorsCopy[index++];
  224. if (cm === cmStart ||
  225. cm !== editors[index] && !editors.includes(cm)) {
  226. continue;
  227. }
  228. cm.setOption(key, value);
  229. if (performance.now() - t0 > THROTTLE_AFTER_MS) {
  230. break;
  231. }
  232. }
  233. if (index >= total) {
  234. if (progress) {
  235. progress.remove();
  236. }
  237. return;
  238. }
  239. if (!progress &&
  240. index < total / 2 &&
  241. t0 - timeStart > THROTTLE_SHOW_PROGRESS_AFTER_MS) {
  242. let option = $('#editor.' + key);
  243. if (option) {
  244. if (option.type === 'checkbox') {
  245. option = (option.labels || [])[0] || option.nextElementSibling || option;
  246. }
  247. progress = document.body.appendChild($element({
  248. className: 'set-option-progress',
  249. targetElement: option,
  250. }));
  251. }
  252. }
  253. if (progress) {
  254. const optionBounds = progress.targetElement.getBoundingClientRect();
  255. const bounds = {
  256. top: optionBounds.top + window.scrollY + 1,
  257. left: optionBounds.left + window.scrollX + 1,
  258. width: (optionBounds.width - 2) * index / total | 0,
  259. height: optionBounds.height - 2,
  260. };
  261. const style = progress.style;
  262. for (const prop in bounds) {
  263. if (bounds[prop] !== parseFloat(style[prop])) {
  264. style[prop] = bounds[prop] + 'px';
  265. }
  266. }
  267. }
  268. setTimeout(throttleSetOption, 0, {
  269. key,
  270. value,
  271. index,
  272. timeStart,
  273. cmStart,
  274. editorsCopy,
  275. progress,
  276. });
  277. }
  278. }
  279. function acmeEventListener(event) {
  280. const el = event.target;
  281. let option = el.id.replace(/^editor\./, '');
  282. //console.log('acmeEventListener heard %s on %s', event.type, el.id);
  283. if (!option) {
  284. console.error('acmeEventListener: no "cm_option" %O', el);
  285. return;
  286. }
  287. let value = el.type === 'checkbox' ? el.checked : el.value;
  288. switch (option) {
  289. case 'tabSize':
  290. CodeMirror.setOption('indentUnit', Number(value));
  291. break;
  292. case 'theme': {
  293. const themeLink = $('#cm-theme');
  294. // use non-localized 'default' internally
  295. if (!value || value === 'default' || value === t('defaultTheme')) {
  296. value = 'default';
  297. if (prefs.get(el.id) !== value) {
  298. prefs.set(el.id, value);
  299. }
  300. themeLink.href = '';
  301. el.selectedIndex = 0;
  302. break;
  303. }
  304. const url = chrome.runtime.getURL('vendor/codemirror/theme/' + value + '.css');
  305. if (themeLink.href === url) {
  306. // preloaded in initCodeMirror()
  307. break;
  308. }
  309. // avoid flicker: wait for the second stylesheet to load, then apply the theme
  310. document.head.appendChild($element({
  311. tag: 'link',
  312. id: 'cm-theme2',
  313. rel: 'stylesheet',
  314. href: url
  315. }));
  316. setTimeout(() => {
  317. CodeMirror.setOption(option, value);
  318. themeLink.remove();
  319. $('#cm-theme2').id = 'cm-theme';
  320. }, 100);
  321. return;
  322. }
  323. case 'autocompleteOnTyping':
  324. editors.forEach(cm => setupAutocomplete(cm, el.checked));
  325. return;
  326. case 'autoCloseBrackets':
  327. Promise.resolve(value && loadScript('/vendor/codemirror/addon/edit/closebrackets.js')).then(() => {
  328. CodeMirror.setOption(option, value);
  329. });
  330. return;
  331. case 'matchHighlight':
  332. switch (value) {
  333. case 'token':
  334. case 'selection':
  335. document.body.dataset[option] = value;
  336. value = {showToken: value === 'token' && /[#.\-\w]/, annotateScrollbar: true};
  337. break;
  338. default:
  339. value = null;
  340. }
  341. option = 'highlightSelectionMatches';
  342. break;
  343. case 'colorpicker':
  344. return;
  345. }
  346. CodeMirror.setOption(option, value);
  347. }
  348. function setupCodeMirror(sectionDiv, code, index) {
  349. const cm = CodeMirror(wrapper => {
  350. $('.code-label', sectionDiv).insertAdjacentElement('afterend', wrapper);
  351. }, {
  352. value: code,
  353. lint: null,
  354. });
  355. const wrapper = cm.display.wrapper;
  356. cm.on('changes', indicateCodeChangeDebounced);
  357. if (prefs.get('editor.autocompleteOnTyping')) {
  358. setupAutocomplete(cm);
  359. }
  360. wrapper.addEventListener('keydown', event => nextPrevEditorOnKeydown(cm, event), true);
  361. cm.on('blur', () => {
  362. editors.lastActive = cm;
  363. hotkeyRerouter.setState(true);
  364. setTimeout(() => {
  365. wrapper.classList.toggle('CodeMirror-active', wrapper.contains(document.activeElement));
  366. });
  367. });
  368. cm.on('focus', () => {
  369. hotkeyRerouter.setState(false);
  370. wrapper.classList.add('CodeMirror-active');
  371. });
  372. cm.on('paste', (cm, event) => {
  373. const text = event.clipboardData.getData('text') || '';
  374. if (
  375. text.includes('@-moz-document') &&
  376. text.replace(/\/\*[\s\S]*?\*\//g, '')
  377. .match(/@-moz-document[\s\r\n]+(url|url-prefix|domain|regexp)\(/)
  378. ) {
  379. event.preventDefault();
  380. fromMozillaFormat();
  381. $('#help-popup').codebox.setValue(text);
  382. $('#help-popup').codebox.clearHistory();
  383. $('#help-popup').codebox.markClean();
  384. }
  385. if (editors.length === 1) {
  386. setTimeout(() => {
  387. if (cm.display.sizer.clientHeight > cm.display.wrapper.clientHeight) {
  388. maximizeCodeHeight.stats = null;
  389. maximizeCodeHeight(cm.getSection(), true);
  390. }
  391. });
  392. }
  393. });
  394. if (!FIREFOX) {
  395. cm.on('mousedown', (cm, event) => toggleContextMenuDelete.call(cm, event));
  396. }
  397. wrapper.classList.add('resize-grip-enabled');
  398. let lastClickTime = 0;
  399. const resizeGrip = wrapper.appendChild(template.resizeGrip.cloneNode(true));
  400. resizeGrip.onmousedown = event => {
  401. if (event.button !== 0) {
  402. return;
  403. }
  404. event.preventDefault();
  405. if (Date.now() - lastClickTime < 500) {
  406. lastClickTime = 0;
  407. toggleSectionHeight(cm);
  408. return;
  409. }
  410. lastClickTime = Date.now();
  411. const minHeight = cm.defaultTextHeight() +
  412. /* .CodeMirror-lines padding */
  413. cm.display.lineDiv.offsetParent.offsetTop +
  414. /* borders */
  415. wrapper.offsetHeight - wrapper.clientHeight;
  416. wrapper.style.pointerEvents = 'none';
  417. document.body.style.cursor = 's-resize';
  418. function resize(e) {
  419. const cmPageY = wrapper.getBoundingClientRect().top + window.scrollY;
  420. const height = Math.max(minHeight, e.pageY - cmPageY);
  421. if (height !== wrapper.clientHeight) {
  422. cm.setSize(null, height);
  423. }
  424. }
  425. document.addEventListener('mousemove', resize);
  426. document.addEventListener('mouseup', function resizeStop() {
  427. document.removeEventListener('mouseup', resizeStop);
  428. document.removeEventListener('mousemove', resize);
  429. wrapper.style.pointerEvents = '';
  430. document.body.style.cursor = '';
  431. });
  432. };
  433. editors.splice(index || editors.length, 0, cm);
  434. return cm;
  435. }
  436. function indicateCodeChange(cm) {
  437. const section = cm.getSection();
  438. setCleanItem(section, cm.isClean(section.savedValue));
  439. updateTitle();
  440. updateLintReportIfEnabled(cm);
  441. }
  442. function indicateCodeChangeDebounced(cm, ...args) {
  443. clearTimeout(cm.state.stylusOnChangeTimer);
  444. cm.state.stylusOnChangeTimer = setTimeout(indicateCodeChange, 200, cm, ...args);
  445. }
  446. function getSectionForChild(e) {
  447. return e.closest('#sections > div');
  448. }
  449. function getSections() {
  450. return $$('#sections > div');
  451. }
  452. // remind Chrome to repaint a previously invisible editor box by toggling any element's transform
  453. // this bug is present in some versions of Chrome (v37-40 or something)
  454. document.addEventListener('scroll', () => {
  455. const style = $('#name').style;
  456. style.webkitTransform = style.webkitTransform ? '' : 'scale(1)';
  457. });
  458. // Shift-Ctrl-Wheel scrolls entire page even when mouse is over a code editor
  459. document.addEventListener('wheel', event => {
  460. if (event.shiftKey && event.ctrlKey && !event.altKey && !event.metaKey) {
  461. // Chrome scrolls horizontally when Shift is pressed but on some PCs this might be different
  462. window.scrollBy(0, event.deltaX || event.deltaY);
  463. event.preventDefault();
  464. }
  465. });
  466. chrome.windows && queryTabs({currentWindow: true}).then(tabs => {
  467. const windowId = tabs[0].windowId;
  468. if (prefs.get('openEditInWindow')) {
  469. if (
  470. /true/.test(sessionStorage.saveSizeOnClose) &&
  471. 'left' in prefs.get('windowPosition', {}) &&
  472. !isWindowMaximized()
  473. ) {
  474. // window was reopened via Ctrl-Shift-T etc.
  475. chrome.windows.update(windowId, prefs.get('windowPosition'));
  476. }
  477. if (tabs.length === 1 && window.history.length === 1) {
  478. chrome.windows.getAll(windows => {
  479. if (windows.length > 1) {
  480. sessionStorageHash('saveSizeOnClose').set(windowId, true);
  481. saveSizeOnClose = true;
  482. }
  483. });
  484. } else {
  485. saveSizeOnClose = sessionStorageHash('saveSizeOnClose').value[windowId];
  486. }
  487. }
  488. });
  489. getOwnTab().then(tab => {
  490. const ownTabId = tab.id;
  491. useHistoryBack = sessionStorageHash('manageStylesHistory').value[ownTabId] === location.href;
  492. if (!chrome.windows) {
  493. return;
  494. }
  495. // When an edit page gets attached or detached, remember its state
  496. // so we can do the same to the next one to open.
  497. chrome.tabs.onAttached.addListener((tabId, info) => {
  498. if (tabId !== ownTabId) {
  499. return;
  500. }
  501. if (info.newPosition !== 0) {
  502. prefs.set('openEditInWindow', false);
  503. return;
  504. }
  505. chrome.windows.get(info.newWindowId, {populate: true}, win => {
  506. // If there's only one tab in this window, it's been dragged to new window
  507. const openEditInWindow = win.tabs.length === 1;
  508. if (openEditInWindow && FIREFOX) {
  509. // FF-only because Chrome retardedly resets the size during dragging
  510. chrome.windows.update(info.newWindowId, prefs.get('windowPosition'));
  511. }
  512. prefs.set('openEditInWindow', openEditInWindow);
  513. });
  514. });
  515. });
  516. function goBackToManage(event) {
  517. if (useHistoryBack) {
  518. event.stopPropagation();
  519. event.preventDefault();
  520. history.back();
  521. }
  522. }
  523. function isWindowMaximized() {
  524. return (
  525. window.screenX <= 0 &&
  526. window.screenY <= 0 &&
  527. window.outerWidth >= screen.availWidth &&
  528. window.outerHeight >= screen.availHeight &&
  529. window.screenX > -10 &&
  530. window.screenY > -10 &&
  531. window.outerWidth < screen.availWidth + 10 &&
  532. window.outerHeight < screen.availHeight + 10
  533. );
  534. }
  535. function rememberWindowSize() {
  536. if (
  537. document.visibilityState === 'visible' &&
  538. prefs.get('openEditInWindow') &&
  539. !isWindowMaximized()
  540. ) {
  541. prefs.set('windowPosition', {
  542. left: window.screenX,
  543. top: window.screenY,
  544. width: window.outerWidth,
  545. height: window.outerHeight,
  546. });
  547. }
  548. }
  549. window.onbeforeunload = () => {
  550. if (saveSizeOnClose) {
  551. rememberWindowSize();
  552. }
  553. document.activeElement.blur();
  554. if (isClean()) {
  555. return;
  556. }
  557. updateLintReportIfEnabled(null, 0);
  558. // neither confirm() nor custom messages work in modern browsers but just in case
  559. return t('styleChangesNotSaved');
  560. function isClean() {
  561. if (editor) {
  562. return !editor.isDirty();
  563. } else {
  564. return isCleanGlobal();
  565. }
  566. }
  567. };
  568. function addAppliesTo(list, name, value) {
  569. const showingEverything = $('.applies-to-everything', list) !== null;
  570. // blow away 'Everything' if it's there
  571. if (showingEverything) {
  572. list.removeChild(list.firstChild);
  573. }
  574. let e;
  575. if (name) {
  576. e = template.appliesTo.cloneNode(true);
  577. $('[name=applies-type]', e).value = name;
  578. $('[name=applies-value]', e).value = value;
  579. $('.remove-applies-to', e).addEventListener('click', removeAppliesTo, false);
  580. } else if (showingEverything || list.hasChildNodes()) {
  581. e = template.appliesTo.cloneNode(true);
  582. if (list.hasChildNodes()) {
  583. $('[name=applies-type]', e).value = $('li:last-child [name="applies-type"]', list).value;
  584. }
  585. $('.remove-applies-to', e).addEventListener('click', removeAppliesTo, false);
  586. } else {
  587. e = template.appliesToEverything.cloneNode(true);
  588. }
  589. $('.add-applies-to', e).addEventListener('click', function () {
  590. addAppliesTo(this.parentNode.parentNode);
  591. }, false);
  592. list.appendChild(e);
  593. }
  594. function addSection(event, section) {
  595. const div = template.section.cloneNode(true);
  596. $('.applies-to-help', div).addEventListener('click', showAppliesToHelp, false);
  597. $('.remove-section', div).addEventListener('click', removeSection, false);
  598. $('.add-section', div).addEventListener('click', addSection, false);
  599. $('.beautify-section', div).addEventListener('click', beautify);
  600. const code = (section || {}).code || '';
  601. const appliesTo = $('.applies-to-list', div);
  602. let appliesToAdded = false;
  603. if (section) {
  604. for (const i in propertyToCss) {
  605. if (section[i]) {
  606. section[i].forEach(url => {
  607. addAppliesTo(appliesTo, propertyToCss[i], url);
  608. appliesToAdded = true;
  609. });
  610. }
  611. }
  612. }
  613. if (!appliesToAdded) {
  614. addAppliesTo(appliesTo);
  615. }
  616. appliesTo.addEventListener('change', onChange);
  617. appliesTo.addEventListener('input', onChange);
  618. toggleTestRegExpVisibility();
  619. appliesTo.addEventListener('change', toggleTestRegExpVisibility);
  620. $('.test-regexp', div).onclick = () => {
  621. regExpTester.toggle();
  622. regExpTester.update(getRegExps());
  623. };
  624. function getRegExps() {
  625. return [...appliesTo.children]
  626. .map(item =>
  627. !item.matches('.applies-to-everything') &&
  628. $('.applies-type', item).value === 'regexp' &&
  629. $('.applies-value', item).value.trim()
  630. )
  631. .filter(item => item);
  632. }
  633. function toggleTestRegExpVisibility() {
  634. const show = getRegExps().length > 0;
  635. div.classList.toggle('has-regexp', show);
  636. appliesTo.oninput = appliesTo.oninput || show && (event => {
  637. if (
  638. event.target.matches('.applies-value') &&
  639. $('.applies-type', event.target.parentElement).value === 'regexp'
  640. ) {
  641. regExpTester.update(getRegExps());
  642. }
  643. });
  644. }
  645. const sections = $('#sections');
  646. let cm;
  647. if (event) {
  648. const clickedSection = getSectionForChild(event.target);
  649. sections.insertBefore(div, clickedSection.nextElementSibling);
  650. const newIndex = getSections().indexOf(clickedSection) + 1;
  651. cm = setupCodeMirror(div, code, newIndex);
  652. makeSectionVisible(cm);
  653. renderLintReport();
  654. cm.focus();
  655. } else {
  656. sections.appendChild(div);
  657. cm = setupCodeMirror(div, code);
  658. }
  659. div.CodeMirror = cm;
  660. setCleanSection(div);
  661. return div;
  662. }
  663. function removeAppliesTo(event) {
  664. const appliesTo = event.target.parentNode;
  665. const appliesToList = appliesTo.parentNode;
  666. removeAreaAndSetDirty(appliesTo);
  667. if (!appliesToList.hasChildNodes()) {
  668. addAppliesTo(appliesToList);
  669. }
  670. }
  671. function removeSection(event) {
  672. const section = getSectionForChild(event.target);
  673. const cm = section.CodeMirror;
  674. removeAreaAndSetDirty(section);
  675. editors.splice(editors.indexOf(cm), 1);
  676. renderLintReport();
  677. }
  678. function removeAreaAndSetDirty(area) {
  679. const contributors = $$('.style-contributor', area);
  680. if (!contributors.length) {
  681. setCleanItem(area, false);
  682. }
  683. contributors.some(node => {
  684. if (node.savedValue) {
  685. // it's a saved section, so make it dirty and stop the enumeration
  686. setCleanItem(area, false);
  687. return true;
  688. } else {
  689. // it's an empty section, so undirty the applies-to items,
  690. // otherwise orphaned ids would keep the style dirty
  691. setCleanItem(node, true);
  692. }
  693. });
  694. updateTitle();
  695. area.parentNode.removeChild(area);
  696. }
  697. function makeSectionVisible(cm) {
  698. const section = cm.getSection();
  699. const bounds = section.getBoundingClientRect();
  700. if (
  701. (bounds.bottom > window.innerHeight && bounds.top > 0) ||
  702. (bounds.top < 0 && bounds.bottom < window.innerHeight)
  703. ) {
  704. if (bounds.top < 0) {
  705. window.scrollBy(0, bounds.top - 1);
  706. } else {
  707. window.scrollBy(0, bounds.bottom - window.innerHeight + 1);
  708. }
  709. }
  710. }
  711. function setupGlobalSearch() {
  712. const originalCommand = {
  713. find: CodeMirror.commands.find,
  714. findNext: CodeMirror.commands.findNext,
  715. findPrev: CodeMirror.commands.findPrev,
  716. replace: CodeMirror.commands.replace
  717. };
  718. const originalOpenDialog = CodeMirror.prototype.openDialog;
  719. const originalOpenConfirm = CodeMirror.prototype.openConfirm;
  720. // cm.state.search for last used 'find'
  721. let curState;
  722. function shouldIgnoreCase(query) {
  723. // treat all-lowercase non-regexp queries as case-insensitive
  724. return typeof query === 'string' && query === query.toLowerCase();
  725. }
  726. function updateState(cm, newState) {
  727. if (!newState) {
  728. if (cm.state.search) {
  729. return cm.state.search;
  730. }
  731. if (!curState) {
  732. return null;
  733. }
  734. newState = curState;
  735. }
  736. cm.state.search = {
  737. query: newState.query,
  738. overlay: newState.overlay,
  739. annotate: cm.showMatchesOnScrollbar(newState.query, shouldIgnoreCase(newState.query))
  740. };
  741. cm.addOverlay(newState.overlay);
  742. return cm.state.search;
  743. }
  744. // overrides the original openDialog with a clone of the provided template
  745. function customizeOpenDialog(cm, template, callback) {
  746. cm.openDialog = (tmpl, cb, opt) => {
  747. // invoke 'callback' and bind 'this' to the original callback
  748. originalOpenDialog.call(cm, template.cloneNode(true), callback.bind(cb), opt);
  749. };
  750. setTimeout(() => { cm.openDialog = originalOpenDialog; }, 0);
  751. refocusMinidialog(cm);
  752. }
  753. function focusClosestCM(activeCM) {
  754. editors.lastActive = activeCM;
  755. const cm = getEditorInSight();
  756. if (cm !== activeCM) {
  757. cm.focus();
  758. }
  759. return cm;
  760. }
  761. function find(activeCM) {
  762. activeCM = focusClosestCM(activeCM);
  763. customizeOpenDialog(activeCM, template.find, function (query) {
  764. this(query);
  765. curState = activeCM.state.search;
  766. if (editors.length === 1 || !curState.query) {
  767. return;
  768. }
  769. editors.forEach(cm => {
  770. if (cm !== activeCM) {
  771. cm.execCommand('clearSearch');
  772. updateState(cm, curState);
  773. }
  774. });
  775. if (CodeMirror.cmpPos(curState.posFrom, curState.posTo) === 0) {
  776. findNext(activeCM);
  777. }
  778. });
  779. originalCommand.find(activeCM);
  780. }
  781. function findNext(activeCM, reverse) {
  782. let state = updateState(activeCM);
  783. if (!state || !state.query) {
  784. find(activeCM);
  785. return;
  786. }
  787. let pos = activeCM.getCursor(reverse ? 'from' : 'to');
  788. // clear the selection, don't move the cursor
  789. activeCM.setSelection(activeCM.getCursor());
  790. const rxQuery = typeof state.query === 'object'
  791. ? state.query : stringAsRegExp(state.query, shouldIgnoreCase(state.query) ? 'i' : '');
  792. if (
  793. document.activeElement &&
  794. document.activeElement.name === 'applies-value' &&
  795. searchAppliesTo(activeCM)
  796. ) {
  797. return;
  798. }
  799. let cm = activeCM;
  800. for (let i = 0; i < editors.length; i++) {
  801. state = updateState(cm);
  802. if (!cm.hasFocus()) {
  803. pos = reverse ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(0, 0);
  804. }
  805. const searchCursor = cm.getSearchCursor(state.query, pos, shouldIgnoreCase(state.query));
  806. if (searchCursor.find(reverse)) {
  807. if (editors.length > 1) {
  808. makeSectionVisible(cm);
  809. cm.focus();
  810. }
  811. // speedup the original findNext
  812. state.posFrom = reverse ? searchCursor.to() : searchCursor.from();
  813. state.posTo = CodeMirror.Pos(state.posFrom.line, state.posFrom.ch);
  814. originalCommand[reverse ? 'findPrev' : 'findNext'](cm);
  815. return;
  816. } else if (!reverse && searchAppliesTo(cm)) {
  817. return;
  818. }
  819. cm = editors[(editors.indexOf(cm) + (reverse ? -1 + editors.length : 1)) % editors.length];
  820. if (reverse && searchAppliesTo(cm)) {
  821. return;
  822. }
  823. }
  824. // nothing found so far, so call the original search with wrap-around
  825. originalCommand[reverse ? 'findPrev' : 'findNext'](activeCM);
  826. function searchAppliesTo(cm) {
  827. let inputs = $$('.applies-value', cm.getSection());
  828. if (reverse) {
  829. inputs = inputs.reverse();
  830. }
  831. inputs.splice(0, inputs.indexOf(document.activeElement) + 1);
  832. return inputs.some(input => {
  833. const match = rxQuery.exec(input.value);
  834. if (match) {
  835. input.focus();
  836. const end = match.index + match[0].length;
  837. // scroll selected part into view in long inputs,
  838. // works only outside of current event handlers chain, hence timeout=0
  839. setTimeout(() => {
  840. input.setSelectionRange(end, end);
  841. input.setSelectionRange(match.index, end);
  842. }, 0);
  843. return true;
  844. }
  845. });
  846. }
  847. }
  848. function findPrev(cm) {
  849. findNext(cm, true);
  850. }
  851. function replace(activeCM, all) {
  852. let queue;
  853. let query;
  854. let replacement;
  855. activeCM = focusClosestCM(activeCM);
  856. customizeOpenDialog(activeCM, template[all ? 'replaceAll' : 'replace'], function (txt) {
  857. query = txt;
  858. customizeOpenDialog(activeCM, template.replaceWith, function (txt) {
  859. replacement = txt;
  860. queue = editors.rotate(-editors.indexOf(activeCM));
  861. if (all) {
  862. editors.forEach(doReplace);
  863. } else {
  864. doReplace();
  865. }
  866. });
  867. this(query);
  868. });
  869. originalCommand.replace(activeCM, all);
  870. function doReplace() {
  871. const cm = queue.shift();
  872. if (!cm) {
  873. if (!all) {
  874. editors.lastActive.focus();
  875. }
  876. return;
  877. }
  878. // hide the first two dialogs (replace, replaceWith)
  879. cm.openDialog = (tmpl, callback) => {
  880. cm.openDialog = (tmpl, callback) => {
  881. cm.openDialog = originalOpenDialog;
  882. if (all) {
  883. callback(replacement);
  884. } else {
  885. doConfirm(cm);
  886. callback(replacement);
  887. if (!$('.CodeMirror-dialog', cm.getWrapperElement())) {
  888. // no dialog == nothing found in the current CM, move to the next
  889. doReplace();
  890. }
  891. }
  892. };
  893. callback(query);
  894. };
  895. originalCommand.replace(cm, all);
  896. }
  897. function doConfirm(cm) {
  898. let wrapAround = false;
  899. const origPos = cm.getCursor();
  900. cm.openConfirm = function overrideConfirm(tmpl, callbacks, opt) {
  901. const ovrCallbacks = callbacks.map(callback => () => {
  902. makeSectionVisible(cm);
  903. cm.openConfirm = overrideConfirm;
  904. setTimeout(() => { cm.openConfirm = originalOpenConfirm; }, 0);
  905. const pos = cm.getCursor();
  906. callback();
  907. const cmp = CodeMirror.cmpPos(cm.getCursor(), pos);
  908. wrapAround |= cmp <= 0;
  909. const dlg = $('.CodeMirror-dialog', cm.getWrapperElement());
  910. if (!dlg || cmp === 0 || wrapAround && CodeMirror.cmpPos(cm.getCursor(), origPos) >= 0) {
  911. if (dlg) {
  912. dlg.remove();
  913. }
  914. doReplace();
  915. }
  916. });
  917. originalOpenConfirm.call(cm, template.replaceConfirm.cloneNode(true), ovrCallbacks, opt);
  918. };
  919. }
  920. }
  921. function replaceAll(cm) {
  922. replace(cm, true);
  923. }
  924. CodeMirror.commands.find = find;
  925. CodeMirror.commands.findNext = findNext;
  926. CodeMirror.commands.findPrev = findPrev;
  927. CodeMirror.commands.replace = replace;
  928. CodeMirror.commands.replaceAll = replaceAll;
  929. }
  930. function jumpToLine(cm) {
  931. const cur = cm.getCursor();
  932. refocusMinidialog(cm);
  933. cm.openDialog(template.jumpToLine.cloneNode(true), str => {
  934. const m = str.match(/^\s*(\d+)(?:\s*:\s*(\d+))?\s*$/);
  935. if (m) {
  936. cm.setCursor(m[1] - 1, m[2] ? m[2] - 1 : cur.ch);
  937. }
  938. }, {value: cur.line + 1});
  939. }
  940. function toggleStyle() {
  941. if (editor) {
  942. editor.toggleStyle();
  943. } else {
  944. toggleSectionStyle();
  945. }
  946. }
  947. function toggleSectionStyle() {
  948. $('#enabled').checked = !$('#enabled').checked;
  949. save();
  950. }
  951. function toggleSectionHeight(cm) {
  952. if (cm.state.toggleHeightSaved) {
  953. // restore previous size
  954. cm.setSize(null, cm.state.toggleHeightSaved);
  955. cm.state.toggleHeightSaved = 0;
  956. } else {
  957. // maximize
  958. const wrapper = cm.display.wrapper;
  959. const allBounds = $('#sections').getBoundingClientRect();
  960. const pageExtrasHeight = allBounds.top + window.scrollY +
  961. parseFloat(getComputedStyle($('#sections')).paddingBottom);
  962. const sectionExtrasHeight = cm.getSection().clientHeight - wrapper.offsetHeight;
  963. cm.state.toggleHeightSaved = wrapper.clientHeight;
  964. cm.setSize(null, window.innerHeight - sectionExtrasHeight - pageExtrasHeight);
  965. const bounds = cm.getSection().getBoundingClientRect();
  966. if (bounds.top < 0 || bounds.bottom > window.innerHeight) {
  967. window.scrollBy(0, bounds.top);
  968. }
  969. }
  970. }
  971. function setupAutocomplete(cm, enable = true) {
  972. const onOff = enable ? 'on' : 'off';
  973. cm[onOff]('changes', autocompleteOnTyping);
  974. cm[onOff]('pick', autocompletePicked);
  975. }
  976. function autocompleteOnTyping(cm, [info], debounced) {
  977. if (
  978. cm.state.completionActive ||
  979. info.origin && !info.origin.includes('input') ||
  980. !info.text.last
  981. ) {
  982. return;
  983. }
  984. if (cm.state.autocompletePicked) {
  985. cm.state.autocompletePicked = false;
  986. return;
  987. }
  988. if (!debounced) {
  989. debounce(autocompleteOnTyping, 100, cm, [info], true);
  990. return;
  991. }
  992. if (info.text.last.match(/[-a-z!]+$/i)) {
  993. cm.state.autocompletePicked = false;
  994. cm.options.hintOptions.completeSingle = false;
  995. cm.execCommand('autocomplete');
  996. setTimeout(() => {
  997. cm.options.hintOptions.completeSingle = true;
  998. });
  999. }
  1000. }
  1001. function autocompletePicked(cm) {
  1002. cm.state.autocompletePicked = true;
  1003. }
  1004. function refocusMinidialog(cm) {
  1005. const section = cm.getSection();
  1006. if (!$('.CodeMirror-dialog', section)) {
  1007. return;
  1008. }
  1009. // close the currently opened minidialog
  1010. cm.focus();
  1011. // make sure to focus the input in newly opened minidialog
  1012. setTimeout(() => {
  1013. $('.CodeMirror-dialog', section).focus();
  1014. }, 0);
  1015. }
  1016. function nextPrevEditor(cm, direction) {
  1017. cm = editors[(editors.indexOf(cm) + direction + editors.length) % editors.length];
  1018. makeSectionVisible(cm);
  1019. cm.focus();
  1020. return cm;
  1021. }
  1022. function nextPrevEditorOnKeydown(cm, event) {
  1023. const key = event.which;
  1024. if (key < 37 || key > 40 || event.shiftKey || event.altKey || event.metaKey) {
  1025. return;
  1026. }
  1027. const {line, ch} = cm.getCursor();
  1028. switch (key) {
  1029. case 37:
  1030. // arrow Left
  1031. if (line || ch) {
  1032. return;
  1033. }
  1034. // fallthrough to arrow Up
  1035. case 38:
  1036. // arrow Up
  1037. if (line > 0 || cm === editors[0]) {
  1038. return;
  1039. }
  1040. event.preventDefault();
  1041. event.stopPropagation();
  1042. cm = nextPrevEditor(cm, -1);
  1043. cm.setCursor(cm.doc.size - 1, key === 37 ? 1e20 : ch);
  1044. break;
  1045. case 39:
  1046. // arrow Right
  1047. if (line < cm.doc.size - 1 || ch < cm.getLine(line).length - 1) {
  1048. return;
  1049. }
  1050. // fallthrough to arrow Down
  1051. case 40:
  1052. // arrow Down
  1053. if (line < cm.doc.size - 1 || cm === editors.last) {
  1054. return;
  1055. }
  1056. event.preventDefault();
  1057. event.stopPropagation();
  1058. cm = nextPrevEditor(cm, 1);
  1059. cm.setCursor(0, 0);
  1060. break;
  1061. }
  1062. const animation = (cm.getSection().firstElementChild.getAnimations() || [])[0];
  1063. if (animation) {
  1064. animation.playbackRate = -1;
  1065. animation.currentTime = 2000;
  1066. animation.play();
  1067. }
  1068. }
  1069. function getEditorInSight(nearbyElement) {
  1070. // priority: 1. associated CM for applies-to element 2. last active if visible 3. first visible
  1071. let cm;
  1072. if (nearbyElement && nearbyElement.className.indexOf('applies-') >= 0) {
  1073. cm = getSectionForChild(nearbyElement).CodeMirror;
  1074. } else {
  1075. cm = editors.lastActive;
  1076. }
  1077. // closest editor should have at least 2 lines visible
  1078. const lineHeight = editors[0].defaultTextHeight();
  1079. const scrollY = window.scrollY;
  1080. const windowBottom = scrollY + window.innerHeight - 2 * lineHeight;
  1081. const allSectionsContainerTop = scrollY + $('#sections').getBoundingClientRect().top;
  1082. const distances = [];
  1083. const alreadyInView = cm && offscreenDistance(null, cm) === 0;
  1084. return alreadyInView ? cm : findClosest();
  1085. function offscreenDistance(index, cm) {
  1086. if (index >= 0 && distances[index] !== undefined) {
  1087. return distances[index];
  1088. }
  1089. const section = (cm || editors[index]).getSection();
  1090. const top = allSectionsContainerTop + section.offsetTop;
  1091. if (top < scrollY + lineHeight) {
  1092. return Math.max(0, scrollY - top - lineHeight);
  1093. }
  1094. if (top < windowBottom) {
  1095. return 0;
  1096. }
  1097. const distance = top - windowBottom + section.offsetHeight;
  1098. if (index >= 0) {
  1099. distances[index] = distance;
  1100. }
  1101. return distance;
  1102. }
  1103. function findClosest() {
  1104. const last = editors.length - 1;
  1105. let a = 0;
  1106. let b = last;
  1107. let c;
  1108. let cm, distance;
  1109. while (a < b - 1) {
  1110. c = (a + b) / 2 | 0;
  1111. distance = offscreenDistance(c);
  1112. if (!distance || !c) {
  1113. break;
  1114. }
  1115. const distancePrev = offscreenDistance(c - 1);
  1116. const distanceNext = c < last ? offscreenDistance(c + 1) : 1e20;
  1117. if (distancePrev <= distance && distance <= distanceNext) {
  1118. b = c;
  1119. } else {
  1120. a = c;
  1121. }
  1122. }
  1123. while (b && offscreenDistance(b - 1) <= offscreenDistance(b)) {
  1124. b--;
  1125. }
  1126. cm = editors[b];
  1127. if (distances[b] > 0) {
  1128. makeSectionVisible(cm);
  1129. }
  1130. return cm;
  1131. }
  1132. }
  1133. function beautify(event) {
  1134. loadScript('/vendor-overwrites/beautify/beautify-css-mod.js')
  1135. .then(() => {
  1136. if (!window.css_beautify && window.exports) {
  1137. window.css_beautify = window.exports.css_beautify;
  1138. }
  1139. })
  1140. .then(doBeautify);
  1141. function doBeautify() {
  1142. const tabs = prefs.get('editor.indentWithTabs');
  1143. const options = prefs.get('editor.beautify');
  1144. options.indent_size = tabs ? 1 : prefs.get('editor.tabSize');
  1145. options.indent_char = tabs ? '\t' : ' ';
  1146. const section = getSectionForChild(event.target);
  1147. const scope = section ? [section.CodeMirror] : editors;
  1148. showHelp(t('styleBeautify'), '<div class="beautify-options">' +
  1149. optionHtml('.selector1,', 'selector_separator_newline') +
  1150. optionHtml('.selector2,', 'newline_before_open_brace') +
  1151. optionHtml('{', 'newline_after_open_brace') +
  1152. optionHtml('border: none;', 'newline_between_properties', true) +
  1153. optionHtml('display: block;', 'newline_before_close_brace', true) +
  1154. optionHtml('}', 'newline_between_rules') +
  1155. `<label style="display: block; clear: both;"><input data-option="indent_conditional" type="checkbox"
  1156. ${options.indent_conditional !== false ? 'checked' : ''}>` +
  1157. t('styleBeautifyIndentConditional') + '</label>' +
  1158. '</div>' +
  1159. '<div><button role="undo"></button></div>');
  1160. const undoButton = $('#help-popup button[role="undo"]');
  1161. undoButton.textContent = t(scope.length === 1 ? 'undo' : 'undoGlobal');
  1162. undoButton.addEventListener('click', () => {
  1163. let undoable = false;
  1164. scope.forEach(cm => {
  1165. if (cm.beautifyChange && cm.beautifyChange[cm.changeGeneration()]) {
  1166. delete cm.beautifyChange[cm.changeGeneration()];
  1167. cm.undo();
  1168. cm.scrollIntoView(cm.getCursor());
  1169. undoable |= cm.beautifyChange[cm.changeGeneration()];
  1170. }
  1171. });
  1172. undoButton.disabled = !undoable;
  1173. });
  1174. scope.forEach(cm => {
  1175. setTimeout(() => {
  1176. const pos = options.translate_positions =
  1177. [].concat.apply([], cm.doc.sel.ranges.map(r =>
  1178. [Object.assign({}, r.anchor), Object.assign({}, r.head)]));
  1179. const text = cm.getValue();
  1180. const newText = css_beautify(text, options);
  1181. if (newText !== text) {
  1182. if (!cm.beautifyChange || !cm.beautifyChange[cm.changeGeneration()]) {
  1183. // clear the list if last change wasn't a css-beautify
  1184. cm.beautifyChange = {};
  1185. }
  1186. cm.setValue(newText);
  1187. const selections = [];
  1188. for (let i = 0; i < pos.length; i += 2) {
  1189. selections.push({anchor: pos[i], head: pos[i + 1]});
  1190. }
  1191. cm.setSelections(selections);
  1192. cm.beautifyChange[cm.changeGeneration()] = true;
  1193. undoButton.disabled = false;
  1194. }
  1195. }, 0);
  1196. });
  1197. $('.beautify-options').onchange = ({target}) => {
  1198. const value = target.type === 'checkbox' ? target.checked : target.selectedIndex > 0;
  1199. prefs.set('editor.beautify', Object.assign(options, {[target.dataset.option]: value}));
  1200. if (target.parentNode.hasAttribute('newline')) {
  1201. target.parentNode.setAttribute('newline', value.toString());
  1202. }
  1203. doBeautify();
  1204. };
  1205. function optionHtml(label, optionName, indent) {
  1206. const value = options[optionName];
  1207. return '<div newline="' + value.toString() + '">' +
  1208. '<span' + (indent ? ' indent' : '') + '>' + label + '</span>' +
  1209. '<select data-option="' + optionName + '">' +
  1210. '<option' + (value ? '' : ' selected') + '>&nbsp;</option>' +
  1211. '<option' + (value ? ' selected' : '') + '>\\n</option>' +
  1212. '</select></div>';
  1213. }
  1214. }
  1215. }
  1216. function init() {
  1217. initCodeMirror();
  1218. getStyle().then(style => {
  1219. styleId = style.id;
  1220. sessionStorage.justEditedStyleId = styleId;
  1221. if (!isUsercss(style)) {
  1222. initWithSectionStyle({style});
  1223. } else {
  1224. editor = createSourceEditor(style);
  1225. }
  1226. });
  1227. function getStyle() {
  1228. const id = new URLSearchParams(location.search).get('id');
  1229. if (!id) {
  1230. // match should be 2 - one for the whole thing, one for the parentheses
  1231. // This is an add
  1232. $('#heading').textContent = t('addStyleTitle');
  1233. return Promise.resolve(createEmptyStyle());
  1234. }
  1235. $('#heading').textContent = t('editStyleHeading');
  1236. // This is an edit
  1237. return getStylesSafe({id}).then(styles => {
  1238. let style = styles[0];
  1239. if (!style) {
  1240. style = createEmptyStyle();
  1241. history.replaceState({}, document.title, location.pathname);
  1242. }
  1243. return style;
  1244. });
  1245. }
  1246. function createEmptyStyle() {
  1247. const params = new URLSearchParams(location.search);
  1248. const style = {
  1249. id: null,
  1250. name: '',
  1251. enabled: true,
  1252. sections: [{code: ''}]
  1253. };
  1254. for (const i in CssToProperty) {
  1255. if (params.get(i)) {
  1256. style.sections[0][CssToProperty[i]] = [params.get(i)];
  1257. }
  1258. }
  1259. return style;
  1260. }
  1261. }
  1262. function setStyleMeta(style) {
  1263. $('#name').value = style.name || '';
  1264. $('#enabled').checked = style.enabled !== false;
  1265. $('#url').href = style.url || '';
  1266. }
  1267. function isUsercss(style) {
  1268. return (
  1269. style.usercssData ||
  1270. !style.id && prefs.get('newStyleAsUsercss')
  1271. );
  1272. }
  1273. function initWithSectionStyle({style, codeIsUpdated}) {
  1274. setStyleMeta(style);
  1275. if (codeIsUpdated !== false) {
  1276. editors.length = 0;
  1277. getSections().forEach(div => div.remove());
  1278. addSections(style.sections.length ? style.sections : [{code: ''}]);
  1279. initHooks();
  1280. }
  1281. setCleanGlobal();
  1282. updateTitle();
  1283. }
  1284. function addSections(sections, onAdded = () => {}) {
  1285. if (addSections.running) {
  1286. console.error('addSections cannot be re-entered: please report to the developers');
  1287. // TODO: handle this properly e.g. on update/import
  1288. return;
  1289. }
  1290. addSections.running = true;
  1291. maximizeCodeHeight.stats = null;
  1292. // make a shallow copy since we might run asynchronously
  1293. // and the original array might get modified
  1294. sections = sections.slice();
  1295. const t0 = performance.now();
  1296. const divs = [];
  1297. let index = 0;
  1298. return new Promise(function run(resolve) {
  1299. while (index < sections.length) {
  1300. const div = addSection(null, sections[index]);
  1301. maximizeCodeHeight(div, index === sections.length - 1);
  1302. onAdded(div, index);
  1303. divs.push(div);
  1304. maybeFocusFirstCM();
  1305. index++;
  1306. const elapsed = performance.now() - t0;
  1307. if (elapsed > 500) {
  1308. setGlobalProgress(index, sections.length);
  1309. }
  1310. if (elapsed > 100) {
  1311. // after 100ms the sections are added asynchronously
  1312. setTimeout(run, 0, resolve);
  1313. return;
  1314. }
  1315. }
  1316. editors.last.state.renderLintReportNow = true;
  1317. addSections.running = false;
  1318. setGlobalProgress();
  1319. resolve(divs);
  1320. });
  1321. function maybeFocusFirstCM() {
  1322. const isPageLocked = document.documentElement.style.pointerEvents;
  1323. if (divs[0] && (isPageLocked ? divs.length === sections.length : index === 0)) {
  1324. makeSectionVisible(divs[0].CodeMirror);
  1325. divs[0].CodeMirror.focus();
  1326. }
  1327. }
  1328. }
  1329. function initCollapsibles() {
  1330. function saveStateDelayed(event) {
  1331. if (event.target.closest('.intercepts-click')) {
  1332. event.preventDefault();
  1333. } else {
  1334. setTimeout(saveState, 0, event.target.closest('details'));
  1335. }
  1336. }
  1337. function saveState(el) {
  1338. prefs.set(`editor.${el.id}.expanded`, el.open);
  1339. }
  1340. function loadState(key, value) {
  1341. $('#' + key.split('.')[1]).open = value;
  1342. }
  1343. const collapsibles = $$('#header details');
  1344. for (const el of collapsibles) {
  1345. el.open = prefs.get(`editor.${el.id}.expanded`);
  1346. $('h2', el).addEventListener('click', saveStateDelayed);
  1347. }
  1348. prefs.subscribe(collapsibles.map(el => `editor.${el.id}.expanded`), loadState);
  1349. }
  1350. function initHooks() {
  1351. if (initHooks.alreadyDone) {
  1352. return;
  1353. }
  1354. initHooks.alreadyDone = true;
  1355. $$('#header .style-contributor').forEach(node => {
  1356. node.addEventListener('change', onChange);
  1357. node.addEventListener('input', onChange);
  1358. });
  1359. $('#toggle-style-help').addEventListener('click', showToggleStyleHelp);
  1360. $('#to-mozilla').addEventListener('click', showMozillaFormat, false);
  1361. $('#to-mozilla-help').addEventListener('click', showToMozillaHelp, false);
  1362. $('#from-mozilla').addEventListener('click', fromMozillaFormat);
  1363. $('#beautify').addEventListener('click', beautify);
  1364. $('#save-button').addEventListener('click', save, false);
  1365. $('#sections-help').addEventListener('click', showSectionHelp, false);
  1366. $('#keyMap-help').addEventListener('click', showKeyMapHelp, false);
  1367. $('#cancel-button').addEventListener('click', goBackToManage);
  1368. initCollapsibles();
  1369. initLint();
  1370. if (!FIREFOX) {
  1371. $$([
  1372. 'input:not([type])',
  1373. 'input[type="text"]',
  1374. 'input[type="search"]',
  1375. 'input[type="number"]',
  1376. ].join(',')
  1377. ).forEach(e => e.addEventListener('mousedown', toggleContextMenuDelete));
  1378. }
  1379. window.addEventListener('load', function _() {
  1380. window.removeEventListener('load', _);
  1381. window.addEventListener('resize', () => debounce(rememberWindowSize, 100));
  1382. });
  1383. setupGlobalSearch();
  1384. }
  1385. function toggleContextMenuDelete(event) {
  1386. if (chrome.contextMenus && event.button === 2 && prefs.get('editor.contextDelete')) {
  1387. chrome.contextMenus.update('editor.contextDelete', {
  1388. enabled: Boolean(
  1389. this.selectionStart !== this.selectionEnd ||
  1390. this.somethingSelected && this.somethingSelected()
  1391. ),
  1392. }, ignoreChromeError);
  1393. }
  1394. }
  1395. function maximizeCodeHeight(sectionDiv, isLast) {
  1396. const cm = sectionDiv.CodeMirror;
  1397. const stats = maximizeCodeHeight.stats = maximizeCodeHeight.stats || {totalHeight: 0, deltas: []};
  1398. if (!stats.cmActualHeight) {
  1399. stats.cmActualHeight = getComputedHeight(cm.display.wrapper);
  1400. }
  1401. if (!stats.sectionMarginTop) {
  1402. stats.sectionMarginTop = parseFloat(getComputedStyle(sectionDiv).marginTop);
  1403. }
  1404. const sectionTop = sectionDiv.getBoundingClientRect().top - stats.sectionMarginTop;
  1405. if (!stats.firstSectionTop) {
  1406. stats.firstSectionTop = sectionTop;
  1407. }
  1408. const extrasHeight = getComputedHeight(sectionDiv) - stats.cmActualHeight;
  1409. const cmMaxHeight = window.innerHeight - extrasHeight - sectionTop - stats.sectionMarginTop;
  1410. const cmDesiredHeight = cm.display.sizer.clientHeight + 2 * cm.defaultTextHeight();
  1411. const cmGrantableHeight = Math.max(stats.cmActualHeight, Math.min(cmMaxHeight, cmDesiredHeight));
  1412. stats.deltas.push(cmGrantableHeight - stats.cmActualHeight);
  1413. stats.totalHeight += cmGrantableHeight + extrasHeight;
  1414. if (!isLast) {
  1415. return;
  1416. }
  1417. stats.totalHeight += stats.firstSectionTop;
  1418. if (stats.totalHeight <= window.innerHeight) {
  1419. editors.forEach((cm, index) => {
  1420. cm.setSize(null, stats.deltas[index] + stats.cmActualHeight);
  1421. });
  1422. return;
  1423. }
  1424. // scale heights to fill the gap between last section and bottom edge of the window
  1425. const sections = $('#sections');
  1426. const available = window.innerHeight - sections.getBoundingClientRect().bottom -
  1427. parseFloat(getComputedStyle(sections).marginBottom);
  1428. if (available <= 0) {
  1429. return;
  1430. }
  1431. const totalDelta = stats.deltas.reduce((sum, d) => sum + d, 0);
  1432. const q = available / totalDelta;
  1433. const baseHeight = stats.cmActualHeight - stats.sectionMarginTop;
  1434. stats.deltas.forEach((delta, index) => {
  1435. editors[index].setSize(null, baseHeight + Math.floor(q * delta));
  1436. });
  1437. }
  1438. function updateTitle() {
  1439. const DIRTY_TITLE = '* $';
  1440. const name = $('#name').savedValue;
  1441. const clean = isCleanGlobal();
  1442. const title = styleId === null ? t('addStyleTitle') : t('editStyleTitle', [name]);
  1443. document.title = clean ? title : DIRTY_TITLE.replace('$', title);
  1444. }
  1445. function validate() {
  1446. const name = $('#name').value;
  1447. if (name === '') {
  1448. $('#name').focus();
  1449. return t('styleMissingName');
  1450. }
  1451. // validate the regexps
  1452. if ($$('.applies-to-list').some(list => {
  1453. list.childNodes.some(li => {
  1454. if (li.className === template.appliesToEverything.className) {
  1455. return false;
  1456. }
  1457. const valueElement = $('[name=applies-value]', li);
  1458. const type = $('[name=applies-type]', li).value;
  1459. const value = valueElement.value;
  1460. if (type && value) {
  1461. if (type === 'regexp') {
  1462. try {
  1463. new RegExp(value);
  1464. } catch (ex) {
  1465. valueElement.focus();
  1466. return true;
  1467. }
  1468. }
  1469. }
  1470. return false;
  1471. });
  1472. })) {
  1473. return t('styleBadRegexp');
  1474. }
  1475. return null;
  1476. }
  1477. function updateLintReportIfEnabled(...args) {
  1478. if (CodeMirror.defaults.lint) {
  1479. updateLintReport(...args);
  1480. }
  1481. }
  1482. function save() {
  1483. if (editor) {
  1484. editor.save();
  1485. } else {
  1486. saveSectionStyle();
  1487. }
  1488. }
  1489. function saveSectionStyle() {
  1490. updateLintReportIfEnabled(null, 0);
  1491. const error = validate();
  1492. if (error) {
  1493. alert(error);
  1494. return;
  1495. }
  1496. const name = $('#name').value;
  1497. const enabled = $('#enabled').checked;
  1498. saveStyleSafe({
  1499. id: styleId,
  1500. name: name,
  1501. enabled: enabled,
  1502. reason: 'editSave',
  1503. sections: getSectionsHashes()
  1504. })
  1505. .then(saveComplete);
  1506. }
  1507. function getSectionsHashes() {
  1508. const sections = [];
  1509. getSections().forEach(div => {
  1510. const meta = getMeta(div);
  1511. const code = div.CodeMirror.getValue();
  1512. if (/^\s*$/.test(code) && Object.keys(meta).length === 0) {
  1513. return;
  1514. }
  1515. meta.code = code;
  1516. sections.push(meta);
  1517. });
  1518. return sections;
  1519. }
  1520. function getMeta(e) {
  1521. const meta = {urls: [], urlPrefixes: [], domains: [], regexps: []};
  1522. $('.applies-to-list', e).childNodes.forEach(li => {
  1523. if (li.className === template.appliesToEverything.className) {
  1524. return;
  1525. }
  1526. const type = $('[name=applies-type]', li).value;
  1527. const value = $('[name=applies-value]', li).value;
  1528. if (type && value) {
  1529. const property = CssToProperty[type];
  1530. meta[property].push(value);
  1531. }
  1532. });
  1533. return meta;
  1534. }
  1535. function saveComplete(style) {
  1536. styleId = style.id;
  1537. sessionStorage.justEditedStyleId = styleId;
  1538. setCleanGlobal();
  1539. // Go from new style URL to edit style URL
  1540. if (location.href.indexOf('id=') === -1) {
  1541. history.replaceState({}, document.title, 'edit.html?id=' + style.id);
  1542. $('#heading').textContent = t('editStyleHeading');
  1543. }
  1544. updateTitle();
  1545. }
  1546. function showMozillaFormat() {
  1547. const popup = showCodeMirrorPopup(t('styleToMozillaFormatTitle'), '', {readOnly: true});
  1548. popup.codebox.setValue(toMozillaFormat());
  1549. popup.codebox.execCommand('selectAll');
  1550. }
  1551. function toMozillaFormat() {
  1552. return mozParser.format({sections: getSectionsHashes()});
  1553. }
  1554. function fromMozillaFormat() {
  1555. const popup = showCodeMirrorPopup(t('styleFromMozillaFormatPrompt'),
  1556. $element({appendChild: [
  1557. $element({
  1558. tag: 'button',
  1559. name: 'import-append',
  1560. textContent: t('importAppendLabel'),
  1561. title: t('importAppendTooltip'),
  1562. onclick: doImport,
  1563. }),
  1564. $element({
  1565. tag: 'button',
  1566. name: 'import-replace',
  1567. textContent: t('importReplaceLabel'),
  1568. title: t('importReplaceTooltip'),
  1569. onclick: () => doImport({replaceOldStyle: true}),
  1570. }),
  1571. ]}));
  1572. const contents = $('.contents', popup);
  1573. contents.insertBefore(popup.codebox.display.wrapper, contents.firstElementChild);
  1574. popup.codebox.focus();
  1575. popup.codebox.on('changes', cm => {
  1576. popup.classList.toggle('ready', !cm.isBlank());
  1577. });
  1578. // overwrite default extraKeys as those are inapplicable in popup context
  1579. popup.codebox.options.extraKeys = {
  1580. 'Ctrl-Enter': doImport,
  1581. 'Shift-Ctrl-Enter': () => doImport({replaceOldStyle: true}),
  1582. };
  1583. function doImport({replaceOldStyle = false}) {
  1584. lockPageUI(true);
  1585. new Promise(setTimeout)
  1586. .then(() => mozParser.parse(popup.codebox.getValue().trim()))
  1587. .then(sections => {
  1588. removeOldSections(replaceOldStyle);
  1589. return addSections(sections, div => setCleanItem(div, false));
  1590. })
  1591. .then(sectionDivs => {
  1592. sectionDivs.forEach(div => updateLintReportIfEnabled(div.CodeMirror, 1));
  1593. $('.dismiss', popup).onclick();
  1594. })
  1595. .catch(showError)
  1596. .then(() => lockPageUI(false));
  1597. }
  1598. function removeOldSections(removeAll) {
  1599. let toRemove;
  1600. if (removeAll) {
  1601. toRemove = editors.slice().reverse();
  1602. } else if (editors.last.isBlank() && $('.applies-to-everything', editors.last.getSection())) {
  1603. toRemove = [editors.last];
  1604. } else {
  1605. return;
  1606. }
  1607. toRemove.forEach(cm => removeSection({target: cm.getSection()}));
  1608. }
  1609. function lockPageUI(locked) {
  1610. document.documentElement.style.pointerEvents = locked ? 'none' : '';
  1611. popup.classList.toggle('ready', locked ? false : !popup.codebox.isBlank());
  1612. popup.codebox.options.readOnly = locked;
  1613. popup.codebox.display.wrapper.style.opacity = locked ? '.5' : '';
  1614. }
  1615. function showError(errors) {
  1616. showHelp(t('styleFromMozillaFormatError'), $element({
  1617. tag: 'pre',
  1618. textContent: Array.isArray(errors) ? errors.join('\n') : errors,
  1619. }));
  1620. }
  1621. }
  1622. function showSectionHelp() {
  1623. showHelp(t('styleSectionsTitle'), t('sectionHelp'));
  1624. }
  1625. function showAppliesToHelp() {
  1626. showHelp(t('appliesLabel'), t('appliesHelp'));
  1627. }
  1628. function showToMozillaHelp() {
  1629. showHelp(t('styleMozillaFormatHeading'), t('styleToMozillaFormatHelp'));
  1630. }
  1631. function showToggleStyleHelp() {
  1632. showHelp(t('helpAlt'), t('styleEnabledToggleHint'));
  1633. }
  1634. function showKeyMapHelp() {
  1635. const keyMap = mergeKeyMaps({}, prefs.get('editor.keyMap'), CodeMirror.defaults.extraKeys);
  1636. const keyMapSorted = Object.keys(keyMap)
  1637. .map(key => ({key: key, cmd: keyMap[key]}))
  1638. .concat([{key: 'Shift-Ctrl-Wheel', cmd: 'scrollWindow'}])
  1639. .sort((a, b) => (a.cmd < b.cmd || (a.cmd === b.cmd && a.key < b.key) ? -1 : 1));
  1640. showHelp(t('cm_keyMap') + ': ' + prefs.get('editor.keyMap'),
  1641. '<table class="keymap-list">' +
  1642. '<thead><tr><th><input placeholder="' + t('helpKeyMapHotkey') + '" type="search"></th>' +
  1643. '<th><input placeholder="' + t('helpKeyMapCommand') + '" type="search"></th></tr></thead>' +
  1644. '<tbody>' + keyMapSorted.map(value =>
  1645. '<tr><td>' + value.key + '</td><td>' + value.cmd + '</td></tr>'
  1646. ).join('') +
  1647. '</tbody>' +
  1648. '</table>');
  1649. const table = $('#help-popup table');
  1650. table.addEventListener('input', filterTable);
  1651. const inputs = $$('input', table);
  1652. inputs[0].addEventListener('keydown', hotkeyHandler);
  1653. inputs[1].focus();
  1654. function hotkeyHandler(event) {
  1655. const keyName = CodeMirror.keyName(event);
  1656. if (keyName === 'Esc' || keyName === 'Tab' || keyName === 'Shift-Tab') {
  1657. return;
  1658. }
  1659. event.preventDefault();
  1660. event.stopPropagation();
  1661. // normalize order of modifiers,
  1662. // for modifier-only keys ('Ctrl-Shift') a dummy main key has to be temporarily added
  1663. const keyMap = {};
  1664. keyMap[keyName.replace(/(Shift|Ctrl|Alt|Cmd)$/, '$&-dummy')] = '';
  1665. const normalizedKey = Object.keys(CodeMirror.normalizeKeyMap(keyMap))[0];
  1666. this.value = normalizedKey.replace('-dummy', '');
  1667. filterTable(event);
  1668. }
  1669. function filterTable(event) {
  1670. const input = event.target;
  1671. const col = input.parentNode.cellIndex;
  1672. inputs[1 - col].value = '';
  1673. table.tBodies[0].childNodes.forEach(row => {
  1674. const cell = row.children[col];
  1675. const text = cell.textContent;
  1676. const query = stringAsRegExp(input.value, 'gi');
  1677. const test = query.test(text);
  1678. row.style.display = input.value && test === false ? 'none' : '';
  1679. if (input.value && test) {
  1680. cell.textContent = '';
  1681. let offset = 0;
  1682. text.replace(query, (match, index) => {
  1683. if (index > offset) {
  1684. cell.appendChild(document.createTextNode(text.substring(offset, index)));
  1685. }
  1686. cell.appendChild($element({tag: 'mark', textContent: match}));
  1687. offset = index + match.length;
  1688. });
  1689. if (offset + 1 !== text.length) {
  1690. cell.appendChild(document.createTextNode(text.substring(offset)));
  1691. }
  1692. }
  1693. else {
  1694. cell.textContent = text;
  1695. }
  1696. // clear highlight from the other column
  1697. const otherCell = row.children[1 - col];
  1698. if (otherCell.children.length) {
  1699. const text = otherCell.textContent;
  1700. otherCell.textContent = text;
  1701. }
  1702. });
  1703. }
  1704. function mergeKeyMaps(merged, ...more) {
  1705. more.forEach(keyMap => {
  1706. if (typeof keyMap === 'string') {
  1707. keyMap = CodeMirror.keyMap[keyMap];
  1708. }
  1709. Object.keys(keyMap).forEach(key => {
  1710. let cmd = keyMap[key];
  1711. // filter out '...', 'attach', etc. (hotkeys start with an uppercase letter)
  1712. if (!merged[key] && !key.match(/^[a-z]/) && cmd !== '...') {
  1713. if (typeof cmd === 'function') {
  1714. // for 'emacs' keymap: provide at least something meaningful (hotkeys and the function body)
  1715. // for 'vim*' keymaps: almost nothing as it doesn't rely on CM keymap mechanism
  1716. cmd = cmd.toString().replace(/^function.*?\{[\s\r\n]*([\s\S]+?)[\s\r\n]*\}$/, '$1');
  1717. merged[key] = cmd.length <= 200 ? cmd : cmd.substr(0, 200) + '...';
  1718. } else {
  1719. merged[key] = cmd;
  1720. }
  1721. }
  1722. });
  1723. if (keyMap.fallthrough) {
  1724. merged = mergeKeyMaps(merged, keyMap.fallthrough);
  1725. }
  1726. });
  1727. return merged;
  1728. }
  1729. }
  1730. function showHelp(title, body) {
  1731. const div = $('#help-popup');
  1732. div.classList.remove('big');
  1733. $('.contents', div).textContent = '';
  1734. $('.contents', div).appendChild(typeof body === 'string' ? tHTML(body) : body);
  1735. $('.title', div).textContent = title;
  1736. if (getComputedStyle(div).display === 'none') {
  1737. document.addEventListener('keydown', closeHelp);
  1738. // avoid chaining on multiple showHelp() calls
  1739. $('.dismiss', div).onclick = closeHelp;
  1740. }
  1741. // reset any inline styles
  1742. div.style = 'display: block';
  1743. return div;
  1744. function closeHelp(e) {
  1745. if (
  1746. !e ||
  1747. e.type === 'click' ||
  1748. ((e.keyCode || e.which) === 27 && !e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey)
  1749. ) {
  1750. div.style.display = '';
  1751. const contents = $('.contents');
  1752. contents.textContent = '';
  1753. clearTimeout(contents.timer);
  1754. document.removeEventListener('keydown', closeHelp);
  1755. }
  1756. }
  1757. }
  1758. function showCodeMirrorPopup(title, html, options) {
  1759. const popup = showHelp(title, html);
  1760. popup.classList.add('big');
  1761. popup.codebox = CodeMirror($('.contents', popup), Object.assign({
  1762. mode: 'css',
  1763. lineNumbers: true,
  1764. lineWrapping: true,
  1765. foldGutter: true,
  1766. gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
  1767. matchBrackets: true,
  1768. lint: linterConfig.getForCodeMirror(),
  1769. styleActiveLine: true,
  1770. theme: prefs.get('editor.theme'),
  1771. keyMap: prefs.get('editor.keyMap')
  1772. }, options));
  1773. popup.codebox.focus();
  1774. popup.codebox.on('focus', () => { hotkeyRerouter.setState(false); });
  1775. popup.codebox.on('blur', () => { hotkeyRerouter.setState(true); });
  1776. return popup;
  1777. }
  1778. chrome.runtime.onMessage.addListener(onRuntimeMessage);
  1779. function replaceStyle(request) {
  1780. const codeIsUpdated = request.codeIsUpdated !== false;
  1781. if (!isUsercss(request.style)) {
  1782. initWithSectionStyle(request);
  1783. return;
  1784. }
  1785. if (!codeIsUpdated) {
  1786. editor.replaceMeta(request.style);
  1787. return;
  1788. }
  1789. askDiscardChanges()
  1790. .then(result => {
  1791. if (result) {
  1792. editor.replaceStyle(request.style);
  1793. } else {
  1794. editor.setStyleDirty(request.style);
  1795. }
  1796. });
  1797. function askDiscardChanges() {
  1798. if (!editor.isTouched()) {
  1799. return Promise.resolve(true);
  1800. }
  1801. return messageBox.confirm(t('styleUpdateDiscardChanges'));
  1802. }
  1803. }
  1804. function onRuntimeMessage(request) {
  1805. switch (request.method) {
  1806. case 'styleUpdated':
  1807. if (styleId && styleId === request.style.id && request.reason !== 'editSave' && request.reason !== 'config') {
  1808. if ((request.style.sections[0] || {}).code === null) {
  1809. // the code-less style came from notifyAllTabs
  1810. onBackgroundReady().then(() => {
  1811. request.style = BG.cachedStyles.byId.get(request.style.id);
  1812. replaceStyle(request);
  1813. });
  1814. } else {
  1815. replaceStyle(request);
  1816. }
  1817. }
  1818. break;
  1819. case 'styleDeleted':
  1820. if (styleId === request.id || editor && editor.getStyle().id === request.id) {
  1821. window.onbeforeunload = () => {};
  1822. closeCurrentTab();
  1823. break;
  1824. }
  1825. break;
  1826. case 'prefChanged':
  1827. if ('editor.smartIndent' in request.prefs) {
  1828. CodeMirror.setOption('smartIndent', request.prefs['editor.smartIndent']);
  1829. }
  1830. break;
  1831. case 'editDeleteText':
  1832. document.execCommand('delete');
  1833. break;
  1834. }
  1835. }
  1836. function getComputedHeight(el) {
  1837. const compStyle = getComputedStyle(el);
  1838. return el.getBoundingClientRect().height +
  1839. parseFloat(compStyle.marginTop) + parseFloat(compStyle.marginBottom);
  1840. }
  1841. function getCodeMirrorThemes() {
  1842. if (!chrome.runtime.getPackageDirectoryEntry) {
  1843. const themes = [
  1844. chrome.i18n.getMessage('defaultTheme'),
  1845. '3024-day',
  1846. '3024-night',
  1847. 'abcdef',
  1848. 'ambiance',
  1849. 'ambiance-mobile',
  1850. 'base16-dark',
  1851. 'base16-light',
  1852. 'bespin',
  1853. 'blackboard',
  1854. 'cobalt',
  1855. 'colorforth',
  1856. 'dracula',
  1857. 'duotone-dark',
  1858. 'duotone-light',
  1859. 'eclipse',
  1860. 'elegant',
  1861. 'erlang-dark',
  1862. 'hopscotch',
  1863. 'icecoder',
  1864. 'isotope',
  1865. 'lesser-dark',
  1866. 'liquibyte',
  1867. 'material',
  1868. 'mbo',
  1869. 'mdn-like',
  1870. 'midnight',
  1871. 'monokai',
  1872. 'neat',
  1873. 'neo',
  1874. 'night',
  1875. 'panda-syntax',
  1876. 'paraiso-dark',
  1877. 'paraiso-light',
  1878. 'pastel-on-dark',
  1879. 'railscasts',
  1880. 'rubyblue',
  1881. 'seti',
  1882. 'solarized',
  1883. 'the-matrix',
  1884. 'tomorrow-night-bright',
  1885. 'tomorrow-night-eighties',
  1886. 'ttcn',
  1887. 'twilight',
  1888. 'vibrant-ink',
  1889. 'xq-dark',
  1890. 'xq-light',
  1891. 'yeti',
  1892. 'zenburn',
  1893. ];
  1894. localStorage.codeMirrorThemes = themes.join(' ');
  1895. return Promise.resolve(themes);
  1896. }
  1897. return new Promise(resolve => {
  1898. chrome.runtime.getPackageDirectoryEntry(rootDir => {
  1899. rootDir.getDirectory('vendor/codemirror/theme', {create: false}, themeDir => {
  1900. themeDir.createReader().readEntries(entries => {
  1901. const themes = [
  1902. chrome.i18n.getMessage('defaultTheme')
  1903. ].concat(
  1904. entries.filter(entry => entry.isFile)
  1905. .sort((a, b) => (a.name < b.name ? -1 : 1))
  1906. .map(entry => entry.name.replace(/\.css$/, ''))
  1907. );
  1908. localStorage.codeMirrorThemes = themes.join(' ');
  1909. resolve(themes);
  1910. });
  1911. });
  1912. });
  1913. });
  1914. }
  1915. function setGlobalProgress(done, total) {
  1916. const progressElement = $('#global-progress') ||
  1917. total && document.body.appendChild($element({id: 'global-progress'}));
  1918. if (total) {
  1919. const progress = (done / Math.max(done, total) * 100).toFixed(1);
  1920. progressElement.style.borderLeftWidth = progress + 'vw';
  1921. setTimeout(() => {
  1922. progressElement.title = progress + '%';
  1923. });
  1924. } else if (progressElement) {
  1925. progressElement.remove();
  1926. }
  1927. }