Merge pull request #239 from openstyles/colorpicker
Colorpicker in the editor
This commit is contained in:
commit
5d905c2952
|
@ -1,2 +1,3 @@
|
||||||
vendor/
|
vendor/
|
||||||
vendor-overwrites/
|
vendor-overwrites/*
|
||||||
|
!vendor-overwrites/colorpicker
|
||||||
|
|
|
@ -127,6 +127,10 @@
|
||||||
"message": "Autocomplete on typing",
|
"message": "Autocomplete on typing",
|
||||||
"description": "Label for the checkbox in the style editor."
|
"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": {
|
"cm_indentWithTabs": {
|
||||||
"message": "Use tabs with smart indentation",
|
"message": "Use tabs with smart indentation",
|
||||||
"description": "Label for the checkbox controlling tabs with smart indentation option for the style editor."
|
"description": "Label for the checkbox controlling tabs with smart indentation option for the style editor."
|
||||||
|
@ -171,6 +175,14 @@
|
||||||
"message": "Theme",
|
"message": "Theme",
|
||||||
"description": "Label for the style editor's CSS 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": {
|
"dysfunctional": {
|
||||||
"message": "Stylus cannot function in private windows because Firefox disallows direct connection to the internal background page context of the extension.",
|
"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"
|
"description": "Displayed in Firefox when its settings make Stylus dysfunctional"
|
||||||
|
|
|
@ -190,6 +190,10 @@
|
||||||
<input id="editor.autocompleteOnTyping" type="checkbox">
|
<input id="editor.autocompleteOnTyping" type="checkbox">
|
||||||
<label for="editor.autocompleteOnTyping" i18n-text="cm_autocompleteOnTyping"></label>
|
<label for="editor.autocompleteOnTyping" i18n-text="cm_autocompleteOnTyping"></label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="option">
|
||||||
|
<input id="editor.colorpicker" type="checkbox">
|
||||||
|
<label for="editor.colorpicker" i18n-text="cm_colorpicker"></label>
|
||||||
|
</div>
|
||||||
<div class="option aligned">
|
<div class="option aligned">
|
||||||
<label id="tabSize-label" for="editor.tabSize" i18n-text="cm_tabSize"></label>
|
<label id="tabSize-label" for="editor.tabSize" i18n-text="cm_tabSize"></label>
|
||||||
<input id="editor.tabSize" type="number" min="0">
|
<input id="editor.tabSize" type="number" min="0">
|
||||||
|
|
44
edit/edit.js
44
edit/edit.js
|
@ -7,6 +7,12 @@
|
||||||
/* global closeCurrentTab regExpTester messageBox */
|
/* global closeCurrentTab regExpTester messageBox */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
onDOMready()
|
||||||
|
.then(() => Promise.all([
|
||||||
|
onColorpickerReady(),
|
||||||
|
]))
|
||||||
|
.then(init);
|
||||||
|
|
||||||
let styleId = null;
|
let styleId = null;
|
||||||
// only the actually dirty items here
|
// only the actually dirty items here
|
||||||
let dirty = {};
|
let dirty = {};
|
||||||
|
@ -362,6 +368,8 @@ function acmeEventListener(event) {
|
||||||
}
|
}
|
||||||
option = 'highlightSelectionMatches';
|
option = 'highlightSelectionMatches';
|
||||||
break;
|
break;
|
||||||
|
case 'colorpicker':
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
CodeMirror.setOption(option, value);
|
CodeMirror.setOption(option, value);
|
||||||
}
|
}
|
||||||
|
@ -1298,8 +1306,6 @@ function beautify(event) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onDOMready().then(init);
|
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
initCodeMirror();
|
initCodeMirror();
|
||||||
getStyle().then(style => {
|
getStyle().then(style => {
|
||||||
|
@ -2065,3 +2071,37 @@ function setGlobalProgress(done, total) {
|
||||||
progressElement.remove();
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
24
js/prefs.js
24
js/prefs.js
|
@ -56,6 +56,11 @@ var prefs = new function Prefs() {
|
||||||
|
|
||||||
'editor.appliesToLineWidget': true, // show applies-to line widget on the editor
|
'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
|
'iconset': 0, // 0 = dark-themed icon
|
||||||
// 1 = light-themed icon
|
// 1 = light-themed icon
|
||||||
|
|
||||||
|
@ -136,9 +141,13 @@ var prefs = new function Prefs() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (hasChanged) {
|
if (hasChanged) {
|
||||||
const listener = onChange.specific.get(key);
|
const specific = onChange.specific.get(key);
|
||||||
if (listener) {
|
if (typeof specific === 'function') {
|
||||||
listener(key, value);
|
specific(key, value);
|
||||||
|
} else if (specific instanceof Set) {
|
||||||
|
for (const listener of specific.values()) {
|
||||||
|
listener(key, value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (const listener of onChange.any.values()) {
|
for (const listener of onChange.any.values()) {
|
||||||
listener(key, value);
|
listener(key, value);
|
||||||
|
@ -164,7 +173,14 @@ var prefs = new function Prefs() {
|
||||||
// listener: function (key, value)
|
// listener: function (key, value)
|
||||||
if (keys) {
|
if (keys) {
|
||||||
for (const key of 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 {
|
} else {
|
||||||
onChange.any.add(listener);
|
onChange.any.add(listener);
|
||||||
|
|
21
vendor-overwrites/colorpicker/LICENSE
Normal file
21
vendor-overwrites/colorpicker/LICENSE
Normal file
|
@ -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.
|
388
vendor-overwrites/colorpicker/colorpicker.css
Normal file
388
vendor-overwrites/colorpicker/colorpicker.css
Normal file
|
@ -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);
|
||||||
|
}
|
869
vendor-overwrites/colorpicker/colorpicker.js
Normal file
869
vendor-overwrites/colorpicker/colorpicker.js
Normal file
|
@ -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
|
||||||
|
});
|
457
vendor-overwrites/colorpicker/colorview.js
Normal file
457
vendor-overwrites/colorpicker/colorview.js
Normal file
|
@ -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();
|
||||||
|
})();
|
Loading…
Reference in New Issue
Block a user