From a4560e6b055b47060c033d98ba4068340c519eb8 Mon Sep 17 00:00:00 2001 From: Noel Delgado Date: Sun, 1 Mar 2020 00:55:10 -0600 Subject: [PATCH] :recycle::sparkles::heavy_plus_sign: General update: - code refactor - added HSV, CMKYK and LAB support - added Google Font dependency --- .eslintrc.json | 2 +- index.html | 221 ++++++++++------ package.json | 10 +- src/css/bundles/main.css | 12 +- src/css/lib/components/buttons.css | 39 ++- src/css/lib/components/color-item.css | 82 +++--- src/css/lib/components/footer.css | 37 +++ src/css/lib/components/header.css | 6 + src/css/lib/components/inputs.css | 9 +- src/css/lib/components/toast.css | 7 +- src/css/lib/style.css | 93 ++----- src/js/bundles/main.js | 2 +- src/js/lib/app.js | 245 +++++++++--------- .../lib/components/fe-color-matrix-input.js | 57 ++++ src/js/lib/components/input-hex.js | 39 +++ src/js/lib/components/input-numeric.js | 65 +++++ src/js/lib/components/input.js | 152 ++++++----- src/js/lib/components/toast.js | 45 ++-- src/js/lib/utils/index.js | 28 +- 19 files changed, 713 insertions(+), 438 deletions(-) create mode 100644 src/css/lib/components/footer.css create mode 100644 src/css/lib/components/header.css create mode 100644 src/js/lib/components/fe-color-matrix-input.js create mode 100644 src/js/lib/components/input-hex.js create mode 100644 src/js/lib/components/input-numeric.js diff --git a/.eslintrc.json b/.eslintrc.json index 9f5aae5..5a7dfe4 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,5 +1,5 @@ { - "ignorePatterns": ["node_modules/", "out/", "build/"], + "ignorePatterns": ["node_modules/", "jsdoc/", "build/"], "env": { "browser": true, "es6": true diff --git a/index.html b/index.html index bfec8bb..d6b0aa5 100644 --- a/index.html +++ b/index.html @@ -7,108 +7,173 @@ + + - - + +
- - yacc + + + +
-
-
- -
-
- -
-
-
-
- -
-
- -
-
-
-
- -
-
- -
-
-
-
- -
-
- -
-
-
-
- - -
+
    +
  • +
    + +
    +
    + +
    +
  • +
  • +
    + +
    +
    + +
    +
  • +
  • +
    + +
    +
    + +
    +
  • +
  • +
    + +
    +
    + +
    +
  • +
  • +
    + +
    +
    + +
    +
  • +
  • +
    + +
    +
    + +
    +
  • +
  • +
    + +
    +
    + +
    +
  • +
+
+
-
- + diff --git a/package.json b/package.json index c7a0d72..8b0b0a5 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "build": "npm run min-css && npm run min-js", "min-css": "uglify -s build/css/main.css -o build/css/main.css -c", "min-js": "uglify -s build/js/main.js -o build/js/main.js", - "svg": "svg-sprite -s --symbol-dest=svg-sprite-symbol --symbol-prefix=.svg- --ss=sprite.svg --si --sx --shape-id-generator=svg-%s --dest=src/assets src/assets/svg/*.svg" + "svg": "svg-sprite -s --symbol-dest=svg-sprite-symbol --symbol-prefix=.svg- --ss=sprite.svg --si --sx --shape-id-generator=svg-%s --dest=src/assets src/assets/svg/*.svg", + "docs": "jsdoc -c .jsdoc.json" }, "author": "", "license": "MIT", @@ -67,7 +68,9 @@ ] }, "postcss-cssnext": { - "browsers": ["last 1 version"], + "browsers": [ + "last 1 version" + ], "features": { "customProperties": false, "calc": false @@ -87,6 +90,9 @@ "browser" ] } + }, + "buble": { + "objectAssign": "Object.assign" } } } diff --git a/src/css/bundles/main.css b/src/css/bundles/main.css index 49e81fa..7279d3d 100644 --- a/src/css/bundles/main.css +++ b/src/css/bundles/main.css @@ -5,9 +5,11 @@ $bundle: true */ @import '@simonwep/pickr/dist/themes/nano.min.css'; @import '../lib/vendor-overrides/pickr'; +@import '../lib/components/header'; +@import '../lib/components/footer'; @import '../lib/components/buttons'; -@import '../lib/components/color-item'; @import '../lib/components/inputs'; +@import '../lib/components/color-item'; @import '../lib/components/toast'; @import '../lib/utils'; @import '../lib/style'; @@ -21,8 +23,9 @@ $bundle: true --cc-transition-timing: ease-out; --cc-transition-duration: 260ms; - font-family: system-ui; + font-family: 'Roboto Mono', monospace; font-size: 16px; + line-height: 1.618; } * { @@ -32,8 +35,11 @@ $bundle: true } body { - user-select: none; font-kerning: none; + user-select: none; + -webkit-font-smoothing: antialiased; + -webkit-tap-highlight-color: transparent; + -webkit-touch-callout: none; } a { diff --git a/src/css/lib/components/buttons.css b/src/css/lib/components/buttons.css index b34c586..34aaf9c 100644 --- a/src/css/lib/components/buttons.css +++ b/src/css/lib/components/buttons.css @@ -3,9 +3,11 @@ button { font-size: 0.75rem; font-family: inherit; font-weight: inherit; + padding: 0.5rem; border: 2px solid var(--cc-border-color); - background-color: var(--cc-text-color); - color: var(--cc-main-solid-color); + background-color: transparent; + color: var(--cc-text-color); + border-radius: 3px; transition: all var(--cc-transition-duration) var(--cc-transition-timing); transition-property: color, background-color, border-color, box-shadow; @@ -17,32 +19,23 @@ button { background-color: var(--cc-border-color); box-shadow: none; } -} -.cc-btn { - border-radius: 3px; - border: none; - padding: 0.5rem 1rem; + &.cc-btn { + border: none; - & > * { - display: inline-block; - vertical-align: middle; + & > * { + display: inline-block; + vertical-align: middle; + } } -} - -button.cc-btn-icon { - width: 64px; - height: 64px; - border-radius: 50%; - padding: 0; - &.cc-btn-sm { - width: 32px; - height: 32px; - background-color: var(--cc-main-color); - color: var(--cc-text-color); + &.cc-btn-icon { + width: 64px; + height: 64px; + border-radius: 50%; + padding: 0; - & > svg { + & svg { display: block; margin: 0 auto; } diff --git a/src/css/lib/components/color-item.css b/src/css/lib/components/color-item.css index 3f6b3f2..663b073 100644 --- a/src/css/lib/components/color-item.css +++ b/src/css/lib/components/color-item.css @@ -1,60 +1,62 @@ -:root { - --cc-fsd: 10; - --cc-color-item-input-font-size: { - font-size: calc(100vw / var(--cc-fsd)); - - @media (min-width: 1000px) { - font-size: calc(1000px / var(--cc-fsd)); - } +.color-items { + display: grid; + grid-template-columns: auto; + max-width: 800px; + padding-top: 1rem; + list-style: none; + + @media (min-width: 768px) { + grid-template-columns: [col] 50% [col] 50%; + padding-top: 0; } } -.cc-color-item { +.color-item { display: flex; + font-size: 36px; min-height: 48px; + margin-bottom: 0.5rem; &.-hex { - --cc-fsd: 7; - @apply --cc-color-item-input-font-size; - margin: -0.1em 0 -0.2em; - } - - &.-rgb { - --cc-fsd: 12; - @apply --cc-color-item-input-font-size; - } + font-size: 36px; - &.-hsl { - --cc-fsd: 13; - @apply --cc-color-item-input-font-size; - } + & input { + text-transform: uppercase; + } - &.-hwb { - --cc-fsd: 13; - @apply --cc-color-item-input-font-size; + @media (min-width: 768px) { + font-size: 144px; + grid-column: col / span 2; + margin: 0; + } } } -/* -.cc-color-item__label { +.color-item__btn { display: flex; align-items: center; - font-size: 0.75rem; - padding: 0 0.5rem 0 1rem; - & > span { - width: 10px; - writing-mode: vertical-lr; - transform: rotate(180deg); + & button { + display: flex; + width: 60px; + + & > * { + display: flex; + align-items: center; + } } -} -*/ -.cc-color-item__btn { - order: 1; + & svg { + margin-right: 0.25rem; + } } -.cc-color-item__input-wrapper { - flex: 1; - order: 2; +.color-item__input { + display: flex; + position: relative; + overflow: hidden; + + & input { + letter-spacing: -2px; + } } diff --git a/src/css/lib/components/footer.css b/src/css/lib/components/footer.css new file mode 100644 index 0000000..e9d95f2 --- /dev/null +++ b/src/css/lib/components/footer.css @@ -0,0 +1,37 @@ +footer { + display: flex; + font-size: 0.75rem; + padding: 1.5rem 1rem 0; + + & > .footer__info { + display: flex; + align-items: center; + position: relative; + padding-left: 1rem; + margin-left: 1rem; + + & > :not(:last-child) { + margin-right: 1rem; + } + + &::before { + content: ''; + position: absolute; + left: 0; + top: 25%; + bottom: 25%; + border-left-width: 1px; + border-left-style: dotted; + } + + @media (min-width: 768px) { + padding-left: 2rem; + margin-left: 2rem; + + & > :not(:last-child) { + margin-right: 1.5rem; + } + } + } +} + diff --git a/src/css/lib/components/header.css b/src/css/lib/components/header.css new file mode 100644 index 0000000..3696b78 --- /dev/null +++ b/src/css/lib/components/header.css @@ -0,0 +1,6 @@ +header { + position: relative; + font-style: italic; + padding: 0 1rem; +} + diff --git a/src/css/lib/components/inputs.css b/src/css/lib/components/inputs.css index 3d5c512..b9ff34b 100644 --- a/src/css/lib/components/inputs.css +++ b/src/css/lib/components/inputs.css @@ -1,13 +1,10 @@ -.cc-input { +input.cc-input { border: none; width: 100%; font-family: inherit; font-size: inherit; - padding: 0; - margin: 0; - font-weight: 900; - letter-spacing: -2px; - background: rgba(0, 0, 0, 0); + font-weight: inherit; + background-color: transparent; color: inherit; outline: none; text-overflow: ellipsis; diff --git a/src/css/lib/components/toast.css b/src/css/lib/components/toast.css index e74e1e4..b06121a 100644 --- a/src/css/lib/components/toast.css +++ b/src/css/lib/components/toast.css @@ -3,6 +3,7 @@ position: fixed; top: 1rem; left: 1rem; + max-width: calc(100vw - 2rem); &.show > .toast { opacity: 1; @@ -11,14 +12,12 @@ } .toast { - display: inline-block; - padding: 0.75rem 1.25rem; - font-weight: 400; + padding: 0.5rem 1rem; background: var(--cc-text-color); color: var(--cc-main-color); border-radius: 3px; opacity: 0; - transform: translate3d(-1rem, 0, 0); + transform: translate3d(-0.5rem, 0, 0); transition: all var(--cc-transition-duration) var(--cc-transition-timing); transition-property: opacity, transform; } diff --git a/src/css/lib/style.css b/src/css/lib/style.css index 745c9ad..5b98952 100644 --- a/src/css/lib/style.css +++ b/src/css/lib/style.css @@ -4,39 +4,28 @@ html { * CSS gradient checkerboard pattern * https://stackoverflow.com/questions/35361986/css-gradient-checkerboard-pattern */ - background-image: linear-gradient(45deg, #808080 25%, transparent 25%), linear-gradient(-45deg, #808080 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #808080 75%), linear-gradient(-45deg, transparent 75%, #808080 75%); - background-size: 20px 20px; - background-position: 0 0, 0 10px, 10px -10px, -10px 0px; + --cb-pattern-size: 40px; + --cb-pattern-color: rgba(0,0,0,0.05); + background-image: + linear-gradient(45deg, var(--cb-pattern-color) 25%, transparent 25%), + linear-gradient(-45deg, var(--cb-pattern-color) 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, var(--cb-pattern-color) 75%), + linear-gradient(-45deg, transparent 75%, var(--cb-pattern-color) 75%); + background-size: var(--cb-pattern-size) var(--cb-pattern-size); + background-position: 0 0, + 0 calc(var(--cb-pattern-size) / 2), + calc(var(--cb-pattern-size) / 2) calc((var(--cb-pattern-size) / 2) * -1), + calc((var(--cb-pattern-size) / 2) * -1) 0; } body { min-height: 100%; - line-height: 1; - font-weight: 800; color: var(--cc-text-color); background-color: var(--cc-main-color); transition: all var(--cc-transition-duration) var(--cc-transition-timing); transition-property: color, background-color; - - &::before, &::after { - content: ''; - position: fixed; - left: 0; - width: 0.5rem; - height: 50%; - } - - &::before { - background: black; - top: 0; - } - - &::after { - background: white; - top: 50%; - } } ::selection { @@ -44,67 +33,39 @@ body { color: var(--cc-main-solid-color); } +a { + transition: opacity var(--cc-transition-duration); + &:hover { + opacity: 0.7; + } +} + .root { + width: 100%; margin: auto 0; - padding: 2rem 0 2rem 1rem; + padding: 1rem 0; } -header { - position: relative; - font-style: italic; +main { + padding-left: 1rem; } -.logo { - width: 16px; - height: 16px; +/* +.rainbow { display: inline-block; - border-radius: 50%; background-image: radial-gradient(circle at top center, #F00 50%, transparent 100%), radial-gradient(circle at bottom right, #0F0 50%, transparent 100%), radial-gradient(circle at bottom left, #00F 50%, transparent 100%) ; background-blend-mode: lighten; - box-shadow: 0 0 0 1px var(--cc-border-color); -} - -.cc-extra-items { - padding: 0.5rem 0 0 0; -} - -footer { - font-size: 0.625rem; - padding: 1.5rem 1rem 0 0; - - & a { - transition: opacity 300ms linear; - &:hover { opacity: 0.7; } - } - - & > .footer__info { - position: relative; - flex-direction: column; - justify-content: center; - padding-left: 2rem; - margin-left: 2rem; - - &::before { - content: ''; - position: absolute; - left: 0; - top: 25%; - bottom: 25%; - border-left-width: 1px; - border-left-style: dotted; - } - } + background-color: var(--cc-main-color); } +*/ .cc-by { font-size: 0.625rem; position: fixed; bottom: 1rem; right: 1rem; - font-size: 0.75rem; } - diff --git a/src/js/bundles/main.js b/src/js/bundles/main.js index bc17996..066947d 100644 --- a/src/js/bundles/main.js +++ b/src/js/bundles/main.js @@ -19,4 +19,4 @@ catch (err) { const app = new App(data); app.run(); -// window.app = app; +window.app = app; diff --git a/src/js/lib/app.js b/src/js/lib/app.js index 2904ded..a58bf9a 100644 --- a/src/js/lib/app.js +++ b/src/js/lib/app.js @@ -1,102 +1,85 @@ -const { log } = console; - +/** + * Application main handler + * @module App + */ import Color from 'color'; -import Pickr from '@simonwep/pickr/dist/pickr.es5.min'; import Copy from 'copy-text-to-clipboard'; -import Input from '~/src/js/lib/components/input.js'; +import Pickr from '@simonwep/pickr/dist/pickr.es5.min'; +import InputHex from '~/src/js/lib/components/input-hex.js'; +import InputNumeric from '~/src/js/lib/components/input-numeric.js'; +import FeColorMatrixInput from '~/src/js/lib/components/fe-color-matrix-input.js'; import Toast from '~/src/js/lib/components/toast.js'; import { $, $$, autoBind, getRandomHex } from '~/src/js/lib/utils' window.Color = Color; -const internals = {}; -/** - * Checks if a string is a valid color format for `color` library - * @param {string} color - * @return {boolean} - */ -internals.validateColor = (color) => { - try { - Color(color); - return true; - } - catch (err) { - return false; - } -}; - -/** - * Application main handler - */ -export default class App { +const internals = { /** - * @return {Object} default class properties + * Checks if a string is a valid color input for the `color` library. + * @param {string} str + * @return {boolean} */ - static get initialtProps() { - return { - config: {}, - /** - * Color’s instance reference - * @typeof {Color} - */ - color: null, - /** - * Pickr’s instance reference - * @typeof {Pickr} - */ - pickr: null, - /** - * Node elements collection reference - */ - ui: {} + validateColor(str) { + try { + Color(str); + return true; } - } - - /** - * @return {Object} default configuraton values - */ - static get defaultConfig() { - return { - color: '#FFFFFF', - title: document.title + catch (err) { + return false; } } +}; +export default class App { /** - * @param {Object} config override default config - * @param {string} [config.color='#FFF'] + * Create App instance. + * @param {Object} config - override default config + * @param {string} [config.color='#FFFFFF'] - Initial app color + * @param {string} [config.title=document.title] - Prefix for document.title when history changes + * @return {this} */ constructor(config) { const app = this; - Object.assign(app, app.constructor.initialtProps); - Object.assign(app.config, app.constructor.defaultConfig, config); + Object.assign(app.config = {}, { + color: '#FFFFFF', + title: document.title + }, config); + + app.color = null; + app.pickr = null; + app.ui = {}; app.ui = { - randomBtn: $('[data-random]'), - copyToClipboardButtons: $$('[data-js-copy-to-clipboard-btn]'), + randomButton: $('[data-js-random-btn]'), colorPickerButton: $('[data-js-colorpicker-btn]'), - fcmInput: $('[data-js-fecolormatrix-input]'), - - hexInput: new Input({ - el: $('[data-js-hex-input]'), - validate: internals.validateColor - }), - rgbInput: new Input({ + copyToClipboardButtons: $$('[data-js-copy-to-clipboard-btn]'), + hexInput: new InputHex({ el: $('[data-js-hex-input]') }), + rgbInput: new InputNumeric({ el: $('[data-js-rgb-input]'), - validate: internals.validateColor + model: 'rgb' }), - hslInput: new Input({ + hslInput: new InputNumeric({ el: $('[data-js-hsl-input]'), - validate: internals.validateColor + model: 'hsl' }), - hwbInput: new Input({ + hwbInput: new InputNumeric({ el: $('[data-js-hwb-input]'), - validate: internals.validateColor + model: 'hwb' }), - - toast: new Toast({ - el: $('.toast-wrapper') - }) + hsvInput: new InputNumeric({ + el: $('[data-js-hsv-input]'), + model: 'hsv' + }), + cmykInput: new InputNumeric({ + el: $('[data-js-cmyk-input]'), + model: 'cmyk' + }), + labInput: new InputNumeric({ + el: $('[data-js-lab-input]'), + model: 'lab' + }), + fcmInput: new FeColorMatrixInput({ el: $('[data-js-fecolormatrix-input]') }), + toast: new Toast({ el: $('.toast-wrapper') }) }; app.pickr = new Pickr({ @@ -113,11 +96,8 @@ export default class App { } /** - * Boot the app: - * - enable inputs and buttons - * - bind events and update the history entry - * @public - * @return {App} + * Boot the app. + * @return {this} */ run() { const app = this; @@ -127,6 +107,10 @@ export default class App { app.ui.rgbInput.enable(); app.ui.hslInput.enable(); app.ui.hwbInput.enable(); + app.ui.hsvInput.enable(); + app.ui.cmykInput.enable(); + app.ui.labInput.enable(); + app.ui.randomButton.removeAttribute('disabled'); app.ui.colorPickerButton.removeAttribute('disabled'); app.ui.copyToClipboardButtons.forEach(b => b.removeAttribute('disabled')); @@ -139,20 +123,23 @@ export default class App { } /** - * Register event listeners + * Register event listeners and its handlers. * @private - * @return {App} + * @return {this} */ _bindEvents() { const app = this; autoBind([ - [ app.ui.randomBtn, 'click', '_randomClickHandler' ], + [ app.ui.randomButton, 'click', '_randomClickHandler' ], [ app.ui.copyToClipboardButtons, 'click', '_copyColorToClipboardHandler' ], - [ app.ui.hexInput.element, 'isValid', '_isInputValidHandler' ], - [ app.ui.rgbInput.element, 'isValid', '_isInputValidHandler' ], - [ app.ui.hslInput.element, 'isValid', '_isInputValidHandler' ], - [ app.ui.hwbInput.element, 'isValid', '_isInputValidHandler' ], + [ app.ui.hexInput.element, 'validChange', '_isInputValidHandler' ], + [ app.ui.rgbInput.element, 'validChange', '_isInputValidHandler' ], + [ app.ui.hslInput.element, 'validChange', '_isInputValidHandler' ], + [ app.ui.hwbInput.element, 'validChange', '_isInputValidHandler' ], + [ app.ui.hsvInput.element, 'validChange', '_isInputValidHandler' ], + [ app.ui.cmykInput.element, 'validChange', '_isInputValidHandler' ], + [ app.ui.labInput.element, 'validChange', '_isInputValidHandler' ], [ window, 'popstate', '_popStateHandler' ] ], app); @@ -164,44 +151,46 @@ export default class App { } /** - * Handles copyToClipboardButtons’ click event + * Handles app.ui.copyToClipboardButtons’ click event. + * Gets the reference to the text to be copied from the Element.dataset. + * If the copy was successful a message is displayed to the user. * @private - * @return undefined */ _copyColorToClipboardHandler(ev) { const [ property, selector ] = ev.currentTarget.dataset.copy.split(':'); const value = $(selector)[property]; if (Copy(value)) { - this.ui.toast.show(`${value} copied`); + this.ui.toast.show(`“${value}” copied to clipboard`); } } /** - * Handles app.ui.randomBtn’s click event + * Handles app.ui.randomButton’s click event. + * Calls app._updateUI passing a random hex string as param. * @private - * @return undefined */ _randomClickHandler() { this._updateUI(getRandomHex()); } /** - * Handles Input’s isValid custom event + * Handles Input’s validChange custom event. + * Calls app._updateUI passing the color received on the events’s detail and the referece to the input as params.preventElementUpdate for the input itself to not be updated. * @private - * @return undefined */ _isInputValidHandler(ev) { - this._updateUI(ev.detail.text(), { + this._updateUI(ev.detail.color(), { preventElementUpdate: ev.target }); } /** - * Handles popstate event - * Defaults to location.hash if state or state.color is falsy + * Handles popstate event. + * Gets the color data from the event state. If no color in state it defaults to location.hash. + * Returns if the color data is not valid. + * Updates the UI with the passed color and preventHistoryUpdate otherwise. * @private - * @return undefined */ _popStateHandler(ev) { let color = (ev.state && ev.state.color) || location.hash; @@ -216,58 +205,62 @@ export default class App { } /** - * Updates the UI with passed color param. - * Sets new app.color instance with to get color space conversions - * Creates a new history state if - * @param {string} [color='#000'] + * Updates the main UI color. + * @param {string} [color='#000'] - String color to update the whole UI with. * @param {Object} [params={}] - * @property {boolean=} preventHistoryUpdate - * @property {HTMLInputElement=} preventElementUpdate do not update this element + * @param {boolean} [params.preventHistoryUpdate=false] - Do not add a state to the browser’s session history stack. + * @param {HTMLInputElement=} params.preventElementUpdate - This input element will not be updated. * @private */ - _updateUI(color = '#000', params = {}) { + _updateUI(color = '#000000', params = {}) { + const { preventHistoryUpdate = false, preventElementUpdate } = params; const app = this; - const { preventHistoryUpdate, preventElementUpdate } = params; app.color = Color(color); app.pickr.setColor(color); - let hex = app.color.hex(); - let hexa = app.pickr.getColor().toHEXA().toString(); - let rgba = app.color.rgb().string(); - let hsla = app.color.hsl().string(0); - let hwb = app.color.hwb().string(0); - let alpha = app.color.valpha; - let [ r, g, b] = app.color.unitArray(); + const hex = app.color.hex(); + const hexa = app.pickr.getColor().toHEXA().toString(); + const rgba = app.color.rgb().round().array().toString(); + const hsla = app.color.hsl().round().array().toString(); + const hwb = app.color.hwb().round().array().toString(); + const hsv = app.color.hsv().round().array().toString(); + const cmyk = app.color.cmyk().round().array().toString(); + const lab = app.color.lab().round().array().toString(); - let isLight = app.color.isLight(); - let textColor = app.color.mix(Color(isLight ? '#000' : '#fff'), 0.8); - let borderColor = app.color.mix(Color(isLight ? '#000' : '#fff'), 0.2); + const isLight = app.color.isLight(); + const textColor = app.color.mix(Color(isLight? '#000' : '#fff'), 0.8); + const borderColor = app.color.mix(Color(isLight? '#000' : '#fff'), 0.2); - if (preventElementUpdate !== app.ui.hexInput.element) { - app.ui.hexInput.setValue(hexa); - } + if (preventElementUpdate !== app.ui.hexInput.element) + app.ui.hexInput.setValue(hexa.replace('#', '')); - if (preventElementUpdate !== app.ui.rgbInput.element) { + if (preventElementUpdate !== app.ui.rgbInput.element) app.ui.rgbInput.setValue(rgba); - } - if (preventElementUpdate !== app.ui.hslInput.element) { + if (preventElementUpdate !== app.ui.hslInput.element) app.ui.hslInput.setValue(hsla); - } - if (preventElementUpdate !== app.ui.hwbInput.element) { + if (preventElementUpdate !== app.ui.hwbInput.element) app.ui.hwbInput.setValue(hwb); - } - app.ui.fcmInput.value = `${r.toFixed(2)}\t0\t0\t0\t0\n0\t${g.toFixed(2)}\t0\t0\t0\n0\t0\t${b.toFixed(2)}\t0\t0\n0\t0\t0\t1\t0`; + if (preventElementUpdate !== app.ui.hsvInput.element) + app.ui.hsvInput.setValue(hsv); + + if (preventElementUpdate !== app.ui.cmykInput.element) + app.ui.cmykInput.setValue(cmyk); + + if (preventElementUpdate !== app.ui.labInput.element) + app.ui.labInput.setValue(lab); + + app.ui.fcmInput.setValue(app.color.unitArray()); - document.documentElement.style.setProperty('--cc-main-color', hsla); + document.documentElement.style.setProperty('--cc-main-color', app.color.hsl()); document.documentElement.style.setProperty('--cc-main-solid-color', hex); document.documentElement.style.setProperty('--cc-text-color', textColor.toString()); document.documentElement.style.setProperty('--cc-border-color', borderColor.toString()); - if (!preventHistoryUpdate) { + if (preventHistoryUpdate === false) { history.pushState({ color: hexa }, null, `${hexa}`); document.title = `${app.config.title} - ${hexa}`; } diff --git a/src/js/lib/components/fe-color-matrix-input.js b/src/js/lib/components/fe-color-matrix-input.js new file mode 100644 index 0000000..9fcb372 --- /dev/null +++ b/src/js/lib/components/fe-color-matrix-input.js @@ -0,0 +1,57 @@ +/** + * Handle feColorMatrix hidden texarea updates. + * @module FeColorMatrixInput + */ +const { round } = Math; + +const internals = { + /** + * Automatically adds 0, 1 or 2 decimals. + * @param {number} num + * @return {number} + */ + formatDecimals: (num) => round(num * 100) / 100 +}; + +export default class FeColorMatrixInput { + /** + * Create FeColorMatrixInput instance. + * @param {Object} config - override default config. + * @param {NodeElement} [config.el=null] - Main widget element. + * @return {this} + */ + constructor(config = {}) { + Object.assign(this.config = {}, { + el: null + }, config); + } + + /** + * @param {Array} arr - An array containing the red, green, blue and alpha values. + * @param {number} arr.r - red channel + * @param {number} arr.g - green channel + * @param {number} arr.b - blue channel + * @param {number} [arr.a=1] - alpha channel + * @return {this} + */ + setValue([r, g, b, a = 1]) { + this.config.el.value = this._getColorMatrixValue(r, g, b, a); + return this; + } + + /** + * Format feColorMatrix’s values + * @param {number} r - red channel + * @param {number} g - green channel + * @param {number} b - blue channel + * @param {number} a - alpha channel + * @private + * @return {string} 5x5 matrix identity + */ + _getColorMatrixValue(...channels) { + return `${internals.formatDecimals(channels[0])}\t0\t0\t0\t0 + \n0\t${internals.formatDecimals(channels[1])}\t0\t0\t0 + \n0\t0\t${internals.formatDecimals(channels[2])}\t0\t0 + \n0\t0\t0\t${internals.formatDecimals(channels[3])}\t0`; + } +} diff --git a/src/js/lib/components/input-hex.js b/src/js/lib/components/input-hex.js new file mode 100644 index 0000000..acbc67d --- /dev/null +++ b/src/js/lib/components/input-hex.js @@ -0,0 +1,39 @@ +/** + * Handle hex input validations and updates. + * @module InputHex + * @extends Input + */ +import Color from 'color'; +import Input from '~/src/js/lib/components/input'; + +const internals = { + /** + * Checks if an input value is a valid hex color form for the `color` library. + * If it does not contains a `#` character it will prepend it before the check. + * @return {boolean} + */ + validateHexColorFromInput(input) { + const el = input.element; + el.value = el.value.toUpperCase(); + let value = (el.value.includes('#')? el.value : `#${el.value}`); + + try { + Color(value); + input.validFormat = value.replace('#', ''); + input.validColorString = value; + return true; + } + catch (err) { + return false; + } + } +} + +export default class InputHex extends Input { + constructor(config) { + super({ + ...config, + validate: internals.validateHexColorFromInput + }); + } +} diff --git a/src/js/lib/components/input-numeric.js b/src/js/lib/components/input-numeric.js new file mode 100644 index 0000000..e6faf56 --- /dev/null +++ b/src/js/lib/components/input-numeric.js @@ -0,0 +1,65 @@ +/** + * Handle numeric input validations and updates. + * @module InputNumeric + * @extends Input + */ +import Color from 'color'; +import Input from '~/src/js/lib/components/input'; + +const internals = { + /** + * Match any character that is not a digit (0-9), a dot '.' or a dash '-'. + * */ + reNonDigit: new RegExp(/[^\d.-]/g), + + /** + * Remove any character that is not a digit, a dot or a dash. + * @param {string} str - string to search and replace + * @return {string} a new string with the specified values removed + */ + digit: (str) => str.replace(internals.reNonDigit, ''), + + /** + * Transform a string to a number primitive. + * @param {string} str + * @retun {number} + */ + number: (str) => Number(str), + // number: (str) => +str, + + /** + * Checks if an input value is a valid color for the `color` library by passing an array of values and its color model. + * @return {boolean} + */ + validateColorFromNumericInput(input) { + const el = input.element; + let value = el.value.split(',').map(internals.digit).map(internals.number); + + if (value.length < 3) + return false; + + if (value.every(n => isFinite(n)) === false) + return false; + + try { + let color = Color(value, input.config.model); + + input.validFormat = color.round().array().toString(); + input.validColorString = color.round().string(); + + return true; + } + catch (err) { + return false; + } + } +}; + +export default class InputNumeric extends Input { + constructor(config) { + super({ + ...config, + validate: internals.validateColorFromNumericInput + }); + } +} diff --git a/src/js/lib/components/input.js b/src/js/lib/components/input.js index c7b75e5..a4e2724 100644 --- a/src/js/lib/components/input.js +++ b/src/js/lib/components/input.js @@ -1,104 +1,132 @@ -const { log } = console; - -import { autoBind } from '~/src/js/lib/utils'; - -const internals = { - rs: (string) => string.replace(/\s/g,'') -}; +/** + * Inputs base class. + * @module Input + */ +import { debounce } from '~/src/js/lib/utils'; export default class Input { - static get defaultProps() { - return { - config: {}, - latestValidValue: '' - } - } + /** + * Create Input instance. + * @param {Object} config - override default config. + * @param {NodeElement} [config.el=null] - Main widget element. + * @param {string} [config.model=null] - Input color model supported by the `color` library. + * @param {number} [config.debounceMilliseconds=300] - Delay in milliseconds for debounced input event. + * @param {Function} [config.validate=() => true] - Custom function to perform input validations. + * @return {this} + */ + constructor(config) { + const input = this; - static get defaultConfig() { - return { + Object.assign(input.config = {}, { el: null, + model: null, + debounceMilliseconds: 300, validate: () => true - } - } + }, config); - constructor(config) { - const input = this; - - Object.assign(input, input.constructor.defaultProps); - Object.assign(input.config, input.constructor.defaultConfig, config); + input.events = {}; + input.validColorString = null; + input.validFormat = null; + input.latestValidValue = null; input._bindEvents(); } - enable() { - this.config.el.removeAttribute('disabled'); - return this; - } - get element() { return this.config.el; } - get value() { - return this.element.value; - } - - set value(value) { - this.config.element.value = value; + /** + * Removes input’s attribute `disabled`. + * @return {this} + */ + enable() { + this.element.removeAttribute('disabled'); + return this; } + /** + * Updates the input’s value. + * @return {this} + */ setValue(value) { const input = this; - input.element.value = value; - input.latestValidValue = value; + if (value !== input.latestValidValue) { + input.element.value = value; + input.latestValidValue = value; + input.validFormat = value; + } return input; } + /** + * Register event listeners. + * @private + * @return {this} + */ _bindEvents() { const input = this; - input.events = {}; - input.events.isValid = new CustomEvent('isValid', { - bubbles: true, - testData: 100, + input.events.validChange = new CustomEvent('validChange', { detail: { - el: input.element, - text: () => input.element.value + color: () => input.validColorString } }); - autoBind([ - [ input.config.el, 'input', '_inputHandler' ] - ], input); + input._inputHandler = input._inputHandler.bind(input); + input.element.addEventListener('input', debounce(input._inputHandler, input.config.debounceMilliseconds)); + + input._blurHandler = input._blurHandler.bind(input); + input.element.addEventListener('blur', input._blurHandler); return input; } - _inputHandler(ev) { - const input = this; + /** + * Compares the current valid input format with the previously emitted value. + * @private + * @return {boolean} + */ + _hasChanged() { + return (this.latestValidValue !== this.validFormat); + } - if (input.config.validate) { - if (input.config.validate(input.element.value)) { - input.element.setAttribute('aria-invalid', 'false'); - if (internals.rs(input.latestValidValue) !== internals.rs(input.element.value)) { - input.latestValidValue = input.element.value; - input.element.dispatchEvent(input.events.isValid); - } - return input; - } + /** + * Input’s input event handler. + * @private + * @emits {validChange} if the `config.validate` function resolves with a truthy value and the input’s value is different than the previously emitted value. + */ + _inputHandler() { + const input = this; + if (!input.config.validate(input)) { input.element.setAttribute('aria-invalid', 'true'); + return; + } + + input.element.setAttribute('aria-invalid', 'false'); - return input; + if (input._hasChanged()) { + input.latestValidValue = input.validFormat; + input.element.dispatchEvent(input.events.validChange); } + } + + /** + * Input’s blur event handler. + * Restores the latest valid value if needed. + * @private + */ + _blurHandler() { + const input = this; + const validFormat = input.validFormat; - // if (this.el.validity.valid) { - // if (rs(this.latestValidValue) !== rs(this.el.value)) { - // this.latestValidValue = this.el.value; - // this.el.dispatchEvent(this.events.isValid); - // } - // } + if (validFormat && (input.element.value !== validFormat)) { + input.element.value = validFormat; + input.latestValidValue = validFormat; + input.element.setAttribute('aria-invalid', 'false'); + } } } diff --git a/src/js/lib/components/toast.js b/src/js/lib/components/toast.js index 14e52ba..305b6aa 100644 --- a/src/js/lib/components/toast.js +++ b/src/js/lib/components/toast.js @@ -1,30 +1,32 @@ -const { log } = console; - +/** + * Provide messages about app processes at the top-left of the screen. + * @module Toast + */ export default class Toast { - static get defaultProps() { - return { - config: {}, - item: null, - _timeout: null - } - } + /** + * Create Toast instance. + * @param {Object} config - override default config. + * @param {NodeElement} [config.el=null] - Main widget element. + * @param {number} [config.duration=2500] - Total time to show the message in milliseconds. + * @return {this} + */ + constructor(config = {}) { + const toast = this; - static get defaultConfig() { - return { + Object.assign(toast.config = {}, { el: null, duration: 2500 - } - } - - constructor(config) { - const toast = this; - - Object.assign(toast, toast.constructor.defaultProps); - Object.assign(toast.config, toast.constructor.defaultConfig, config); + }, config); toast.item = toast.config.el.querySelector('.toast'); + toast._timeout = null; } + /** + * Show app message for "config.duration" time. + * @param {string} text - message to be displayed. + * @return {this} + */ show(text) { const toast = this; @@ -32,7 +34,10 @@ export default class Toast { toast.config.el.classList.add('show'); - if (toast._timeout) clearTimeout(toast._timeout); + if (toast._timeout) { + clearTimeout(toast._timeout); + } + toast._timeout = setTimeout(() => { clearTimeout(toast._timeout); toast.config.el.classList.remove('show'); diff --git a/src/js/lib/utils/index.js b/src/js/lib/utils/index.js index 4b7f301..45137fd 100644 --- a/src/js/lib/utils/index.js +++ b/src/js/lib/utils/index.js @@ -1,14 +1,15 @@ +/** @module utils */ + +/** querySelector alias */ export const $ = document.querySelector.bind(document); +/** queryAllSelector alias */ export const $$ = document.querySelectorAll.bind(document); /** * Returns a random hexadecimal color code without the hash. - * @property getRandomHexColor {Function} - * @return 000000 {String} + * @returns 000000 {String} */ -export function getRandomHex() { - return `#${Math.random().toString(16).slice(2, 8)}`; -} +export const getRandomHex = () => `#${Math.random().toString(16).slice(2, 8)}`; /** * @usage @@ -16,7 +17,7 @@ export function getRandomHex() { * [ , <'eventname'>, * ], ) */ -export function autoBind(arr, ctx) { +export const autoBind = (arr, ctx) => { const listen = (node, ev, handler) => node.addEventListener(ev, handler, false); arr.forEach(rule => { ctx[rule[2]] = ctx[rule[2]].bind(ctx); @@ -26,3 +27,18 @@ export function autoBind(arr, ctx) { return listen(rule[0], rule[1], ctx[rule[2]]); }); } + +/** + * Limit the amount of times a function is called. + * @see https://medium.com/@TCAS3/debounce-deep-dive-javascript-es6-e6f8d983b7a1 + */ +export const debounce = (fn, time) => { + let timeout = null; + + return function debounceFn() { + const functionCall = () => fn.apply(this, arguments); + + clearTimeout(timeout); + timeout = setTimeout(functionCall, time); + } +}