Merge pull request #239 from openstyles/colorpicker

Colorpicker in the editor
This commit is contained in:
tophf 2017-11-20 15:21:40 +03:00 committed by GitHub
commit 5d905c2952
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 1815 additions and 7 deletions

View File

@ -1,2 +1,3 @@
vendor/
vendor-overwrites/
vendor-overwrites/*
!vendor-overwrites/colorpicker

View File

@ -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"

View File

@ -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">

View File

@ -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));
}
}

View File

@ -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);

View 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.

View 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);
}

View 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
});

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