edit.js: pull out hooks, sections, keymap help, beautify
just a simple preparation for a proper encapsulation in the future
This commit is contained in:
		
							parent
							
								
									6704956b22
								
							
						
					
					
						commit
						0e61de2920
					
				|  | @ -28,6 +28,9 @@ | |||
|     <script src="edit/applies-to-line-widget.js"></script> | ||||
|     <script src="edit/source-editor.js"></script> | ||||
|     <script src="edit/colorpicker-helper.js"></script> | ||||
|     <script src="edit/beautify.js"></script> | ||||
|     <script src="edit/sections.js"></script> | ||||
|     <script src="edit/show-keymap-help.js"></script> | ||||
|     <script src="edit/edit.js"></script> | ||||
| 
 | ||||
|     <script src="vendor/codemirror/lib/codemirror.js"></script> | ||||
|  | @ -62,8 +65,10 @@ | |||
|     <script src="vendor/codemirror/keymap/emacs.js"></script> | ||||
|     <script src="vendor/codemirror/keymap/vim.js"></script> | ||||
| 
 | ||||
|     <script src="/edit/match-highlighter-helper.js"></script> | ||||
|     <script src="/edit/codemirror-default.js"></script> | ||||
|     <script src="edit/match-highlighter-helper.js"></script> | ||||
|     <script src="edit/codemirror-editing-hooks.js"></script> | ||||
|     <script src="edit/codemirror-default.js"></script> | ||||
| 
 | ||||
|     <link rel="stylesheet" href="/edit/codemirror-default.css"> | ||||
|     <link id="cm-theme" rel="stylesheet"> | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										96
									
								
								edit/beautify.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								edit/beautify.js
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,96 @@ | |||
| /* | ||||
| global CodeMirror loadScript css_beautify | ||||
| global editors getSectionForChild showHelp | ||||
| */ | ||||
| 'use strict'; | ||||
| 
 | ||||
