Merge pull request #239 from openstyles/colorpicker
Colorpicker in the editor
This commit is contained in:
commit
5d905c2952
|
@ -1,2 +1,3 @@
|
|||
vendor/
|
||||
vendor-overwrites/
|
||||
vendor-overwrites/*
|
||||
!vendor-overwrites/colorpicker
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -190,6 +190,10 @@
|
|||
<input id="editor.autocompleteOnTyping" type="checkbox">
|
||||
<label for="editor.autocompleteOnTyping" i18n-text="cm_autocompleteOnTyping"></label>
|
||||
</div>
|
||||
<div class="option">
|
||||
<input id="editor.colorpicker" type="checkbox">
|
||||
<label for="editor.colorpicker" i18n-text="cm_colorpicker"></label>
|
||||
</div>
|
||||
<div class="option aligned">
|
||||
<label id="tabSize-label" for="editor.tabSize" i18n-text="cm_tabSize"></label>
|
||||
<input id="editor.tabSize" type="number" min="0">
|
||||
|
|
44
edit/edit.js
44
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));
|
||||
}
|
||||
}
|
||||
|
|
20
js/prefs.js
20
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,10 +141,14 @@ var prefs = new function Prefs() {
|
|||
}
|
||||
}
|
||||
if (hasChanged) {
|
||||
const listener = onChange.specific.get(key);
|
||||
if (listener) {
|
||||
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) {
|
||||
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);
|
||||
|
|
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