diff --git a/.eslintignore b/.eslintignore index a710e413..f1fc323f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,3 @@ vendor/ -vendor-overwrites/ +vendor-overwrites/* +!vendor-overwrites/colorpicker diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 83d22b05..12cba426 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -127,6 +127,10 @@ "message": "Autocomplete on typing", "description": "Label for the checkbox in the style editor." }, + "cm_colorpicker": { + "message": "Colorpickers for CSS colors", + "description": "Label for the checkbox controlling colorpicker option for the style editor." + }, "cm_indentWithTabs": { "message": "Use tabs with smart indentation", "description": "Label for the checkbox controlling tabs with smart indentation option for the style editor." @@ -171,6 +175,14 @@ "message": "Theme", "description": "Label for the style editor's CSS theme." }, + "colorpickerSwitchFormatTooltip": { + "message": "Switch formats: HEX -> RGB -> HSL", + "description": "Tooltip for the switch button in the color picker popup in the style editor." + }, + "colorpickerTooltip": { + "message": "Open color picker", + "description": "Tooltip for the colored squares shown before CSS colors in the style editor." + }, "dysfunctional": { "message": "Stylus cannot function in private windows because Firefox disallows direct connection to the internal background page context of the extension.", "description": "Displayed in Firefox when its settings make Stylus dysfunctional" diff --git a/edit.html b/edit.html index 6e20b003..34e99fd6 100644 --- a/edit.html +++ b/edit.html @@ -190,6 +190,10 @@ +
+ + +
diff --git a/edit/edit.js b/edit/edit.js index 14c88382..0775ede5 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -7,6 +7,12 @@ /* global closeCurrentTab regExpTester messageBox */ 'use strict'; +onDOMready() + .then(() => Promise.all([ + onColorpickerReady(), + ])) + .then(init); + let styleId = null; // only the actually dirty items here let dirty = {}; @@ -362,6 +368,8 @@ function acmeEventListener(event) { } option = 'highlightSelectionMatches'; break; + case 'colorpicker': + return; } CodeMirror.setOption(option, value); } @@ -1298,8 +1306,6 @@ function beautify(event) { } } -onDOMready().then(init); - function init() { initCodeMirror(); getStyle().then(style => { @@ -2065,3 +2071,37 @@ function setGlobalProgress(done, total) { progressElement.remove(); } } + +function onColorpickerReady() { + const scripts = [ + '/vendor-overwrites/colorpicker/colorpicker.css', + '/vendor-overwrites/colorpicker/colorpicker.js', + '/vendor-overwrites/colorpicker/colorview.js', + ]; + prefs.subscribe(['editor.colorpicker'], colorpickerOnDemand); + return prefs.get('editor.colorpicker') && colorpickerOnDemand(null, true); + + function colorpickerOnDemand(id, enabled) { + return loadScript(enabled && scripts) + .then(() => setColorpickerOption(id, enabled)); + } + + function setColorpickerOption(id, enabled) { + CodeMirror.defaults.colorpicker = enabled && { + forceUpdate: editors.length > 0, + tooltip: t('colorpickerTooltip'), + popupOptions: { + tooltipForSwitcher: t('colorpickerSwitchFormatTooltip'), + hexUppercase: prefs.get('editor.colorpicker.hexUppercase'), + hideDelay: 5000, + embedderCallback: state => { + if (state && state.hexUppercase !== prefs.get('editor.colorpicker.hexUppercase')) { + prefs.set('editor.colorpicker.hexUppercase', state.hexUppercase); + } + }, + }, + }; + // on page load runs before CodeMirror.setOption is defined + editors.forEach(cm => cm.setOption('colorpicker', CodeMirror.defaults.colorpicker)); + } +} diff --git a/js/prefs.js b/js/prefs.js index 59735db2..1c93a06c 100644 --- a/js/prefs.js +++ b/js/prefs.js @@ -56,6 +56,11 @@ var prefs = new function Prefs() { 'editor.appliesToLineWidget': true, // show applies-to line widget on the editor + // show CSS colors as clickable colored rectangles + 'editor.colorpicker': true, + // #DEAD or #beef + 'editor.colorpicker.hexUppercase': false, + 'iconset': 0, // 0 = dark-themed icon // 1 = light-themed icon @@ -136,9 +141,13 @@ var prefs = new function Prefs() { } } if (hasChanged) { - const listener = onChange.specific.get(key); - if (listener) { - listener(key, value); + const specific = onChange.specific.get(key); + if (typeof specific === 'function') { + specific(key, value); + } else if (specific instanceof Set) { + for (const listener of specific.values()) { + listener(key, value); + } } for (const listener of onChange.any.values()) { listener(key, value); @@ -164,7 +173,14 @@ var prefs = new function Prefs() { // listener: function (key, value) if (keys) { for (const key of keys) { - onChange.specific.set(key, listener); + const existing = onChange.specific.get(key); + if (!existing) { + onChange.specific.set(key, listener); + } else if (existing instanceof Set) { + existing.add(listener); + } else { + onChange.specific.set(key, new Set([existing, listener])); + } } } else { onChange.any.add(listener); diff --git a/vendor-overwrites/colorpicker/LICENSE b/vendor-overwrites/colorpicker/LICENSE new file mode 100644 index 00000000..1ea38875 --- /dev/null +++ b/vendor-overwrites/colorpicker/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 jinho park (cyberuls@gmail.com, easylogic) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor-overwrites/colorpicker/colorpicker.css b/vendor-overwrites/colorpicker/colorpicker.css new file mode 100644 index 00000000..532a6a07 --- /dev/null +++ b/vendor-overwrites/colorpicker/colorpicker.css @@ -0,0 +1,388 @@ +/* codemirror colorview */ + +.cm-colorview { + position: relative; + white-space: nowrap; +} + +.cm-colorview::before { + content: ""; + position: relative; + display: inline-block; + box-sizing: content-box; + margin: 0 3px; + width: 8px; + height: 8px; + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAJElEQVQYV2NctWrVfwYkEBYWxojMZ6SDAmT7QGx0K1EcRBsFAADeG/3M/HteAAAAAElFTkSuQmCC"); + background-repeat: repeat; +} + +.cm-colorview + .cm-colorview.cm-overlay::before, +.cm-colorview.cm-overlay + .cm-colorview::before { + content: none; +} + +.codemirror-colorview-background { + position: absolute; + left: 2px; + top: 2px; + width: 10px; + height: 10px; + box-sizing: border-box; + border: 1px solid #8e8e8e; + content: ""; + cursor: pointer; +} + +.codemirror-colorview-background:hover { + border-color: #494949; +} + +/* colorpicker */ + +.colorpicker-theme-light { + --main-background-color: #fff; + --main-border-color: #ccc; + + --label-color: #666; + --label-color-hover: #000; + + --input-background-color: #fff; + --input-background-color-hover: #ddd; + --input-background-color-focus: #fff; + + --input-color: #444; + --input-color-focus: #000; + + --input-border-color: #bbb; + --input-border-color-focus: #888; + --input-border-color-hover: #444; + + --invalid-border-color: hsl(0, 100%, 50%); + --invalid-background-color: hsla(0, 100%, 50%, 0.15); + --invalid-color: hsl(0, 100%, 40%); +} + +.colorpicker-theme-dark { + --main-background-color: #242424; + --main-border-color: #888; + + --label-color: #aaa; + --label-color-hover: #eee; + + --input-background-color: #222; + --input-background-color-hover: #222; + --input-background-color-focus: #383838; + + --input-color: #ddd; + --input-color-focus: #fff; + + --input-border-color: #505050; + --input-border-color-focus: #777; + --input-border-color-hover: #888; + + --invalid-border-color: hsl(0, 100%, 27%); + --invalid-background-color: hsla(0, 100%, 50%, 0.3); + --invalid-color: hsl(0, 100%, 75%); +} + +.colorpicker-popup { + --switcher-width: 30px; + position: relative; + width: 350px; + z-index: 1000; + transition: opacity .5s; + color: var(--label-color); + border: 1px solid var(--main-border-color); + background-color: var(--main-background-color); + box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.12); + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; +} + +.colorpicker-popup[data-fading="1"] { + opacity: .75; +} + +.colorpicker-popup[data-fading="2"] { + opacity: 0; +} + +.colorpicker-saturation-container { + position: relative; + height: 120px; + overflow: hidden; + cursor: pointer; +} + +.colorpicker-opacity-bar { + position: absolute; + display: block; + content: ""; + left: 0; + right: 0; + bottom: 0; + top: 0; + background: linear-gradient(to right, rgba(232, 232, 232, 0), rgba(232, 232, 232, 1)); +} + +.colorpicker-saturation { + position: relative; + width: 100%; + height: 100%; + background-color: rgba(204, 154, 129, 0); + background-image: linear-gradient(to right, #FFF, rgba(204, 154, 129, 0)); + background-repeat: repeat-x; +} + +.colorpicker-value { + position: relative; + width: 100%; + height: 100%; + background-image: linear-gradient(to top, #000, rgba(204, 154, 129, 0)); +} + +.colorpicker-drag-pointer { + position: absolute; + width: 10px; + height: 10px; + -webkit-border-radius: 50%; + -moz-border-radius: 50%; + border-radius: 50%; + left: -5px; + top: -5px; + border: 1px solid #fff; + box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.05); +} + +.colorpicker-sliders { + position: relative; + padding: 10px 0 6px 0; + border-top: 1px solid transparent; +} + +.colorpicker-theme-dark .colorpicker-sliders { + border-color: var(--input-border-color); +} + +.colorpicker-swatch, +.colorpicker-empty { + position: absolute; + left: 11px; + top: 17px; + width: 30px; + height: 30px; + -webkit-border-radius: 50%; + -moz-border-radius: 50%; + border-radius: 50%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.colorpicker-empty { + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAJElEQVQYV2NctWrVfwYkEBYWxojMZ6SDAmT7QGx0K1EcRBsFAADeG/3M/HteAAAAAElFTkSuQmCC") repeat; +} + +.colorpicker-hue { + position: relative; + padding: 6px 12px; + margin: 0 0 0 45px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.colorpicker-hue-container { + position: relative; + width: 100%; + height: 10px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + cursor: pointer; + background: linear-gradient(to right, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%); +} + +.colorpicker-opacity { + position: relative; + padding: 3px 12px; + margin: 0 0 0 45px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.colorpicker-opacity-container { + position: relative; + width: 100%; + height: 10px; + z-index: 2; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + cursor: pointer; + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAJElEQVQYV2NctWrVfwYkEBYWxojMZ6SDAmT7QGx0K1EcRBsFAADeG/3M/HteAAAAAElFTkSuQmCC"); + background-repeat: repeat; +} + +.colorpicker-hue-knob, +.colorpicker-opacity-knob { + position: absolute; + cursor: pointer; + top: 50% !important; + margin-top: -7px !important; + left: -3px; + width: 12px; + height: 12px; + -webkit-border-radius: 50px; + -moz-border-radius: 50px; + border-radius: 50px; + border: 1px solid rgba(0, 0, 0, 0.5); + box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.1); + background-color: #fff; +} + +.colorpicker-input-container { + position: relative; + -webkit-box-sizing: padding-box; + -moz-box-sizing: padding-box; + box-sizing: padding-box; +} + +.colorpicker-input-group { + display: none; + position: relative; + padding: 0 5px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + margin-right: calc(var(--switcher-width) - 10px); +} + +.colorpicker-input-group[data-active] { + display: flex; +} + +.colorpicker-input-field { + display: block; + position: relative; + flex: 1; + padding: 5px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.colorpicker-input-field[class$="-a"] { + flex-grow: 1.5; +} + +.colorpicker-hsl-h::before { + content: "\b0"; /* degree */ + position: absolute; + right: -2px; + top: 8px; +} + +.colorpicker-hsl-s::before, +.colorpicker-hsl-l::before { + content: "%"; + position: absolute; + right: -1ex; + top: 8px; + font-size: 10px; +} + +.colorpicker-input { + text-align: center; + width: 100%; + padding: 3px 5px; + font-size: 11px; + font-weight: bold; + box-sizing: border-box; + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + -o-user-select: text; + user-select: text; + border: 1px solid var(--input-border-color); + background-color: var(--input-background-color); + color: var(--input-color); +} + +.colorpicker-theme-dark .colorpicker-input::-webkit-inner-spin-button { + -webkit-filter: invert(1); + filter: invert(1); +} + +.colorpicker-input:hover { + border-color: var(--input-border-color-hover); +} + +.colorpicker-input:focus { + color: var(--input-color-focus); + border-color: var(--input-border-color-focus); + background-color: var(--input-background-color-focus); +} + +.colorpicker-theme-dark input:focus { + outline: none !important; +} + +.colorpicker-input:invalid { + border-color: var(--invalid-border-color); + background-color: var(--invalid-background-color); + color: var(--invalid-color); +} + +.colorpicker-title { + text-align: center; + font-size: 12px; + font-family: monospace; + display: flex; + justify-content: center; + color: var(--label-color); +} + +.colorpicker-title-action { + cursor: pointer; +} + +.colorpicker-title-action[data-active] { + font-weight: bold; + color: var(--input-color); + cursor: default; + pointer-events: none; +} + +.colorpicker-format-change { + position: absolute; + display: block; + width: var(--switcher-width); + top: 0; + right: 0; + bottom: 0; + overflow: hidden; +} + +.colorpicker-format-change-button { + width: 100%; + height: 100%; + background: transparent; + border: 0; + cursor: pointer; + outline: none; + font-family: monospace !important; + font-size: var(--switcher-width) !important; + margin-top: -5px; + color: var(--label-color); + text-align: center; +} + +.colorpicker-format-change-button:hover { + color: var(--label-color-hover); +} diff --git a/vendor-overwrites/colorpicker/colorpicker.js b/vendor-overwrites/colorpicker/colorpicker.js new file mode 100644 index 00000000..c430d5cc --- /dev/null +++ b/vendor-overwrites/colorpicker/colorpicker.js @@ -0,0 +1,869 @@ +/* global CodeMirror */ +'use strict'; + +CodeMirror.defineExtension('colorpicker', function () { + const cm = this; + const CSS_PREFIX = 'colorpicker-'; + const HUE_COLORS = [ + {hex: '#ff0000', start: .0}, + {hex: '#ffff00', start: .17}, + {hex: '#00ff00', start: .33}, + {hex: '#00ffff', start: .50}, + {hex: '#0000ff', start: .67}, + {hex: '#ff00ff', start: .83}, + {hex: '#ff0000', start: 1} + ]; + + let HSV = {}; + let currentFormat; + + let initialized = false; + let shown = false; + let options = {}; + + let $root; + let $sat, $satPointer; + let $hue, $hueKnob; + let $opacity, $opacityBar, $opacityKnob; + let $swatch; + let $formatChangeButton; + let $hexCode; + const $inputGroups = {}; + const $inputs = {}; + const $rgb = {}; + const $hsl = {}; + const $hexLettercase = {}; + + const dragging = { + saturationPointerPos: {x: 0, y: 0}, + hueKnobPos: 0, + saturation: false, + hue: false, + opacity: false, + }; + + let prevFocusedElement; + let lastOutputColor; + let userActivity; + + let timerCloseColorPicker; + let timerFadeColorPicker; + + const PUBLIC_API = { + $root, + show, + hide, + setColor, + getColor, + options, + }; + return PUBLIC_API; + + //region DOM + + function init() { + // simplified createElement + function $(a, b) { + const cls = typeof a === 'string' || Array.isArray(a) ? a : ''; + const props = b || a; + const {tag = 'div', children} = props || {}; + const el = document.createElement(tag); + el.className = (Array.isArray(cls) ? cls : [cls]) + .map(c => (c ? CSS_PREFIX + c : '')) + .join(' '); + if (!props) { + return el; + } + for (const child of Array.isArray(children) ? children : [children]) { + if (child) { + el.appendChild(child instanceof Node ? child : document.createTextNode(child)); + } + } + delete props.tag; + delete props.children; + return Object.assign(el, props); + } + const alphaPattern = /^\s*(0+\.?|0*\.\d+|0*1\.?|0*1\.0*)?\s*$/.source; + $root = $('popup', {children: [ + $sat = $('saturation-container', {children: [ + $('saturation', {children: [ + $('value', {children: [ + $satPointer = $('drag-pointer'), + ]}), + ]}), + ]}), + $('sliders', {children: [ + $('hue', {children: [ + $hue = $('hue-container', {children: [ + $hueKnob = $('hue-knob'), + ]}), + ]}), + $('opacity', {children: [ + $opacity = $('opacity-container', {children: [ + $opacityBar = $('opacity-bar'), + $opacityKnob = $('opacity-knob'), + ]}), + ]}), + $('empty'), + $swatch = $('swatch'), + ]}), + $(['input-container', 'hex'], {children: [ + $inputGroups.hex = $(['input-group', 'hex'], {children: [ + $(['input-field', 'hex'], {children: [ + $hexCode = $('input', {tag: 'input', type: 'text', spellcheck: false, + pattern: /^\s*#([a-fA-F\d]{3}([a-fA-F\d]([a-fA-F\d]{2}([a-fA-F\d]{2})?)?)?)\s*$/.source + }), + $('title', {children: [ + $hexLettercase.true = $('title-action', {textContent: 'HEX'}), + '\xA0/\xA0', + $hexLettercase.false = $('title-action', {textContent: 'hex'}), + ]}), + ]}), + ]}), + $inputGroups.rgb = $(['input-group', 'rgb'], {children: [ + $(['input-field', 'rgb-r'], {children: [ + $rgb.r = $('input', {tag: 'input', type: 'number', min: 0, max: 255, step: 1}), + $('title', {textContent: 'R'}), + ]}), + $(['input-field', 'rgb-g'], {children: [ + $rgb.g = $('input', {tag: 'input', type: 'number', min: 0, max: 255, step: 1}), + $('title', {textContent: 'G'}), + ]}), + $(['input-field', 'rgb-b'], {children: [ + $rgb.b = $('input', {tag: 'input', type: 'number', min: 0, max: 255, step: 1}), + $('title', {textContent: 'B'}), + ]}), + $(['input-field', 'rgb-a'], {children: [ + $rgb.a = $('input', {tag: 'input', type: 'text', pattern: alphaPattern, spellcheck: false}), + $('title', {textContent: 'A'}), + ]}), + ]}), + $inputGroups.hsl = $(['input-group', 'hsl'], {children: [ + $(['input-field', 'hsl-h'], {children: [ + $hsl.h = $('input', {tag: 'input', type: 'number', step: 1}), + $('title', {textContent: 'H'}), + ]}), + $(['input-field', 'hsl-s'], {children: [ + $hsl.s = $('input', {tag: 'input', type: 'number', min: 0, max: 100, step: 1}), + $('title', {textContent: 'S'}), + ]}), + $(['input-field', 'hsl-l'], {children: [ + $hsl.l = $('input', {tag: 'input', type: 'number', min: 0, max: 100, step: 1}), + $('title', {textContent: 'L'}), + ]}), + $(['input-field', 'hsl-a'], {children: [ + $hsl.a = $('input', {tag: 'input', type: 'text', pattern: alphaPattern, spellcheck: false}), + $('title', {textContent: 'A'}), + ]}), + ]}), + $('format-change', {children: [ + $formatChangeButton = $('format-change-button', {textContent: '↔'}), + ]}), + ]}), + ]}); + + $inputs.hex = [$hexCode]; + $inputs.rgb = [$rgb.r, $rgb.g, $rgb.b, $rgb.a]; + $inputs.hsl = [$hsl.h, $hsl.s, $hsl.l, $hsl.a]; + const inputsToArray = inputs => inputs.map(el => parseFloat(el.value)); + const inputsToHexString = () => $hexCode.value.trim(); + const inputsToRGB = ([r, g, b, a] = inputsToArray($inputs.rgb)) => ({r, g, b, a, type: 'rgb'}); + const inputsToHSL = ([h, s, l, a] = inputsToArray($inputs.hsl)) => ({h, s, l, a, type: 'hsl'}); + Object.defineProperty($inputs.hex, 'color', {get: inputsToHexString}); + Object.defineProperty($inputs.rgb, 'color', {get: inputsToRGB}); + Object.defineProperty($inputs.hsl, 'color', {get: inputsToHSL}); + Object.defineProperty($inputs, 'color', {get: () => $inputs[currentFormat].color}); + + HUE_COLORS.forEach(color => Object.assign(color, stringToColor(color.hex))); + + initialized = true; + } + + //endregion + //region Public API + + function show(opt) { + if (!initialized) { + init(); + } + $root.style = ` + display: block; + position: fixed; + left: -10000px; + top: -10000px; + `.replace(/;/g, '!important;'); + $root.classList.add(CSS_PREFIX + 'theme-' + + (opt.theme === 'dark' || opt.theme === 'light' ? opt.theme : guessTheme())); + document.body.appendChild($root); + + shown = true; + + HSV = {}; + currentFormat = ''; + options = PUBLIC_API.options = opt; + prevFocusedElement = document.activeElement; + userActivity = 0; + lastOutputColor = opt.color; + $formatChangeButton.title = opt.tooltipForSwitcher || ''; + opt.hideDelay = Math.max(0, opt.hideDelay) || 2000; + + registerEvents(); + reposition(); + setFromColor(opt.color); + setFromHexLettercaseElement(); + $inputs[currentFormat][0].focus(); + } + + function hide() { + if (shown) { + unregisterEvents(); + focusNoScroll(prevFocusedElement); + $root.remove(); + shown = false; + } + } + + function setColor(color) { + switch (typeof color) { + case 'string': + color = stringToColor(color); + break; + case 'object': { + const {r, g, b, a} = color; + if (!isNaN(r) && !isNaN(g) && !isNaN(b)) { + color = {r, g, b, a, type: 'rgb'}; + break; + } + const {h, s, l} = color; + if (!isNaN(h) && !isNaN(s) && !isNaN(l)) { + color = {h, s, l, a, type: 'hsl'}; + break; + } + } + // fallthrough + default: + return false; + } + if (color) { + if (!initialized) { + init(); + } + setFromColor(color); + } + return Boolean(color); + } + + function getColor(type) { + if (!initialized) { + return; + } + readCurrentColorFromRamps(); + const color = type === 'hsl' ? HSVtoHSL(HSV) : HSVtoRGB(HSV); + return type ? colorToString(color, type) : color; + } + + //endregion + //region DOM-to-state + + function readCurrentColorFromRamps() { + if ($sat.offsetWidth === 0) { + HSV.h = HSV.s = HSV.v = 0; + } else { + const {x, y} = dragging.saturationPointerPos; + HSV.h = snapToInt((dragging.hueKnobPos / $hue.offsetWidth) * 360); + HSV.s = x / $sat.offsetWidth; + HSV.v = ($sat.offsetHeight - y) / $sat.offsetHeight; + } + } + + function setFromSaturationElement(event) { + event.preventDefault(); + const w = $sat.offsetWidth; + const h = $sat.offsetHeight; + const deltaX = event.clientX - parseFloat($root.style.left); + const deltaY = event.clientY - parseFloat($root.style.top); + const x = dragging.saturationPointerPos.x = constrain(0, w, deltaX); + const y = dragging.saturationPointerPos.y = constrain(0, h, deltaY); + + $satPointer.style.left = `${x - 5}px`; + $satPointer.style.top = `${y - 5}px`; + + readCurrentColorFromRamps(); + renderInputs(); + } + + function setFromHueElement(event) { + const {left, width} = getScreenBounds($hue); + const currentX = event ? getTouchPosition(event).clientX : left + width * (HSV.h / 360); + const normalizedH = constrain(0, 1, (currentX - left) / width); + const x = dragging.hueKnobPos = width * normalizedH; + $hueKnob.style.left = (x - Math.round($hueKnob.offsetWidth / 2)) + 'px'; + $sat.style.backgroundColor = hueDistanceToColorString(normalizedH); + HSV.h = event ? Math.round(normalizedH * 360) : HSV.h; + renderInputs(); + } + + function setFromOpacityElement(event) { + const {left, width} = getScreenBounds($opacity); + const normalized = constrain(0, 1, (getTouchPosition(event).clientX - left) / width); + const x = width * normalized; + $opacityKnob.style.left = (x - Math.ceil($opacityKnob.offsetWidth / 2)) + 'px'; + HSV.a = Math.round(normalized * 100) / 100; + renderInputs(); + } + + function setFromFormatElement() { + userActivity = performance.now(); + const nextFormat = {hex: 'rgb', rgb: 'hsl', hsl: 'hex'}[currentFormat]; + HSV.a = isNaN(HSV.a) ? 1 : HSV.a; + switchInputGroup(nextFormat); + renderInputs(); + } + + function setFromHexLettercaseElement() { + const isUpper = Boolean(options.hexUppercase); + $hexLettercase[isUpper].dataset.active = ''; + delete $hexLettercase[!isUpper].dataset.active; + const value = $hexCode.value; + $hexCode.value = isUpper ? value.toUpperCase() : value.toLowerCase(); + setFromInputs(); + } + + function setFromInputs() { + userActivity = performance.now(); + if ($inputs[currentFormat].every(validateInput)) { + setFromColor($inputs.color); + } + } + + function validateInput(el) { + const isAlpha = el.type === 'text'; + let isValid = (isAlpha || el.value.trim()) && el.checkValidity(); + if (!isAlpha && !isValid && currentFormat === 'rgb') { + isValid = parseAs(el, parseInt); + } else if (isAlpha && !isValid) { + isValid = parseAs(el, parseFloat); + } + if (isAlpha && isValid) { + isValid = lastOutputColor !== colorToString($inputs.color); + } + return isValid; + } + //endregion + //region State-to-DOM + + function setFromColor(color = '#FF0000') { + color = typeof color === 'string' ? stringToColor(color) : color; + const newHSV = color.type === 'hsl' ? HSLtoHSV(color) : RGBtoHSV(color); + if (Object.keys(newHSV).every(k => Math.abs(newHSV[k] - HSV[k]) < 1e-3)) { + return; + } + HSV = newHSV; + renderKnobs(color); + switchInputGroup(color.type); + setFromHueElement(); + } + + function switchInputGroup(format) { + if (currentFormat === format) { + return; + } + if (currentFormat) { + delete $inputGroups[currentFormat].dataset.active; + } else { + for (const format in $inputGroups) { + delete $inputGroups[format].dataset.active; + } + } + $inputGroups[format].dataset.active = ''; + currentFormat = format; + } + + function renderKnobs(color) { + const x = $sat.offsetWidth * HSV.s; + const y = $sat.offsetHeight * (1 - HSV.v); + $satPointer.style.left = (x - 5) + 'px'; + $satPointer.style.top = (y - 5) + 'px'; + dragging.saturationPointerPos = {x, y}; + + const hueX = $hue.offsetWidth * (HSV.h / 360); + $hueKnob.style.left = (hueX - 7.5) + 'px'; + dragging.hueKnobPos = hueX; + + const opacityX = $opacity.offsetWidth * (isNaN(HSV.a) ? 1 : HSV.a); + $opacityKnob.style.left = (opacityX - 7.5) + 'px'; + + $sat.style.backgroundColor = color; + } + + function renderInputs() { + const rgb = HSVtoRGB(HSV); + switch (currentFormat) { + case 'hex': + $hexCode.value = colorToString(rgb, 'hex'); + break; + case 'rgb': { + $rgb.r.value = rgb.r; + $rgb.g.value = rgb.g; + $rgb.b.value = rgb.b; + $rgb.a.value = alphaToString() || 1; + break; + } + case 'hsl': { + const {h, s, l} = HSVtoHSL(HSV); + $hsl.h.value = h; + $hsl.s.value = s; + $hsl.l.value = l; + $hsl.a.value = alphaToString() || 1; + } + } + $swatch.style.backgroundColor = colorToString(rgb, 'rgb'); + $opacityBar.style.background = 'linear-gradient(to right,' + + colorToString(Object.assign(rgb, {a: 0}), 'rgb') + ',' + + colorToString(Object.assign(rgb, {a: 1}), 'rgb') + ')'; + colorpickerCallback(); + } + + //endregion + //region Event listeners + + function onHexLettercaseClicked() { + options.hexUppercase = !options.hexUppercase; + setFromHexLettercaseElement(); + } + + function onSaturationMouseDown(event) { + if (event.button === 0) { + setFromSaturationElement(event); + dragging.saturation = true; + captureMouse(); + } + } + + function onSaturationMouseUp() { + if (event.button === 0) { + dragging.saturation = false; + releaseMouse(); + } + } + + function onHueKnobMouseDown(event) { + if (event.button === 0) { + dragging.hue = true; + captureMouse(); + } + } + + function onOpacityKnobMouseDown() { + if (event.button === 0) { + dragging.opacity = true; + captureMouse(); + } + } + + function onHueMouseDown(event) { + if (event.button === 0) { + dragging.hue = true; + setFromHueElement(event); + captureMouse(); + } + } + + function onOpacityMouseDown(event) { + if (event.button === 0) { + dragging.opacity = true; + setFromOpacityElement(event); + captureMouse(); + } + } + + function onMouseUp(event) { + if (event.button === 0) { + releaseMouse(); + dragging.saturation = dragging.hue = dragging.opacity = false; + if (!event.target.closest('.codemirror-colorview, .colorpicker-popup, .CodeMirror')) { + hide(); + } + } + } + + function onMouseMove(event) { + if (event.button !== 0) { + return; + } + if (dragging.saturation) { + setFromSaturationElement(event); + } else if (dragging.hue) { + setFromHueElement(event); + } else if (dragging.opacity) { + setFromOpacityElement(event); + } + } + + function stopSnoozing() { + clearTimeout(timerCloseColorPicker); + clearTimeout(timerFadeColorPicker); + if ($root.dataset.fading) { + delete $root.dataset.fading; + } + } + + function snooze() { + clearTimeout(timerFadeColorPicker); + timerFadeColorPicker = setTimeout(fade, options.hideDelay / 2); + } + + function onKeyDown(e) { + if (!e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) { + switch (e.which) { + case 13: + colorpickerCallback(); + // fallthrough to 27 + case 27: + e.preventDefault(); + e.stopPropagation(); + hide(); + break; + } + } + } + + function onCloseRequest(event) { + if (event.detail !== PUBLIC_API) { + hide(); + } + } + + //endregion + //region Event utilities + + function colorpickerCallback(colorString = currentColorToString()) { + if ( + userActivity && + $inputs[currentFormat].every(el => el.checkValidity()) && + typeof options.callback === 'function' + ) { + lastOutputColor = colorString.replace(/\b0\./g, '.'); + options.callback(lastOutputColor); + } + } + + function captureMouse() { + document.addEventListener('mouseup', onMouseUp); + document.addEventListener('mousemove', onMouseMove); + userActivity = performance.now(); + } + + function releaseMouse() { + document.removeEventListener('mouseup', onMouseUp); + document.removeEventListener('mousemove', onMouseMove); + userActivity = performance.now(); + } + + function getTouchPosition(event) { + return event.touches && event.touches[0] || event; + } + + function registerEvents() { + window.addEventListener('keydown', onKeyDown, true); + window.addEventListener('close-colorpicker-popup', onCloseRequest, true); + $root.addEventListener('mouseleave', snooze); + $root.addEventListener('mouseenter', stopSnoozing); + $root.addEventListener('input', setFromInputs); + $formatChangeButton.addEventListener('click', setFromFormatElement); + $sat.addEventListener('mousedown', onSaturationMouseDown); + $sat.addEventListener('mouseup', onSaturationMouseUp); + $hueKnob.addEventListener('mousedown', onHueKnobMouseDown); + $opacityKnob.addEventListener('mousedown', onOpacityKnobMouseDown); + $hue.addEventListener('mousedown', onHueMouseDown); + $opacity.addEventListener('mousedown', onOpacityMouseDown); + $hexLettercase.true.addEventListener('click', onHexLettercaseClicked); + $hexLettercase.false.addEventListener('click', onHexLettercaseClicked); + + stopSnoozing(); + timerFadeColorPicker = setTimeout(fade, options.hideDelay / 2); + } + + function unregisterEvents() { + window.removeEventListener('keydown', onKeyDown, true); + window.removeEventListener('close-colorpicker-popup', hide, true); + $root.removeEventListener('mouseleave', snooze); + $root.removeEventListener('mouseenter', stopSnoozing); + $root.removeEventListener('input', setFromInputs); + $formatChangeButton.removeEventListener('click', setFromFormatElement); + $sat.removeEventListener('mousedown', onSaturationMouseDown); + $sat.removeEventListener('mouseup', onSaturationMouseUp); + $hueKnob.removeEventListener('mousedown', onHueKnobMouseDown); + $opacityKnob.removeEventListener('mousedown', onOpacityKnobMouseDown); + $hue.removeEventListener('mousedown', onHueMouseDown); + $opacity.removeEventListener('mousedown', onOpacityMouseDown); + $hexLettercase.true.removeEventListener('click', onHexLettercaseClicked); + $hexLettercase.false.removeEventListener('click', onHexLettercaseClicked); + releaseMouse(); + stopSnoozing(); + } + + //endregion + //region Color conversion utilities + + function colorToString({r, g, b, h, s, l, a}, type = currentFormat) { + a = alphaToString(a); + const hasA = Boolean(a); + switch (type) { + case 'hex': { + const rgbStr = (0x1000000 + (r << 16) + (g << 8) + (b | 0)).toString(16).slice(1); + const aStr = hasA ? (0x100 + Math.round(a * 255)).toString(16).slice(1) : ''; + const hexStr = `#${rgbStr + aStr}`.replace(/^#(.)\1(.)\2(.)\3(?:(.)\4)?$/, '#$1$2$3$4'); + return options.hexUppercase ? hexStr.toUpperCase() : hexStr.toLowerCase(); + } + case 'rgb': + return hasA ? + `rgba(${r}, ${g}, ${b}, ${a})` : + `rgb(${r}, ${g}, ${b})`; + case 'hsl': + return hasA ? + `hsla(${h}, ${s}%, ${l}%, ${a})` : + `hsl(${h}, ${s}%, ${l}%)`; + } + } + + function stringToColor(str) { + if (typeof str !== 'string') { + return; + } + str = str.trim(); + if (str.startsWith('rgb')) { + const [r, g, b, a = 1] = str.replace(/rgba?\(|\)/g, '').split(',').map(parseFloat); + return {type: 'rgb', r, g, b, a}; + } + if (str.startsWith('hsl')) { + const [h, s, l, a = 1] = str.replace(/hsla?\(|\)/g, '').split(',').map(parseFloat); + return {type: 'hsl', h, s, l, a}; + } + if (str.startsWith('#')) { + str = str.slice(1); + const [r, g, b, a = 255] = str.length <= 4 ? + str.match(/(.)/g).map(c => parseInt(c + c, 16)) : + str.match(/(..)/g).map(c => parseInt(c, 16)); + return {type: 'hex', r, g, b, a: a === 255 ? undefined : a / 255}; + } + return; + } + + function RGBtoHSV({r, g, b, a}) { + r /= 255; + g /= 255; + b /= 255; + const MaxC = Math.max(r, g, b); + const MinC = Math.min(r, g, b); + const DeltaC = MaxC - MinC; + + let h = + DeltaC === 0 ? 0 : + MaxC === r ? 60 * (((g - b) / DeltaC) % 6) : + MaxC === g ? 60 * (((b - r) / DeltaC) + 2) : + MaxC === b ? 60 * (((r - g) / DeltaC) + 4) : + 0; + h = + h < 0 ? h % 360 + 360 : + h > 360 ? h % 360 : + h; + return { + h, + s: MaxC === 0 ? 0 : DeltaC / MaxC, + v: MaxC, + a, + }; + } + + function HSVtoRGB({h, s, v}) { + if (h === 360) { + h = 0; + } + const C = s * v; + const X = C * (1 - Math.abs((h / 60) % 2 - 1)); + const m = v - C; + const [r, g, b] = + h >= 0 && h < 60 ? [C, X, 0] : + h >= 60 && h < 120 ? [X, C, 0] : + h >= 120 && h < 180 ? [0, C, X] : + h >= 180 && h < 240 ? [0, X, C] : + h >= 240 && h < 300 ? [X, 0, C] : + h >= 300 && h < 360 ? [C, 0, X] : []; + return { + r: snapToInt(Math.round((r + m) * 255)), + g: snapToInt(Math.round((g + m) * 255)), + b: snapToInt(Math.round((b + m) * 255)), + }; + } + + function HSLtoHSV({h, s, l, a}) { + const t = s * (l < 50 ? l : 100 - l) / 100; + return { + h, + s: t + l ? 200 * t / (t + l) / 100 : 0, + v: (t + l) / 100, + a, + }; + } + + function HSVtoHSL({h, s, v}) { + const l = (2 - s) * v / 2; + const t = l < .5 ? l * 2 : 2 - l * 2; + return { + h: Math.round(h), + s: Math.round(t ? s * v / t * 100 : 0), + l: Math.round(l * 100), + }; + } + + function currentColorToString(format = currentFormat, alpha = HSV.a) { + const converted = format === 'hsl' ? HSVtoHSL(HSV) : HSVtoRGB(HSV); + converted.a = isNaN(alpha) || alpha === 1 ? undefined : alpha; + return colorToString(converted, format); + } + + function mixColorToString(start, end, amount) { + const obj = { + r: start.r + (end.r - start.r) * amount, + g: start.g + (end.g - start.g) * amount, + b: start.b + (end.b - start.b) * amount, + a: 1, + }; + return colorToString(obj, 'hex'); + } + + function hueDistanceToColorString(hueRatio) { + let prevColor; + for (const color of HUE_COLORS) { + if (prevColor && color.start >= hueRatio) { + return mixColorToString(prevColor, color, + (hueRatio - prevColor.start) / (color.start - prevColor.start)); + } + prevColor = color; + } + return HUE_COLORS[0].hex; + } + + function alphaToString(a = HSV.a) { + return isNaN(a) ? '' : + a.toString().slice(0, 8) + .replace(/(\.[^0]*)0+$/, '$1') + .replace(/^1$/, ''); + } + + //endregion + //region Miscellaneous utilities + + function reposition() { + const width = $root.offsetWidth; + const height = $root.offsetHeight; + + // set left position for color picker + let elementScreenLeft = options.left - document.scrollingElement.scrollLeft; + const bodyWidth = document.scrollingElement.scrollWidth; + if (width + elementScreenLeft > bodyWidth) { + elementScreenLeft -= (width + elementScreenLeft) - bodyWidth; + } + if (elementScreenLeft < 0) { + elementScreenLeft = 0; + } + + // set top position for color picker + let elementScreenTop = options.top - document.scrollingElement.scrollTop; + if (height + elementScreenTop > window.innerHeight) { + elementScreenTop = window.innerHeight - height; + } + if (elementScreenTop < options.top) { + elementScreenTop = options.top - height - 20; + } + if (elementScreenTop < 0) { + elementScreenTop = 0; + } + + // set position + $root.style.left = elementScreenLeft + 'px'; + $root.style.top = elementScreenTop + 'px'; + } + + function fade({fadingStage = 1} = {}) { + const timeInactive = performance.now() - userActivity; + const delay = options.hideDelay / 2; + if (userActivity && timeInactive < delay) { + timerFadeColorPicker = setTimeout(fade, delay - timeInactive, 2); + clearTimeout(timerCloseColorPicker); + delete $root.dataset.fading; + return; + } + $root.dataset.fading = fadingStage; + if (fadingStage === 1) { + timerFadeColorPicker = setTimeout(fade, Math.max(0, delay - 500), {fadingStage: 2}); + } else { + timerCloseColorPicker = setTimeout(hide, 500); + } + } + + function focusNoScroll(el) { + if (el) { + const {scrollY: y, scrollX: x} = window; + el.focus({preventScroll: true}); + el = null; + if (window.scrollY !== y || window.scrollX !== x) { + window.scrollTo(x, y); + } + } + } + + function getScreenBounds(el) { + const bounds = el.getBoundingClientRect(); + const {scrollTop, scrollLeft} = document.scrollingElement; + return { + top: bounds.top + scrollTop, + left: bounds.left + scrollLeft, + width: bounds.width, + height: bounds.height, + }; + } + + function guessTheme() { + const realColor = {r: 255, g: 255, b: 255, a: 1}; + const start = ((cm.display.renderedView || [])[0] || {}).text || cm.display.lineDiv; + for (let el = start; el; el = el.parentElement) { + const bgColor = getComputedStyle(el).backgroundColor; + const [r, g, b, a = 255] = bgColor.match(/\d+/g).map(Number); + if (!a) { + continue; + } + const mixedA = 1 - (1 - a / 255) * (1 - realColor.a); + const q1 = a / 255 / mixedA; + const q2 = realColor.a * (1 - mixedA) / mixedA; + realColor.r = Math.round(r * q1 + realColor.r * q2); + realColor.g = Math.round(g * q1 + realColor.g * q2); + realColor.b = Math.round(b * q1 + realColor.b * q2); + realColor.a = mixedA; + } + // https://www.w3.org/TR/AERT#color-contrast + const {r, g, b} = realColor; + const brightness = r * .299 + g * .587 + b * .114; + return brightness < 128 ? 'dark' : 'light'; + } + + function constrain(min, max, value) { + return value < min ? min : value > max ? max : value; + } + + function snapToInt(num) { + const int = Math.round(num); + return Math.abs(int - num) < 1e-3 ? int : num; + } + + function parseAs(el, parser) { + const num = parser(el.value); + if (!isNaN(num)) { + el.value = num; + return true; + } + } + + //endregion +}); diff --git a/vendor-overwrites/colorpicker/colorview.js b/vendor-overwrites/colorpicker/colorview.js new file mode 100644 index 00000000..7086926b --- /dev/null +++ b/vendor-overwrites/colorpicker/colorview.js @@ -0,0 +1,457 @@ +/* global CodeMirror */ +'use strict'; + +(() => { + const OWN_TOKEN_NAME = 'colorview'; + const OWN_DOM_CLASS = 'cm-' + OWN_TOKEN_NAME; + const OWN_BACKGROUND_CLASS = 'codemirror-colorview-background'; + const HOOKED_TOKEN = new Map([ + ['atom', colorizeAtom], + ['keyword', colorizeKeyword], + ].map(([name, fn]) => [name, {override: name + ' ' + OWN_TOKEN_NAME, process: fn}])); + + const NAMED_COLORS = getNamedColorsMap(); + const TRANSPARENT = { + color: 'transparent', + colorValue: 'rgba(0, 0, 0, 0)', // as per the CSS spec + }; + const RX_COLOR = { + hex: /#(?:[a-f\d]{3,4}|[a-f\d]{6}|[a-f\d]{8})\b/yi, + rgb: /rgb\((?:\s*\d{1,3}\s*,\s*){2}\d{1,3}\s*\)/yi, + rgba: /rgba\((?:\s*\d{1,3}\s*,\s*){3}\d*\.?\d+\s*\)/yi, + hsl: /hsl\(\s*(?:-?\d+|-?\d*\.\d+)\s*(?:,\s*(?:-?\d+|-?\d*\.\d+)%\s*){2}\)/yi, + hsla: /hsla\(\s*(?:-?\d+|-?\d*\.\d+)\s*(?:,\s*(?:-?\d+|-?\d*\.\d+)%\s*){2},\s*(?:-?\d+|-?\d*\.\d+)\s*\)/yi, + named: new RegExp([...NAMED_COLORS.keys()].join('|'), 'i'), + }; + + const CodeMirrorEvents = { + update(cm) { + if (cm.state.colorpicker.cache.size) { + renderVisibleTokens(cm); + } + }, + keyup(cm) { + const popup = cm.state.colorpicker.popup; + if (popup && popup.options.isShortCut === false) { + popup.hide(); + } + }, + mousedown(cm, event) { + const self = cm.state.colorpicker; + const isMarker = event.button === 0 && event.target.classList.contains(OWN_BACKGROUND_CLASS); + window.dispatchEvent(new CustomEvent('close-colorpicker-popup', {detail: isMarker && self.popup})); + if (isMarker) { + event.preventDefault(); + self.openPopupForToken(event.target.parentNode); + } + }, + }; + + function registerEvents(cm) { + Object.keys(CodeMirrorEvents).forEach(name => cm.on(name, CodeMirrorEvents[name])); + } + + function unregisterEvents(cm) { + Object.keys(CodeMirrorEvents).forEach(name => cm.off(name, CodeMirrorEvents[name])); + } + + function registerHooks() { + const mx = CodeMirror.modeExtensions.css; + if (!mx || mx.token !== colorizeToken) { + CodeMirror.extendMode('css', { + token: colorizeToken, + }); + } + } + + function unregisterHooks() { + const mx = CodeMirror.modeExtensions.css; + if (mx && mx.token === colorizeToken) { + delete mx.token; + } + } + + function resetMode(cm) { + cm.setOption('mode', cm.getMode().name); + } + + function colorizeToken(stream, state) { + const token = this._token.apply(this, arguments); + const hookedToken = token && HOOKED_TOKEN.get(token); + if (!token || !hookedToken) { + return token; + } + const data = state.colorpicker = (state.colorpicker || {}); + const cache = data.cache = (data.cache || stream.lineOracle.doc.cm.state.colorpicker.cache); + const string = stream.string; + const sameString = string === data.lastString; + + data.lastString = string; + + let lineCache = data.lineCache = (sameString ? data.lineCache : cache.get(string)); + if (lineCache && lineCache.get(stream.start)) { + return hookedToken.override; + } + + const color = hookedToken.process(stream); + if (color) { + if (!lineCache) { + lineCache = data.lineCache = new Map(); + cache.set(string, lineCache); + } + lineCache.set(stream.start, color); + lineCache.set('lastAccessTime', performance.now()); + return hookedToken.override; + } + + return token; + } + + function colorizeAtom(stream) { + const {start, pos, string} = stream; + const c1 = string.charAt(start); + if ((c1 === 't' || c1 === 'T') && string.slice(start, pos).toLowerCase() === 'transparent') { + return TRANSPARENT; + } + const maybeHex = c1 === '#'; + const s = !maybeHex && string.charAt(pos) === '(' && string.slice(start, pos).toLowerCase(); + if (maybeHex || (s === 'rgb' || s === 'rgba' || s === 'hsl' || s === 'hsla')) { + const rx = maybeHex ? RX_COLOR.hex : RX_COLOR[s]; + rx.lastIndex = start; + const match = rx.exec(string); + return match && {color: match[0]}; + } + } + + function colorizeKeyword(stream) { + const {start, pos, string} = stream; + if (string.charAt(start) !== '!') { + const color = string.slice(start, pos); + const colorValue = NAMED_COLORS.get(color.toLowerCase()); + return colorValue && {color, colorValue}; + } + } + + function renderVisibleTokens(cm) { + const {cache, options} = cm.state.colorpicker; + let line = cm.display.viewFrom - 1; + for (const {line: lineHandle, text} of cm.display.renderedView) { + if (!lineHandle.parent) { + continue; + } + line++; + const styles = lineHandle.styles; + if (!styles) { + continue; + } + const lineCache = cache.get(lineHandle.text); + if (!lineCache) { + continue; + } + let lineCacheAlive = false; + let elementIndex = 0; + let elements; + for (let i = 1; i < styles.length; i += 2) { + const token = styles[i + 1]; + if (!token || !token.includes(OWN_TOKEN_NAME)) { + continue; + } + const start = styles[i - 2] || 0; + const data = lineCache.get(start); + if (!data) { + continue; + } + elements = elements || text.getElementsByClassName(OWN_DOM_CLASS); + const el = elements[elementIndex++]; + if (el.colorpickerData && el.colorpickerData.color === data.color) { + continue; + } + el.colorpickerData = Object.assign({line, ch: start}, data); + let bg = el.firstElementChild; + if (!bg) { + bg = document.createElement('div'); + bg.className = OWN_BACKGROUND_CLASS; + bg.title = options.tooltip; + el.appendChild(bg); + } + bg.style.setProperty('background-color', data.color, 'important'); + lineCacheAlive = true; + } + if (lineCacheAlive) { + lineCache.set('lastAccessTime', performance.now()); + } + } + trimCache(cm); + } + + function trimCache(cm, debounced) { + if (!debounced) { + clearTimeout(trimCache.timer); + trimCache.timer = setTimeout(trimCache, 20e3, cm, true); + return; + } + const cutoff = performance.now() - 60e3; + const {cache} = cm.state.colorpicker; + const textToKeep = new Set(); + cm.doc.iter(({text}) => textToKeep.add(text)); + for (const [text, lineCache] of cache.entries()) { + if (lineCache.get('lastAccessTime') < cutoff && !textToKeep.has(text)) { + cache.delete(text); + } + } + } + + function getNamedColorsMap() { + return new Map([ + ['aliceblue', '#f0f8ff'], + ['antiquewhite', '#faebd7'], + ['aqua', '#00ffff'], + ['aquamarine', '#7fffd4'], + ['azure', '#f0ffff'], + ['beige', '#f5f5dc'], + ['bisque', '#ffe4c4'], + ['black', '#000000'], + ['blanchedalmond', '#ffebcd'], + ['blue', '#0000ff'], + ['blueviolet', '#8a2be2'], + ['brown', '#a52a2a'], + ['burlywood', '#deb887'], + ['cadetblue', '#5f9ea0'], + ['chartreuse', '#7fff00'], + ['chocolate', '#d2691e'], + ['coral', '#ff7f50'], + ['cornflowerblue', '#6495ed'], + ['cornsilk', '#fff8dc'], + ['crimson', '#dc143c'], + ['cyan', '#00ffff'], + ['darkblue', '#00008b'], + ['darkcyan', '#008b8b'], + ['darkgoldenrod', '#b8860b'], + ['darkgray', '#a9a9a9'], + ['darkgreen', '#006400'], + ['darkgrey', '#a9a9a9'], + ['darkkhaki', '#bdb76b'], + ['darkmagenta', '#8b008b'], + ['darkolivegreen', '#556b2f'], + ['darkorange', '#ff8c00'], + ['darkorchid', '#9932cc'], + ['darkred', '#8b0000'], + ['darksalmon', '#e9967a'], + ['darkseagreen', '#8fbc8f'], + ['darkslateblue', '#483d8b'], + ['darkslategray', '#2f4f4f'], + ['darkslategrey', '#2f4f4f'], + ['darkturquoise', '#00ced1'], + ['darkviolet', '#9400d3'], + ['deeppink', '#ff1493'], + ['deepskyblue', '#00bfff'], + ['dimgray', '#696969'], + ['dimgrey', '#696969'], + ['dodgerblue', '#1e90ff'], + ['firebrick', '#b22222'], + ['floralwhite', '#fffaf0'], + ['forestgreen', '#228b22'], + ['fuchsia', '#ff00ff'], + ['gainsboro', '#dcdcdc'], + ['ghostwhite', '#f8f8ff'], + ['gold', '#ffd700'], + ['goldenrod', '#daa520'], + ['gray', '#808080'], + ['green', '#008000'], + ['greenyellow', '#adff2f'], + ['grey', '#808080'], + ['honeydew', '#f0fff0'], + ['hotpink', '#ff69b4'], + ['indianred', '#cd5c5c'], + ['indigo', '#4b0082'], + ['ivory', '#fffff0'], + ['khaki', '#f0e68c'], + ['lavender', '#e6e6fa'], + ['lavenderblush', '#fff0f5'], + ['lawngreen', '#7cfc00'], + ['lemonchiffon', '#fffacd'], + ['lightblue', '#add8e6'], + ['lightcoral', '#f08080'], + ['lightcyan', '#e0ffff'], + ['lightgoldenrodyellow', '#fafad2'], + ['lightgray', '#d3d3d3'], + ['lightgreen', '#90ee90'], + ['lightgrey', '#d3d3d3'], + ['lightpink', '#ffb6c1'], + ['lightsalmon', '#ffa07a'], + ['lightseagreen', '#20b2aa'], + ['lightskyblue', '#87cefa'], + ['lightslategray', '#778899'], + ['lightslategrey', '#778899'], + ['lightsteelblue', '#b0c4de'], + ['lightyellow', '#ffffe0'], + ['lime', '#00ff00'], + ['limegreen', '#32cd32'], + ['linen', '#faf0e6'], + ['magenta', '#ff00ff'], + ['maroon', '#800000'], + ['mediumaquamarine', '#66cdaa'], + ['mediumblue', '#0000cd'], + ['mediumorchid', '#ba55d3'], + ['mediumpurple', '#9370db'], + ['mediumseagreen', '#3cb371'], + ['mediumslateblue', '#7b68ee'], + ['mediumspringgreen', '#00fa9a'], + ['mediumturquoise', '#48d1cc'], + ['mediumvioletred', '#c71585'], + ['midnightblue', '#191970'], + ['mintcream', '#f5fffa'], + ['mistyrose', '#ffe4e1'], + ['moccasin', '#ffe4b5'], + ['navajowhite', '#ffdead'], + ['navy', '#000080'], + ['oldlace', '#fdf5e6'], + ['olive', '#808000'], + ['olivedrab', '#6b8e23'], + ['orange', '#ffa500'], + ['orangered', '#ff4500'], + ['orchid', '#da70d6'], + ['palegoldenrod', '#eee8aa'], + ['palegreen', '#98fb98'], + ['paleturquoise', '#afeeee'], + ['palevioletred', '#db7093'], + ['papayawhip', '#ffefd5'], + ['peachpuff', '#ffdab9'], + ['peru', '#cd853f'], + ['pink', '#ffc0cb'], + ['plum', '#dda0dd'], + ['powderblue', '#b0e0e6'], + ['purple', '#800080'], + ['rebeccapurple', '#663399'], + ['red', '#ff0000'], + ['rosybrown', '#bc8f8f'], + ['royalblue', '#4169e1'], + ['saddlebrown', '#8b4513'], + ['salmon', '#fa8072'], + ['sandybrown', '#f4a460'], + ['seagreen', '#2e8b57'], + ['seashell', '#fff5ee'], + ['sienna', '#a0522d'], + ['silver', '#c0c0c0'], + ['skyblue', '#87ceeb'], + ['slateblue', '#6a5acd'], + ['slategray', '#708090'], + ['slategrey', '#708090'], + ['snow', '#fffafa'], + ['springgreen', '#00ff7f'], + ['steelblue', '#4682b4'], + ['tan', '#d2b48c'], + ['teal', '#008080'], + ['thistle', '#d8bfd8'], + ['tomato', '#ff6347'], + ['turquoise', '#40e0d0'], + ['violet', '#ee82ee'], + ['wheat', '#f5deb3'], + ['white', '#ffffff'], + ['whitesmoke', '#f5f5f5'], + ['yellow', '#ffff00'], + ['yellowgreen', '#9acd32'], + ]); + } + + class ColorMarker { + constructor(cm, { + tooltip = 'Open color picker', + popupOptions = {}, + colorpicker, + forceUpdate, + } = {}) { + this.cm = cm; + this.options = { + tooltip, + popup: Object.assign({ + hideDelay: 2000, + hexUppercase: false, + tooltipForSwitcher: 'Switch formats: HEX -> RGB -> HSL', + }, popupOptions), + }; + this.popup = cm.colorpicker ? cm.colorpicker() : colorpicker; + this.cache = new Map(); + registerHooks(cm); + registerEvents(cm); + if (forceUpdate) { + resetMode(cm); + } + } + + destroy() { + unregisterHooks(this.cm); + unregisterEvents(this.cm); + resetMode(this.cm); + this.cm.state.colorpicker = null; + } + + openPopup(defaultColor = '#FFFFFF') { + const cursor = this.cm.getCursor(); + const data = { + line: cursor.line, + ch: cursor.ch, + color: defaultColor, + isShortCut: true, + }; + for (const {from, marker} of this.cm.getLineHandle(cursor.line).markedSpans || []) { + if (from <= data.ch && (marker.replacedWith || {}).colorpickerData) { + const {color, colorValue} = marker.replacedWith.colorpickerData; + if (data.ch <= from + color.length) { + data.ch = from; + data.color = color; + data.colorValue = colorValue; + break; + } + } + } + this.openPopupForToken({colorpickerData: data}); + } + + openPopupForToken({colorpickerData: data}) { + if (this.popup) { + const {left, bottom: top} = this.cm.charCoords(data, 'window'); + this.popup.show(Object.assign(this.options.popup, data, { + top, + left, + cm: this.cm, + color: data.colorValue || data.color, + prevColor: data.color, + isShortCut: false, + callback: ColorMarker.popupOnChange, + })); + } + } + + closePopup() { + if (this.popup) { + this.popup.hide(); + } + } + + static popupOnChange(newColor) { + const {cm, line, ch, embedderCallback} = this; + const to = {line, ch: ch + this.prevColor.length}; + if (cm.getRange(this, to) !== newColor) { + this.prevColor = newColor; + cm.replaceRange(newColor, this, to, '*colorpicker'); + } + if (typeof embedderCallback === 'function') { + embedderCallback(this); + } + } + } + + CodeMirror.defineOption('colorpicker', false, (cm, value, oldValue) => { + if (oldValue && oldValue !== CodeMirror.Init && cm.state.colorpicker) { + cm.state.colorpicker.destroy(); + } + if (value) { + cm.state.colorpicker = new ColorMarker(cm, value); + } + }); + + // initial runMode is performed by CodeMirror before setting our option + // so we register the hooks right away - not a problem as our js is loaded on demand + registerHooks(); +})();