edit.js 62 KB

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