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();
+})();