diff --git a/.eslintignore b/.eslintignore index a710e413..f1fc323f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,3 @@ vendor/ -vendor-overwrites/ +vendor-overwrites/* +!vendor-overwrites/colorpicker diff --git a/_locales/en/messages.json b/_locales/en/messages.json index fb0236c7..d7f0fa2a 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -127,6 +127,10 @@ "message": "Autocomplete on typing", "description": "Label for the checkbox in the style editor." }, + "cm_colorpicker": { + "message": "Colorpickers for CSS colors", + "description": "Label for the checkbox controlling colorpicker option for the style editor." + }, "cm_indentWithTabs": { "message": "Use tabs with smart indentation", "description": "Label for the checkbox controlling tabs with smart indentation option for the style editor." @@ -171,6 +175,14 @@ "message": "Theme", "description": "Label for the style editor's CSS theme." }, + "colorpickerSwitchFormatTooltip": { + "message": "Switch formats: HEX -> RGB -> HSL", + "description": "Tooltip for the switch button in the color picker popup in the style editor." + }, + "colorpickerTooltip": { + "message": "Open color picker", + "description": "Tooltip for the colored squares shown before CSS colors in the style editor." + }, "dysfunctional": { "message": "Stylus cannot function in private windows because Firefox disallows direct connection to the internal background page context of the extension.", "description": "Displayed in Firefox when its settings make Stylus dysfunctional" diff --git a/edit.html b/edit.html index 6e20b003..34e99fd6 100644 --- a/edit.html +++ b/edit.html @@ -190,6 +190,10 @@ +
+ + +
diff --git a/edit/edit.js b/edit/edit.js index 14c88382..0775ede5 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -7,6 +7,12 @@ /* global closeCurrentTab regExpTester messageBox */ 'use strict'; +onDOMready() + .then(() => Promise.all([ + onColorpickerReady(), + ])) + .then(init); + let styleId = null; // only the actually dirty items here let dirty = {}; @@ -362,6 +368,8 @@ function acmeEventListener(event) { } option = 'highlightSelectionMatches'; break; + case 'colorpicker': + return; } CodeMirror.setOption(option, value); } @@ -1298,8 +1306,6 @@ function beautify(event) { } } -onDOMready().then(init); - function init() { initCodeMirror(); getStyle().then(style => { @@ -2065,3 +2071,37 @@ function setGlobalProgress(done, total) { progressElement.remove(); } } + +function onColorpickerReady() { + const scripts = [ + '/vendor-overwrites/colorpicker/colorpicker.css', + '/vendor-overwrites/colorpicker/colorpicker.js', + '/vendor-overwrites/colorpicker/colorview.js', + ]; + prefs.subscribe(['editor.colorpicker'], colorpickerOnDemand); + return prefs.get('editor.colorpicker') && colorpickerOnDemand(null, true); + + function colorpickerOnDemand(id, enabled) { + return loadScript(enabled && scripts) + .then(() => setColorpickerOption(id, enabled)); + } + + function setColorpickerOption(id, enabled) { + CodeMirror.defaults.colorpicker = enabled && { + forceUpdate: editors.length > 0, + tooltip: t('colorpickerTooltip'), + popupOptions: { + tooltipForSwitcher: t('colorpickerSwitchFormatTooltip'), + hexUppercase: prefs.get('editor.colorpicker.hexUppercase'), + hideDelay: 5000, + embedderCallback: state => { + if (state && state.hexUppercase !== prefs.get('editor.colorpicker.hexUppercase')) { + prefs.set('editor.colorpicker.hexUppercase', state.hexUppercase); + } + }, + }, + }; + // on page load runs before CodeMirror.setOption is defined + editors.forEach(cm => cm.setOption('colorpicker', CodeMirror.defaults.colorpicker)); + } +} diff --git a/js/prefs.js b/js/prefs.js index 20903389..1c93a06c 100644 --- a/js/prefs.js +++ b/js/prefs.js @@ -56,6 +56,11 @@ var prefs = new function Prefs() { 'editor.appliesToLineWidget': true, // show applies-to line widget on the editor + // show CSS colors as clickable colored rectangles + 'editor.colorpicker': true, + // #DEAD or #beef + 'editor.colorpicker.hexUppercase': false, + 'iconset': 0, // 0 = dark-themed icon // 1 = light-themed icon diff --git a/vendor-overwrites/colorpicker/colorpicker.css b/vendor-overwrites/colorpicker/colorpicker.css index f80b45c2..532a6a07 100644 --- a/vendor-overwrites/colorpicker/colorpicker.css +++ b/vendor-overwrites/colorpicker/colorpicker.css @@ -1,57 +1,151 @@ - /* codemirror colorview */ -.codemirror-colorview { - border : 1px solid #8e8e8e; - position: relative; - display : inline-block; - box-sizing : border-box; - margin : 0px 2px; - width : 10px; - height : 10px; - cursor: pointer; - background-image : url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAJElEQVQYV2NctWrVfwYkEBYWxojMZ6SDAmT7QGx0K1EcRBsFAADeG/3M/HteAAAAAElFTkSuQmCC"); - background-repeat: repeat; -} - -.codemirror-colorview .codemirror-colorview-background { - content: ""; - position: absolute; - left:0px; - right:0px; - bottom:0px; - top:0px; -} - -.codemirror-colorview:hover { - border-color: #494949; -} - - -/* codemirror-colorpicker */ - -.codemirror-colorpicker { +.cm-colorview { position: relative; - width: 226px; - z-index: 1000; + white-space: nowrap; } -.codemirror-colorpicker > .color { + +.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; } -.codemirror-colorpicker > .color > .saturation { + +.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; } -.codemirror-colorpicker > .color > .saturation > .value { + +.colorpicker-value { position: relative; width: 100%; height: 100%; + background-image: linear-gradient(to top, #000, rgba(204, 154, 129, 0)); } -.codemirror-colorpicker > .color > .saturation > .value > .drag-pointer { + +.colorpicker-drag-pointer { position: absolute; width: 10px; height: 10px; @@ -60,21 +154,25 @@ border-radius: 50%; left: -5px; top: -5px; + border: 1px solid #fff; + box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.05); } -.codemirror-colorpicker > .control { + +.colorpicker-sliders { position: relative; - padding: 18px 0px 14px 0px; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - -o-user-select: none; - user-select: none; + padding: 10px 0 6px 0; + border-top: 1px solid transparent; } -.codemirror-colorpicker > .control > .color, -.codemirror-colorpicker > .control > .empty { + +.colorpicker-theme-dark .colorpicker-sliders { + border-color: var(--input-border-color); +} + +.colorpicker-swatch, +.colorpicker-empty { position: absolute; left: 11px; - top: 24px; + top: 17px; width: 30px; height: 30px; -webkit-border-radius: 50%; @@ -84,33 +182,41 @@ -moz-box-sizing: border-box; box-sizing: border-box; } -.codemirror-colorpicker > .control > .hue { + +.colorpicker-empty { + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAJElEQVQYV2NctWrVfwYkEBYWxojMZ6SDAmT7QGx0K1EcRBsFAADeG/3M/HteAAAAAElFTkSuQmCC") repeat; +} + +.colorpicker-hue { position: relative; padding: 6px 12px; - margin: 0px 0px 0px 45px; + margin: 0 0 0 45px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; - cursor: pointer; } -.codemirror-colorpicker > .control > .hue > .hue-container { + +.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%); } -.codemirror-colorpicker > .control > .opacity { + +.colorpicker-opacity { position: relative; padding: 3px 12px; - margin: 0px 0px 0px 45px; + margin: 0 0 0 45px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; - cursor: pointer; } -.codemirror-colorpicker > .control > .opacity > .opacity-container { + +.colorpicker-opacity-container { position: relative; width: 100%; height: 10px; @@ -118,10 +224,13 @@ -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; } -.codemirror-colorpicker > .control .drag-bar, -.codemirror-colorpicker > .control .drag-bar2 { +.colorpicker-hue-knob, +.colorpicker-opacity-knob { position: absolute; cursor: pointer; top: 50% !important; @@ -132,179 +241,148 @@ -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; } -.codemirror-colorpicker > .information { + +.colorpicker-input-container { position: relative; -webkit-box-sizing: padding-box; -moz-box-sizing: padding-box; box-sizing: padding-box; } -.codemirror-colorpicker > .information.hex > .information-item.hex { - display: flex; -} - -.codemirror-colorpicker > .information.rgb > .information-item.rgb { - display: flex; -} - -.codemirror-colorpicker > .information.hsl > .information-item.hsl { - display: flex; -} - -.codemirror-colorpicker > .information > .information-item { - display:none; +.colorpicker-input-group { + display: none; position: relative; - padding: 0px 5px; + padding: 0 5px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; - margin-right:40px; + margin-right: calc(var(--switcher-width) - 10px); } -.codemirror-colorpicker > .information > .information-change { - position:absolute; - display:block; - width:40px; - top:0px; - right:0px; - bottom:0px; +.colorpicker-input-group[data-active] { + display: flex; } -.codemirror-colorpicker > .information > .information-change > .format-change-button { - width:100%; - height:100%; - background:transparent; - border:0px; - cursor:pointer; - outline:none; -} - -.codemirror-colorpicker > .information > .information-item > .input-field { - display:block; - flex:1; +.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; } -.codemirror-colorpicker > .information > .information-item > .input-field > input { - text-align: center; - width:100%; - padding:3px 5px; - font-size:11px; - color: #333; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - 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 #cbcbcb; +.colorpicker-input-field[class$="-a"] { + flex-grow: 1.5; } -.codemirror-colorpicker > .information > .information-item > .input-field > .title { - text-align:center; - font-size:12px; - color:#a9a9a9; -} - -.codemirror-colorpicker > .information > input { +.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; - height: 20px; - bottom: 20px; - padding: 0 0 0 2px; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; +} + +.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); } - -.codemirror-colorpicker { - border: 1px solid #ececec; - background-color: #fff; - -webkit-box-shadow: 0 0px 10px 0px rgba(0, 0, 0, 0.12); - -moz-box-shadow: 0 0px 10px 0px rgba(0, 0, 0, 0.12); - box-shadow: 0 0px 10px 0px rgba(0, 0, 0, 0.12); -} -.codemirror-colorpicker > .color > .saturation { - background-color: rgba(204, 154, 129, 0); - background-image: -moz-linear-gradient(left, #FFF, rgba(204, 154, 129, 0)); - background-image: -webkit-gradient(linear, 0 0, 100% 0, from(#FFF), to(rgba(204, 154, 129, 0))); - background-image: -webkit-linear-gradient(left, #FFF, rgba(204, 154, 129, 0)); - background-image: -o-linear-gradient(left, #FFF, rgba(204, 154, 129, 0)); - background-image: linear-gradient(to right, #FFF, rgba(204, 154, 129, 0)); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#00cc9a81', GradientType=1); -} -.codemirror-colorpicker > .color > .saturation > .value { - background-image: -webkit-gradient(linear, 0 100%, 0 0, from(#000000), to(rgba(204, 154, 129, 0))); - background-image: -webkit-linear-gradient(bottom, #000000, rgba(204, 154, 129, 0)); - background-image: -moz-linear-gradient(bottom, #000000, rgba(204, 154, 129, 0)); - background-image: -o-linear-gradient(bottom, #000000, rgba(204, 154, 129, 0)); - background-image: -ms-linear-gradient(bottom, #000000, rgba(204, 154, 129, 0)); - background-image: linear-gradient(to top, #000000, rgba(204, 154, 129, 0)); -} -.codemirror-colorpicker > .color > .saturation > .value > .drag-pointer { - border: 1px solid #fff; - -webkit-box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.05); - -moz-box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.05); - box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.05); -} -.codemirror-colorpicker > .control > .hue > .hue-container { - background: -moz-linear-gradient(left, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%); - background: -ms-linear-gradient(left, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%); - background: -o-linear-gradient(left, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%); - background: -webkit-gradient(linear, left top, right top, from(#ff0000), color-stop(0.17, #ffff00), color-stop(0.33, #00ff00), color-stop(0.5, #00ffff), color-stop(0.67, #0000ff), color-stop(0.83, #ff00ff), to(#ff0000)); - background: -webkit-linear-gradient(left, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%); - background: linear-gradient(to right, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%); -} -.codemirror-colorpicker > .control > .opacity > .opacity-container > .color-bar { - position:absolute; - display:block; - content:""; - left:0px; - right:0px; - bottom:0px; - top:0px; - background: -moz-linear-gradient(left, rgba(232, 232, 232, 0), rgba(232, 232, 232, 1)); - background: -ms-linear-gradient(left, rgba(232, 232, 232, 0), rgba(232, 232, 232, 1)); - background: -o-linear-gradient(left, rgba(232, 232, 232, 0), rgba(232, 232, 232, 1)); - background: -webkit-gradient(linear, left top, right top, from(rgba(232, 232, 232, 0)), to(rgba(232, 232, 232, 1))); - background: -webkit-linear-gradient(left, rgba(232, 232, 232, 0), rgba(232, 232, 232, 1)); - background: linear-gradient(to right, rgba(232, 232, 232, 0) , rgba(232, 232, 232, 1) ); -} -.codemirror-colorpicker > .control > .opacity > .opacity-container { - background-image : url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAJElEQVQYV2NctWrVfwYkEBYWxojMZ6SDAmT7QGx0K1EcRBsFAADeG/3M/HteAAAAAElFTkSuQmCC"); - background-repeat: repeat; +.colorpicker-theme-dark .colorpicker-input::-webkit-inner-spin-button { + -webkit-filter: invert(1); + filter: invert(1); } -.codemirror-colorpicker > .control > .empty { - background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAJElEQVQYV2NctWrVfwYkEBYWxojMZ6SDAmT7QGx0K1EcRBsFAADeG/3M/HteAAAAAElFTkSuQmCC") repeat; +.colorpicker-input:hover { + border-color: var(--input-border-color-hover); } -.codemirror-colorpicker > .control .drag-bar, -.codemirror-colorpicker > .control .drag-bar2 { - border: 1px solid rgba(0, 0, 0, 0.5); - -webkit-box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.1); - -moz-box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.1); - box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.1); - background-color: #FFFFFF; + +.colorpicker-input:focus { + color: var(--input-color-focus); + border-color: var(--input-border-color-focus); + background-color: var(--input-background-color-focus); } -.codemirror-colorpicker > .information { - /*border-top: 1px solid #e8e8e8;*/ + +.colorpicker-theme-dark input:focus { + outline: none !important; } -.codemirror-colorpicker > .information > .title { - color: #a3a3a3; + +.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); } -.codemirror-colorpicker > .information > .input { - color: #333; -} \ No newline at end of file diff --git a/vendor-overwrites/colorpicker/colorpicker.js b/vendor-overwrites/colorpicker/colorpicker.js index 25fc8741..c430d5cc 100644 --- a/vendor-overwrites/colorpicker/colorpicker.js +++ b/vendor-overwrites/colorpicker/colorpicker.js @@ -1,1369 +1,869 @@ -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["codemirror" ], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - - CodeMirror.defineExtension("colorpicker", function () { - - var cm = this; - - var color = { - - trim : function (str) { - return str.replace(/^\s+|\s+$/g, ''); - }, - - /** - * @method format - * - * convert color to format string - * - * // hex - * color.format({ r : 255, g : 255, b : 255 }, 'hex') // #FFFFFF - * - * // rgb - * color.format({ r : 255, g : 255, b : 255 }, 'rgb') // rgba(255, 255, 255, 0.5); - * - * // rgba - * color.format({ r : 255, g : 255, b : 255, a : 0.5 }, 'rgb') // rgba(255, 255, 255, 0.5); - * - * @param {Object} obj obj has r, g, b and a attributes - * @param {"hex"/"rgb"} type format string type - * @returns {*} - */ - format : function(obj, type) { - if (type == 'hex') { - var r = obj.r.toString(16); - if (obj.r < 16) r = "0" + r; - - var g = obj.g.toString(16); - if (obj.g < 16) g = "0" + g; - - var b = obj.b.toString(16); - if (obj.b < 16) b = "0" + b; - - return "#" + [r, g, b].join(""); - } else if (type == 'rgb') { - if (typeof obj.a == 'undefined') { - return "rgb(" + [obj.r, obj.g, obj.b].join(",") + ")"; - } else { - return "rgba(" + [obj.r, obj.g, obj.b, obj.a].join(",") + ")"; - } - } else if (type == 'hsl') { - if (typeof obj.a == 'undefined') { - return "hsl(" + [obj.h, obj.s + '%', obj.l + '%'].join(",") + ")"; - } else { - return "hsla(" + [obj.h, obj.s + '%', obj.l + '%', obj.a].join(",") + ")"; - } - } - - return obj; - }, - - /** - * @method rgb - * - * parse string to rgb color - * - * color.rgb("#FF0000") === { r : 255, g : 0, b : 0 } - * - * color.rgb("rgb(255, 0, 0)") == { r : 255, g : 0, b : } - * - * @param {String} str color string - * @returns {Object} rgb object - */ - parse : function (str) { - if (typeof str == 'string') { - if (str.indexOf("rgb(") > -1) { - var arr = str.replace("rgb(", "").replace(")","").split(","); - - for(var i = 0, len = arr.length; i < len; i++) { - arr[i] = parseInt(color.trim(arr[i]), 10); - } - - return { type : 'rgb', r : arr[0], g : arr[1], b : arr[2], a : 1 }; - } else if (str.indexOf("rgba(") > -1) { - var arr = str.replace("rgba(", "").replace(")", "").split(","); - - for (var i = 0, len = arr.length; i < len; i++) { - - if (len - 1 == i) { - arr[i] = parseFloat(color.trim(arr[i])); - } else { - arr[i] = parseInt(color.trim(arr[i]), 10); - } - } - - return {type: 'rgb', r: arr[0], g: arr[1], b: arr[2], a: arr[3]}; - } else if (str.indexOf("hsl(") > -1) { - var arr = str.replace("hsl(", "").replace(")","").split(","); - - for(var i = 0, len = arr.length; i < len; i++) { - arr[i] = parseInt(color.trim(arr[i]), 10); - } - - var obj = { type : 'hsl', h : arr[0], s : arr[1], l : arr[2], a : 1 }; - - var temp = color.HSLtoRGB(obj.h, obj.s, obj.l); - - obj.r = temp.r; - obj.g = temp.g; - obj.b = temp.b; - - return obj; - } else if (str.indexOf("hsla(") > -1) { - var arr = str.replace("hsla(", "").replace(")", "").split(","); - - for (var i = 0, len = arr.length; i < len; i++) { - - if (len - 1 == i) { - arr[i] = parseFloat(color.trim(arr[i])); - } else { - arr[i] = parseInt(color.trim(arr[i]), 10); - } - } - - var obj = {type: 'hsl', h: arr[0], s: arr[1], l: arr[2], a: arr[3]}; - - var temp = color.HSLtoRGB(obj.h, obj.s, obj.l); - - obj.r = temp.r; - obj.g = temp.g; - obj.b = temp.b; - - return obj; - } else if (str.indexOf("#") == 0) { - - str = str.replace("#", ""); - - var arr = []; - if (str.length == 3) { - for(var i = 0, len = str.length; i < len; i++) { - var char = str.substr(i, 1); - arr.push(parseInt(char+char, 16)); - } - } else { - for(var i = 0, len = str.length; i < len; i+=2) { - arr.push(parseInt(str.substr(i, 2), 16)); - } - } - - return { type : 'hex', r : arr[0], g : arr[1], b : arr[2], a : 1 }; - } - } - - return str; - - }, - - /** - * @method HSVtoRGB - * - * convert hsv to rgb - * - * color.HSVtoRGB(0,0,1) === #FFFFF === { r : 255, g : 0, b : 0 } - * - * @param {Number} H hue color number (min : 0, max : 360) - * @param {Number} S Saturation number (min : 0, max : 1) - * @param {Number} V Value number (min : 0, max : 1 ) - * @returns {Object} - */ - HSVtoRGB : function (H, S, V) { - - if (H == 360) { - H = 0; - } - - var C = S * V; - var X = C * (1 - Math.abs((H/60) % 2 -1) ); - var m = V - C; - - var temp = []; - - if (0 <= H && H < 60) { temp = [C, X, 0]; } - else if (60 <= H && H < 120) { temp = [X, C, 0]; } - else if (120 <= H && H < 180) { temp = [0, C, X]; } - else if (180 <= H && H < 240) { temp = [0, X, C]; } - else if (240 <= H && H < 300) { temp = [X, 0, C]; } - else if (300 <= H && H < 360) { temp = [C, 0, X]; } - - return { - r : Math.ceil((temp[0] + m) * 255), - g : Math.ceil((temp[1] + m) * 255), - b : Math.ceil((temp[2] + m) * 255) - }; - }, - - /** - * @method RGBtoHSV - * - * convert rgb to hsv - * - * color.RGBtoHSV(0, 0, 255) === { h : 240, s : 1, v : 1 } === '#FFFF00' - * - * @param {Number} R red color value - * @param {Number} G green color value - * @param {Number} B blue color value - * @return {Object} hsv color code - */ - RGBtoHSV : function (R, G, B) { - - var R1 = R / 255; - var G1 = G / 255; - var B1 = B / 255; - - var MaxC = Math.max(R1, G1, B1); - var MinC = Math.min(R1, G1, B1); - - var DeltaC = MaxC - MinC; - - var H = 0; - - if (DeltaC == 0) { H = 0; } - else if (MaxC == R1) { - H = 60 * (( (G1 - B1) / DeltaC) % 6); - } else if (MaxC == G1) { - H = 60 * (( (B1 - R1) / DeltaC) + 2); - } else if (MaxC == B1) { - H = 60 * (( (R1 - G1) / DeltaC) + 4); - } - - if (H < 0) { - H = 360 + H; - } - - var S = 0; - - if (MaxC == 0) S = 0; - else S = DeltaC / MaxC; - - var V = MaxC; - - return { h : H, s : S, v : V }; - }, - - RGBtoHSL : function (r, g, b) { - r /= 255, g /= 255, b /= 255; - var max = Math.max(r, g, b), min = Math.min(r, g, b); - var h, s, l = (max + min) / 2; - - if(max == min){ - h = s = 0; // achromatic - }else{ - var d = max - min; - s = l > 0.5 ? d / (2 - max - min) : d / (max + min); - switch(max){ - case r: h = (g - b) / d + (g < b ? 6 : 0); break; - case g: h = (b - r) / d + 2; break; - case b: h = (r - g) / d + 4; break; - } - h /= 6; - } - - return { h : Math.round(h * 360) , s : Math.round(s * 100), l : Math.round(l * 100)}; - }, - - HUEtoRGB : function (p, q, t) { - if(t < 0) t += 1; - if(t > 1) t -= 1; - if(t < 1/6) return p + (q - p) * 6 * t; - if(t < 1/2) return q; - if(t < 2/3) return p + (q - p) * (2/3 - t) * 6; - return p; - }, - - HSLtoRGB : function (h, s, l) { - var r, g, b; - - h /= 360; - s /= 100; - l /= 100; - - if(s == 0){ - r = g = b = l; // achromatic - }else{ - var q = l < 0.5 ? l * (1 + s) : l + s - l * s; - var p = 2 * l - q; - r = this.HUEtoRGB(p, q, h + 1/3); - g = this.HUEtoRGB(p, q, h); - b = this.HUEtoRGB(p, q, h - 1/3); - } - - return { r : r * 255, g : g * 255, b : b * 255 }; - } - }; - - var hue_color = [ - { rgb : '#ff0000', start : .0 }, - { rgb : '#ffff00', start : .17 }, - { rgb : '#00ff00', start : .33 }, - { rgb : '#00ffff', start : .50 }, - { rgb : '#0000ff', start : .67 }, - { rgb : '#ff00ff', start : .83 }, - { rgb : '#ff0000', start : 1 } - ]; - - var $body, $root, $hue, $color, $value, $saturation, $drag_pointer, $drag_bar, - $control, $controlPattern, $controlColor, $hueContainer, $opacity, $opacityContainer, $opacityColorBar, $formatChangeButton, - $opacity_drag_bar, $information, $informationChange; - - var currentA, currentH, currentS, currentV; - var $hexCode; - var $rgb_r, $rgb_g, $rgb_b, $rgb_a; - var $hsl_h, $hsl_s, $hsl_l, $hsl_a; - var cssPrefix = getCssValuePrefix(); - - var colorpickerCallback = function () {}; - var counter = 0; - var cached = {}; - var isColorPickerShow = false; - var isShortCut = false; - var hideDelay = 2000; - - function dom(tag, className, attr) { - - if (typeof tag != 'string') { - this.el = tag; - } else { - - var el = document.createElement(tag); - - this.uniqId = counter++; - - el.className = className; - - attr = attr || {}; - - for(var k in attr) { - el.setAttribute(k, attr[k]); - } - - this.el = el; - } - } - - dom.prototype.closest = function (cls) { - - var temp = this; - var checkCls = false; - - while(!(checkCls = temp.hasClass(cls))) { - if (temp.el.parentNode) { - temp = new dom(temp.el.parentNode); - } else { - return null; - } - } - - if (checkCls) { - return temp; - } - - return null; - } - - dom.prototype.removeClass = function (cls) { - this.el.className = color.trim((" " + this.el.className + " ").replace(' ' + cls + ' ', ' ')); - } - - dom.prototype.hasClass = function (cls) { - if (!this.el.className) - { - return false; - } else { - var newClass = ' ' + this.el.className + ' '; - return newClass.indexOf(' ' + cls + ' ') > -1; - } - } - - dom.prototype.addClass = function (cls) { - if (!this.hasClass(cls)) { - this.el.className = this.el.className + " " + cls; - } - - } - - dom.prototype.html = function (html) { - this.el.innerHTML = html; - - return this; - } - - dom.prototype.empty = function () { - return this.html(''); - } - - dom.prototype.append = function (el) { - - if (typeof el == 'string') { - this.el.appendChild(document.createTextNode(el)); - } else { - this.el.appendChild(el.el || el); - } - - return this; - } - - dom.prototype.appendTo = function (target) { - var t = target.el ? target.el : target; - - t.appendChild(this.el); - - return this; - } - - dom.prototype.remove = function () { - if (this.el.parentNode) { - this.el.parentNode.removeChild(this.el); - } - - return this; - } - - dom.prototype.text = function () { - return this.el.textContent; - } - - dom.prototype.css = function (key, value) { - if (arguments.length == 2) { - this.el.style[key] = value; - } else if (arguments.length == 1) { - - if (typeof key == 'string') { - return getComputedStyle(this.el)[key]; - } else { - var keys = key || {}; - for(var k in keys) { - this.el.style[k] = keys[k]; - } - } - - } - - return this; - } - - dom.prototype.offset = function () { - var rect = this.el.getBoundingClientRect(); - - return { - top: rect.top + document.body.scrollTop, - left: rect.left + document.body.scrollLeft - }; - } - - dom.prototype.position = function () { - return { - top: parseFloat(this.el.style.top), - left: parseFloat(this.el.style.left) - }; - } - - dom.prototype.width = function () { - return this.el.offsetWidth; - } - - dom.prototype.height = function () { - return this.el.offsetHeight; - } - - dom.prototype.dataKey = function (key) { - return this.uniqId + '.' + key; - } - - dom.prototype.data = function (key, value) { - if (arguments.length == 2) { - cached[this.dataKey(key)] = value; - } else if (arguments.length == 1) { - return cached[this.dataKey(key)]; - } else { - var keys = Object.keys(cached); - - var uniqId = this.uniqId + "."; - return keys.filter(function (key) { - if (key.indexOf(uniqId) == 0) { - return true; - } - - return false; - }).map(function (value) { - return cached[value]; - }) - } - - return this; - } - - dom.prototype.val = function (value) { - if (arguments.length == 0) { - return this.el.value; - } else if (arguments.length == 1) { - this.el.value = value; - } - - return this; - } - - dom.prototype.int = function () { - return parseInt(this.val(), 10); - } - - dom.prototype.show = function () { - return this.css('display', 'block'); - } - - dom.prototype.hide = function () { - return this.css('display', 'none'); - } - - function setRGBInput(r, g, b) { - $rgb_r.val(r); - $rgb_g.val(g); - $rgb_b.val(b); - $rgb_a.val(currentA); - } - - function setHSLInput(h, s, l) { - $hsl_h.val(h); - $hsl_s.val(s + '%'); - $hsl_l.val(l + '%'); - $hsl_a.val(currentA); - } - - function getHexFormat() { - return color.format({ - r : $rgb_r.int(), - g : $rgb_g.int(), - b : $rgb_b.int() - }, 'hex'); - } - - function convertRGB() { - return color.HSVtoRGB(currentH, currentS, currentV); - } - - function convertHEX() { - return color.format(convertRGB(), 'hex'); - } - - function convertHSL() { - var rgb = color.HSVtoRGB(currentH, currentS, currentV); - return color.RGBtoHSL(rgb.r, rgb.g, rgb.b); - } - - function getFormattedColor (format) { - format = format || 'hex'; - - if (format == 'rgb') { - var rgb = convertRGB(); - rgb.a = currentA == 1 ? undefined : currentA; - return color.format(rgb, 'rgb'); - } else if (format == 'hsl') { - var hsl = convertHSL(); - hsl.a = currentA == 1 ? undefined : currentA; - return color.format(hsl, 'hsl'); - } else { - var rgb = convertRGB(); - return color.format(rgb, 'hex'); - } - } - - function setControlColor (color) { - $controlColor.css('background-color', color); - } - - function setInputColor() { - - var format = $information.data('format') || 'hex'; - - var rgb = null; - if (format == 'hex') { - $hexCode.val(convertHEX()); - } else if (format == 'rgb') { - var rgb = convertRGB(); - setRGBInput(rgb.r, rgb.g, rgb.b); - } else if (format == 'hsl') { - var hsl = convertHSL(); - setHSLInput(hsl.h, hsl.s, hsl.l); - } - - // set background - setControlColor(getFormattedColor('rgb')); - - var rgb = convertRGB(); - var colorString = color.format(rgb, 'rgb'); - setOpacityColorBar(colorString); - - if (typeof colorpickerCallback == 'function') { - - if (!isNaN(currentA)) { - colorpickerCallback(getFormattedColor(format)); - } - - } - } - - function setMainColor(e) { - e.preventDefault(); - var pos = $root.position(); // position for screen - var w = $color.width(); - var h = $color.height(); - - var x = e.clientX - pos.left; - var y = e.clientY - pos.top; - - if (x < 0) x = 0; - else if (x > w) x = w; - - if (y < 0) y = 0; - else if (y > h) y = h; - - $drag_pointer.css({ - left: (x - 5) + 'px', - top: (y - 5) + 'px' - }); - - $drag_pointer.data('pos', { x: x, y : y}); - - caculateHSV() - setInputColor(); - } - - function scale (startColor, endColor, t) { - var obj = { - r : parseInt(startColor.r + (endColor.r - startColor.r) * t, 10) , - g : parseInt(startColor.g + (endColor.g - startColor.g) * t, 10), - b : parseInt(startColor.b + (endColor.b - startColor.b) * t, 10) - }; - - return color.format(obj, 'hex'); - - } - - function checkHueColor(p) { - var startColor, endColor; - - for(var i = 0; i < hue_color.length;i++) { - if (hue_color[i].start >= p) { - startColor = hue_color[i-1]; - endColor = hue_color[i]; - break; - } - } - - if (startColor && endColor) { - return scale(startColor, endColor, (p - startColor.start)/(endColor.start - startColor.start)); - } - - return hue_color[0].rgb; - } - - function setBackgroundColor (color) { - $color.css("background-color", color); - } - - function setCurrentH (h) { - currentH = h; - } - - function setHueColor(e) { - var min = $hueContainer.offset().left; - var max = min + $hueContainer.width(); - var current = e ? pos(e).clientX : min + (max - min) * (currentH / 360); - - var dist; - if (current < min) { - dist = 0; - } else if (current > max) { - dist = 100; - } else { - dist = (current - min) / (max - min) * 100; - } - - var x = ($hueContainer.width() * (dist/100)); - - $drag_bar.css({ - left: (x -Math.ceil($drag_bar.width()/2)) + 'px' - }); - - $drag_bar.data('pos', { x : x}); - - var hueColor = checkHueColor(dist/100); - - setBackgroundColor(hueColor); - setCurrentH((dist/100) * 360); - setInputColor(); - } - - function getCssValuePrefix() - { - var rtrnVal = '';//default to standard syntax - var prefixes = ['', '-o-', '-ms-', '-moz-', '-webkit-']; - - // Create a temporary DOM object for testing - var dom = document.createElement('div'); - - for (var i = 0; i < prefixes.length; i++) - { - // Attempt to set the style - dom.style.background = prefixes[i] + 'linear-gradient(#000000, #ffffff)'; - - // Detect if the style was successfully set - if (dom.style.background) - { - rtrnVal = prefixes[i]; - } - } - - dom = null; - delete dom; - - return rtrnVal; - } - - function setOpacityColorBar(hueColor) { - var rgb = color.parse(hueColor); - - rgb.a = 0; - var start = color.format(rgb, 'rgb'); - - rgb.a = 1; - var end = color.format(rgb, 'rgb'); - - var prefix = cssPrefix; - $opacityColorBar.css('background', 'linear-gradient(to right, ' + start + ', ' + end + ')'); - } - - function setOpacity(e) { - var min = $opacityContainer.offset().left; - var max = min + $opacityContainer.width(); - var current = pos(e).clientX; - var dist; - - if (current < min) { - dist = 0; - } else if (current > max) { - dist = 100; - } else { - dist = (current - min) / (max - min) * 100; - } - - var x = ($opacityContainer.width() * (dist/100)); - - $opacity_drag_bar.css({ - left: (x -Math.ceil($opacity_drag_bar.width()/2)) + 'px' - }); - - $opacity_drag_bar.data('pos', { x : x }); - - caculateOpacity(); - currentFormat(); - setInputColor(); - } - - function caculateOpacity() { - var opacityPos = $opacity_drag_bar.data('pos') || { x : 0 }; - var a = Math.round((opacityPos.x / $opacityContainer.width()) * 100) / 100; - - currentA = isNaN(a) ? 1 : a; - } - - function caculateHSV() { - var pos = $drag_pointer.data('pos') || { x : 0, y : 0 }; - var huePos = $drag_bar.data('pos') || { x : 0 }; - - var width = $color.width(); - var height = $color.height(); - - var h = (huePos.x / $hueContainer.width()) * 360; - var s = (pos.x / width); - var v = ((height - pos.y) / height); - - if (width == 0) { - h = 0; - s = 0; - v = 0; - } - - currentH = h; - currentS = s; - currentV = v; - } - - function pos(e) { - if (e.touches && e.touches[0]) { - return e.touches[0]; - } - - return e; - } - - function checkNumberKey(e) { - var code = e.which, - isExcept = false; - - if(code == 37 || code == 39 || code == 8 || code == 46 || code == 9) - isExcept = true; - - if(!isExcept && (code < 48 || code > 57)) - return false; - - return true; - } - - function setRGBtoHexColor(e) { - var r = $rgb_r.val(), - g = $rgb_g.val(), - b = $rgb_b.val(); - - if(r == "" || g == "" || b == "") return; - - if(parseInt(r) > 255) $rgb_r.val(255); - else $rgb_r.val(parseInt(r)); - - if(parseInt(g) > 255) $rgb_g.val(255); - else $rgb_g.val(parseInt(g)); - - if(parseInt(b) > 255) $rgb_b.val(255); - else $rgb_b.val(parseInt(b)); - - initColor(getHexFormat()); - } - - function setColorUI() { - var x = $color.width() * currentS, y = $color.height() * ( 1 - currentV ); - - $drag_pointer.css({ - left : (x - 5) + "px", - top : (y - 5) + "px" - }); - - $drag_pointer.data('pos', { x : x, y : y }); - - var hueX = $hueContainer.width() * (currentH / 360); - - $drag_bar.css({ - left : (hueX - 7.5) + 'px' - }); - - $drag_bar.data('pos', { x : hueX }); - - var opacityX = $opacityContainer.width() * (currentA || 0); - - $opacity_drag_bar.css({ - left : (opacityX - 7.5) + 'px' - }); - - $opacity_drag_bar.data('pos', { x : opacityX }); - } - - function setCurrentHSV (h, s, v, a) { - currentA = a; - currentH = h; - currentS = s; - currentV = v; - } - - function setCurrentFormat (format) { - $information.data('format', format); - initFormat(); - } - - - - function initColor(newColor) { - var c = newColor || "#FF0000", colorObj = color.parse(c); - - setCurrentFormat(colorObj.type); - setBackgroundColor(c); - - var hsv = color.RGBtoHSV(colorObj.r, colorObj.g, colorObj.b); - - setCurrentHSV(hsv.h, hsv.s, hsv.v, colorObj.a); - setColorUI(); - setHueColor(); - setInputColor(); - } - - function addEvent (dom, eventName, callback) { - dom.addEventListener(eventName, callback); - } - - function removeEvent(dom, eventName, callback) { - dom.removeEventListener(eventName, callback); - } - - function EventColorMouseDown(e) { - $color.data('isDown', true); - setMainColor(e); - } - - function EventColorMouseUp(e) { - $color.data('isDown', false); - } - - function EventDragBarMouseDown (e) { - e.preventDefault(); - $hue.data('isDown', true); - } - - function EventOpacityDragBarMouseDown(e) { - e.preventDefault(); - $opacity.data('isDown', true); - } - - function EventHueMouseDown (e) { - $hue.data('isDown', true); - setHueColor(e); - } - - function EventOpacityMouseDown (e) { - $opacity.data('isDown', true); - setOpacity(e); - } - - function EventHexCodeKeyDown(e) { - if(e.which < 65 || e.which > 70) { - return checkNumberKey(e); - } - } - - function EventHexCodeKeyUp (e) { - var code = $hexCode.val(); - - if(code.charAt(0) == '#' && code.length == 7) { - initColor(code); - } - } - - function EventFormatChangeClick(e) { - nextFormat(); - } - - function initEvent() { - addEvent($color.el, 'mousedown', EventColorMouseDown); - addEvent($color.el, 'mouseup', EventColorMouseUp); - addEvent($drag_bar.el, 'mousedown', EventDragBarMouseDown); - addEvent($opacity_drag_bar.el, 'mousedown', EventOpacityDragBarMouseDown); - addEvent($hueContainer.el, 'mousedown', EventHueMouseDown); - addEvent($opacityContainer.el, 'mousedown', EventOpacityMouseDown); - addEvent($hexCode.el, 'keydown', EventHexCodeKeyDown); - addEvent($hexCode.el, 'keyup', EventHexCodeKeyUp); - - addEvent($rgb_r.el, 'keydown', checkNumberKey); - addEvent($rgb_r.el, 'keyup', setRGBtoHexColor); - addEvent($rgb_g.el, 'keydown', checkNumberKey); - addEvent($rgb_g.el, 'keyup', setRGBtoHexColor); - addEvent($rgb_b.el, 'keydown', checkNumberKey); - addEvent($rgb_b.el, 'keyup', setRGBtoHexColor); - - addEvent(document, 'mouseup', EventDocumentMouseUp); - addEvent(document, 'mousemove', EventDocumentMouseMove); - - addEvent($formatChangeButton.el, 'click', EventFormatChangeClick) - } - - function checkColorPickerClass(el) { - var hasColorView = new dom(el).closest('codemirror-colorview'); - var hasColorPicker = new dom(el).closest('codemirror-colorpicker'); - var hasCodeMirror = new dom(el).closest('CodeMirror'); - var IsInHtml = el.nodeName == 'HTML'; - - return !!(hasColorPicker || hasColorView || hasCodeMirror); - } - - function checkInHtml (el) { - var IsInHtml = el.nodeName == 'HTML'; - - return IsInHtml; - } - - function EventDocumentMouseUp (e) { - $color.data('isDown', false); - $hue.data('isDown', false); - $opacity.data('isDown', false); - - // when color picker clicked in outside - if (checkInHtml(e.target)) { - //setHideDelay(hideDelay); - } else if (checkColorPickerClass(e.target) == false ) { - hide(); - } - - } - - function EventDocumentMouseMove(e) { - if ($color.data('isDown')) { - setMainColor(e); - } - - if ($hue.data('isDown')) { - setHueColor(e); - } - - if ($opacity.data('isDown')) { - setOpacity(e); - } - } - - function destroy() { - removeEvent($color.el, 'mousedown', EventColorMouseDown); - removeEvent($color.el, 'mouseup', EventColorMouseUp); - removeEvent($drag_bar.el, 'mousedown', EventDragBarMouseDown); - removeEvent($opacity_drag_bar.el, 'mousedown', EventOpacityDragBarMouseDown); - removeEvent($hueContainer.el, 'mousedown', EventHueMouseDown); - removeEvent($opacityContainer.el, 'mousedown', EventOpacityMouseDown); - removeEvent($hexCode.el, 'keydown', EventHexCodeKeyDown); - removeEvent($hexCode.el, 'keyup', EventHexCodeKeyUp); - removeEvent($rgb_r.el, 'keydown', checkNumberKey); - removeEvent($rgb_r.el, 'keyup', setRGBtoHexColor); - removeEvent($rgb_g.el, 'keydown', checkNumberKey); - removeEvent($rgb_g.el, 'keyup', setRGBtoHexColor); - removeEvent($rgb_b.el, 'keydown', checkNumberKey); - removeEvent($rgb_b.el, 'keyup', setRGBtoHexColor); - removeEvent(document, 'mouseup', EventDocumentMouseUp); - removeEvent(document, 'mousemove', EventDocumentMouseMove); - removeEvent($formatChangeButton.el, 'click', EventFormatChangeClick); - - // remove color picker callback - colorpickerCallback = undefined; - } - - function currentFormat () { - var current_format = $information.data('format') || 'hex'; - if (currentA < 1 && current_format == 'hex' ) { - var next_format = 'rgb'; - $information.removeClass(current_format); - $information.addClass(next_format); - $information.data('format', next_format); - - setInputColor(); - } - } - - function initFormat () { - var current_format = $information.data('format') || 'hex'; - - $information.removeClass('hex'); - $information.removeClass('rgb'); - $information.removeClass('hsl'); - $information.addClass(current_format); - } - - function nextFormat() { - var current_format = $information.data('format') || 'hex'; - - var next_format = 'hex'; - if (current_format == 'hex') { - next_format = 'rgb'; - } else if (current_format == 'rgb') { - next_format = 'hsl'; - } else if (current_format == 'hsl') { - if (currentA == 1) { - next_format = 'hex'; - } else { - next_format = 'rgb'; - } - } - - $information.removeClass(current_format); - $information.addClass(next_format); - $information.data('format', next_format); - - setInputColor(); - } - - function makeInputField(type) { - var item = new dom('div', 'information-item '+ type); - - if (type == 'hex') { - var field = new dom('div', 'input-field hex'); - - $hexCode = new dom('input', 'input', { type : 'text' }); - - field.append($hexCode); - field.append(new dom('div', 'title').html('HEX')); - - item.append(field); - - } else if (type == 'rgb') { - var field = new dom('div', 'input-field rgb-r'); - $rgb_r = new dom('input', 'input', { type : 'text' }); - - field.append($rgb_r); - field.append(new dom('div', 'title').html('R')); - - item.append(field); - - field = new dom('div', 'input-field rgb-g'); - $rgb_g = new dom('input', 'input', { type : 'text' }); - - field.append($rgb_g); - field.append(new dom('div', 'title').html('G')); - - item.append(field); - - field = new dom('div', 'input-field rgb-b'); - $rgb_b = new dom('input', 'input', { type : 'text' }); - - field.append($rgb_b); - field.append(new dom('div', 'title').html('B')); - - item.append(field); - - // rgba - field = new dom('div', 'input-field rgb-a'); - $rgb_a = new dom('input', 'input', { type : 'text' }); - - field.append($rgb_a); - field.append(new dom('div', 'title').html('A')); - - item.append(field); - - } else if (type == 'hsl') { - var field = new dom('div', 'input-field hsl-h'); - $hsl_h = new dom('input', 'input', { type : 'text' }); - - field.append($hsl_h); - field.append(new dom('div', 'title').html('H')); - - item.append(field); - - field = new dom('div', 'input-field hsl-s'); - $hsl_s = new dom('input', 'input', { type : 'text' }); - - field.append($hsl_s); - field.append(new dom('div', 'title').html('S')); - - item.append(field); - - field = new dom('div', 'input-field hsl-l'); - $hsl_l = new dom('input', 'input', { type : 'text' }); - - field.append($hsl_l); - field.append(new dom('div', 'title').html('L')); - - item.append(field); - - // rgba - field = new dom('div', 'input-field hsl-a'); - $hsl_a = new dom('input', 'input', { type : 'text' }); - - field.append($hsl_a); - field.append(new dom('div', 'title').html('A')); - - item.append(field); - } - - return item; - } - - function init() { - $body = new dom(document.body); - - $root = new dom('div', 'codemirror-colorpicker'); - $color = new dom('div', 'color'); - $drag_pointer = new dom('div', 'drag-pointer' ); - $value = new dom( 'div', 'value' ); - $saturation = new dom('div', 'saturation' ); - - $control = new dom('div', 'control' ); - $controlPattern = new dom('div', 'empty' ); - $controlColor = new dom('div', 'color' ); - $hue = new dom('div', 'hue' ); - $hueContainer = new dom('div', 'hue-container' ); - $drag_bar = new dom('div', 'drag-bar' ); - $opacity = new dom('div', 'opacity' ); - $opacityContainer = new dom('div', 'opacity-container' ); - $opacityColorBar = new dom('div', 'color-bar' ); - - $opacity_drag_bar = new dom('div', 'drag-bar2' ); - - $information = new dom('div', 'information hex' ); - - $informationChange = new dom('div', 'information-change'); - - $formatChangeButton = new dom('button', 'format-change-button', { type : 'button'}).html('↔'); - $informationChange.append($formatChangeButton); - - - $information.append(makeInputField('hex')); - $information.append(makeInputField('rgb')); - $information.append(makeInputField('hsl')); - $information.append($informationChange); - - - $value.append($drag_pointer); - $saturation.append($value); - $color.append($saturation); - - $hueContainer.append($drag_bar); - $hue.append($hueContainer); - - $opacityContainer.append($opacityColorBar); - $opacityContainer.append($opacity_drag_bar); - $opacity.append($opacityContainer); - - $control.append($hue); - $control.append($opacity); - $control.append($controlPattern); - $control.append($controlColor); - - $root.append($color); - $root.append($control); - $root.append($information); - - initHueColors(); - //initEvent(); - initColor(); - }; - - function initHueColors () { - for(var i = 0, len = hue_color.length; i < len; i++) { - var hue = hue_color[i]; - - var obj = color.parse(hue.rgb); - - hue.r = obj.r; - hue.g = obj.g; - hue.b = obj.b; - } - } - - /** - * public methods - */ - function setColor(value) { - if(typeof(value) == "object") { - if(!value.r || !value.g || !value.b) - return; - - initColor(color.format(value, "hex")); - } else if(typeof(value) == "string") { - if(value.charAt(0) != "#") - return; - - initColor(value); - } - } - - function getColor(type) { - caculateHSV(); - var rgb = convertRGB(); - - if (type) { - return color.format(rgb, type); - } - - return rgb; - } - - function definePostion (opt) { - - var width = $root.width(); - var height = $root.height(); - - // set left position for color picker - var elementScreenLeft = opt.left - $body.el.scrollLeft ; - if (width + elementScreenLeft > window.innerWidth) { - elementScreenLeft -= (width + elementScreenLeft) - window.innerWidth; - } - if (elementScreenLeft < 0) { elementScreenLeft = 0; } - - // set top position for color picker - var elementScreenTop = opt.top - $body.el.scrollTop ; - if (height + elementScreenTop > window.innerHeight) { - elementScreenTop -= (height + elementScreenTop) - window.innerHeight; - } - if (elementScreenTop < 0) { elementScreenTop = 0; } - - // set position - $root.css({ - left : elementScreenLeft + 'px', - top : elementScreenTop + 'px' - }); - } - - function show (opt, color, callback) { - destroy(); - initEvent(); - $root.appendTo(document.body); - - $root.css({ - position: 'fixed', // color picker has fixed position - left : '-10000px', - top : '-10000px' - }); - - $root.show(); - - definePostion(opt); - - isColorPickerShow = true; - - isShortCut = opt.isShortCut || false; - - initColor(color); - - // define colorpicker callback - colorpickerCallback = function (colorString) { - callback(colorString); - } - - // define hide delay - hideDelay = opt.hideDelay || 2000; - if (hideDelay > 0) { - setHideDelay(hideDelay); - } - - } - - - var timerCloseColorPicker; - function setHideDelay (delayTime) { - delayTime = delayTime || 0; - removeEvent($root.el, 'mouseenter'); - removeEvent($root.el, 'mouseleave'); - - addEvent($root.el, 'mouseenter', function () { - clearTimeout(timerCloseColorPicker); - }); - - addEvent($root.el, 'mouseleave', function () { - clearTimeout(timerCloseColorPicker); - timerCloseColorPicker = setTimeout(hide, delayTime); - }); - - clearTimeout(timerCloseColorPicker); - timerCloseColorPicker = setTimeout(hide, delayTime); - } - - function hide () { - if (isColorPickerShow) { - destroy(); - $root.hide(); - $root.remove(); - isColorPickerShow = false; - } - - } - +/* 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); + } - return { - isShortCut : function () { - return isShortCut; - }, - $root: $root, - show: show, - hide: hide, - setColor: setColor, - getColor: getColor - } - }) + function getColor(type) { + if (!initialized) { + return; + } + readCurrentColorFromRamps(); + const color = type === 'hsl' ? HSVtoHSL(HSV) : HSVtoRGB(HSV); + return type ? colorToString(color, type) : color; + } + //endregion + //region DOM-to-state + + function readCurrentColorFromRamps() { + if ($sat.offsetWidth === 0) { + HSV.h = HSV.s = HSV.v = 0; + } else { + const {x, y} = dragging.saturationPointerPos; + HSV.h = snapToInt((dragging.hueKnobPos / $hue.offsetWidth) * 360); + HSV.s = x / $sat.offsetWidth; + HSV.v = ($sat.offsetHeight - y) / $sat.offsetHeight; + } + } + + function setFromSaturationElement(event) { + event.preventDefault(); + const w = $sat.offsetWidth; + const h = $sat.offsetHeight; + const deltaX = event.clientX - parseFloat($root.style.left); + const deltaY = event.clientY - parseFloat($root.style.top); + const x = dragging.saturationPointerPos.x = constrain(0, w, deltaX); + const y = dragging.saturationPointerPos.y = constrain(0, h, deltaY); + + $satPointer.style.left = `${x - 5}px`; + $satPointer.style.top = `${y - 5}px`; + + readCurrentColorFromRamps(); + renderInputs(); + } + + function setFromHueElement(event) { + const {left, width} = getScreenBounds($hue); + const currentX = event ? getTouchPosition(event).clientX : left + width * (HSV.h / 360); + const normalizedH = constrain(0, 1, (currentX - left) / width); + const x = dragging.hueKnobPos = width * normalizedH; + $hueKnob.style.left = (x - Math.round($hueKnob.offsetWidth / 2)) + 'px'; + $sat.style.backgroundColor = hueDistanceToColorString(normalizedH); + HSV.h = event ? Math.round(normalizedH * 360) : HSV.h; + renderInputs(); + } + + function setFromOpacityElement(event) { + const {left, width} = getScreenBounds($opacity); + const normalized = constrain(0, 1, (getTouchPosition(event).clientX - left) / width); + const x = width * normalized; + $opacityKnob.style.left = (x - Math.ceil($opacityKnob.offsetWidth / 2)) + 'px'; + HSV.a = Math.round(normalized * 100) / 100; + renderInputs(); + } + + function setFromFormatElement() { + userActivity = performance.now(); + const nextFormat = {hex: 'rgb', rgb: 'hsl', hsl: 'hex'}[currentFormat]; + HSV.a = isNaN(HSV.a) ? 1 : HSV.a; + switchInputGroup(nextFormat); + renderInputs(); + } + + function setFromHexLettercaseElement() { + const isUpper = Boolean(options.hexUppercase); + $hexLettercase[isUpper].dataset.active = ''; + delete $hexLettercase[!isUpper].dataset.active; + const value = $hexCode.value; + $hexCode.value = isUpper ? value.toUpperCase() : value.toLowerCase(); + setFromInputs(); + } + + function setFromInputs() { + userActivity = performance.now(); + if ($inputs[currentFormat].every(validateInput)) { + setFromColor($inputs.color); + } + } + + function validateInput(el) { + const isAlpha = el.type === 'text'; + let isValid = (isAlpha || el.value.trim()) && el.checkValidity(); + if (!isAlpha && !isValid && currentFormat === 'rgb') { + isValid = parseAs(el, parseInt); + } else if (isAlpha && !isValid) { + isValid = parseAs(el, parseFloat); + } + if (isAlpha && isValid) { + isValid = lastOutputColor !== colorToString($inputs.color); + } + return isValid; + } + //endregion + //region State-to-DOM + + function setFromColor(color = '#FF0000') { + color = typeof color === 'string' ? stringToColor(color) : color; + const newHSV = color.type === 'hsl' ? HSLtoHSV(color) : RGBtoHSV(color); + if (Object.keys(newHSV).every(k => Math.abs(newHSV[k] - HSV[k]) < 1e-3)) { + return; + } + HSV = newHSV; + renderKnobs(color); + switchInputGroup(color.type); + setFromHueElement(); + } + + function switchInputGroup(format) { + if (currentFormat === format) { + return; + } + if (currentFormat) { + delete $inputGroups[currentFormat].dataset.active; + } else { + for (const format in $inputGroups) { + delete $inputGroups[format].dataset.active; + } + } + $inputGroups[format].dataset.active = ''; + currentFormat = format; + } + + function renderKnobs(color) { + const x = $sat.offsetWidth * HSV.s; + const y = $sat.offsetHeight * (1 - HSV.v); + $satPointer.style.left = (x - 5) + 'px'; + $satPointer.style.top = (y - 5) + 'px'; + dragging.saturationPointerPos = {x, y}; + + const hueX = $hue.offsetWidth * (HSV.h / 360); + $hueKnob.style.left = (hueX - 7.5) + 'px'; + dragging.hueKnobPos = hueX; + + const opacityX = $opacity.offsetWidth * (isNaN(HSV.a) ? 1 : HSV.a); + $opacityKnob.style.left = (opacityX - 7.5) + 'px'; + + $sat.style.backgroundColor = color; + } + + function renderInputs() { + const rgb = HSVtoRGB(HSV); + switch (currentFormat) { + case 'hex': + $hexCode.value = colorToString(rgb, 'hex'); + break; + case 'rgb': { + $rgb.r.value = rgb.r; + $rgb.g.value = rgb.g; + $rgb.b.value = rgb.b; + $rgb.a.value = alphaToString() || 1; + break; + } + case 'hsl': { + const {h, s, l} = HSVtoHSL(HSV); + $hsl.h.value = h; + $hsl.s.value = s; + $hsl.l.value = l; + $hsl.a.value = alphaToString() || 1; + } + } + $swatch.style.backgroundColor = colorToString(rgb, 'rgb'); + $opacityBar.style.background = 'linear-gradient(to right,' + + colorToString(Object.assign(rgb, {a: 0}), 'rgb') + ',' + + colorToString(Object.assign(rgb, {a: 1}), 'rgb') + ')'; + colorpickerCallback(); + } + + //endregion + //region Event listeners + + function onHexLettercaseClicked() { + options.hexUppercase = !options.hexUppercase; + setFromHexLettercaseElement(); + } + + function onSaturationMouseDown(event) { + if (event.button === 0) { + setFromSaturationElement(event); + dragging.saturation = true; + captureMouse(); + } + } + + function onSaturationMouseUp() { + if (event.button === 0) { + dragging.saturation = false; + releaseMouse(); + } + } + + function onHueKnobMouseDown(event) { + if (event.button === 0) { + dragging.hue = true; + captureMouse(); + } + } + + function onOpacityKnobMouseDown() { + if (event.button === 0) { + dragging.opacity = true; + captureMouse(); + } + } + + function onHueMouseDown(event) { + if (event.button === 0) { + dragging.hue = true; + setFromHueElement(event); + captureMouse(); + } + } + + function onOpacityMouseDown(event) { + if (event.button === 0) { + dragging.opacity = true; + setFromOpacityElement(event); + captureMouse(); + } + } + + function onMouseUp(event) { + if (event.button === 0) { + releaseMouse(); + dragging.saturation = dragging.hue = dragging.opacity = false; + if (!event.target.closest('.codemirror-colorview, .colorpicker-popup, .CodeMirror')) { + hide(); + } + } + } + + function onMouseMove(event) { + if (event.button !== 0) { + return; + } + if (dragging.saturation) { + setFromSaturationElement(event); + } else if (dragging.hue) { + setFromHueElement(event); + } else if (dragging.opacity) { + setFromOpacityElement(event); + } + } + + function stopSnoozing() { + clearTimeout(timerCloseColorPicker); + clearTimeout(timerFadeColorPicker); + if ($root.dataset.fading) { + delete $root.dataset.fading; + } + } + + function snooze() { + clearTimeout(timerFadeColorPicker); + timerFadeColorPicker = setTimeout(fade, options.hideDelay / 2); + } + + function onKeyDown(e) { + if (!e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) { + switch (e.which) { + case 13: + colorpickerCallback(); + // fallthrough to 27 + case 27: + e.preventDefault(); + e.stopPropagation(); + hide(); + break; + } + } + } + + function onCloseRequest(event) { + if (event.detail !== PUBLIC_API) { + hide(); + } + } + + //endregion + //region Event utilities + + function colorpickerCallback(colorString = currentColorToString()) { + if ( + userActivity && + $inputs[currentFormat].every(el => el.checkValidity()) && + typeof options.callback === 'function' + ) { + lastOutputColor = colorString.replace(/\b0\./g, '.'); + options.callback(lastOutputColor); + } + } + + function captureMouse() { + document.addEventListener('mouseup', onMouseUp); + document.addEventListener('mousemove', onMouseMove); + userActivity = performance.now(); + } + + function releaseMouse() { + document.removeEventListener('mouseup', onMouseUp); + document.removeEventListener('mousemove', onMouseMove); + userActivity = performance.now(); + } + + function getTouchPosition(event) { + return event.touches && event.touches[0] || event; + } + + function registerEvents() { + window.addEventListener('keydown', onKeyDown, true); + window.addEventListener('close-colorpicker-popup', onCloseRequest, true); + $root.addEventListener('mouseleave', snooze); + $root.addEventListener('mouseenter', stopSnoozing); + $root.addEventListener('input', setFromInputs); + $formatChangeButton.addEventListener('click', setFromFormatElement); + $sat.addEventListener('mousedown', onSaturationMouseDown); + $sat.addEventListener('mouseup', onSaturationMouseUp); + $hueKnob.addEventListener('mousedown', onHueKnobMouseDown); + $opacityKnob.addEventListener('mousedown', onOpacityKnobMouseDown); + $hue.addEventListener('mousedown', onHueMouseDown); + $opacity.addEventListener('mousedown', onOpacityMouseDown); + $hexLettercase.true.addEventListener('click', onHexLettercaseClicked); + $hexLettercase.false.addEventListener('click', onHexLettercaseClicked); + + stopSnoozing(); + timerFadeColorPicker = setTimeout(fade, options.hideDelay / 2); + } + + function unregisterEvents() { + window.removeEventListener('keydown', onKeyDown, true); + window.removeEventListener('close-colorpicker-popup', hide, true); + $root.removeEventListener('mouseleave', snooze); + $root.removeEventListener('mouseenter', stopSnoozing); + $root.removeEventListener('input', setFromInputs); + $formatChangeButton.removeEventListener('click', setFromFormatElement); + $sat.removeEventListener('mousedown', onSaturationMouseDown); + $sat.removeEventListener('mouseup', onSaturationMouseUp); + $hueKnob.removeEventListener('mousedown', onHueKnobMouseDown); + $opacityKnob.removeEventListener('mousedown', onOpacityKnobMouseDown); + $hue.removeEventListener('mousedown', onHueMouseDown); + $opacity.removeEventListener('mousedown', onOpacityMouseDown); + $hexLettercase.true.removeEventListener('click', onHexLettercaseClicked); + $hexLettercase.false.removeEventListener('click', onHexLettercaseClicked); + releaseMouse(); + stopSnoozing(); + } + + //endregion + //region Color conversion utilities + + function colorToString({r, g, b, h, s, l, a}, type = currentFormat) { + a = alphaToString(a); + const hasA = Boolean(a); + switch (type) { + case 'hex': { + const rgbStr = (0x1000000 + (r << 16) + (g << 8) + (b | 0)).toString(16).slice(1); + const aStr = hasA ? (0x100 + Math.round(a * 255)).toString(16).slice(1) : ''; + const hexStr = `#${rgbStr + aStr}`.replace(/^#(.)\1(.)\2(.)\3(?:(.)\4)?$/, '#$1$2$3$4'); + return options.hexUppercase ? hexStr.toUpperCase() : hexStr.toLowerCase(); + } + case 'rgb': + return hasA ? + `rgba(${r}, ${g}, ${b}, ${a})` : + `rgb(${r}, ${g}, ${b})`; + case 'hsl': + return hasA ? + `hsla(${h}, ${s}%, ${l}%, ${a})` : + `hsl(${h}, ${s}%, ${l}%)`; + } + } + + function stringToColor(str) { + if (typeof str !== 'string') { + return; + } + str = str.trim(); + if (str.startsWith('rgb')) { + const [r, g, b, a = 1] = str.replace(/rgba?\(|\)/g, '').split(',').map(parseFloat); + return {type: 'rgb', r, g, b, a}; + } + if (str.startsWith('hsl')) { + const [h, s, l, a = 1] = str.replace(/hsla?\(|\)/g, '').split(',').map(parseFloat); + return {type: 'hsl', h, s, l, a}; + } + if (str.startsWith('#')) { + str = str.slice(1); + const [r, g, b, a = 255] = str.length <= 4 ? + str.match(/(.)/g).map(c => parseInt(c + c, 16)) : + str.match(/(..)/g).map(c => parseInt(c, 16)); + return {type: 'hex', r, g, b, a: a === 255 ? undefined : a / 255}; + } + return; + } + + function RGBtoHSV({r, g, b, a}) { + r /= 255; + g /= 255; + b /= 255; + const MaxC = Math.max(r, g, b); + const MinC = Math.min(r, g, b); + const DeltaC = MaxC - MinC; + + let h = + DeltaC === 0 ? 0 : + MaxC === r ? 60 * (((g - b) / DeltaC) % 6) : + MaxC === g ? 60 * (((b - r) / DeltaC) + 2) : + MaxC === b ? 60 * (((r - g) / DeltaC) + 4) : + 0; + h = + h < 0 ? h % 360 + 360 : + h > 360 ? h % 360 : + h; + return { + h, + s: MaxC === 0 ? 0 : DeltaC / MaxC, + v: MaxC, + a, + }; + } + + function HSVtoRGB({h, s, v}) { + if (h === 360) { + h = 0; + } + const C = s * v; + const X = C * (1 - Math.abs((h / 60) % 2 - 1)); + const m = v - C; + const [r, g, b] = + h >= 0 && h < 60 ? [C, X, 0] : + h >= 60 && h < 120 ? [X, C, 0] : + h >= 120 && h < 180 ? [0, C, X] : + h >= 180 && h < 240 ? [0, X, C] : + h >= 240 && h < 300 ? [X, 0, C] : + h >= 300 && h < 360 ? [C, 0, X] : []; + return { + r: snapToInt(Math.round((r + m) * 255)), + g: snapToInt(Math.round((g + m) * 255)), + b: snapToInt(Math.round((b + m) * 255)), + }; + } + + function HSLtoHSV({h, s, l, a}) { + const t = s * (l < 50 ? l : 100 - l) / 100; + return { + h, + s: t + l ? 200 * t / (t + l) / 100 : 0, + v: (t + l) / 100, + a, + }; + } + + function HSVtoHSL({h, s, v}) { + const l = (2 - s) * v / 2; + const t = l < .5 ? l * 2 : 2 - l * 2; + return { + h: Math.round(h), + s: Math.round(t ? s * v / t * 100 : 0), + l: Math.round(l * 100), + }; + } + + function currentColorToString(format = currentFormat, alpha = HSV.a) { + const converted = format === 'hsl' ? HSVtoHSL(HSV) : HSVtoRGB(HSV); + converted.a = isNaN(alpha) || alpha === 1 ? undefined : alpha; + return colorToString(converted, format); + } + + function mixColorToString(start, end, amount) { + const obj = { + r: start.r + (end.r - start.r) * amount, + g: start.g + (end.g - start.g) * amount, + b: start.b + (end.b - start.b) * amount, + a: 1, + }; + return colorToString(obj, 'hex'); + } + + function hueDistanceToColorString(hueRatio) { + let prevColor; + for (const color of HUE_COLORS) { + if (prevColor && color.start >= hueRatio) { + return mixColorToString(prevColor, color, + (hueRatio - prevColor.start) / (color.start - prevColor.start)); + } + prevColor = color; + } + return HUE_COLORS[0].hex; + } + + function alphaToString(a = HSV.a) { + return isNaN(a) ? '' : + a.toString().slice(0, 8) + .replace(/(\.[^0]*)0+$/, '$1') + .replace(/^1$/, ''); + } + + //endregion + //region Miscellaneous utilities + + function reposition() { + const width = $root.offsetWidth; + const height = $root.offsetHeight; + + // set left position for color picker + let elementScreenLeft = options.left - document.scrollingElement.scrollLeft; + const bodyWidth = document.scrollingElement.scrollWidth; + if (width + elementScreenLeft > bodyWidth) { + elementScreenLeft -= (width + elementScreenLeft) - bodyWidth; + } + if (elementScreenLeft < 0) { + elementScreenLeft = 0; + } + + // set top position for color picker + let elementScreenTop = options.top - document.scrollingElement.scrollTop; + if (height + elementScreenTop > window.innerHeight) { + elementScreenTop = window.innerHeight - height; + } + if (elementScreenTop < options.top) { + elementScreenTop = options.top - height - 20; + } + if (elementScreenTop < 0) { + elementScreenTop = 0; + } + + // set position + $root.style.left = elementScreenLeft + 'px'; + $root.style.top = elementScreenTop + 'px'; + } + + function fade({fadingStage = 1} = {}) { + const timeInactive = performance.now() - userActivity; + const delay = options.hideDelay / 2; + if (userActivity && timeInactive < delay) { + timerFadeColorPicker = setTimeout(fade, delay - timeInactive, 2); + clearTimeout(timerCloseColorPicker); + delete $root.dataset.fading; + return; + } + $root.dataset.fading = fadingStage; + if (fadingStage === 1) { + timerFadeColorPicker = setTimeout(fade, Math.max(0, delay - 500), {fadingStage: 2}); + } else { + timerCloseColorPicker = setTimeout(hide, 500); + } + } + + function focusNoScroll(el) { + if (el) { + const {scrollY: y, scrollX: x} = window; + el.focus({preventScroll: true}); + el = null; + if (window.scrollY !== y || window.scrollX !== x) { + window.scrollTo(x, y); + } + } + } + + function getScreenBounds(el) { + const bounds = el.getBoundingClientRect(); + const {scrollTop, scrollLeft} = document.scrollingElement; + return { + top: bounds.top + scrollTop, + left: bounds.left + scrollLeft, + width: bounds.width, + height: bounds.height, + }; + } + + function guessTheme() { + const realColor = {r: 255, g: 255, b: 255, a: 1}; + const start = ((cm.display.renderedView || [])[0] || {}).text || cm.display.lineDiv; + for (let el = start; el; el = el.parentElement) { + const bgColor = getComputedStyle(el).backgroundColor; + const [r, g, b, a = 255] = bgColor.match(/\d+/g).map(Number); + if (!a) { + continue; + } + const mixedA = 1 - (1 - a / 255) * (1 - realColor.a); + const q1 = a / 255 / mixedA; + const q2 = realColor.a * (1 - mixedA) / mixedA; + realColor.r = Math.round(r * q1 + realColor.r * q2); + realColor.g = Math.round(g * q1 + realColor.g * q2); + realColor.b = Math.round(b * q1 + realColor.b * q2); + realColor.a = mixedA; + } + // https://www.w3.org/TR/AERT#color-contrast + const {r, g, b} = realColor; + const brightness = r * .299 + g * .587 + b * .114; + return brightness < 128 ? 'dark' : 'light'; + } + + function constrain(min, max, value) { + return value < min ? min : value > max ? max : value; + } + + function snapToInt(num) { + const int = Math.round(num); + return Math.abs(int - num) < 1e-3 ? int : num; + } + + function parseAs(el, parser) { + const num = parser(el.value); + if (!isNaN(num)) { + el.value = num; + return true; + } + } + + //endregion }); diff --git a/vendor-overwrites/colorpicker/colorview.js b/vendor-overwrites/colorpicker/colorview.js index b53fec3e..7086926b 100644 --- a/vendor-overwrites/colorpicker/colorview.js +++ b/vendor-overwrites/colorpicker/colorview.js @@ -1,434 +1,457 @@ -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; +/* global CodeMirror */ +'use strict'; - // color names - var color_names={aliceblue:"rgb(240, 248, 255)",antiquewhite:"rgb(250, 235, 215)",aqua:"rgb(0, 255, 255)",aquamarine:"rgb(127, 255, 212)",azure:"rgb(240, 255, 255)",beige:"rgb(245, 245, 220)",bisque:"rgb(255, 228, 196)",black:"rgb(0, 0, 0)",blanchedalmond:"rgb(255, 235, 205)",blue:"rgb(0, 0, 255)",blueviolet:"rgb(138, 43, 226)",brown:"rgb(165, 42, 42)",burlywood:"rgb(222, 184, 135)",cadetblue:"rgb(95, 158, 160)",chartreuse:"rgb(127, 255, 0)",chocolate:"rgb(210, 105, 30)",coral:"rgb(255, 127, 80)",cornflowerblue:"rgb(100, 149, 237)",cornsilk:"rgb(255, 248, 220)",crimson:"rgb(237, 20, 61)",cyan:"rgb(0, 255, 255)",darkblue:"rgb(0, 0, 139)",darkcyan:"rgb(0, 139, 139)",darkgoldenrod:"rgb(184, 134, 11)",darkgray:"rgb(169, 169, 169)",darkgrey:"rgb(169, 169, 169)",darkgreen:"rgb(0, 100, 0)",darkkhaki:"rgb(189, 183, 107)",darkmagenta:"rgb(139, 0, 139)",darkolivegreen:"rgb(85, 107, 47)",darkorange:"rgb(255, 140, 0)",darkorchid:"rgb(153, 50, 204)",darkred:"rgb(139, 0, 0)",darksalmon:"rgb(233, 150, 122)",darkseagreen:"rgb(143, 188, 143)",darkslateblue:"rgb(72, 61, 139)",darkslategray:"rgb(47, 79, 79)",darkslategrey:"rgb(47, 79, 79)",darkturquoise:"rgb(0, 206, 209)",darkviolet:"rgb(148, 0, 211)",deeppink:"rgb(255, 20, 147)",deepskyblue:"rgb(0, 191, 255)",dimgray:"rgb(105, 105, 105)",dimgrey:"rgb(105, 105, 105)",dodgerblue:"rgb(30, 144, 255)",firebrick:"rgb(178, 34, 34)",floralwhite:"rgb(255, 250, 240)",forestgreen:"rgb(34, 139, 34)",fuchsia:"rgb(255, 0, 255)",gainsboro:"rgb(220, 220, 220)",ghostwhite:"rgb(248, 248, 255)",gold:"rgb(255, 215, 0)",goldenrod:"rgb(218, 165, 32)",gray:"rgb(128, 128, 128)",grey:"rgb(128, 128, 128)",green:"rgb(0, 128, 0)",greenyellow:"rgb(173, 255, 47)",honeydew:"rgb(240, 255, 240)",hotpink:"rgb(255, 105, 180)",indianred:"rgb(205, 92, 92)",indigo:"rgb(75, 0, 130)",ivory:"rgb(255, 255, 240)",khaki:"rgb(240, 230, 140)",lavender:"rgb(230, 230, 250)",lavenderblush:"rgb(255, 240, 245)",lawngreen:"rgb(124, 252, 0)",lemonchiffon:"rgb(255, 250, 205)",lightblue:"rgb(173, 216, 230)",lightcoral:"rgb(240, 128, 128)",lightcyan:"rgb(224, 255, 255)",lightgoldenrodyellow:"rgb(250, 250, 210)",lightgreen:"rgb(144, 238, 144)",lightgray:"rgb(211, 211, 211)",lightgrey:"rgb(211, 211, 211)",lightpink:"rgb(255, 182, 193)",lightsalmon:"rgb(255, 160, 122)",lightseagreen:"rgb(32, 178, 170)",lightskyblue:"rgb(135, 206, 250)",lightslategray:"rgb(119, 136, 153)",lightslategrey:"rgb(119, 136, 153)",lightsteelblue:"rgb(176, 196, 222)",lightyellow:"rgb(255, 255, 224)",lime:"rgb(0, 255, 0)",limegreen:"rgb(50, 205, 50)",linen:"rgb(250, 240, 230)",magenta:"rgb(255, 0, 255)",maroon:"rgb(128, 0, 0)",mediumaquamarine:"rgb(102, 205, 170)",mediumblue:"rgb(0, 0, 205)",mediumorchid:"rgb(186, 85, 211)",mediumpurple:"rgb(147, 112, 219)",mediumseagreen:"rgb(60, 179, 113)",mediumslateblue:"rgb(123, 104, 238)",mediumspringgreen:"rgb(0, 250, 154)",mediumturquoise:"rgb(72, 209, 204)",mediumvioletred:"rgb(199, 21, 133)",midnightblue:"rgb(25, 25, 112)",mintcream:"rgb(245, 255, 250)",mistyrose:"rgb(255, 228, 225)",moccasin:"rgb(255, 228, 181)",navajowhite:"rgb(255, 222, 173)",navy:"rgb(0, 0, 128)",oldlace:"rgb(253, 245, 230)",olive:"rgb(128, 128, 0)",olivedrab:"rgb(107, 142, 35)",orange:"rgb(255, 165, 0)",orangered:"rgb(255, 69, 0)",orchid:"rgb(218, 112, 214)",palegoldenrod:"rgb(238, 232, 170)",palegreen:"rgb(152, 251, 152)",paleturquoise:"rgb(175, 238, 238)",palevioletred:"rgb(219, 112, 147)",papayawhip:"rgb(255, 239, 213)",peachpuff:"rgb(255, 218, 185)",peru:"rgb(205, 133, 63)",pink:"rgb(255, 192, 203)",plum:"rgb(221, 160, 221)",powderblue:"rgb(176, 224, 230)",purple:"rgb(128, 0, 128)",rebeccapurple:"rgb(102, 51, 153)",red:"rgb(255, 0, 0)",rosybrown:"rgb(188, 143, 143)",royalblue:"rgb(65, 105, 225)",saddlebrown:"rgb(139, 69, 19)",salmon:"rgb(250, 128, 114)",sandybrown:"rgb(244, 164, 96)",seagreen:"rgb(46, 139, 87)",seashell:"rgb(255, 245, 238)",sienna:"rgb(160, 82, 45)",silver:"rgb(192, 192, 192)",skyblue:"rgb(135, 206, 235)",slateblue:"rgb(106, 90, 205)",slategray:"rgb(112, 128, 144)",slategrey:"rgb(112, 128, 144)",snow:"rgb(255, 250, 250)",springgreen:"rgb(0, 255, 127)",steelblue:"rgb(70, 130, 180)",tan:"rgb(210, 180, 140)",teal:"rgb(0, 128, 128)",thistle:"rgb(216, 191, 216)",tomato:"rgb(255, 99, 71)",turquoise:"rgb(64, 224, 208)",violet:"rgb(238, 130, 238)",wheat:"rgb(245, 222, 179)",white:"rgb(255, 255, 255)",whitesmoke:"rgb(245, 245, 245)",yellow:"rgb(255, 255, 0)",yellowgreen:"rgb(154, 205, 50)",transparent:"rgba(0, 0, 0, 0)"}; +(() => { + 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}])); - var colorpicker_class = 'codemirror-colorview'; - var colorpicker_background_class = 'codemirror-colorview-background'; + 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'), + }; - // Excluded tokens do not show color views.. - var excluded_token = ['comment']; + 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); + } + }, + }; - CodeMirror.defineOption("colorpicker", false, function (cm, val, old) { + function registerEvents(cm) { + Object.keys(CodeMirrorEvents).forEach(name => cm.on(name, CodeMirrorEvents[name])); + } - if (old && old != CodeMirror.Init) { + function unregisterEvents(cm) { + Object.keys(CodeMirrorEvents).forEach(name => cm.off(name, CodeMirrorEvents[name])); + } - if (cm.state.colorpicker) - { - cm.state.colorpicker.destroy(); - cm.state.colorpicker = null; + function registerHooks() { + const mx = CodeMirror.modeExtensions.css; + if (!mx || mx.token !== colorizeToken) { + CodeMirror.extendMode('css', { + token: colorizeToken, + }); + } + } - } - // remove event listener + 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; } - - if (val) - { - cm.state.colorpicker = new codemirror_colorpicker(cm, val); + const start = styles[i - 2] || 0; + const data = lineCache.get(start); + if (!data) { + continue; } - }); - - function onChange(cm, evt) { - if (evt.origin == 'setValue') { // if content is changed by setValue method, it initialize markers - cm.state.colorpicker.close_color_picker(); - cm.state.colorpicker.init_color_update(); - cm.state.colorpicker.style_color_update(); - } else { - cm.state.colorpicker.style_color_update(cm.getCursor().line); + elements = elements || text.getElementsByClassName(OWN_DOM_CLASS); + const el = elements[elementIndex++]; + if (el.colorpickerData && el.colorpickerData.color === data.color) { + continue; } - - } - - function onUpdate(cm, evt) { - if (!cm.state.colorpicker.isUpdate) { - cm.state.colorpicker.isUpdate = true; - cm.state.colorpicker.close_color_picker(); - cm.state.colorpicker.init_color_update(); - cm.state.colorpicker.style_color_update(); + 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); + } } - function onRefresh(cm, evt) { - onChange(cm, { origin : 'setValue'}); + destroy() { + unregisterHooks(this.cm); + unregisterEvents(this.cm); + resetMode(this.cm); + this.cm.state.colorpicker = null; } - function onKeyup(cm, evt) { - cm.state.colorpicker.keyup(evt); - } - - function onMousedown(cm, evt) { - if (cm.state.colorpicker.is_edit_mode()) - { - cm.state.colorpicker.check_mousedown(evt); + 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}); } - function onPaste (cm, evt) { - onChange(cm, { origin : 'setValue'}); + 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, + })); + } } - function onScroll (cm) { - cm.state.colorpicker.close_color_picker(); + closePopup() { + if (this.popup) { + this.popup.hide(); + } } - function debounce (callback, delay) { - - var t = undefined; - - return function (cm, e) { - if (t) { - clearTimeout(t); - } - - t = setTimeout(function () { - callback(cm, e); - }, delay || 300); - } + 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); + } } + } - function has_class(el, cls) { - if (!el || !el.className) { - return false; - } else { - var newClass = ' ' + el.className + ' '; - return newClass.indexOf(' ' + cls + ' ') > -1; - } + CodeMirror.defineOption('colorpicker', false, (cm, value, oldValue) => { + if (oldValue && oldValue !== CodeMirror.Init && cm.state.colorpicker) { + cm.state.colorpicker.destroy(); } - - function codemirror_colorpicker (cm, opt) { - var self = this; - - if (typeof opt == 'boolean') - { - opt = { mode : 'view' }; - } else { - opt = Object.assign({ mode: 'view' }, opt || {}); - } - - this.opt = opt; - this.cm = cm; - this.markers = {}; - - // set excluded token - excluded_token = this.opt.excluded_token || excluded_token; - - if (this.cm.colorpicker) { - this.colorpicker = this.cm.colorpicker(); - } else if (this.opt.colorpicker) { - this.colorpicker = this.opt.colorpicker; - } - - this.init_event(); - + if (value) { + cm.state.colorpicker = new ColorMarker(cm, value); } + }); - codemirror_colorpicker.prototype.init_event = function () { - - this.cm.on('mousedown', onMousedown); - this.cm.on('keyup', onKeyup); - this.cm.on('change', onChange); - this.cm.on('update', onUpdate); - this.cm.on('refresh', onRefresh); - - // create paste callback - this.onPasteCallback = (function (cm, callback) { - return function (evt) { - callback.call(this, cm, evt); - } - })(this.cm, onPaste); - - this.cm.getWrapperElement().addEventListener('paste', this.onPasteCallback); - - if (this.is_edit_mode()) - { - this.cm.on('scroll', debounce(onScroll, 50)); - } - - } - - codemirror_colorpicker.prototype.is_edit_mode = function () { - return this.opt.mode == 'edit'; - } - - codemirror_colorpicker.prototype.is_view_mode = function () { - return this.opt.mode == 'view'; - } - - codemirror_colorpicker.prototype.destroy = function () { - this.cm.off('mousedown', onMousedown); - this.cm.off('keyup', onKeyup); - this.cm.off('change', onChange) - this.cm.getWrapperElement().removeEventListener('paste', this.onPasteCallback); - - if (this.is_edit_mode()) - { - this.cm.off('scroll'); - } - } - - codemirror_colorpicker.prototype.hasClass = function (el, className) { - if (!el.className) - { - return false; - } else { - var newClass = ' ' + el.className + ' '; - return newClass.indexOf(' ' + className + ' ') > -1; - } - } - - codemirror_colorpicker.prototype.check_mousedown = function (evt) { - if (this.hasClass(evt.target, colorpicker_background_class) ) - { - this.open_color_picker(evt.target.parentNode); - } else { - this.close_color_picker(); - } - } - - codemirror_colorpicker.prototype.popup_color_picker = function (defalutColor) { - var cursor = this.cm.getCursor(); - var self = this; - var colorMarker = { - lineNo : cursor.line, - ch : cursor.ch, - color: defalutColor || '#FFFFFF', - isShortCut : true - }; - - Object.keys(this.markers).forEach(function(key) { - var searchKey = "#" + key; - if (searchKey.indexOf( "#" + colorMarker.lineNo + ":") > -1) { - var marker = self.markers[key]; - - if (marker.ch <= colorMarker.ch && colorMarker.ch <= marker.ch + marker.color.length) { - // when cursor has marker - colorMarker.ch = marker.ch; - colorMarker.color = marker.color; - colorMarker.nameColor = marker.nameColor; - } - - } - }); - - this.open_color_picker(colorMarker); - } - - codemirror_colorpicker.prototype.open_color_picker = function (el) { - var lineNo = el.lineNo; - var ch = el.ch; - var nameColor = el.nameColor; - var color = el.color; - - - if (this.colorpicker) { - var self = this; - var prevColor = color; - var pos = this.cm.charCoords({line : lineNo, ch : ch }); - this.colorpicker.show({ - left : pos.left, - top : pos.bottom, - isShortCut : el.isShortCut || false, - hideDelay : self.opt.hideDelay || 2000 - }, nameColor || color, function (newColor) { - self.cm.replaceRange(newColor, { line : lineNo, ch : ch } , { line : lineNo, ch : ch + prevColor.length }, '*colorpicker'); - prevColor = newColor; - }); - - } - - } - - codemirror_colorpicker.prototype.close_color_picker = function (el) { - if (this.colorpicker) - { - this.colorpicker.hide(); - } - } - - codemirror_colorpicker.prototype.key = function (lineNo, ch) { - return [lineNo, ch].join(":"); - } - - - codemirror_colorpicker.prototype.keyup = function (evt) { - - if (this.colorpicker ) { - if (evt.key == 'Escape') { - this.colorpicker.hide(); - } else if (this.colorpicker.isShortCut() == false) { - this.colorpicker.hide(); - } - } - } - - codemirror_colorpicker.prototype.init_color_update = function () { - this.markers = {}; // initialize marker list - } - - codemirror_colorpicker.prototype.style_color_update = function (lineHandle) { - - if (lineHandle) { - this.match(lineHandle); - } else { - var max = this.cm.lineCount(); - - for(var lineNo = 0; lineNo < max; lineNo++) { - this.match(lineNo); - } - } - - } - - codemirror_colorpicker.prototype.empty_marker = function (lineNo, lineHandle) { - var list = lineHandle.markedSpans || []; - - for(var i = 0, len = list.length; i < len; i++) { - var key = this.key(lineNo, list[i].from); - - if (key && has_class(list[i].marker.replacedWith, colorpicker_class)) { - delete this.markers[key]; - list[i].marker.clear(); - } - - } - } - - codemirror_colorpicker.prototype.color_regexp = /(#(?:[\da-f]{3}){1,2}|rgb\((?:\s*\d{1,3},\s*){2}\d{1,3}\s*\)|rgba\((?:\s*\d{1,3},\s*){3}\d*\.?\d+\s*\)|hsl\(\s*\d{1,3}(?:,\s*\d{1,3}%){2}\s*\)|hsla\(\s*\d{1,3}(?:,\s*\d{1,3}%){2},\s*\d*\.?\d+\s*\)|([\w_\-]+))/gi; - - codemirror_colorpicker.prototype.match_result = function (lineHandle) { - return lineHandle.text.match(this.color_regexp); - } - - codemirror_colorpicker.prototype.match = function (lineNo) { - var lineHandle = this.cm.getLineHandle(lineNo); - - this.empty_marker(lineNo, lineHandle); - - var result = this.match_result(lineHandle); - if (result) - { - var obj = { next : 0 }; - for(var i = 0, len = result.length; i < len; i++) { - - if (result[i].indexOf('#') > -1 || result[i].indexOf('rgb') > -1 || result[i].indexOf('hsl') > -1) { - this.render(obj, lineNo, lineHandle, result[i]); - } else { - var nameColor = color_names[result[i]]; - if (nameColor) { - this.render(obj, lineNo, lineHandle, result[i], nameColor); - } - } - } - } - } - - codemirror_colorpicker.prototype.make_element = function () { - var el = document.createElement('div'); - - el.className = colorpicker_class; - - if (this.is_edit_mode()) - { - el.title ="open color picker"; - } else { - el.title =""; - } - - el.back_element = this.make_background_element(); - el.appendChild(el.back_element); - - return el; - } - - codemirror_colorpicker.prototype.make_background_element = function () { - var el = document.createElement('div'); - - el.className = colorpicker_background_class; - - return el; - } - - codemirror_colorpicker.prototype.set_state = function (lineNo, start, color, nameColor) { - var marker = this.create_marker(lineNo, start); - - - marker.lineNo = lineNo; - marker.ch = start; - marker.color = color; - marker.nameColor = nameColor; - - return marker; - } - - codemirror_colorpicker.prototype.create_marker = function (lineNo, start) { - - var key = this.key(lineNo,start); - - if (!this.markers[key]) { - this.markers[key] = this.make_element(); - } - - - return this.markers[key]; - - } - - codemirror_colorpicker.prototype.has_marker = function (lineNo, start) { - var key = this.key(lineNo,start); - return !!(this.markers[key]) - } - - codemirror_colorpicker.prototype.update_element = function (el, color) { - el.back_element.style.backgroundColor = color; - } - - codemirror_colorpicker.prototype.set_mark = function (line, ch, el) { - this.cm.setBookmark({ line : line, ch : ch}, { widget : el, handleMouseEvents : true} ); - - } - - codemirror_colorpicker.prototype.is_excluded_token = function (line, ch) { - var token = this.cm.getTokenAt({line : line, ch : ch}); - var count = 0; - for(var i = 0, len = excluded_token.length; i < len; i++) { - if (token.type === excluded_token[i]) { - count++; - break; - } - } - - return count > 0; // true is that it has a excluded token - } - - codemirror_colorpicker.prototype.render = function (cursor, lineNo, lineHandle, color, nameColor) { - var start = lineHandle.text.indexOf(color, cursor.next); - - if (this.is_excluded_token(lineNo, start) === true) { - // excluded token do not show. - return; - } - - cursor.next = start + color.length; - - if (this.has_marker(lineNo, start)) - { - this.update_element(this.create_marker(lineNo, start), nameColor || color); - this.set_state(lineNo, start, color, nameColor); - return; - } - - var el = this.create_marker(lineNo, start); - - this.update_element(el, nameColor || color); - this.set_state(lineNo, start, color, nameColor || color); - this.set_mark(lineNo, start, el); - } -}); + // 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(); +})();