Add: codemirror-factory
This commit is contained in:
		
							parent
							
								
									15a1f552f6
								
							
						
					
					
						commit
						d26ce3238e
					
				|  | @ -228,69 +228,48 @@ | |||
|     return isBlank; | ||||
|   }); | ||||
| 
 | ||||
|   // doubleclick option
 | ||||
|   if (typeof editors !== 'undefined') { | ||||
|     const fn = (cm, repeat) => | ||||
|       repeat === 'double' ? | ||||
|         {unit: selectTokenOnDoubleclick} : | ||||
|         {}; | ||||
|     const configure = (_, enabled) => { | ||||
|       editors.forEach(cm => cm.setOption('configureMouse', enabled ? fn : null)); | ||||
|       CodeMirror.defaults.configureMouse = enabled ? fn : null; | ||||
|     }; | ||||
|     configure(null, prefs.get('editor.selectByTokens')); | ||||
|     prefs.subscribe(['editor.selectByTokens'], configure); | ||||
|   // editor commands
 | ||||
|   for (const name of ['save', 'toggleStyle', 'nextEditor', 'prevEditor']) { | ||||
|     CodeMirror.commands[name] = () => editor[name](); | ||||
|   } | ||||
| 
 | ||||
|   function selectTokenOnDoubleclick(cm, pos) { | ||||
|     let {ch} = pos; | ||||
|     const {line, sticky} = pos; | ||||
|     const {text, styles} = cm.getLineHandle(line); | ||||
|   // CodeMirror convenience commands
 | ||||
|   Object.assign(CodeMirror.commands, { | ||||
|     toggleEditorFocus, | ||||
|     jumpToLine, | ||||
|     commentSelection, | ||||
|   }); | ||||
| 
 | ||||
|     const execAt = (rx, i) => (rx.lastIndex = i) && null || rx.exec(text); | ||||
|     const at = (rx, i) => (rx.lastIndex = i) && null || rx.test(text); | ||||
|     const atWord = ch => at(/\w/y, ch); | ||||
|     const atSpace = ch => at(/\s/y, ch); | ||||
| 
 | ||||
|     const atTokenEnd = styles.indexOf(ch, 1); | ||||
|     ch += atTokenEnd < 0 ? 0 : sticky === 'before' && atWord(ch - 1) ? 0 : atSpace(ch + 1) ? 0 : 1; | ||||
|     ch = Math.min(text.length, ch); | ||||
|     const type = cm.getTokenTypeAt({line, ch: ch + (sticky === 'after' ? 1 : 0)}); | ||||
|     if (atTokenEnd > 0) ch--; | ||||
| 
 | ||||
|     const isCss = type && !/^(comment|string)/.test(type); | ||||
|     const isNumber = type === 'number'; | ||||
|     const isSpace = atSpace(ch); | ||||
|     let wordChars = | ||||
|       isNumber ? /[-+\w.%]/y : | ||||
|       isCss ? /[-\w@]/y : | ||||
|       isSpace ? /\s/y : | ||||
|       atWord(ch) ? /\w/y : /[^\w\s]/y; | ||||
| 
 | ||||
|     let a = ch; | ||||
|     while (a && at(wordChars, a)) a--; | ||||
|     a += !a && at(wordChars, a) || isCss && at(/[.!#@]/y, a) ? 0 : at(wordChars, a + 1); | ||||
| 
 | ||||
|     let b, found; | ||||
| 
 | ||||
|     if (isNumber) { | ||||
|       b = a + execAt(/[+-]?[\d.]+(e\d+)?|$/yi, a)[0].length; | ||||
|       found = b >= ch; | ||||
|       if (!found) { | ||||
|         a = b; | ||||
|         ch = a; | ||||
|   function jumpToLine(cm) { | ||||
|     const cur = cm.getCursor(); | ||||
|     const oldDialog = $('.CodeMirror-dialog', cm.display.wrapper); | ||||
|     if (oldDialog) { | ||||
|       // close the currently opened minidialog
 | ||||
|       cm.focus(); | ||||
|     } | ||||
|     // make sure to focus the input in newly opened minidialog
 | ||||
|     // setTimeout(() => {
 | ||||
|       // $('.CodeMirror-dialog', section).focus();
 | ||||
|     // });
 | ||||
|     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}); | ||||
|   } | ||||
| 
 | ||||
|     if (!found) { | ||||
|       wordChars = isCss ? /[-\w]*/y : new RegExp(wordChars.source + '*', 'uy'); | ||||
|       b = ch + execAt(wordChars, ch)[0].length; | ||||
|     } | ||||
|   function commentSelection(cm) { | ||||
|     cm.blockComment(cm.getCursor('from'), cm.getCursor('to'), {fullLines: false}); | ||||
|   } | ||||
| 
 | ||||
|     return { | ||||
|       from: {line, ch: a}, | ||||
|       to: {line, ch: b}, | ||||
|     }; | ||||
|   function toggleEditorFocus(cm) { | ||||
|     if (!cm) return; | ||||
|     if (cm.hasFocus()) { | ||||
|       setTimeout(() => cm.display.input.blur()); | ||||
|     } else { | ||||
|       cm.focus(); | ||||
|     } | ||||
|   } | ||||
| })(); | ||||
| 
 | ||||
|  |  | |||
|  | @ -6,356 +6,13 @@ global messageBox | |||
| 'use strict'; | ||||
| 
 | ||||
| onDOMscriptReady('/codemirror.js').then(() => { | ||||
|   const COMMANDS = { | ||||
|     save, | ||||
|     toggleStyle, | ||||
|     toggleEditorFocus, | ||||
|     jumpToLine, | ||||
|     nextEditor, prevEditor, | ||||
|     commentSelection, | ||||
|   }; | ||||
|   const ORIGINAL_COMMANDS = { | ||||
|     insertTab: CodeMirror.commands.insertTab, | ||||
|   }; | ||||
|   // reroute handling to nearest editor when keypress resolves to one of these commands
 | ||||
|   const REROUTED = new Set([ | ||||
|     'save', | ||||
|     'toggleStyle', | ||||
|     'jumpToLine', | ||||
|     'nextEditor', 'prevEditor', | ||||
|     'toggleEditorFocus', | ||||
|     'find', 'findNext', 'findPrev', 'replace', 'replaceAll', | ||||
|     'colorpicker', | ||||
|   ]); | ||||
|   Object.assign(CodeMirror.prototype, { | ||||
|     // getSection,
 | ||||
|     rerouteHotkeys, | ||||
|   }); | ||||
|   Object.assign(CodeMirror.commands, COMMANDS); | ||||
|   rerouteHotkeys(true); | ||||
| 
 | ||||
|   CodeMirror.defineInitHook(cm => { | ||||
|     if (!cm.display.wrapper.closest('#sections')) { | ||||
|       return; | ||||
|     } | ||||
|     if (prefs.get('editor.autocompleteOnTyping')) { | ||||
|       setupAutocomplete(cm); | ||||
|     } | ||||
|     const wrapper = cm.display.wrapper; | ||||
|     cm.on('blur', () => { | ||||
|       cm.rerouteHotkeys(true); | ||||
|       setTimeout(() => { | ||||
|         wrapper.classList.toggle('CodeMirror-active', wrapper.contains(document.activeElement)); | ||||
|       }); | ||||
|     }); | ||||
|     cm.on('focus', () => { | ||||
|       cm.rerouteHotkeys(false); | ||||
|       wrapper.classList.add('CodeMirror-active'); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   // FIXME: pull this into a module
 | ||||
|   window.rerouteHotkeys = rerouteHotkeys; | ||||
| 
 | ||||
|   prefs.subscribe(null, onPrefChanged); | ||||
| 
 | ||||
|   ////////////////////////////////////////////////
 | ||||
| 
 | ||||
|   function getOption(o) { | ||||
|     return CodeMirror.defaults[o]; | ||||
|   } | ||||
| 
 | ||||
|   function setOption(o, v) { | ||||
|     CodeMirror.defaults[o] = v; | ||||
|     if (!editor) { | ||||
|       return; | ||||
|     } | ||||
|     const editors = editor.getEditors(); | ||||
|     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 = editor.getLastActivatedEditor(), | ||||
|     editorsCopy = editor.getEditors().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; | ||||
|     const editors = editor.getEditors(); | ||||
|     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( | ||||
|           $create('.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) { | ||||
|     return editor.nextEditor(cm); | ||||
|   } | ||||
| 
 | ||||
|   function prevEditor(cm) { | ||||
|     return editor.prevEditor(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 commentSelection(cm) { | ||||
|     cm.blockComment(cm.getCursor('from'), cm.getCursor('to'), {fullLines: false}); | ||||
|   } | ||||
| 
 | ||||
|   function toggleEditorFocus(cm) { | ||||
|     if (!cm) return; | ||||
|     if (cm.hasFocus()) { | ||||
|       setTimeout(() => cm.display.input.blur()); | ||||
|     } else { | ||||
|       cm.focus(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   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 onPrefChanged(key, value) { | ||||
|     let option = key.replace(/^editor\./, ''); | ||||
|     if (!option) { | ||||
|       console.error('no "cm_option"', key); | ||||
|       return; | ||||
|     } | ||||
|     switch (option) { | ||||
|       case 'tabSize': | ||||
|         value = Number(value); | ||||
|         CodeMirror.setOption('indentUnit', value); | ||||
|         break; | ||||
| 
 | ||||
|       case 'indentWithTabs': | ||||
|         CodeMirror.commands.insertTab = value ? | ||||
|           ORIGINAL_COMMANDS.insertTab : | ||||
|           CodeMirror.commands.insertSoftTab; | ||||
|         break; | ||||
| 
 | ||||
|       case 'theme': { | ||||
|         const themeLink = $('#cm-theme'); | ||||
|         // use non-localized 'default' internally
 | ||||
|         if (!value || value === 'default' || value === t('defaultTheme')) { | ||||
|           value = 'default'; | ||||
|           if (prefs.get(key) !== value) { | ||||
|             prefs.set(key, value); | ||||
|           } | ||||
|           themeLink.href = ''; | ||||
|           $('#editor.theme').value = value; | ||||
|           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($create('link#cm-theme2', {rel: 'stylesheet', href: url})); | ||||
|         setTimeout(() => { | ||||
|           CodeMirror.setOption(option, value); | ||||
|           themeLink.remove(); | ||||
|           $('#cm-theme2').id = 'cm-theme'; | ||||
|         }, 100); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       case 'autocompleteOnTyping': | ||||
|         if (editor) { | ||||
|           // FIXME: this won't work with removed sections
 | ||||
|           editor.getEditors().forEach(cm => setupAutocomplete(cm, value)); | ||||
|         } | ||||
|         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 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](editor.closestVisible(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 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 save() { | ||||
|     editor.save(); | ||||
|   } | ||||
| 
 | ||||
|   function toggleStyle() { | ||||
|     editor.toggleStyle(); | ||||
|   } | ||||
| }); | ||||
|  |  | |||
							
								
								
									
										311
									
								
								edit/codemirror-factory.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										311
									
								
								edit/codemirror-factory.js
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,311 @@ | |||
| /* global CodeMirror loadScript rerouteHotkeys */ | ||||
| 'use strict'; | ||||
| /* | ||||
| All cm instances created by this module are collected so we can broadcast prefs | ||||
| settings to them. You should `cmFactory.destroy(cm)` to unregister the listener | ||||
| when the instance is not used anymore. | ||||
| */ | ||||
| const cmFactory = (() => { | ||||
|   const editors = new Set(); | ||||
|   // used by `indentWithTabs` option
 | ||||
|   const INSERT_TAB_COMMAND = CodeMirror.commands.insertTab; | ||||
|   const INSERT_SOFT_TAB_COMMAND = CodeMirror.commands.insertSoftTab; | ||||
|   prefs.subscribe(null, onPrefChanged); | ||||
|   return {create, destroy, setOption}; | ||||
| 
 | ||||
|   function onPrefChanged(key, value) { | ||||
|     let option = key.replace(/^editor\./, ''); | ||||
|     if (!option) { | ||||
|       console.error('no "cm_option"', key); | ||||
|       return; | ||||
|     } | ||||
|     switch (option) { | ||||
|       case 'tabSize': | ||||
|         value = Number(value); | ||||
|         setOption('indentUnit', value); | ||||
|         break; | ||||
| 
 | ||||
|       case 'indentWithTabs': | ||||
|         CodeMirror.commands.insertTab = value ? | ||||
|           INSERT_TAB_COMMAND : | ||||
|           INSERT_SOFT_TAB_COMMAND; | ||||
|         break; | ||||
| 
 | ||||
|       case 'theme': { | ||||
|         const themeLink = $('#cm-theme'); | ||||
|         // use non-localized 'default' internally
 | ||||
|         if (!value || value === 'default' || value === t('defaultTheme')) { | ||||
|           value = 'default'; | ||||
|           if (prefs.get(key) !== value) { | ||||
|             prefs.set(key, value); | ||||
|           } | ||||
|           themeLink.href = ''; | ||||
|           $('#editor.theme').value = value; | ||||
|           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($create('link#cm-theme2', {rel: 'stylesheet', href: url})); | ||||
|         setTimeout(() => { | ||||
|           setOption(option, value); | ||||
|           themeLink.remove(); | ||||
|           $('#cm-theme2').id = 'cm-theme'; | ||||
|         }, 100); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       case 'autocompleteOnTyping': | ||||
|         for (const cm of editors) { | ||||
|           setupAutocomplete(cm, value); | ||||
|         } | ||||
|         return; | ||||
| 
 | ||||
|       case 'autoCloseBrackets': | ||||
|         Promise.resolve(value && loadScript('/vendor/codemirror/addon/edit/closebrackets.js')).then(() => { | ||||
|           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': | ||||
|         // FIXME: this is implemented in `colorpicker-helper.js`.
 | ||||
|         return; | ||||
| 
 | ||||
|       case 'selectByTokens': | ||||
|         option = 'configureMouse'; | ||||
|         value = value ? configureMouseFn : null; | ||||
|         break; | ||||
|     } | ||||
|     setOption(option, value); | ||||
|   } | ||||
| 
 | ||||
|   function configureMouseFn(cm, repeat) { | ||||
|     return repeat === 'double' ? | ||||
|       {unit: selectTokenOnDoubleclick} : | ||||
|       {}; | ||||
|   } | ||||
| 
 | ||||
|   function selectTokenOnDoubleclick(cm, pos) { | ||||
|     let {ch} = pos; | ||||
|     const {line, sticky} = pos; | ||||
|     const {text, styles} = cm.getLineHandle(line); | ||||
| 
 | ||||
|     const execAt = (rx, i) => (rx.lastIndex = i) && null || rx.exec(text); | ||||
|     const at = (rx, i) => (rx.lastIndex = i) && null || rx.test(text); | ||||
|     const atWord = ch => at(/\w/y, ch); | ||||
|     const atSpace = ch => at(/\s/y, ch); | ||||
| 
 | ||||
|     const atTokenEnd = styles.indexOf(ch, 1); | ||||
|     ch += atTokenEnd < 0 ? 0 : sticky === 'before' && atWord(ch - 1) ? 0 : atSpace(ch + 1) ? 0 : 1; | ||||
|     ch = Math.min(text.length, ch); | ||||
|     const type = cm.getTokenTypeAt({line, ch: ch + (sticky === 'after' ? 1 : 0)}); | ||||
|     if (atTokenEnd > 0) ch--; | ||||
| 
 | ||||
|     const isCss = type && !/^(comment|string)/.test(type); | ||||
|     const isNumber = type === 'number'; | ||||
|     const isSpace = atSpace(ch); | ||||
|     let wordChars = | ||||
|       isNumber ? /[-+\w.%]/y : | ||||
|       isCss ? /[-\w@]/y : | ||||
|       isSpace ? /\s/y : | ||||
|       atWord(ch) ? /\w/y : /[^\w\s]/y; | ||||
| 
 | ||||
|     let a = ch; | ||||
|     while (a && at(wordChars, a)) a--; | ||||
|     a += !a && at(wordChars, a) || isCss && at(/[.!#@]/y, a) ? 0 : at(wordChars, a + 1); | ||||
| 
 | ||||
|     let b, found; | ||||
| 
 | ||||
|     if (isNumber) { | ||||
|       b = a + execAt(/[+-]?[\d.]+(e\d+)?|$/yi, a)[0].length; | ||||
|       found = b >= ch; | ||||
|       if (!found) { | ||||
|         a = b; | ||||
|         ch = a; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     if (!found) { | ||||
|       wordChars = isCss ? /[-\w]*/y : new RegExp(wordChars.source + '*', 'uy'); | ||||
|       b = ch + execAt(wordChars, ch)[0].length; | ||||
|     } | ||||
| 
 | ||||
|     return { | ||||
|       from: {line, ch: a}, | ||||
|       to: {line, ch: b}, | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   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 destroy(cm) { | ||||
|     editors.delete(cm); | ||||
|   } | ||||
| 
 | ||||
|   function create(init, options) { | ||||
|     const cm = CodeMirror(init, options); | ||||
|     if (prefs.get('editor.autocompleteOnTyping')) { | ||||
|       setupAutocomplete(cm); | ||||
|     } | ||||
|     cm.lastActive = 0; | ||||
|     const wrapper = cm.display.wrapper; | ||||
|     cm.on('blur', () => { | ||||
|       rerouteHotkeys(true); | ||||
|       setTimeout(() => { | ||||
|         wrapper.classList.toggle('CodeMirror-active', wrapper.contains(document.activeElement)); | ||||
|       }); | ||||
|     }); | ||||
|     cm.on('focus', () => { | ||||
|       rerouteHotkeys(false); | ||||
|       wrapper.classList.add('CodeMirror-active'); | ||||
|       cm.lastActive = Date.now(); | ||||
|     }); | ||||
|     editors.add(cm); | ||||
|     cm.distroy = () => editors.delete(cm); | ||||
|     return cm; | ||||
|   } | ||||
| 
 | ||||
|   function getLastActivated() { | ||||
|     let result; | ||||
|     for (const cm of editors) { | ||||
|       if (!result || result.lastActive < cm.lastActive) { | ||||
|         result = cm; | ||||
|       } | ||||
|     } | ||||
|     return result; | ||||
|   } | ||||
| 
 | ||||
|   function setOption(key, value) { | ||||
|     CodeMirror.defaults[key] = value; | ||||
|     if (editors.size > 4 && (key === 'theme' || key === 'lineWrapping')) { | ||||
|       throttleSetOption({key, value, index: 0}); | ||||
|       return; | ||||
|     } | ||||
|     for (const editor of editors) { | ||||
|       editor.setOption(key, value); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function throttleSetOption({ | ||||
|     key, | ||||
|     value, | ||||
|     index, | ||||
|     timeStart = performance.now(), | ||||
|     editorsCopy = [...editors], | ||||
|     cmStart = getLastActivated(), | ||||
|     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 || !editors.has(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( | ||||
|           $create('.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, | ||||
|     }); | ||||
|   } | ||||
| })(); | ||||
|  | @ -1,4 +1,4 @@ | |||
| /* global CodeMirror loadScript editors showHelp */ | ||||
| /* global CodeMirror loadScript showHelp cmFactory */ | ||||
| 'use strict'; | ||||
| 
 | ||||
| onDOMscriptReady('/colorview.js').then(() => { | ||||
|  | @ -20,7 +20,8 @@ onDOMscriptReady('/colorview.js').then(() => { | |||
|         defaults.extraKeys[keyName] = 'colorpicker'; | ||||
|       } | ||||
|       defaults.colorpicker = { | ||||
|         forceUpdate: editors.length > 0, | ||||
|         // FIXME: who uses this?
 | ||||
|         // forceUpdate: editor.getEditors().length > 0,
 | ||||
|         tooltip: t('colorpickerTooltip'), | ||||
|         popup: { | ||||
|           tooltipForSwitcher: t('colorpickerSwitchFormatTooltip'), | ||||
|  | @ -38,8 +39,7 @@ onDOMscriptReady('/colorview.js').then(() => { | |||
|         delete defaults.extraKeys[keyName]; | ||||
|       } | ||||
|     } | ||||
|     // on page load runs before CodeMirror.setOption is defined
 | ||||
|     editors.forEach(cm => cm.setOption('colorpicker', defaults.colorpicker)); | ||||
|     cmFactory.setOption('colorpicker', defaults.colorpicker); | ||||
|   } | ||||
| 
 | ||||
|   function registerHotkey(id, hotkey) { | ||||
|  |  | |||
							
								
								
									
										13
									
								
								edit/edit.js
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								edit/edit.js
									
									
									
									
									
								
							|  | @ -5,7 +5,7 @@ global closeCurrentTab regExpTester messageBox | |||
| global setupCodeMirror | ||||
| global beautify | ||||
| global sectionsToMozFormat | ||||
| global moveFocus editorWorker msg createSectionEditor | ||||
| global moveFocus editorWorker msg createSectionsEditor rerouteHotkeys | ||||
| */ | ||||
| 'use strict'; | ||||
| 
 | ||||
|  | @ -229,7 +229,7 @@ preinit(); | |||
|         $('#lint').addEventListener('scroll', hideLintHeaderOnScroll, {passive: true}); | ||||
|         window.addEventListener('resize', () => debounce(rememberWindowSize, 100)); | ||||
| 
 | ||||
|         editor = usercss ? createSourceEditor(style) : createSectionEditor(style); | ||||
|         editor = usercss ? createSourceEditor(style) : createSectionsEditor(style); | ||||
|         if (editor.ready) { | ||||
|           return editor.ready(); | ||||
|         } | ||||
|  | @ -362,11 +362,6 @@ function onRuntimeMessage(request) { | |||
|         break; | ||||
|       } | ||||
|       break; | ||||
|     case 'prefChanged': | ||||
|       if ('editor.smartIndent' in request.prefs) { | ||||
|         CodeMirror.setOption('smartIndent', request.prefs['editor.smartIndent']); | ||||
|       } | ||||
|       break; | ||||
|     case 'editDeleteText': | ||||
|       document.execCommand('delete'); | ||||
|       break; | ||||
|  | @ -531,7 +526,7 @@ function showCodeMirrorPopup(title, html, options) { | |||
|     keyMap: prefs.get('editor.keyMap') | ||||
|   }, options)); | ||||
|   cm.focus(); | ||||
|   cm.rerouteHotkeys(false); | ||||
|   rerouteHotkeys(false); | ||||
| 
 | ||||
|   document.documentElement.style.pointerEvents = 'none'; | ||||
|   popup.style.pointerEvents = 'auto'; | ||||
|  | @ -550,7 +545,7 @@ function showCodeMirrorPopup(title, html, options) { | |||
|     window.removeEventListener('closeHelp', _); | ||||
|     window.removeEventListener('keydown', onKeyDown, true); | ||||
|     document.documentElement.style.removeProperty('pointer-events'); | ||||
|     cm.rerouteHotkeys(true); | ||||
|     rerouteHotkeys(true); | ||||
|     cm = popup.codebox = null; | ||||
|   }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -212,7 +212,7 @@ onDOMready().then(() => { | |||
|       state.activeAppliesTo || | ||||
|       state.cm); | ||||
|     const cmExtra = $('body > :not(#sections) .CodeMirror'); | ||||
|     state.editors = cmExtra ? [cmExtra.CodeMirror] : editors; | ||||
|     state.editors = cmExtra ? [cmExtra.CodeMirror] : editor.getEditors(); | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,4 +1,5 @@ | |||
| /* global memoize editorWorker showCodeMirrorPopup loadScript messageBox LINTER_DEFAULTS*/ | ||||
| /* global memoize editorWorker showCodeMirrorPopup loadScript messageBox | ||||
|   LINTER_DEFAULTS rerouteHotkeys */ | ||||
| 'use strict'; | ||||
| 
 | ||||
| (() => { | ||||
|  | @ -50,10 +51,10 @@ | |||
|     }); | ||||
|     cm.on('changes', updateButtonState); | ||||
| 
 | ||||
|     cm.rerouteHotkeys(false); | ||||
|     rerouteHotkeys(false); | ||||
|     window.addEventListener('closeHelp', function _() { | ||||
|       window.removeEventListener('closeHelp', _); | ||||
|       cm.rerouteHotkeys(true); | ||||
|       rerouteHotkeys(true); | ||||
|       cm = null; | ||||
|     }); | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										50
									
								
								edit/reroute-hotkeys.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								edit/reroute-hotkeys.js
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,50 @@ | |||
| /* global CodeMirror editor */ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const rerouteHotkeys = (() => { | ||||
|   // reroute handling to nearest editor when keypress resolves to one of these commands
 | ||||
|   const REROUTED = new Set([ | ||||
|     'save', | ||||
|     'toggleStyle', | ||||
|     'jumpToLine', | ||||
|     'nextEditor', 'prevEditor', | ||||
|     'toggleEditorFocus', | ||||
|     'find', 'findNext', 'findPrev', 'replace', 'replaceAll', | ||||
|     'colorpicker', | ||||
|   ]); | ||||
| 
 | ||||
|   rerouteHotkeys(true); | ||||
| 
 | ||||
|   return rerouteHotkeys; | ||||
| 
 | ||||
|   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) { | ||||
|     if (!editor) { | ||||
|       return; | ||||
|     } | ||||
|     const keyName = CodeMirror.keyName(event); | ||||
|     if (!keyName) { | ||||
|       return; | ||||
|     } | ||||
|     const rerouteCommand = name => { | ||||
|       if (REROUTED.has(name)) { | ||||
|         CodeMirror.commands[name](editor.closestVisible(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(); | ||||
|     } | ||||
|   } | ||||
| })(); | ||||
|  | @ -134,7 +134,6 @@ function createSectionsEditor(style) { | |||
|     isDirty: dirty.isDirty, | ||||
|     getStyle: () => style, | ||||
|     getEditors, | ||||
|     getLastActivatedEditor, | ||||
|     scrollToEditor, | ||||
|     getStyleId: () => style.id, | ||||
|     getEditorTitle: cm => { | ||||
|  | @ -162,7 +161,7 @@ function createSectionsEditor(style) { | |||
|       nearbyElement instanceof CodeMirror ? nearbyElement : | ||||
|       nearbyElement instanceof Node && | ||||
|         (nearbyElement.closest('#sections > .section') || {}).CodeMirror || | ||||
|       editor.getLastActivatedEditor(); | ||||
|       getLastActivatedEditor(); | ||||
|     if (nearbyElement instanceof Node && cm) { | ||||
|       const {left, top} = nearbyElement.getBoundingClientRect(); | ||||
|       const bounds = cm.display.wrapper.getBoundingClientRect(); | ||||
|  | @ -172,7 +171,7 @@ function createSectionsEditor(style) { | |||
|       } | ||||
|     } | ||||
|     // closest editor should have at least 2 lines visible
 | ||||
|     const lineHeight = editor.getEditors()[0].defaultTextHeight(); | ||||
|     const lineHeight = sections[0].cm.defaultTextHeight(); | ||||
|     const scrollY = window.scrollY; | ||||
|     const windowBottom = scrollY + window.innerHeight - 2 * lineHeight; | ||||
|     const allSectionsContainerTop = scrollY + $('#sections').getBoundingClientRect().top; | ||||
|  | @ -203,7 +202,7 @@ function createSectionsEditor(style) { | |||
|     } | ||||
| 
 | ||||
|     function findClosest() { | ||||
|       const editors = editor.getEditors(); | ||||
|       const editors = getEditors(); | ||||
|       const last = editors.length - 1; | ||||
|       let a = 0; | ||||
|       let b = last; | ||||
|  | @ -228,7 +227,7 @@ function createSectionsEditor(style) { | |||
|       } | ||||
|       const cm = editors[b]; | ||||
|       if (distances[b] > 0) { | ||||
|         editor.scrollToEditor(cm); | ||||
|         scrollToEditor(cm); | ||||
|       } | ||||
|       return cm; | ||||
|     } | ||||
|  |  | |||
|  | @ -400,7 +400,6 @@ function createSourceEditor(style) { | |||
|     isDirty: dirty.isDirty, | ||||
|     getStyle: () => style, | ||||
|     getEditors: () => [cm], | ||||
|     getLastActivatedEditor: () => cm, | ||||
|     scrollToEditor: () => {}, | ||||
|     getStyleId: () => style.id, | ||||
|     getEditorTitle: () => '', | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user