| function beautify(event) { | ||||
|   loadScript('/vendor-overwrites/beautify/beautify-css-mod.js') | ||||
|     .then(() => { | ||||
|       if (!window.css_beautify && window.exports) { | ||||
|         window.css_beautify = window.exports.css_beautify; | ||||
|       } | ||||
|     }) | ||||
|     .then(doBeautify); | ||||
| 
 | ||||
|   function doBeautify() { | ||||
|     const tabs = prefs.get('editor.indentWithTabs'); | ||||
|     const options = prefs.get('editor.beautify'); | ||||
|     options.indent_size = tabs ? 1 : prefs.get('editor.tabSize'); | ||||
|     options.indent_char = tabs ? '\t' : ' '; | ||||
| 
 | ||||
|     const section = getSectionForChild(event.target); | ||||
|     const scope = section ? [section.CodeMirror] : editors; | ||||
| 
 | ||||
|     showHelp(t('styleBeautify'), '<div class="beautify-options">' + | ||||
|       optionHtml('.selector1,', 'selector_separator_newline') + | ||||
|       optionHtml('.selector2,', 'newline_before_open_brace') + | ||||
|       optionHtml('{', 'newline_after_open_brace') + | ||||
|       optionHtml('border: none;', 'newline_between_properties', true) + | ||||
|       optionHtml('display: block;', 'newline_before_close_brace', true) + | ||||
|       optionHtml('}', 'newline_between_rules') + | ||||
|       `<label style="display: block; clear: both;"><input data-option="indent_conditional" type="checkbox"
 | ||||
|         ${options.indent_conditional !== false ? 'checked' : ''}>` +
 | ||||
|         t('styleBeautifyIndentConditional') + '</label>' + | ||||
|       '</div>' + | ||||
|       '<div><button role="undo"></button></div>'); | ||||
| 
 | ||||
|     const undoButton = $('#help-popup button[role="undo"]'); | ||||
|     undoButton.textContent = t(scope.length === 1 ? 'undo' : 'undoGlobal'); | ||||
|     undoButton.addEventListener('click', () => { | ||||
|       let undoable = false; | ||||
|       scope.forEach(cm => { | ||||
|         if (cm.beautifyChange && cm.beautifyChange[cm.changeGeneration()]) { | ||||
|           delete cm.beautifyChange[cm.changeGeneration()]; | ||||
|           cm.undo(); | ||||
|           cm.scrollIntoView(cm.getCursor()); | ||||
|           undoable |= cm.beautifyChange[cm.changeGeneration()]; | ||||
|         } | ||||
|       }); | ||||
|       undoButton.disabled = !undoable; | ||||
|     }); | ||||
| 
 | ||||
|     scope.forEach(cm => { | ||||
|       setTimeout(() => { | ||||
|         const pos = options.translate_positions = | ||||
|           [].concat.apply([], cm.doc.sel.ranges.map(r => | ||||
|             [Object.assign({}, r.anchor), Object.assign({}, r.head)])); | ||||
|         const text = cm.getValue(); | ||||
|         const newText = css_beautify(text, options); | ||||
|         if (newText !== text) { | ||||
|           if (!cm.beautifyChange || !cm.beautifyChange[cm.changeGeneration()]) { | ||||
|             // clear the list if last change wasn't a css-beautify
 | ||||
|             cm.beautifyChange = {}; | ||||
|           } | ||||
|           cm.setValue(newText); | ||||
|           const selections = []; | ||||
|           for (let i = 0; i < pos.length; i += 2) { | ||||
|             selections.push({anchor: pos[i], head: pos[i + 1]}); | ||||
|           } | ||||
|           cm.setSelections(selections); | ||||
|           cm.beautifyChange[cm.changeGeneration()] = true; | ||||
|           undoButton.disabled = false; | ||||
|         } | ||||
|       }, 0); | ||||
|     }); | ||||
| 
 | ||||
|     $('.beautify-options').onchange = ({target}) => { | ||||
|       const value = target.type === 'checkbox' ? target.checked : target.selectedIndex > 0; | ||||
|       prefs.set('editor.beautify', Object.assign(options, {[target.dataset.option]: value})); | ||||
|       if (target.parentNode.hasAttribute('newline')) { | ||||
|         target.parentNode.setAttribute('newline', value.toString()); | ||||
|       } | ||||
|       doBeautify(); | ||||
|     }; | ||||
| 
 | ||||
|     function optionHtml(label, optionName, indent) { | ||||
|       const value = options[optionName]; | ||||
|       return '<div newline="' + value.toString() + '">' + | ||||
|         '<span' + (indent ? ' indent' : '') + '>' + label + '</span>' + | ||||
|         '<select data-option="' + optionName + '">' + | ||||
|           '<option' + (value ? '' : ' selected') + '> </option>' + | ||||
|           '<option' + (value ? ' selected' : '') + '>\\n</option>' + | ||||
|         '</select></div>'; | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										687
									
								
								edit/codemirror-editing-hooks.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										687
									
								
								edit/codemirror-editing-hooks.js
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,687 @@ | |||
| /* | ||||
| global CodeMirror linterConfig loadScript | ||||
| global editors editor styleId | ||||
| global save toggleStyle setupAutocomplete makeSectionVisible getSectionForChild | ||||
| */ | ||||
| 'use strict'; | ||||
| 
 | ||||
| onDOMready().then(() => { | ||||
|   CodeMirror.defaults.lint = linterConfig.getForCodeMirror(); | ||||
| 
 | ||||
|   const COMMANDS = { | ||||
|     save, | ||||
|     toggleStyle, | ||||
|     jumpToLine, | ||||
|     nextEditor, prevEditor, | ||||
|     find, findNext, findPrev, replace, replaceAll, | ||||
|   }; | ||||
|   // reroute handling to nearest editor when keypress resolves to one of these commands
 | ||||
|   const REROUTED = new Set([ | ||||
|     ...Object.keys(COMMANDS), | ||||
|     'colorpicker', | ||||
|   ]); | ||||
| 
 | ||||
|   const ORIGINAL_COMMAND = { | ||||
|     find: CodeMirror.commands.find, | ||||
|     findNext: CodeMirror.commands.findNext, | ||||
|     findPrev: CodeMirror.commands.findPrev, | ||||
|     replace: CodeMirror.commands.replace | ||||
|   }; | ||||
|   const ORIGINAL_METHOD = { | ||||
|     openDialog: CodeMirror.prototype.openDialog, | ||||
|     openConfirm: CodeMirror.prototype.openConfirm, | ||||
|   }; | ||||
| 
 | ||||
|   Object.assign(CodeMirror, { | ||||
|     getOption, | ||||
|     setOption, | ||||
|   }); | ||||
|   Object.assign(CodeMirror.commands, | ||||
|     COMMANDS); | ||||
|   Object.assign(CodeMirror.prototype, { | ||||
|     getSection, | ||||
|     rerouteHotkeys, | ||||
|   }); | ||||
| 
 | ||||
|   // cm.state.search for last used 'find'
 | ||||
|   let searchState; | ||||
| 
 | ||||
|   buildOptionsElements(); | ||||
|   setupLivePrefs(); | ||||
|   rerouteHotkeys(true); | ||||
|   $('#options').addEventListener('change', onOptionElementChanged); | ||||
| 
 | ||||
|   return; | ||||
| 
 | ||||
|   ////////////////////////////////////////////////
 | ||||
| 
 | ||||
|   function getOption(o) { | ||||
|     return CodeMirror.defaults[o]; | ||||
|   } | ||||
| 
 | ||||
|   function setOption(o, v) { | ||||
|     CodeMirror.defaults[o] = v; | ||||
|     if (editors.length > 4 && (o === 'theme' || o === 'lineWrapping')) { | ||||
|       throttleSetOption({key: o, value: v, index: 0}); | ||||
|       return; | ||||
|     } | ||||
|     editors.forEach(editor => { | ||||
|       editor.setOption(o, v); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   function throttleSetOption({ | ||||
|     key, | ||||
|     value, | ||||
|     index, | ||||
|     timeStart = performance.now(), | ||||
|     cmStart = editors.lastActive || editors[0], | ||||
|     editorsCopy = editors.slice(), | ||||
|     progress, | ||||
|   }) { | ||||
|     if (index === 0) { | ||||
|       if (!cmStart) { | ||||
|         return; | ||||
|       } | ||||
|       cmStart.setOption(key, value); | ||||
|     } | ||||
| 
 | ||||
|     const THROTTLE_AFTER_MS = 100; | ||||
|     const THROTTLE_SHOW_PROGRESS_AFTER_MS = 100; | ||||
| 
 | ||||
|     const t0 = performance.now(); | ||||
|     const total = editorsCopy.length; | ||||
|     while (index < total) { | ||||
|       const cm = editorsCopy[index++]; | ||||
|       if (cm === cmStart || | ||||
|           cm !== editors[index] && !editors.includes(cm)) { | ||||
|         continue; | ||||
|       } | ||||
|       cm.setOption(key, value); | ||||
|       if (performance.now() - t0 > THROTTLE_AFTER_MS) { | ||||
|         break; | ||||
|       } | ||||
|     } | ||||
|     if (index >= total) { | ||||
|       $.remove(progress); | ||||
|       return; | ||||
|     } | ||||
|     if (!progress && | ||||
|         index < total / 2 && | ||||
|         t0 - timeStart > THROTTLE_SHOW_PROGRESS_AFTER_MS) { | ||||
|       let option = $('#editor.' + key); | ||||
|       if (option) { | ||||
|         if (option.type === 'checkbox') { | ||||
|           option = (option.labels || [])[0] || option.nextElementSibling || option; | ||||
|         } | ||||
|         progress = document.body.appendChild($element({ | ||||
|           className: 'set-option-progress', | ||||
|           targetElement: option, | ||||
|         })); | ||||
|       } | ||||
|     } | ||||
|     if (progress) { | ||||
|       const optionBounds = progress.targetElement.getBoundingClientRect(); | ||||
|       const bounds = { | ||||
|         top: optionBounds.top + window.scrollY + 1, | ||||
|         left: optionBounds.left + window.scrollX + 1, | ||||
|         width: (optionBounds.width - 2) * index / total | 0, | ||||
|         height: optionBounds.height - 2, | ||||
|       }; | ||||
|       const style = progress.style; | ||||
|       for (const prop in bounds) { | ||||
|         if (bounds[prop] !== parseFloat(style[prop])) { | ||||
|           style[prop] = bounds[prop] + 'px'; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     setTimeout(throttleSetOption, 0, { | ||||
|       key, | ||||
|       value, | ||||
|       index, | ||||
|       timeStart, | ||||
|       cmStart, | ||||
|       editorsCopy, | ||||
|       progress, | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   function getSection() { | ||||
|     return this.display.wrapper.parentNode; | ||||
|   } | ||||
| 
 | ||||
|   function nextEditor(cm) { | ||||
|     nextPrevEditor(cm, 1); | ||||
|   } | ||||
| 
 | ||||
|   function prevEditor(cm) { | ||||
|     nextPrevEditor(cm, -1); | ||||
|   } | ||||
| 
 | ||||
|   function nextPrevEditor(cm, direction) { | ||||
|     cm = editors[(editors.indexOf(cm) + direction + editors.length) % editors.length]; | ||||
|     makeSectionVisible(cm); | ||||
|     cm.focus(); | ||||
|     return cm; | ||||
|   } | ||||
| 
 | ||||
|   function jumpToLine(cm) { | ||||
|     const cur = cm.getCursor(); | ||||
|     refocusMinidialog(cm); | ||||
|     cm.openDialog(template.jumpToLine.cloneNode(true), str => { | ||||
|       const m = str.match(/^\s*(\d+)(?:\s*:\s*(\d+))?\s*$/); | ||||
|       if (m) { | ||||
|         cm.setCursor(m[1] - 1, m[2] ? m[2] - 1 : cur.ch); | ||||
|       } | ||||
|     }, {value: cur.line + 1}); | ||||
|   } | ||||
| 
 | ||||
|   function refocusMinidialog(cm) { | ||||
|     const section = cm.getSection(); | ||||
|     if (!$('.CodeMirror-dialog', section)) { | ||||
|       return; | ||||
|     } | ||||
|     // close the currently opened minidialog
 | ||||
|     cm.focus(); | ||||
|     // make sure to focus the input in newly opened minidialog
 | ||||
|     setTimeout(() => { | ||||
|       $('.CodeMirror-dialog', section).focus(); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   function onOptionElementChanged(event) { | ||||
|     const el = event.target; | ||||
|     let option = el.id.replace(/^editor\./, ''); | ||||
|     //console.log('acmeEventListener heard %s on %s', event.type, el.id);
 | ||||
|     if (!option) { | ||||
|       console.error('acmeEventListener: no "cm_option" %O', el); | ||||
|       return; | ||||
|     } | ||||
|     let value = el.type === 'checkbox' ? el.checked : el.value; | ||||
|     switch (option) { | ||||
|       case 'tabSize': | ||||
|         value = Number(value); | ||||
|         CodeMirror.setOption('indentUnit', value); | ||||
|         break; | ||||
| 
 | ||||
|       case 'theme': { | ||||
|         const themeLink = $('#cm-theme'); | ||||
|         // use non-localized 'default' internally
 | ||||
|         if (!value || value === 'default' || value === t('defaultTheme')) { | ||||
|           value = 'default'; | ||||
|           if (prefs.get(el.id) !== value) { | ||||
|             prefs.set(el.id, value); | ||||
|           } | ||||
|           themeLink.href = ''; | ||||
|           el.selectedIndex = 0; | ||||
|           break; | ||||
|         } | ||||
|         const url = chrome.runtime.getURL('vendor/codemirror/theme/' + value + '.css'); | ||||
|         if (themeLink.href === url) { | ||||
|           // preloaded in initCodeMirror()
 | ||||
|           break; | ||||
|         } | ||||
|         // avoid flicker: wait for the second stylesheet to load, then apply the theme
 | ||||
|         document.head.appendChild($element({ | ||||
|           tag: 'link', | ||||
|           id: 'cm-theme2', | ||||
|           rel: 'stylesheet', | ||||
|           href: url | ||||
|         })); | ||||
|         setTimeout(() => { | ||||
|           CodeMirror.setOption(option, value); | ||||
|           themeLink.remove(); | ||||
|           $('#cm-theme2').id = 'cm-theme'; | ||||
|         }, 100); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       case 'autocompleteOnTyping': | ||||
|         editors.forEach(cm => setupAutocomplete(cm, el.checked)); | ||||
|         return; | ||||
| 
 | ||||
|       case 'autoCloseBrackets': | ||||
|         Promise.resolve(value && loadScript('/vendor/codemirror/addon/edit/closebrackets.js')).then(() => { | ||||
|           CodeMirror.setOption(option, value); | ||||
|         }); | ||||
|         return; | ||||
| 
 | ||||
|       case 'matchHighlight': | ||||
|         switch (value) { | ||||
|           case 'token': | ||||
|           case 'selection': | ||||
|             document.body.dataset[option] = value; | ||||
|             value = {showToken: value === 'token' && /[#.\-\w]/, annotateScrollbar: true}; | ||||
|             break; | ||||
|           default: | ||||
|             value = null; | ||||
|         } | ||||
|         option = 'highlightSelectionMatches'; | ||||
|         break; | ||||
| 
 | ||||
|       case 'colorpicker': | ||||
|         return; | ||||
|     } | ||||
|     CodeMirror.setOption(option, value); | ||||
|   } | ||||
| 
 | ||||
|   function buildOptionsElements() { | ||||
|     // no need to escape the period in the id
 | ||||
|     const themeControl = $('#editor.theme'); | ||||
|     const themeList = localStorage.codeMirrorThemes; | ||||
|     if (themeList) { | ||||
|       optionsFromArray(themeControl, themeList.split(/\s+/)); | ||||
|     } else { | ||||
|       // Chrome is starting up and shows our edit.html, but the background page isn't loaded yet
 | ||||
|       const theme = prefs.get('editor.theme'); | ||||
|       optionsFromArray(themeControl, [theme === 'default' ? t('defaultTheme') : theme]); | ||||
|       getCodeMirrorThemes().then(() => { | ||||
|         const themes = (localStorage.codeMirrorThemes || '').split(/\s+/); | ||||
|         optionsFromArray(themeControl, themes); | ||||
|         themeControl.selectedIndex = Math.max(0, themes.indexOf(theme)); | ||||
|       }); | ||||
|     } | ||||
|     optionsFromArray($('#editor.keyMap'), Object.keys(CodeMirror.keyMap).sort()); | ||||
|   } | ||||
| 
 | ||||
|   function optionsFromArray(parent, options) { | ||||
|     const fragment = document.createDocumentFragment(); | ||||
|     for (const opt of options) { | ||||
|       fragment.appendChild($element({tag: 'option', textContent: opt})); | ||||
|     } | ||||
|     parent.appendChild(fragment); | ||||
|   } | ||||
| 
 | ||||
|   /////////////////////
 | ||||
| 
 | ||||
|   function shouldIgnoreCase(query) { | ||||
|     // treat all-lowercase non-regexp queries as case-insensitive
 | ||||
|     return typeof query === 'string' && query === query.toLowerCase(); | ||||
|   } | ||||
| 
 | ||||
|   function updateState(cm, newState) { | ||||
|     if (!newState) { | ||||
|       if (cm.state.search) { | ||||
|         return cm.state.search; | ||||
|       } | ||||
|       if (!searchState) { | ||||
|         return null; | ||||
|       } | ||||
|       newState = searchState; | ||||
|     } | ||||
|     cm.state.search = { | ||||
|       query: newState.query, | ||||
|       overlay: newState.overlay, | ||||
|       annotate: cm.showMatchesOnScrollbar(newState.query, shouldIgnoreCase(newState.query)) | ||||
|     }; | ||||
|     cm.addOverlay(newState.overlay); | ||||
|     return cm.state.search; | ||||
|   } | ||||
| 
 | ||||
|   // overrides the original openDialog with a clone of the provided template
 | ||||
|   function customizeOpenDialog(cm, template, callback) { | ||||
|     cm.openDialog = (tmpl, cb, opt) => { | ||||
|       // invoke 'callback' and bind 'this' to the original callback
 | ||||
|       ORIGINAL_METHOD.openDialog.call(cm, template.cloneNode(true), callback.bind(cb), opt); | ||||
|     }; | ||||
|     setTimeout(() => (cm.openDialog = ORIGINAL_METHOD.openDialog)); | ||||
|     refocusMinidialog(cm); | ||||
|   } | ||||
| 
 | ||||
|   function focusClosestCM(activeCM) { | ||||
|     editors.lastActive = activeCM; | ||||
|     const cm = getEditorInSight(); | ||||
|     if (cm !== activeCM) { | ||||
|       cm.focus(); | ||||
|     } | ||||
|     return cm; | ||||
|   } | ||||
| 
 | ||||
|   function find(activeCM) { | ||||
|     activeCM = focusClosestCM(activeCM); | ||||
|     customizeOpenDialog(activeCM, template.find, function (query) { | ||||
|       this(query); | ||||
|       searchState = activeCM.state.search; | ||||
|       if (editors.length === 1 || !searchState.query) { | ||||
|         return; | ||||
|       } | ||||
|       editors.forEach(cm => { | ||||
|         if (cm !== activeCM) { | ||||
|           cm.execCommand('clearSearch'); | ||||
|           updateState(cm, searchState); | ||||
|         } | ||||
|       }); | ||||
|       if (CodeMirror.cmpPos(searchState.posFrom, searchState.posTo) === 0) { | ||||
|         findNext(activeCM); | ||||
|       } | ||||
|     }); | ||||
|     ORIGINAL_COMMAND.find(activeCM); | ||||
|   } | ||||
| 
 | ||||
|   function findNext(activeCM, reverse) { | ||||
|     let state = updateState(activeCM); | ||||
|     if (!state || !state.query) { | ||||
|       find(activeCM); | ||||
|       return; | ||||
|     } | ||||
|     let pos = activeCM.getCursor(reverse ? 'from' : 'to'); | ||||
|     // clear the selection, don't move the cursor
 | ||||
|     activeCM.setSelection(activeCM.getCursor()); | ||||
| 
 | ||||
|     const rxQuery = typeof state.query === 'object' | ||||
|       ? state.query : stringAsRegExp(state.query, shouldIgnoreCase(state.query) ? 'i' : ''); | ||||
| 
 | ||||
|     if ( | ||||
|       document.activeElement && | ||||
|       document.activeElement.name === 'applies-value' && | ||||
|       searchAppliesTo(activeCM) | ||||
|     ) { | ||||
|       return; | ||||
|     } | ||||
|     let cm = activeCM; | ||||
|     for (let i = 0; i < editors.length; i++) { | ||||
|       state = updateState(cm); | ||||
|       if (!cm.hasFocus()) { | ||||
|         pos = reverse ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(0, 0); | ||||
|       } | ||||
|       const searchCursor = cm.getSearchCursor(state.query, pos, shouldIgnoreCase(state.query)); | ||||
|       if (searchCursor.find(reverse)) { | ||||
|         if (editors.length > 1) { | ||||
|           makeSectionVisible(cm); | ||||
|           cm.focus(); | ||||
|         } | ||||
|         // speedup the original findNext
 | ||||
|         state.posFrom = reverse ? searchCursor.to() : searchCursor.from(); | ||||
|         state.posTo = CodeMirror.Pos(state.posFrom.line, state.posFrom.ch); | ||||
|         ORIGINAL_COMMAND[reverse ? 'findPrev' : 'findNext'](cm); | ||||
|         return; | ||||
|       } else if (!reverse && searchAppliesTo(cm)) { | ||||
|         return; | ||||
|       } | ||||
|       cm = editors[(editors.indexOf(cm) + (reverse ? -1 + editors.length : 1)) % editors.length]; | ||||
|       if (reverse && searchAppliesTo(cm)) { | ||||
|         return; | ||||
|       } | ||||
|     } | ||||
|     // nothing found so far, so call the original search with wrap-around
 | ||||
|     ORIGINAL_COMMAND[reverse ? 'findPrev' : 'findNext'](activeCM); | ||||
| 
 | ||||
|     function searchAppliesTo(cm) { | ||||
|       let inputs = $$('.applies-value', cm.getSection()); | ||||
|       if (reverse) { | ||||
|         inputs = inputs.reverse(); | ||||
|       } | ||||
|       inputs.splice(0, inputs.indexOf(document.activeElement) + 1); | ||||
|       return inputs.some(input => { | ||||
|         const match = rxQuery.exec(input.value); | ||||
|         if (match) { | ||||
|           input.focus(); | ||||
|           const end = match.index + match[0].length; | ||||
|           // scroll selected part into view in long inputs,
 | ||||
|           // works only outside of current event handlers chain, hence timeout=0
 | ||||
|           setTimeout(() => { | ||||
|             input.setSelectionRange(end, end); | ||||
|             input.setSelectionRange(match.index, end); | ||||
|           }, 0); | ||||
|           return true; | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function findPrev(cm) { | ||||
|     findNext(cm, true); | ||||
|   } | ||||
| 
 | ||||
|   function replace(activeCM, all) { | ||||
|     let queue; | ||||
|     let query; | ||||
|     let replacement; | ||||
|     activeCM = focusClosestCM(activeCM); | ||||
|     customizeOpenDialog(activeCM, template[all ? 'replaceAll' : 'replace'], function (txt) { | ||||
|       query = txt; | ||||
|       customizeOpenDialog(activeCM, template.replaceWith, txt => { | ||||
|         replacement = txt; | ||||
|         queue = editors.rotate(-editors.indexOf(activeCM)); | ||||
|         if (all) { | ||||
|           editors.forEach(doReplace); | ||||
|         } else { | ||||
|           doReplace(); | ||||
|         } | ||||
|       }); | ||||
|       this(query); | ||||
|     }); | ||||
|     ORIGINAL_COMMAND.replace(activeCM, all); | ||||
| 
 | ||||
|     function doReplace() { | ||||
|       const cm = queue.shift(); | ||||
|       if (!cm) { | ||||
|         if (!all) { | ||||
|           editors.lastActive.focus(); | ||||
|         } | ||||
|         return; | ||||
|       } | ||||
|       // hide the first two dialogs (replace, replaceWith)
 | ||||
|       cm.openDialog = (tmpl, callback) => { | ||||
|         cm.openDialog = (tmpl, callback) => { | ||||
|           cm.openDialog = ORIGINAL_METHOD.openDialog; | ||||
|           if (all) { | ||||
|             callback(replacement); | ||||
|           } else { | ||||
|             doConfirm(cm); | ||||
|             callback(replacement); | ||||
|             if (!$('.CodeMirror-dialog', cm.getWrapperElement())) { | ||||
|               // no dialog == nothing found in the current CM, move to the next
 | ||||
|               doReplace(); | ||||
|             } | ||||
|           } | ||||
|         }; | ||||
|         callback(query); | ||||
|       }; | ||||
|       ORIGINAL_COMMAND.replace(cm, all); | ||||
|     } | ||||
|     function doConfirm(cm) { | ||||
|       let wrapAround = false; | ||||
|       const origPos = cm.getCursor(); | ||||
|       cm.openConfirm = function overrideConfirm(tmpl, callbacks, opt) { | ||||
|         const ovrCallbacks = callbacks.map(callback => () => { | ||||
|           makeSectionVisible(cm); | ||||
|           cm.openConfirm = overrideConfirm; | ||||
|           setTimeout(() => (cm.openConfirm = ORIGINAL_METHOD.openConfirm)); | ||||
| 
 | ||||
|           const pos = cm.getCursor(); | ||||
|           callback(); | ||||
|           const cmp = CodeMirror.cmpPos(cm.getCursor(), pos); | ||||
|           wrapAround |= cmp <= 0; | ||||
| 
 | ||||
|           const dlg = $('.CodeMirror-dialog', cm.getWrapperElement()); | ||||
|           if (!dlg || cmp === 0 || wrapAround && CodeMirror.cmpPos(cm.getCursor(), origPos) >= 0) { | ||||
|             $.remove(dlg); | ||||
|             doReplace(); | ||||
|           } | ||||
|         }); | ||||
|         ORIGINAL_METHOD.openConfirm.call(cm, template.replaceConfirm.cloneNode(true), ovrCallbacks, opt); | ||||
|       }; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function replaceAll(cm) { | ||||
|     replace(cm, true); | ||||
|   } | ||||
| 
 | ||||
|   ////////////////////////////////////////////////
 | ||||
| 
 | ||||
|   function rerouteHotkeys(enable, immediately) { | ||||
|     if (!immediately) { | ||||
|       debounce(rerouteHotkeys, 0, enable, true); | ||||
|     } else if (enable) { | ||||
|       document.addEventListener('keydown', rerouteHandler); | ||||
|     } else { | ||||
|       document.removeEventListener('keydown', rerouteHandler); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function rerouteHandler(event) { | ||||
|     const keyName = CodeMirror.keyName(event); | ||||
|     if (!keyName) { | ||||
|       return; | ||||
|     } | ||||
|     const rerouteCommand = name => { | ||||
|       if (REROUTED.has(name)) { | ||||
|         CodeMirror.commands[name](getEditorInSight(event.target)); | ||||
|         return true; | ||||
|       } | ||||
|     }; | ||||
|     if (CodeMirror.lookupKey(keyName, CodeMirror.defaults.keyMap, rerouteCommand) === 'handled' || | ||||
|         CodeMirror.lookupKey(keyName, CodeMirror.defaults.extraKeys, rerouteCommand) === 'handled') { | ||||
|       event.preventDefault(); | ||||
|       event.stopPropagation(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   ////////////////////////////////////////////////
 | ||||
| 
 | ||||
|   function getEditorInSight(nearbyElement) { | ||||
|     // priority: 1. associated CM for applies-to element 2. last active if visible 3. first visible
 | ||||
|     let cm; | ||||
|     if (nearbyElement && nearbyElement.className.indexOf('applies-') >= 0) { | ||||
|       cm = getSectionForChild(nearbyElement).CodeMirror; | ||||
|     } else { | ||||
|       cm = editors.lastActive; | ||||
|     } | ||||
|     // closest editor should have at least 2 lines visible
 | ||||
|     const lineHeight = editors[0].defaultTextHeight(); | ||||
|     const scrollY = window.scrollY; | ||||
|     const windowBottom = scrollY + window.innerHeight - 2 * lineHeight; | ||||
|     const allSectionsContainerTop = scrollY + $('#sections').getBoundingClientRect().top; | ||||
|     const distances = []; | ||||
|     const alreadyInView = cm && offscreenDistance(null, cm) === 0; | ||||
|     return alreadyInView ? cm : findClosest(); | ||||
| 
 | ||||
|     function offscreenDistance(index, cm) { | ||||
|       if (index >= 0 && distances[index] !== undefined) { | ||||
|         return distances[index]; | ||||
|       } | ||||
|       const section = (cm || editors[index]).getSection(); | ||||
|       const top = allSectionsContainerTop + section.offsetTop; | ||||
|       if (top < scrollY + lineHeight) { | ||||
|         return Math.max(0, scrollY - top - lineHeight); | ||||
|       } | ||||
|       if (top < windowBottom) { | ||||
|         return 0; | ||||
|       } | ||||
|       const distance = top - windowBottom + section.offsetHeight; | ||||
|       if (index >= 0) { | ||||
|         distances[index] = distance; | ||||
|       } | ||||
|       return distance; | ||||
|     } | ||||
| 
 | ||||
|     function findClosest() { | ||||
|       const last = editors.length - 1; | ||||
|       let a = 0; | ||||
|       let b = last; | ||||
|       let c; | ||||
|       let distance; | ||||
|       while (a < b - 1) { | ||||
|         c = (a + b) / 2 | 0; | ||||
|         distance = offscreenDistance(c); | ||||
|         if (!distance || !c) { | ||||
|           break; | ||||
|         } | ||||
|         const distancePrev = offscreenDistance(c - 1); | ||||
|         const distanceNext = c < last ? offscreenDistance(c + 1) : 1e20; | ||||
|         if (distancePrev <= distance && distance <= distanceNext) { | ||||
|           b = c; | ||||
|         } else { | ||||
|           a = c; | ||||
|         } | ||||
|       } | ||||
|       while (b && offscreenDistance(b - 1) <= offscreenDistance(b)) { | ||||
|         b--; | ||||
|       } | ||||
|       const cm = editors[b]; | ||||
|       if (distances[b] > 0) { | ||||
|         makeSectionVisible(cm); | ||||
|       } | ||||
|       return cm; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   ////////////////////////////////////////////////
 | ||||
| 
 | ||||
|   function getCodeMirrorThemes() { | ||||
|     if (!chrome.runtime.getPackageDirectoryEntry) { | ||||
|       const themes = [ | ||||
|         chrome.i18n.getMessage('defaultTheme'), | ||||
|         '3024-day', | ||||
|         '3024-night', | ||||
|         'abcdef', | ||||
|         'ambiance', | ||||
|         'ambiance-mobile', | ||||
|         'base16-dark', | ||||
|         'base16-light', | ||||
|         'bespin', | ||||
|         'blackboard', | ||||
|         'cobalt', | ||||
|         'colorforth', | ||||
|         'dracula', | ||||
|         'duotone-dark', | ||||
|         'duotone-light', | ||||
|         'eclipse', | ||||
|         'elegant', | ||||
|         'erlang-dark', | ||||
|         'hopscotch', | ||||
|         'icecoder', | ||||
|         'isotope', | ||||
|         'lesser-dark', | ||||
|         'liquibyte', | ||||
|         'material', | ||||
|         'mbo', | ||||
|         'mdn-like', | ||||
|         'midnight', | ||||
|         'monokai', | ||||
|         'neat', | ||||
|         'neo', | ||||
|         'night', | ||||
|         'panda-syntax', | ||||
|         'paraiso-dark', | ||||
|         'paraiso-light', | ||||
|         'pastel-on-dark', | ||||
|         'railscasts', | ||||
|         'rubyblue', | ||||
|         'seti', | ||||
|         'solarized', | ||||
|         'the-matrix', | ||||
|         'tomorrow-night-bright', | ||||
|         'tomorrow-night-eighties', | ||||
|         'ttcn', | ||||
|         'twilight', | ||||
|         'vibrant-ink', | ||||
|         'xq-dark', | ||||
|         'xq-light', | ||||
|         'yeti', | ||||
|         'zenburn', | ||||
|       ]; | ||||
|       localStorage.codeMirrorThemes = themes.join(' '); | ||||
|       return Promise.resolve(themes); | ||||
|     } | ||||
|     return new Promise(resolve => { | ||||
|       chrome.runtime.getPackageDirectoryEntry(rootDir => { | ||||
|         rootDir.getDirectory('vendor/codemirror/theme', {create: false}, themeDir => { | ||||
|           themeDir.createReader().readEntries(entries => { | ||||
|             const themes = [ | ||||
|               chrome.i18n.getMessage('defaultTheme') | ||||
|             ].concat( | ||||
|               entries.filter(entry => entry.isFile) | ||||
|                 .sort((a, b) => (a.name < b.name ? -1 : 1)) | ||||
|                 .map(entry => entry.name.replace(/\.css$/, '')) | ||||
|             ); | ||||
|             localStorage.codeMirrorThemes = themes.join(' '); | ||||
|             resolve(themes); | ||||
|           }); | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
| }); | ||||
							
								
								
									
										2078
									
								
								edit/edit.js
									
									
									
									
									
								
							
							
						
						
									
										2078
									
								
								edit/edit.js
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -1,5 +1,5 @@ | |||
| /* global CodeMirror messageBox */ | ||||
| /* global editors makeSectionVisible showCodeMirrorPopup showHelp hotkeyRerouter */ | ||||
| /* global editors makeSectionVisible showCodeMirrorPopup showHelp */ | ||||
| /* global loadScript require CSSLint stylelint */ | ||||
| /* global makeLink */ | ||||
| 'use strict'; | ||||
|  | @ -449,10 +449,10 @@ function setupLinterPopup(config) { | |||
|   cm.on('changes', updateButtonState); | ||||
|   updateButtonState(); | ||||
| 
 | ||||
|   hotkeyRerouter.setState(false); | ||||
|   cm.rerouteHotkeys(false); | ||||
|   window.addEventListener('closeHelp', function _() { | ||||
|     window.removeEventListener('closeHelp', _); | ||||
|     hotkeyRerouter.setState(true); | ||||
|     cm.rerouteHotkeys(true); | ||||
|   }); | ||||
| 
 | ||||
|   loadScript([ | ||||
|  |  | |||
							
								
								
									
										515
									
								
								edit/sections.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										515
									
								
								edit/sections.js
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,515 @@ | |||
| /* | ||||
| global CodeMirror | ||||
| global editors propertyToCss CssToProperty | ||||
| global onChange indicateCodeChange initHooks setCleanGlobal | ||||
| global fromMozillaFormat maximizeCodeHeight toggleContextMenuDelete | ||||
| global setCleanItem updateTitle updateLintReportIfEnabled renderLintReport | ||||
| global showAppliesToHelp beautify regExpTester setGlobalProgress setCleanSection | ||||
| */ | ||||
| 'use strict'; | ||||
| 
 | ||||
| function initWithSectionStyle({style, codeIsUpdated}) { | ||||
|   $('#name').value = style.name || ''; | ||||
|   $('#enabled').checked = style.enabled !== false; | ||||
|   $('#url').href = style.url || ''; | ||||
|   if (codeIsUpdated !== false) { | ||||
|     editors.length = 0; | ||||
|     getSections().forEach(div => div.remove()); | ||||
|     addSections(style.sections.length ? style.sections : [{code: ''}]); | ||||
|     initHooks(); | ||||
|   } | ||||
|   setCleanGlobal(); | ||||
|   updateTitle(); | ||||
| } | ||||
| 
 | ||||
| function addSections(sections, onAdded = () => {}) { | ||||
|   if (addSections.running) { | ||||
|     console.error('addSections cannot be re-entered: please report to the developers'); | ||||
|     // TODO: handle this properly e.g. on update/import
 | ||||
|     return; | ||||
|   } | ||||
|   addSections.running = true; | ||||
|   maximizeCodeHeight.stats = null; | ||||
|   // make a shallow copy since we might run asynchronously
 | ||||
|   // and the original array might get modified
 | ||||
|   sections = sections.slice(); | ||||
|   const t0 = performance.now(); | ||||
|   const divs = []; | ||||
|   let index = 0; | ||||
| 
 | ||||
|   return new Promise(function run(resolve) { | ||||
|     while (index < sections.length) { | ||||
|       const div = addSection(null, sections[index]); | ||||
|       maximizeCodeHeight(div, index === sections.length - 1); | ||||
|       onAdded(div, index); | ||||
|       divs.push(div); | ||||
|       maybeFocusFirstCM(); | ||||
|       index++; | ||||
|       const elapsed = performance.now() - t0; | ||||
|       if (elapsed > 500) { | ||||
|         setGlobalProgress(index, sections.length); | ||||
|       } | ||||
|       if (elapsed > 100) { | ||||
|         // after 100ms the sections are added asynchronously
 | ||||
|         setTimeout(run, 0, resolve); | ||||
|         return; | ||||
|       } | ||||
|     } | ||||
|     editors.last.state.renderLintReportNow = true; | ||||
|     addSections.running = false; | ||||
|     setGlobalProgress(); | ||||
|     resolve(divs); | ||||
|   }); | ||||
| 
 | ||||
|   function maybeFocusFirstCM() { | ||||
|     const isPageLocked = document.documentElement.style.pointerEvents; | ||||
|     if (divs[0] && (isPageLocked ? divs.length === sections.length : index === 0)) { | ||||
|       makeSectionVisible(divs[0].CodeMirror); | ||||
|       divs[0].CodeMirror.focus(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function addSection(event, section) { | ||||
|   const div = template.section.cloneNode(true); | ||||
|   $('.applies-to-help', div).addEventListener('click', showAppliesToHelp, false); | ||||
|   $('.remove-section', div).addEventListener('click', removeSection, false); | ||||
|   $('.add-section', div).addEventListener('click', addSection, false); | ||||
|   $('.beautify-section', div).addEventListener('click', beautify); | ||||
| 
 | ||||
|   const code = (section || {}).code || ''; | ||||
| 
 | ||||
|   const appliesTo = $('.applies-to-list', div); | ||||
|   let appliesToAdded = false; | ||||
| 
 | ||||
|   if (section) { | ||||
|     for (const i in propertyToCss) { | ||||
|       if (section[i]) { | ||||
|         section[i].forEach(url => { | ||||
|           addAppliesTo(appliesTo, propertyToCss[i], url); | ||||
|           appliesToAdded = true; | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   if (!appliesToAdded) { | ||||
|     addAppliesTo(appliesTo); | ||||
|   } | ||||
| 
 | ||||
|   appliesTo.addEventListener('change', onChange); | ||||
|   appliesTo.addEventListener('input', onChange); | ||||
| 
 | ||||
|   toggleTestRegExpVisibility(); | ||||
|   appliesTo.addEventListener('change', toggleTestRegExpVisibility); | ||||
|   $('.test-regexp', div).onclick = () => { | ||||
|     regExpTester.toggle(); | ||||
|     regExpTester.update(getRegExps()); | ||||
|   }; | ||||
| 
 | ||||
|   function getRegExps() { | ||||
|     return [...appliesTo.children] | ||||
|       .map(item => | ||||
|         !item.matches('.applies-to-everything') && | ||||
|         $('.applies-type', item).value === 'regexp' && | ||||
|         $('.applies-value', item).value.trim() | ||||
|       ) | ||||
|       .filter(item => item); | ||||
|   } | ||||
| 
 | ||||
|   function toggleTestRegExpVisibility() { | ||||
|     const show = getRegExps().length > 0; | ||||
|     div.classList.toggle('has-regexp', show); | ||||
|     appliesTo.oninput = appliesTo.oninput || show && (event => { | ||||
|       if (event.target.matches('.applies-value') && | ||||
|           $('.applies-type', event.target.parentElement).value === 'regexp') { | ||||
|         regExpTester.update(getRegExps()); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   const sections = $('#sections'); | ||||
|   let cm; | ||||
|   if (event) { | ||||
|     const clickedSection = getSectionForChild(event.target); | ||||
|     sections.insertBefore(div, clickedSection.nextElementSibling); | ||||
|     const newIndex = getSections().indexOf(clickedSection) + 1; | ||||
|     cm = setupCodeMirror(div, code, newIndex); | ||||
|     makeSectionVisible(cm); | ||||
|     renderLintReport(); | ||||
|     cm.focus(); | ||||
|   } else { | ||||
|     sections.appendChild(div); | ||||
|     cm = setupCodeMirror(div, code); | ||||
|   } | ||||
|   div.CodeMirror = cm; | ||||
|   setCleanSection(div); | ||||
|   return div; | ||||
| } | ||||
| 
 | ||||
| function addAppliesTo(list, name, value) { | ||||
|   const showingEverything = $('.applies-to-everything', list) !== null; | ||||
|   // blow away 'Everything' if it's there
 | ||||
|   if (showingEverything) { | ||||
|     list.removeChild(list.firstChild); | ||||
|   } | ||||
|   let e; | ||||
|   if (name) { | ||||
|     e = template.appliesTo.cloneNode(true); | ||||
|     $('[name=applies-type]', e).value = name; | ||||
|     $('[name=applies-value]', e).value = value; | ||||
|     $('.remove-applies-to', e).addEventListener('click', removeAppliesTo, false); | ||||
|   } else if (showingEverything || list.hasChildNodes()) { | ||||
|     e = template.appliesTo.cloneNode(true); | ||||
|     if (list.hasChildNodes()) { | ||||
|       $('[name=applies-type]', e).value = $('li:last-child [name="applies-type"]', list).value; | ||||
|     } | ||||
|     $('.remove-applies-to', e).addEventListener('click', removeAppliesTo, false); | ||||
|   } else { | ||||
|     e = template.appliesToEverything.cloneNode(true); | ||||
|   } | ||||
|   $('.add-applies-to', e).addEventListener('click', function () { | ||||
|     addAppliesTo(this.parentNode.parentNode); | ||||
|   }, false); | ||||
|   list.appendChild(e); | ||||
| } | ||||
| 
 | ||||
| function setupCodeMirror(sectionDiv, code, index) { | ||||
|   const cm = CodeMirror(wrapper => { | ||||
|     $('.code-label', sectionDiv).insertAdjacentElement('afterend', wrapper); | ||||
|   }, { | ||||
|     value: code, | ||||
|   }); | ||||
|   const wrapper = cm.display.wrapper; | ||||
| 
 | ||||
|   let onChangeTimer; | ||||
|   cm.on('changes', (cm, changes) => { | ||||
|     clearTimeout(onChangeTimer); | ||||
|     onChangeTimer = setTimeout(indicateCodeChange, 200, cm, changes); | ||||
|   }); | ||||
|   if (prefs.get('editor.autocompleteOnTyping')) { | ||||
|     setupAutocomplete(cm); | ||||
|   } | ||||
|   wrapper.addEventListener('keydown', event => nextPrevEditorOnKeydown(cm, event), true); | ||||
|   cm.on('blur', () => { | ||||
|     editors.lastActive = cm; | ||||
|     cm.rerouteHotkeys(true); | ||||
|     setTimeout(() => { | ||||
|       wrapper.classList.toggle('CodeMirror-active', wrapper.contains(document.activeElement)); | ||||
|     }); | ||||
|   }); | ||||
|   cm.on('focus', () => { | ||||
|     cm.rerouteHotkeys(false); | ||||
|     wrapper.classList.add('CodeMirror-active'); | ||||
|   }); | ||||
|   cm.on('paste', (cm, event) => { | ||||
|     const text = event.clipboardData.getData('text') || ''; | ||||
|     if ( | ||||
|       text.includes('@-moz-document') && | ||||
|       text.replace(/\/\*[\s\S]*?\*\//g, '') | ||||
|         .match(/@-moz-document[\s\r\n]+(url|url-prefix|domain|regexp)\(/) | ||||
|     ) { | ||||
|       event.preventDefault(); | ||||
|       fromMozillaFormat(); | ||||
|       $('#help-popup').codebox.setValue(text); | ||||
|       $('#help-popup').codebox.clearHistory(); | ||||
|       $('#help-popup').codebox.markClean(); | ||||
|     } | ||||
|     if (editors.length === 1) { | ||||
|       setTimeout(() => { | ||||
|         if (cm.display.sizer.clientHeight > cm.display.wrapper.clientHeight) { | ||||
|           maximizeCodeHeight.stats = null; | ||||
|           maximizeCodeHeight(cm.getSection(), true); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|   }); | ||||
|   if (!FIREFOX) { | ||||
|     cm.on('mousedown', (cm, event) => toggleContextMenuDelete.call(cm, event)); | ||||
|   } | ||||
| 
 | ||||
|   wrapper.classList.add('resize-grip-enabled'); | ||||
|   let lastClickTime = 0; | ||||
|   const resizeGrip = wrapper.appendChild(template.resizeGrip.cloneNode(true)); | ||||
|   resizeGrip.onmousedown = event => { | ||||
|     if (event.button !== 0) { | ||||
|       return; | ||||
|     } | ||||
|     event.preventDefault(); | ||||
|     if (Date.now() - lastClickTime < 500) { | ||||
|       lastClickTime = 0; | ||||
|       toggleSectionHeight(cm); | ||||
|       return; | ||||
|     } | ||||
|     lastClickTime = Date.now(); | ||||
|     const minHeight = cm.defaultTextHeight() + | ||||
|       /* .CodeMirror-lines padding */ | ||||
|       cm.display.lineDiv.offsetParent.offsetTop + | ||||
|       /* borders */ | ||||
|       wrapper.offsetHeight - wrapper.clientHeight; | ||||
|     wrapper.style.pointerEvents = 'none'; | ||||
|     document.body.style.cursor = 's-resize'; | ||||
|     function resize(e) { | ||||
|       const cmPageY = wrapper.getBoundingClientRect().top + window.scrollY; | ||||
|       const height = Math.max(minHeight, e.pageY - cmPageY); | ||||
|       if (height !== wrapper.clientHeight) { | ||||
|         cm.setSize(null, height); | ||||
|       } | ||||
|     } | ||||
|     document.addEventListener('mousemove', resize); | ||||
|     document.addEventListener('mouseup', function resizeStop() { | ||||
|       document.removeEventListener('mouseup', resizeStop); | ||||
|       document.removeEventListener('mousemove', resize); | ||||
|       wrapper.style.pointerEvents = ''; | ||||
|       document.body.style.cursor = ''; | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   editors.splice(index || editors.length, 0, cm); | ||||
|   return cm; | ||||
| } | ||||
| 
 | ||||
| function indicateCodeChange(cm) { | ||||
|   const section = cm.getSection(); | ||||
|   setCleanItem(section, cm.isClean(section.savedValue)); | ||||
|   updateTitle(); | ||||
|   updateLintReportIfEnabled(cm); | ||||
| } | ||||
| 
 | ||||
| function setupAutocomplete(cm, enable = true) { | ||||
|   const onOff = enable ? 'on' : 'off'; | ||||
|   cm[onOff]('changes', autocompleteOnTyping); | ||||
|   cm[onOff]('pick', autocompletePicked); | ||||
| } | ||||
| 
 | ||||
| function autocompleteOnTyping(cm, [info], debounced) { | ||||
|   if ( | ||||
|     cm.state.completionActive || | ||||
|     info.origin && !info.origin.includes('input') || | ||||
|     !info.text.last | ||||
|   ) { | ||||
|     return; | ||||
|   } | ||||
|   if (cm.state.autocompletePicked) { | ||||
|     cm.state.autocompletePicked = false; | ||||
|     return; | ||||
|   } | ||||
|   if (!debounced) { | ||||
|     debounce(autocompleteOnTyping, 100, cm, [info], true); | ||||
|     return; | ||||
|   } | ||||
|   if (info.text.last.match(/[-a-z!]+$/i)) { | ||||
|     cm.state.autocompletePicked = false; | ||||
|     cm.options.hintOptions.completeSingle = false; | ||||
|     cm.execCommand('autocomplete'); | ||||
|     setTimeout(() => { | ||||
|       cm.options.hintOptions.completeSingle = true; | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function autocompletePicked(cm) { | ||||
|   cm.state.autocompletePicked = true; | ||||
| } | ||||
| 
 | ||||
| function nextPrevEditorOnKeydown(cm, event) { | ||||
|   const key = event.which; | ||||
|   if (key < 37 || key > 40 || event.shiftKey || event.altKey || event.metaKey) { | ||||
|     return; | ||||
|   } | ||||
|   const {line, ch} = cm.getCursor(); | ||||
|   switch (key) { | ||||
|     case 37: | ||||
|       // arrow Left
 | ||||
|       if (line || ch) { | ||||
|         return; | ||||
|       } | ||||
|     // fallthrough to arrow Up
 | ||||
|     case 38: | ||||
|       // arrow Up
 | ||||
|       if (line > 0 || cm === editors[0]) { | ||||
|         return; | ||||
|       } | ||||
|       event.preventDefault(); | ||||
|       event.stopPropagation(); | ||||
|       cm = CodeMirror.commands.prevEditor.call(cm); | ||||
|       cm.setCursor(cm.doc.size - 1, key === 37 ? 1e20 : ch); | ||||
|       break; | ||||
|     case 39: | ||||
|       // arrow Right
 | ||||
|       if (line < cm.doc.size - 1 || ch < cm.getLine(line).length - 1) { | ||||
|         return; | ||||
|       } | ||||
|     // fallthrough to arrow Down
 | ||||
|     case 40: | ||||
|       // arrow Down
 | ||||
|       if (line < cm.doc.size - 1 || cm === editors.last) { | ||||
|         return; | ||||
|       } | ||||
|       event.preventDefault(); | ||||
|       event.stopPropagation(); | ||||
|       cm = CodeMirror.commands.nextEditor.call(cm); | ||||
|       cm.setCursor(0, 0); | ||||
|       break; | ||||
|   } | ||||
|   const animation = (cm.getSection().firstElementChild.getAnimations() || [])[0]; | ||||
|   if (animation) { | ||||
|     animation.playbackRate = -1; | ||||
|     animation.currentTime = 2000; | ||||
|     animation.play(); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function toggleSectionHeight(cm) { | ||||
|   if (cm.state.toggleHeightSaved) { | ||||
|     // restore previous size
 | ||||
|     cm.setSize(null, cm.state.toggleHeightSaved); | ||||
|     cm.state.toggleHeightSaved = 0; | ||||
|   } else { | ||||
|     // maximize
 | ||||
|     const wrapper = cm.display.wrapper; | ||||
|     const allBounds = $('#sections').getBoundingClientRect(); | ||||
|     const pageExtrasHeight = allBounds.top + window.scrollY + | ||||
|       parseFloat(getComputedStyle($('#sections')).paddingBottom); | ||||
|     const sectionExtrasHeight = cm.getSection().clientHeight - wrapper.offsetHeight; | ||||
|     cm.state.toggleHeightSaved = wrapper.clientHeight; | ||||
|     cm.setSize(null, window.innerHeight - sectionExtrasHeight - pageExtrasHeight); | ||||
|     const bounds = cm.getSection().getBoundingClientRect(); | ||||
|     if (bounds.top < 0 || bounds.bottom > window.innerHeight) { | ||||
|       window.scrollBy(0, bounds.top); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function getSectionForChild(e) { | ||||
|   return e.closest('#sections > div'); | ||||
| } | ||||
| 
 | ||||
| function getSections() { | ||||
|   return $$('#sections > div'); | ||||
| } | ||||
| 
 | ||||
| function getSectionsHashes() { | ||||
|   const sections = []; | ||||
|   getSections().forEach(div => { | ||||
|     const meta = {urls: [], urlPrefixes: [], domains: [], regexps: []}; | ||||
|     for (const li of $('.applies-to-list', div).childNodes) { | ||||
|       if (li.className === template.appliesToEverything.className) { | ||||
|         return; | ||||
|       } | ||||
|       const type = $('[name=applies-type]', li).value; | ||||
|       const value = $('[name=applies-value]', li).value; | ||||
|       if (type && value) { | ||||
|         meta[CssToProperty[type]].push(value); | ||||
|       } | ||||
|     } | ||||
|     const code = div.CodeMirror.getValue(); | ||||
|     if (/^\s*$/.test(code) && Object.keys(meta).length === 0) { | ||||
|       return; | ||||
|     } | ||||
|     meta.code = code; | ||||
|     sections.push(meta); | ||||
|   }); | ||||
|   return sections; | ||||
| } | ||||
| 
 | ||||
| function removeAppliesTo(event) { | ||||
|   const appliesTo = event.target.parentNode; | ||||
|   const appliesToList = appliesTo.parentNode; | ||||
|   removeAreaAndSetDirty(appliesTo); | ||||
|   if (!appliesToList.hasChildNodes()) { | ||||
|     addAppliesTo(appliesToList); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function removeSection(event) { | ||||
|   const section = getSectionForChild(event.target); | ||||
|   const cm = section.CodeMirror; | ||||
|   removeAreaAndSetDirty(section); | ||||
|   editors.splice(editors.indexOf(cm), 1); | ||||
|   renderLintReport(); | ||||
| } | ||||
| 
 | ||||
| function removeAreaAndSetDirty(area) { | ||||
|   const contributors = $$('.style-contributor', area); | ||||
|   if (!contributors.length) { | ||||
|     setCleanItem(area, false); | ||||
|   } | ||||
|   contributors.some(node => { | ||||
|     if (node.savedValue) { | ||||
|       // it's a saved section, so make it dirty and stop the enumeration
 | ||||
|       setCleanItem(area, false); | ||||
|       return true; | ||||
|     } else { | ||||
|       // it's an empty section, so undirty the applies-to items,
 | ||||
|       // otherwise orphaned ids would keep the style dirty
 | ||||
|       setCleanItem(node, true); | ||||
|     } | ||||
|   }); | ||||
|   updateTitle(); | ||||
|   area.parentNode.removeChild(area); | ||||
| } | ||||
| 
 | ||||
| function makeSectionVisible(cm) { | ||||
|   const section = cm.getSection(); | ||||
|   const bounds = section.getBoundingClientRect(); | ||||
|   if ( | ||||
|     (bounds.bottom > window.innerHeight && bounds.top > 0) || | ||||
|     (bounds.top < 0 && bounds.bottom < window.innerHeight) | ||||
|   ) { | ||||
|     if (bounds.top < 0) { | ||||
|       window.scrollBy(0, bounds.top - 1); | ||||
|     } else { | ||||
|       window.scrollBy(0, bounds.bottom - window.innerHeight + 1); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function maximizeCodeHeight(sectionDiv, isLast) { | ||||
|   const cm = sectionDiv.CodeMirror; | ||||
|   const stats = maximizeCodeHeight.stats = maximizeCodeHeight.stats || {totalHeight: 0, deltas: []}; | ||||
|   if (!stats.cmActualHeight) { | ||||
|     stats.cmActualHeight = getComputedHeight(cm.display.wrapper); | ||||
|   } | ||||
|   if (!stats.sectionMarginTop) { | ||||
|     stats.sectionMarginTop = parseFloat(getComputedStyle(sectionDiv).marginTop); | ||||
|   } | ||||
|   const sectionTop = sectionDiv.getBoundingClientRect().top - stats.sectionMarginTop; | ||||
|   if (!stats.firstSectionTop) { | ||||
|     stats.firstSectionTop = sectionTop; | ||||
|   } | ||||
|   const extrasHeight = getComputedHeight(sectionDiv) - stats.cmActualHeight; | ||||
|   const cmMaxHeight = window.innerHeight - extrasHeight - sectionTop - stats.sectionMarginTop; | ||||
|   const cmDesiredHeight = cm.display.sizer.clientHeight + 2 * cm.defaultTextHeight(); | ||||
|   const cmGrantableHeight = Math.max(stats.cmActualHeight, Math.min(cmMaxHeight, cmDesiredHeight)); | ||||
|   stats.deltas.push(cmGrantableHeight - stats.cmActualHeight); | ||||
|   stats.totalHeight += cmGrantableHeight + extrasHeight; | ||||
|   if (!isLast) { | ||||
|     return; | ||||
|   } | ||||
|   stats.totalHeight += stats.firstSectionTop; | ||||
|   if (stats.totalHeight <= window.innerHeight) { | ||||
|     editors.forEach((cm, index) => { | ||||
|       cm.setSize(null, stats.deltas[index] + stats.cmActualHeight); | ||||
|     }); | ||||
|     return; | ||||
|   } | ||||
|   // scale heights to fill the gap between last section and bottom edge of the window
 | ||||
|   const sections = $('#sections'); | ||||
|   const available = window.innerHeight - sections.getBoundingClientRect().bottom - | ||||
|     parseFloat(getComputedStyle(sections).marginBottom); | ||||
|   if (available <= 0) { | ||||
|     return; | ||||
|   } | ||||
|   const totalDelta = stats.deltas.reduce((sum, d) => sum + d, 0); | ||||
|   const q = available / totalDelta; | ||||
|   const baseHeight = stats.cmActualHeight - stats.sectionMarginTop; | ||||
|   stats.deltas.forEach((delta, index) => { | ||||
|     editors[index].setSize(null, baseHeight + Math.floor(q * delta)); | ||||
|   }); | ||||
| 
 | ||||
|   function getComputedHeight(el) { | ||||
|     const compStyle = getComputedStyle(el); | ||||
|     return el.getBoundingClientRect().height + | ||||
|       parseFloat(compStyle.marginTop) + parseFloat(compStyle.marginBottom); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										106
									
								
								edit/show-keymap-help.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								edit/show-keymap-help.js
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,106 @@ | |||
| /* global CodeMirror showHelp */ | ||||
| 'use strict'; | ||||
| 
 | ||||
| onDOMready().then(() => { | ||||
|   $('#keyMap-help').addEventListener('click', showKeyMapHelp); | ||||
| }); | ||||
| 
 | ||||
| function showKeyMapHelp() { | ||||
|   const keyMap = mergeKeyMaps({}, prefs.get('editor.keyMap'), CodeMirror.defaults.extraKeys); | ||||
|   const keyMapSorted = Object.keys(keyMap) | ||||
|     .map(key => ({key: key, cmd: keyMap[key]})) | ||||
|     .concat([{key: 'Shift-Ctrl-Wheel', cmd: 'scrollWindow'}]) | ||||
|     .sort((a, b) => (a.cmd < b.cmd || (a.cmd === b.cmd && a.key < b.key) ? -1 : 1)); | ||||
|   showHelp(t('cm_keyMap') + ': ' + prefs.get('editor.keyMap'), | ||||
|     '<table class="keymap-list">' + | ||||
|       '<thead><tr><th><input placeholder="' + t('helpKeyMapHotkey') + '" type="search"></th>' + | ||||
|         '<th><input placeholder="' + t('helpKeyMapCommand') + '" type="search"></th></tr></thead>' + | ||||
|       '<tbody>' + keyMapSorted.map(value => | ||||
|         '<tr><td>' + value.key + '</td><td>' + value.cmd + '</td></tr>' | ||||
|       ).join('') + | ||||
|       '</tbody>' + | ||||
|     '</table>'); | ||||
| 
 | ||||
|   const table = $('#help-popup table'); | ||||
|   table.addEventListener('input', filterTable); | ||||
| 
 | ||||
|   const inputs = $$('input', table); | ||||
|   inputs[0].addEventListener('keydown', hotkeyHandler); | ||||
|   inputs[1].focus(); | ||||
| 
 | ||||
|   function hotkeyHandler(event) { | ||||
|     const keyName = CodeMirror.keyName(event); | ||||
|     if (keyName === 'Esc' || keyName === 'Tab' || keyName === 'Shift-Tab') { | ||||
|       return; | ||||
|     } | ||||
|     event.preventDefault(); | ||||
|     event.stopPropagation(); | ||||
|     // normalize order of modifiers,
 | ||||
|     // for modifier-only keys ('Ctrl-Shift') a dummy main key has to be temporarily added
 | ||||
|     const keyMap = {}; | ||||
|     keyMap[keyName.replace(/(Shift|Ctrl|Alt|Cmd)$/, '$&-dummy')] = ''; | ||||
|     const normalizedKey = Object.keys(CodeMirror.normalizeKeyMap(keyMap))[0]; | ||||
|     this.value = normalizedKey.replace('-dummy', ''); | ||||
|     filterTable(event); | ||||
|   } | ||||
| 
 | ||||
|   function filterTable(event) { | ||||
|     const input = event.target; | ||||
|     const col = input.parentNode.cellIndex; | ||||
|     inputs[1 - col].value = ''; | ||||
|     table.tBodies[0].childNodes.forEach(row => { | ||||
|       const cell = row.children[col]; | ||||
|       const text = cell.textContent; | ||||
|       const query = stringAsRegExp(input.value, 'gi'); | ||||
|       const test = query.test(text); | ||||
|       row.style.display = input.value && test === false ? 'none' : ''; | ||||
|       if (input.value && test) { | ||||
|         cell.textContent = ''; | ||||
|         let offset = 0; | ||||
|         text.replace(query, (match, index) => { | ||||
|           if (index > offset) { | ||||
|             cell.appendChild(document.createTextNode(text.substring(offset, index))); | ||||
|           } | ||||
|           cell.appendChild($element({tag: 'mark', textContent: match})); | ||||
|           offset = index + match.length; | ||||
|         }); | ||||
|         if (offset + 1 !== text.length) { | ||||
|           cell.appendChild(document.createTextNode(text.substring(offset))); | ||||
|         } | ||||
|       } else { | ||||
|         cell.textContent = text; | ||||
|       } | ||||
|       // clear highlight from the other column
 | ||||
|       const otherCell = row.children[1 - col]; | ||||
|       if (otherCell.children.length) { | ||||
|         const text = otherCell.textContent; | ||||
|         otherCell.textContent = text; | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|   function mergeKeyMaps(merged, ...more) { | ||||
|     more.forEach(keyMap => { | ||||
|       if (typeof keyMap === 'string') { | ||||
|         keyMap = CodeMirror.keyMap[keyMap]; | ||||
|       } | ||||
|       Object.keys(keyMap).forEach(key => { | ||||
|         let cmd = keyMap[key]; | ||||
|         // filter out '...', 'attach', etc. (hotkeys start with an uppercase letter)
 | ||||
|         if (!merged[key] && !key.match(/^[a-z]/) && cmd !== '...') { | ||||
|           if (typeof cmd === 'function') { | ||||
|             // for 'emacs' keymap: provide at least something meaningful (hotkeys and the function body)
 | ||||
|             // for 'vim*' keymaps: almost nothing as it doesn't rely on CM keymap mechanism
 | ||||
|             cmd = cmd.toString().replace(/^function.*?\{[\s\r\n]*([\s\S]+?)[\s\r\n]*\}$/, '$1'); | ||||
|             merged[key] = cmd.length <= 200 ? cmd : cmd.substr(0, 200) + '...'; | ||||
|           } else { | ||||
|             merged[key] = cmd; | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|       if (keyMap.fallthrough) { | ||||
|         merged = mergeKeyMaps(merged, keyMap.fallthrough); | ||||
|       } | ||||
|     }); | ||||
|     return merged; | ||||
|   } | ||||
| } | ||||
|  | @ -1,6 +1,5 @@ | |||
| /* global CodeMirror dirtyReporter initLint beautify showKeyMapHelp */ | ||||
| /* global CodeMirror dirtyReporter initLint */ | ||||
| /* global showToggleStyleHelp goBackToManage updateLintReportIfEnabled */ | ||||
| /* global hotkeyRerouter setupAutocomplete */ | ||||
| /* global editors linterConfig updateLinter regExpTester mozParser */ | ||||
| /* global makeLink createAppliesToLineWidget messageBox */ | ||||
| 'use strict'; | ||||
|  | @ -124,10 +123,7 @@ function createSourceEditor(style) { | |||
| 
 | ||||
|   function initHooks() { | ||||
|     $('#save-button').onclick = save; | ||||
|     $('#beautify').onclick = beautify; | ||||
|     $('#keyMap-help').onclick = showKeyMapHelp; | ||||
|     $('#toggle-style-help').onclick = showToggleStyleHelp; | ||||
|     $('#cancel-button').onclick = goBackToManage; | ||||
| 
 | ||||
|     $('#enabled').onchange = function () { | ||||
|       const value = this.checked; | ||||
|  | @ -140,11 +136,13 @@ function createSourceEditor(style) { | |||
|       updateLintReportIfEnabled(cm); | ||||
|     }); | ||||
| 
 | ||||
|     cm.on('focus', () => hotkeyRerouter.setState(false)); | ||||
|     cm.on('blur', () => hotkeyRerouter.setState(true)); | ||||
|     cm.on('focus', () => cm.rerouteHotkeys(false)); | ||||
|     cm.on('blur', () => cm.rerouteHotkeys(true)); | ||||
| 
 | ||||
|     CodeMirror.commands.prevEditor = cm => nextPrevMozDocument(cm, -1); | ||||
|     CodeMirror.commands.nextEditor = cm => nextPrevMozDocument(cm, 1); | ||||
|     CodeMirror.commands.toggleStyle = toggleStyle; | ||||
|     CodeMirror.commands.save = save; | ||||
|   } | ||||
| 
 | ||||
|   function updateMeta() { | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user