├── .gitignore
├── LICENSE
├── README.md
├── advanced_color_picker.zip
├── config
├── babel.config.js
├── eslint.config.js
└── webpack.config.js
├── dist
├── img
│ ├── github.jpg
│ └── transparent.png
├── index.html
└── js
│ ├── cj-color-chunk-120.min.js
│ ├── cj-color-chunk-120.min.js.LICENSE.txt
│ ├── cj-color-chunk-606.min.js
│ ├── cj-color.min.js
│ └── cj-color.min.js.LICENSE.txt
├── package-lock.json
├── package.json
├── screenshot.jpg
├── single_screenshot.jpg
└── src
├── js
├── components
│ ├── buttons
│ │ ├── button-group.js
│ │ ├── button.js
│ │ ├── copy-btn.js
│ │ ├── range-buttons.js
│ │ └── save-btn.js
│ ├── grid.js
│ ├── icon.js
│ ├── inputs
│ │ ├── input-field.js
│ │ └── number-input.js
│ ├── radio-menu.js
│ ├── select-box.js
│ ├── select-box
│ │ └── select-color.js
│ ├── sliders
│ │ ├── drag-slider.js
│ │ └── range-slider.js
│ ├── toggle.js
│ ├── wheel.js
│ └── wrappers
│ │ ├── input-wrap.js
│ │ ├── panel.js
│ │ ├── row.js
│ │ ├── sortable.js
│ │ └── wrapper.js
├── context.js
├── data
│ ├── color-names.js
│ ├── defaults.js
│ └── presets.js
├── error.js
├── hoc
│ ├── draggable.js
│ ├── scrollable.js
│ └── with-context.js
├── index.js
├── loader.js
├── module.js
├── module
│ ├── container.js
│ ├── editor.js
│ ├── editor
│ │ ├── controls.js
│ │ ├── controls
│ │ │ ├── color-controls.js
│ │ │ ├── color-controls
│ │ │ │ ├── color-buttons.js
│ │ │ │ ├── color-fields.js
│ │ │ │ ├── color-fields
│ │ │ │ │ └── color-inputs.js
│ │ │ │ ├── color-list.js
│ │ │ │ ├── color-palette.js
│ │ │ │ └── color-presets.js
│ │ │ ├── gradient-controls.js
│ │ │ └── gradient-controls
│ │ │ │ ├── gradient-switcher.js
│ │ │ │ └── radial-controls.js
│ │ ├── footer.js
│ │ ├── footer
│ │ │ └── user-input.js
│ │ ├── header.js
│ │ ├── header
│ │ │ ├── strip.js
│ │ │ └── strip
│ │ │ │ ├── color-point.js
│ │ │ │ └── hint-point.js
│ │ ├── presets.js
│ │ ├── presets
│ │ │ ├── preset-items.js
│ │ │ └── preset-items
│ │ │ │ ├── delete-preset.js
│ │ │ │ └── preset.js
│ │ ├── sidepanels.js
│ │ └── sidepanels
│ │ │ ├── hints.js
│ │ │ ├── hints
│ │ │ └── hint-pair.js
│ │ │ └── preview.js
│ └── full-preview.js
├── settings.js
└── utils
│ ├── colors.js
│ ├── data.js
│ ├── editor.js
│ ├── global.js
│ ├── gradients.js
│ ├── hsl.js
│ ├── output.js
│ ├── presets.js
│ ├── regexp.js
│ └── utilities.js
└── scss
├── editor.scss
└── swatch.scss
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .DS_STORE
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Jason McElwaine
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Advanced Color Picker
2 | Harnessing the full power of CSS Gradients
3 | (one of the most advanced color pickers on the planet. fight me)
4 |
5 | 
6 |
7 | ## Description
8 |
9 | Advanced Color Picker includes full support for modern CSS Gradients, with the ability to translate any valid CSS Gradient into editable controls.
10 |
11 | ## Features
12 |
13 | * **Stacked Gradients** - Bleed semi-transparent gradients into one another
14 | * **Color Hints** - Change the midpoint transition point between colors
15 | * **Pixel-based units** - Set positions to percentage or pixel-based values
16 | * **Repeating Gradients** - Create interesting patterns with pixel-based units
17 | * **Conic Gradients** - Experiment with conic gradients supported in Chrome & Safari
18 | * **Simple Editor Mode** - Can be used for non-gradient editing (text-color, etc.) via "single" mode
19 | * **Copy/Paste Gradients** - Copy any gradient from the web and paste it into the editor (it's like magic)
20 |
21 | ## Getting Started
22 |
23 | [Download](https://github.com/CodingJack/Advanced-Color-Picker/raw/master/advanced_color_picker.zip) the plugin and copy the files inside the "js" folder to your site. Then add the main script to your web page, setup an input field to be used for the swatch, and "init" the plugin with your custom settings.
24 |
25 | ## Basic Setup & Options
26 |
27 | ```html
28 |
29 |
30 |
31 |
32 |
33 | ```
34 |
35 | ```js
36 | // initial call with custom settings and their defaults
37 | window.advColorPicker( {
38 | // "full" = all controls, "single" = only color controls (no gradients)
39 | mode: 'full',
40 |
41 | // the size of the color picker swatches
42 | size: 24,
43 |
44 | // the color picker swatch skin, "classic" or "light"
45 | skin: 'classic',
46 |
47 | // optional color for the modal background
48 | modalBgColor: 'rgba(0,0,0,0.5)',
49 |
50 | // optional id attribute to apply to the editor's outermost wrapper
51 | editorId: null,
52 |
53 | // allow multi-color stops in output
54 | // multi-color stops allow for condensed output but are not supported in Edge
55 | multiStops: true,
56 |
57 | // allow conic gradients (only supported in webkit browsers)
58 | conic: true,
59 |
60 | // show a warning note for conic gradients (if conic is enabled)
61 | conicNote: false,
62 |
63 | // show the bar at the bottom of the screen displaying the final output value
64 | outputBar: false,
65 |
66 | // set the value of your input when a color is changed
67 | onColorChange: ( input, color ) => input.value = color,
68 |
69 | // your default and/or custom color presets
70 | colorPresets: { defaults: [], custom: [] },
71 |
72 | // your default and/or gradient presets
73 | gradientPresets: { defaults: [], custom: [] },
74 |
75 | // your save/delete preset callback function
76 | onSaveDeletePreset,
77 | } );
78 | ```
79 |
80 | ## data-attr options for input fields
81 | * **type** - "hidden" or "text" required
82 | * **class** - must match the "inputClass" const inside the index.js source file (currently: "cj-colorpicker")
83 | * **value** - any valid CSS color (an empty value will translate to "transparent")
84 | * **data-mode** - "single" (only color controls) or "full" (colors + gradient controls) - default: "full"
85 | * **data-size** - the width/height of the swatch - default: "24"
86 | * **data-skin** - "classic" or "light", the swatch skin - default: "classic"
87 | ```html
88 |
96 | ```
97 |
98 | ## Example "onColorChange" callback
99 | ```js
100 | const onColorChange = ( input, color ) => input.value = color;
101 | ```
102 |
103 | ## Example "onSaveDeletePreset" callback
104 | ```js
105 | const onSaveDeletePreset = ( {
106 | action, // "save" or "delete"
107 | groupChanged, // "color" or "gradient"
108 | colorPresets, // the current custom color presets array
109 | gradientPresets, // the current custom gradient presets array
110 | } ) => {
111 | // example saving to local storage
112 | window.localStorage.setItem( 'presets', JSON.stringify( {
113 | colorPresets,
114 | gradientPresets,
115 | }));
116 | };
117 | ```
118 |
119 | ## Editing JS/SCSS source files
120 | ```
121 | npm install
122 | npm run watch
123 | npm run build
124 | ```
125 |
126 | ## Built With / Technology Used
127 |
128 | * [React](https://www.npmjs.com/package/react)
129 | * [SASS](https://www.npmjs.com/package/sass)
130 | * [Babel](https://www.npmjs.com/package/@babel/core)
131 | * [Webpack](https://www.npmjs.com/package/webpack)
132 | * [ESLint](https://www.npmjs.com/package/eslint)
133 | * [core-js](https://www.npmjs.com/package/core-js)
134 | * [array-move](https://www.npmjs.com/package/array-move)
135 | * [React Sortable HOC](https://www.npmjs.com/package/react-sortable-hoc)
136 | * [Material Icons](https://www.npmjs.com/package/material-icons)
137 |
138 | ## Authors
139 |
140 | * **Jason McElwaine** - *Initial work*
141 |
142 | ## License
143 |
144 | * The original work in this project is licensed under [MIT](https://opensource.org/licenses/MIT)
145 | * All dependencies and cited technology above excluding Material Icons is licensed under [MIT](https://opensource.org/licenses/MIT)
146 | * [Material Icons](https://www.npmjs.com/package/material-icons) is licensed under [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0)
147 |
148 | ## Additional Notes
149 |
150 | * If used in cases where all browsers must be accounted for, set the "multiStops" and "conic" options to false.
151 | * The APP does not automatically write values to the corresponding input field (intentionally). So the init settings should always include an "onColorChange" callback function.
152 | * Pixel based units for positioning and radial sizes have a maximum value of 800px in order to translate them properly into the editor visually.
153 | * drag the mini-preview to dynamically change radial and conic gradient positioning
154 |
155 | ## Single Mode
156 |
157 | * Where the editor is restricted to single colors only (for text color, etc.)
158 |
159 | 
160 |
--------------------------------------------------------------------------------
/advanced_color_picker.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodingJack/Advanced-Color-Picker/1efe6d0a51a2ccdb1be2c091acf0e7c788b672d0/advanced_color_picker.zip
--------------------------------------------------------------------------------
/config/babel.config.js:
--------------------------------------------------------------------------------
1 | const presets = [
2 | [
3 | '@babel/preset-env',
4 | {
5 | targets: {
6 | browsers: ['last 2 versions', 'safari >= 7'],
7 | },
8 | useBuiltIns: 'usage',
9 | corejs: 3,
10 | },
11 | '@babel/preset-react',
12 | ],
13 | ];
14 | const plugins = [
15 | '@babel/plugin-proposal-class-properties',
16 | '@babel/plugin-transform-react-jsx',
17 | ];
18 |
19 | module.exports = { presets, plugins };
20 |
--------------------------------------------------------------------------------
/config/eslint.config.js:
--------------------------------------------------------------------------------
1 | const path = require( 'path' );
2 | const dir = path.resolve( __dirname, '' );
3 |
4 | module.exports = {
5 | overrideConfig: {
6 | env: {
7 | browser: true,
8 | es6: true,
9 | },
10 | plugins: [
11 | 'react',
12 | 'jsx-a11y',
13 | ],
14 | settings: {
15 | react: {
16 | version: 'detect',
17 | },
18 | },
19 | extends: [
20 | 'eslint:recommended',
21 | 'plugin:react/recommended',
22 | 'plugin:jsx-a11y/recommended',
23 | ],
24 | globals: {
25 | require: 'readonly',
26 | ReactDOM: 'readonly',
27 | __webpack_public_path__: 'writable',
28 | },
29 | parser: '@babel/eslint-parser',
30 | parserOptions: {
31 | sourceType: 'module',
32 | ecmaFeatures: {
33 | ecmaVersion: 6,
34 | impliedStrict: true,
35 | jsx: true,
36 | },
37 | babelOptions: {
38 | configFile: `${ dir }/babel.config.js`,
39 | },
40 | requireConfigFile: false,
41 | },
42 | rules: {
43 | 'no-unused-vars': 'error',
44 | 'react/jsx-uses-react': 'error',
45 | 'react/jsx-uses-vars': 'error',
46 | 'no-cond-assign': 'error',
47 | 'no-template-curly-in-string': 'error',
48 | 'no-eval': 'error',
49 | 'no-floating-decimal': 'error',
50 | 'no-implicit-globals': 'error',
51 | 'no-implied-eval': 'error',
52 | 'no-lone-blocks': 'error',
53 | 'no-multi-spaces': 'error',
54 | 'no-multi-str': 'error',
55 | 'no-new': 'error',
56 | 'no-new-func': 'error',
57 | 'no-new-wrappers': 'error',
58 | 'no-param-reassign': 'error',
59 | 'no-return-assign': 'error',
60 | 'no-script-url': 'error',
61 | 'no-self-compare': 'error',
62 | 'no-sequences': 'error',
63 | 'no-unmodified-loop-condition': 'error',
64 | 'no-unused-expressions': 'error',
65 | 'no-useless-call': 'error',
66 | 'no-useless-concat': 'error',
67 | 'no-useless-return': 'error',
68 | radix: 'error',
69 | yoda: 'error',
70 | 'no-delete-var': 'error',
71 | 'no-label-var': 'error',
72 | 'no-useless-escape': 0,
73 | 'react/prop-types': 0,
74 | 'jsx-a11y/click-events-have-key-events': 0,
75 | 'jsx-a11y/no-static-element-interactions': 0,
76 | 'react/display-name': 0,
77 | 'react/jsx-no-target-blank': 0,
78 | },
79 | },
80 | };
81 |
--------------------------------------------------------------------------------
/config/webpack.config.js:
--------------------------------------------------------------------------------
1 | const ESLintPlugin = require( 'eslint-webpack-plugin' );
2 | const TerserPlugin = require( 'terser-webpack-plugin' );
3 | const resolve = require( 'path' ).resolve;
4 | const webpack = require( 'webpack' );
5 |
6 | module.exports = env => {
7 | const plugins = [
8 | new ESLintPlugin( require( './eslint.config.js' ) ),
9 | ];
10 | const { WEBPACK_BUILD: production } = env;
11 | if( ! production ) {
12 | plugins.push( new webpack.SourceMapDevToolPlugin( {} ) );
13 | }
14 | return {
15 | target: 'web',
16 | output: {
17 | path: resolve( 'dist' ),
18 | filename: 'cj-color.min.js',
19 | chunkFilename: 'cj-color-chunk-[id].min.js',
20 | publicPath: 'js/',
21 | },
22 | resolve: {
23 | alias: {
24 | 'react-dom$': 'react-dom/profiling',
25 | 'scheduler/tracing': 'scheduler/tracing-profiling',
26 | },
27 | },
28 | module: {
29 | noParse: [
30 | /benchmark/,
31 | ],
32 | rules: [
33 | {
34 | test: /\.js$/,
35 | enforce: 'pre',
36 | exclude: /node_modules/,
37 | use: [
38 | {
39 | loader: 'babel-loader',
40 | options: require( './babel.config.js' ),
41 | },
42 | {
43 | loader: 'source-map-loader',
44 | options: {},
45 | },
46 | ],
47 | },
48 | {
49 | test: /\.(s*)css$/,
50 | use: [
51 | {
52 | loader: 'style-loader',
53 | options: {
54 | injectType: 'styleTag',
55 | },
56 | },
57 | {
58 | loader: 'css-loader',
59 | options: {
60 | sourceMap: true,
61 | },
62 | },
63 | {
64 | loader: 'sass-loader',
65 | options: {
66 | sourceMap: true,
67 | },
68 | },
69 | ],
70 | },
71 | ],
72 | },
73 | stats: 'error-details',
74 | optimization: {
75 | minimizer: [
76 | new TerserPlugin( {
77 | terserOptions: {
78 | output: {
79 | comments: false,
80 | },
81 | },
82 | extractComments: true,
83 | } ),
84 | ],
85 | },
86 | plugins,
87 | devtool: false,
88 | performance: {
89 | hints: false,
90 | maxEntrypointSize: 300000,
91 | maxAssetSize: 300000
92 | },
93 | };
94 | }
95 |
--------------------------------------------------------------------------------
/dist/img/github.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodingJack/Advanced-Color-Picker/1efe6d0a51a2ccdb1be2c091acf0e7c788b672d0/dist/img/github.jpg
--------------------------------------------------------------------------------
/dist/img/transparent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodingJack/Advanced-Color-Picker/1efe6d0a51a2ccdb1be2c091acf0e7c788b672d0/dist/img/transparent.png
--------------------------------------------------------------------------------
/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
32 |
33 |
34 |
67 |
68 |
69 |
70 |
71 |
160 |
161 |
165 |
168 |
169 |
170 |
188 |
189 |
190 |
206 |
207 |
212 |
229 |
230 |
231 |
238 |
239 |
--------------------------------------------------------------------------------
/dist/js/cj-color-chunk-120.min.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /**
2 | * A better abstraction over CSS.
3 | *
4 | * @copyright Oleg Isonen (Slobodskoi) / Isonen 2014-present
5 | * @website https://github.com/cssinjs/jss
6 | * @license MIT
7 | */
8 |
9 | /** @license React v16.13.1
10 | * react-is.production.min.js
11 | *
12 | * Copyright (c) Facebook, Inc. and its affiliates.
13 | *
14 | * This source code is licensed under the MIT license found in the
15 | * LICENSE file in the root directory of this source tree.
16 | */
17 |
--------------------------------------------------------------------------------
/dist/js/cj-color.min.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*
2 | object-assign
3 | (c) Sindre Sorhus
4 | @license MIT
5 | */
6 |
7 | /** @license React v0.20.2
8 | * scheduler-tracing.profiling.min.js
9 | *
10 | * Copyright (c) Facebook, Inc. and its affiliates.
11 | *
12 | * This source code is licensed under the MIT license found in the
13 | * LICENSE file in the root directory of this source tree.
14 | */
15 |
16 | /** @license React v0.20.2
17 | * scheduler.production.min.js
18 | *
19 | * Copyright (c) Facebook, Inc. and its affiliates.
20 | *
21 | * This source code is licensed under the MIT license found in the
22 | * LICENSE file in the root directory of this source tree.
23 | */
24 |
25 | /** @license React v17.0.2
26 | * react-dom.profiling.min.js
27 | *
28 | * Copyright (c) Facebook, Inc. and its affiliates.
29 | *
30 | * This source code is licensed under the MIT license found in the
31 | * LICENSE file in the root directory of this source tree.
32 | */
33 |
34 | /** @license React v17.0.2
35 | * react.production.min.js
36 | *
37 | * Copyright (c) Facebook, Inc. and its affiliates.
38 | *
39 | * This source code is licensed under the MIT license found in the
40 | * LICENSE file in the root directory of this source tree.
41 | */
42 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "advanced-color-picker",
3 | "version": "1.1.0",
4 | "description": "Advanced Color Picker",
5 | "author": "CodingJack",
6 | "license": "MIT",
7 | "keywords": [
8 | "Color",
9 | "Gradient",
10 | "Color Picker"
11 | ],
12 | "homepage": "https://www.codingjack.com/advanced-color-picker",
13 | "repository": {
14 | "type": "git",
15 | "url": "https://github.com/CodingJack/Advanced-Color-Picker"
16 | },
17 | "bugs": {
18 | "url": "https://github.com/CodingJack/Advanced-Color-Picker",
19 | "email": "support@codingjack.com"
20 | },
21 | "main": "index.js",
22 | "dependencies": {
23 | "array-move": "^3.0.1",
24 | "fix": "^0.0.6",
25 | "react": "^17.0.2",
26 | "react-dom": "^17.0.2",
27 | "react-sortable-hoc": "^2.0.0"
28 | },
29 | "devDependencies": {
30 | "@babel/core": "^7.15.0",
31 | "@babel/eslint-parser": "^7.15.0",
32 | "@babel/plugin-proposal-class-properties": "^7.14.5",
33 | "@babel/plugin-transform-react-jsx": "^7.14.9",
34 | "@babel/preset-env": "^7.15.0",
35 | "@babel/preset-react": "^7.14.5",
36 | "@material-ui/core": "^4.12.3",
37 | "@material-ui/icons": "^4.11.2",
38 | "admin-bro": "^3.2.5",
39 | "babel-loader": "^8.2.2",
40 | "core-js": "^3.16.1",
41 | "css-loader": "^6.2.0",
42 | "eslint": "^7.32.0",
43 | "eslint-plugin-jsx-a11y": "^6.4.1",
44 | "eslint-plugin-react": "^7.24.0",
45 | "eslint-webpack-plugin": "^3.0.1",
46 | "express": "^4.17.1",
47 | "node-sass": "^6.0.1",
48 | "sass-loader": "^12.1.0",
49 | "source-map-loader": "^3.0.0",
50 | "style-loader": "^3.2.1",
51 | "terser-webpack-plugin": "^5.1.4",
52 | "webpack": "^5.49.0",
53 | "webpack-cli": "^4.7.2"
54 | },
55 | "scripts": {
56 | "watch": "webpack --watch ./src/js/index.js --output-path ./dist/js --config ./config/webpack.config.js --mode=development",
57 | "build": "webpack ./src/js/index.js --output-path ./dist/js --config ./config/webpack.config.js --mode=production"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/screenshot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodingJack/Advanced-Color-Picker/1efe6d0a51a2ccdb1be2c091acf0e7c788b672d0/screenshot.jpg
--------------------------------------------------------------------------------
/single_screenshot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodingJack/Advanced-Color-Picker/1efe6d0a51a2ccdb1be2c091acf0e7c788b672d0/single_screenshot.jpg
--------------------------------------------------------------------------------
/src/js/components/buttons/button-group.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Button from './button';
3 |
4 | import {
5 | AppContext,
6 | } from '../../context';
7 |
8 | const {
9 | memo,
10 | useContext,
11 | } = React;
12 |
13 | /*
14 | * @desc creates a group of buttons that act like radio inputs
15 | * @since 1.0.0
16 | */
17 | const ButtonGroup = ({ slug, type, items, active, onChange }) => {
18 | const locale = useContext(AppContext);
19 | const { namespace } = locale;
20 |
21 | return (
22 |
23 | {
24 | Object.keys(items).map(key => {
25 | return (
26 |
37 | );
38 | };
39 |
40 | export default memo(ButtonGroup);
--------------------------------------------------------------------------------
/src/js/components/buttons/button.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Icon from '../icon';
3 |
4 | import {
5 | AppContext,
6 | } from '../../context';
7 |
8 | const {
9 | memo,
10 | useContext,
11 | } = React;
12 |
13 | /*
14 | * @desc creates a clickable button that could have a variety of visual states
15 | * @since 1.0.0
16 | */
17 | const Button = ({
18 | type,
19 | icon,
20 | label,
21 | color,
22 | active,
23 | onClick,
24 | disabled,
25 | className,
26 | activeState,
27 | activeDisabled,
28 | dataAttrs = {},
29 | }) => {
30 | const locale = useContext(AppContext);
31 | const { namespace } = locale;
32 |
33 | let btnClass = `${namespace}-btn`;
34 | let extraClass = className ? ` ${className}` : '';
35 |
36 | if (type !== 'large') {
37 | if (type) {
38 | extraClass += ` ${namespace}-btn-${type}`;
39 | }
40 | } else {
41 | btnClass += '-large';
42 | }
43 | if (active) {
44 | btnClass += ` ${namespace}-btn-active`
45 | }
46 | if (activeState) {
47 | btnClass += ` ${namespace}-btn-active-state`;
48 | }
49 | if (disabled) {
50 | btnClass += ` ${namespace}-disabled`;
51 | } else if (activeDisabled) {
52 | btnClass += ` ${namespace}-active-disabled`;
53 | }
54 | if (color) {
55 | btnClass += ` ${namespace}-btn-${color}`;
56 | }
57 |
58 | return (
59 |
64 | {icon && }
65 | {label && label}
66 |
67 | );
68 | };
69 |
70 | export default memo(Button);
--------------------------------------------------------------------------------
/src/js/components/buttons/copy-btn.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Button from './button';
3 |
4 | import {
5 | AppContext,
6 | } from '../../context';
7 |
8 | const {
9 | Component,
10 | } = React;
11 |
12 | /*
13 | * @desc used to copy a color or gradient from the editor onto the user's clipboard
14 | * @since 1.0.0
15 | */
16 | class CopyBtn extends Component {
17 | constructor() {
18 | super(...arguments);
19 | }
20 |
21 | state = {
22 | copyIcon: 'assignment',
23 | };
24 |
25 | resetIcon = () => {
26 | this.clicked = false;
27 | this.setState({ copyIcon: 'assignment' });
28 | };
29 |
30 | onClick = () => {
31 | if (!this.clicked) {
32 | this.clicked = true;
33 | this.setState({ copyIcon: 'check' });
34 | }
35 | };
36 |
37 | /*
38 | * @desc temporary textarea used to mimic the clipboard copying
39 | * when the button is clicked, the state changes and this is added to the DOM
40 | * then immediately after the copying is done the state is reset and the textarea element is removed
41 | * @since 1.0.0
42 | */
43 | onMountTextArea = textArea => {
44 | if (textArea) {
45 | textArea.select();
46 | document.execCommand('copy');
47 | clearTimeout(this.timer);
48 | this.timer = setTimeout(this.resetIcon, 500);
49 | }
50 | };
51 |
52 | componentWillUnmount() {
53 | clearTimeout(this.timer);
54 | }
55 |
56 | render() {
57 | const { copyIcon } = this.state;
58 | const { value, className, type = 'small' } = this.props;
59 | const copiedValue = value.charAt(0) !== '#' ? value : value.toUpperCase();
60 |
61 | return (
62 | <>
63 |
69 | {copyIcon === 'check' && (
70 |
71 | )}
72 | >
73 | );
74 | }
75 | }
76 |
77 | CopyBtn.contextType = AppContext;
78 |
79 | export default CopyBtn;
--------------------------------------------------------------------------------
/src/js/components/buttons/range-buttons.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Icon from '../../components/icon';
3 |
4 | import {
5 | AppContext,
6 | } from '../../context';
7 |
8 | const {
9 | memo,
10 | useContext,
11 | } = React;
12 |
13 | const defaultIcons = [
14 | 'arrow_drop_up',
15 | 'arrow_drop_down'
16 | ];
17 |
18 | /*
19 | * @desc used for the "up" and "down" arrows to mimic "number input" controls
20 | * @since 1.0.0
21 | */
22 | const RangeButtons = ({
23 | onMouseUp,
24 | onMouseLeave,
25 | arrowUpClick,
26 | arrowDownClick,
27 | icons = defaultIcons,
28 | }) => {
29 | const locale = useContext(AppContext);
30 | const { namespace } = locale;
31 |
32 | return (
33 |
34 |
40 |
41 |
42 |
48 |
49 |
50 |
51 | );
52 | };
53 |
54 | export default memo(RangeButtons);
--------------------------------------------------------------------------------
/src/js/components/buttons/save-btn.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Button from './button';
3 |
4 | import {
5 | EditorContext,
6 | } from '../../context';
7 |
8 | const {
9 | useState,
10 | useEffect,
11 | useContext,
12 | } = React;
13 |
14 | /*
15 | * @desc used for the "save preset" buttons
16 | * @since 1.0.0
17 | */
18 | const SaveBtn = ({
19 | type,
20 | label,
21 | group,
22 | onSave,
23 | className,
24 | currentOutput,
25 | disabled: isDisabled = false,
26 | }) => {
27 | const editorContext = useContext(EditorContext);
28 | const { presets: editorPresets } = editorContext;
29 | const extraClass = !className ? '' : ` ${className}`;
30 |
31 | const [
32 | saveIcon,
33 | updateSaveIcon,
34 | ] = useState('save');
35 |
36 | const onClick = () => {
37 | updateSaveIcon('check');
38 | onSave();
39 | };
40 |
41 | const resetIcon = () => {
42 | updateSaveIcon('save');
43 | };
44 |
45 | // changes the icon to a "check" when clicked and then changes it back to a "save disk" shortly afterward
46 | useEffect(() => {
47 | let timer;
48 | if (saveIcon === 'check') {
49 | timer = setTimeout(resetIcon, 500);
50 | }
51 | return () => {
52 | clearTimeout(timer);
53 | }
54 | }, [saveIcon]);
55 |
56 | // ensures that saving can only be done if the preset doesn't already exist
57 | let disabled;
58 | if (!isDisabled) {
59 | const curOutput = currentOutput.toLowerCase();
60 | const presets = editorPresets[group];
61 | const { defaults, custom } = presets;
62 |
63 | const presetItms = [].concat(defaults).concat(custom);
64 | const { length: presetLength } = presetItms;
65 |
66 | for (let i = 0; i < presetLength; i++) {
67 | const preset = presetItms[i];
68 | const { output: presetOutput } = preset;
69 |
70 | if (presetOutput.toLowerCase() === curOutput) {
71 | disabled = true;
72 | break;
73 | }
74 | }
75 | } else {
76 | disabled = true;
77 | }
78 |
79 | return (
80 |
88 | );
89 | };
90 |
91 | export default SaveBtn;
--------------------------------------------------------------------------------
/src/js/components/grid.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Button from '../components/buttons/button';
3 |
4 | import {
5 | AppContext,
6 | } from '../context';
7 |
8 | const {
9 | useContext,
10 | } = React;
11 |
12 | /*
13 | * @desc creates a 3x3 grid of clickable buttons
14 | * where the buttons act like radio checkboxes
15 | * used for changing the linear-gradient direction when hovering over the preview
16 | * @since 1.0.0
17 | */
18 | const Grid = ({ list, value, onClick }) => {
19 | const locale = useContext(AppContext);
20 | const { namespace } = locale;
21 |
22 | return (
23 |
24 | {
25 | list.map(itm => {
26 | let icon;
27 | let active;
28 | let disabled;
29 | let activeState;
30 |
31 | if (itm !== null) {
32 | icon = 'arrow_right';
33 | if (itm === value) {
34 | active = true;
35 | activeState = true;
36 | }
37 | } else {
38 | disabled = true;
39 | }
40 |
41 | return (
42 |
46 |
55 | );
56 | })
57 | }
58 |
59 | );
60 | };
61 |
62 | export default Grid;
63 |
--------------------------------------------------------------------------------
/src/js/components/icon.js:
--------------------------------------------------------------------------------
1 | /*
2 | * all the Material Icons used in the App
3 | */
4 |
5 | import React from 'react';
6 |
7 | import {
8 | Save,
9 | Close,
10 | Check,
11 | Height,
12 | Launch,
13 | Layers,
14 | AddBox,
15 | Palette,
16 | Gradient,
17 | SwapVert,
18 | CropFree,
19 | ArrowBack,
20 | LibraryAdd,
21 | Assignment,
22 | RotateLeft,
23 | DesktopMac,
24 | LaptopMac,
25 | TabletMac,
26 | PhoneIphone,
27 | LayersClear,
28 | DeleteSweep,
29 | ErrorOutline,
30 | DeleteForever,
31 | ArrowDropUp,
32 | ArrowDropDown,
33 | ArrowForward,
34 | ArrowDownward,
35 | ArrowUpward,
36 | ArrowLeft,
37 | ArrowRight,
38 | InvertColors,
39 | InvertColorsOff,
40 | } from '@material-ui/icons';
41 |
42 |
43 | const icons = {
44 | save: Save,
45 | close: Close,
46 | check: Check,
47 | height: Height,
48 | launch: Launch,
49 | layers: Layers,
50 | add_box: AddBox,
51 | palette: Palette,
52 | gradient: Gradient,
53 | swap_vert: SwapVert,
54 | crop_free: CropFree,
55 | laptop_mac: LaptopMac,
56 | tablet_mac: TabletMac,
57 | desktop_mac: DesktopMac,
58 | phone_iphone: PhoneIphone,
59 | library_add: LibraryAdd,
60 | assignment: Assignment,
61 | rotate_left: RotateLeft,
62 | layers_clear: LayersClear,
63 | arrow_left: ArrowLeft,
64 | arrow_right: ArrowRight,
65 | arrow_back: ArrowBack,
66 | arrow_drop_up: ArrowDropUp,
67 | arrow_drop_down: ArrowDropDown,
68 | arrow_downward: ArrowDownward,
69 | arrow_upward: ArrowUpward,
70 | arrow_forward: ArrowForward,
71 | delete_sweep: DeleteSweep,
72 | error_outline: ErrorOutline,
73 | delete_forever: DeleteForever,
74 | invert_colors: InvertColors,
75 | invert_colors_off: InvertColorsOff,
76 | };
77 |
78 | const Icon = ({ type, style }) => {
79 | const Component = icons[type];
80 | return ;
81 | };
82 |
83 | export default Icon;
84 |
--------------------------------------------------------------------------------
/src/js/components/inputs/input-field.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import NumberInput from './number-input';
3 |
4 | import {
5 | AppContext,
6 | } from '../../context';
7 |
8 | import {
9 | isValidInput,
10 | } from '../../utils/editor';
11 |
12 | const {
13 | PureComponent,
14 | } = React;
15 |
16 | /*
17 | * @desc used manage the state of all number inputs
18 | * as an input field could have an empty or invalid value
19 | * which then needs to be controlled internally
20 | * @since 1.0.0
21 | */
22 | class InputField extends PureComponent {
23 | constructor() {
24 | super(...arguments);
25 | const { value } = this.props;
26 |
27 | this.state = {
28 | value,
29 | isValid: true,
30 | };
31 | }
32 |
33 | /*
34 | * @desc caches the original value before a change begins
35 | * @since 1.0.0
36 | */
37 | onFocus = e => {
38 | const { target } = e;
39 | const { value: newValue } = target;
40 | this.setState({ origValue: parseInt(newValue, 10) });
41 | };
42 |
43 | /*
44 | * @desc if the current input is invalid it will retain its invalid state until it becomes valid again
45 | * this allows for "empty" inputs, i.e. allowing the user to erase everything
46 | * in the input field before typing in a new value
47 | * otherwise the value will simply default to its prop value
48 | * @since 1.0.0
49 | */
50 | static getDerivedStateFromProps(props, state) {
51 | const {
52 | isValid,
53 | value: stateValue,
54 | } = state;
55 |
56 | const { value: propValue } = props;
57 | const value = isValid ? propValue : stateValue;
58 |
59 | return { value };
60 | }
61 |
62 | /*
63 | * @desc possibly reset the value to what it was originally if the new value is invalid
64 | * @since 1.0.0
65 | */
66 | onBlur = () => {
67 | const { isValid } = this.state;
68 |
69 | if (!isValid) {
70 | const { prop } = this.props;
71 | const { origValue } = this.state;
72 | const { onChange } = this.props;
73 |
74 | onChange(origValue, prop);
75 | }
76 | };
77 |
78 | /*
79 | * @desc fires whenever a change occurs, only kicking up the change above if the changed value is valid
80 | * @since 1.0.0
81 | */
82 | onChange = (value, updating, channel, records) => {
83 | const {
84 | min = 0,
85 | max = 100,
86 | } = this.props;
87 |
88 | const isValid = isValidInput(value, min, max);
89 |
90 | this.setState({ value, isValid }, () => {
91 | if (isValid) {
92 | const { onChange } = this.props;
93 | const { prop } = this.props;
94 |
95 | onChange(value, prop, updating, channel, records);
96 | }
97 | });
98 | };
99 |
100 | render() {
101 | const {
102 | prop,
103 | unit,
104 | label,
105 | slider,
106 | channel,
107 | disabled,
108 | inputRef,
109 | className,
110 | allValues,
111 | onMouseEnter,
112 | onMouseLeave,
113 | onChangeUnit,
114 | fetchRecords,
115 | sliderRecord,
116 | min = 0,
117 | max = 100,
118 | numbers = true,
119 | buttons = true,
120 | } = this.props;
121 |
122 | const { value } = this.state;
123 |
124 | return (
125 |
147 | );
148 | }
149 | }
150 |
151 | InputField.contextType = AppContext;
152 |
153 | export default InputField;
154 |
--------------------------------------------------------------------------------
/src/js/components/inputs/number-input.js:
--------------------------------------------------------------------------------
1 | /*
2 | * A note on "Records":
3 | * as an "h", "s" or "l" value changes, the official color could turn black or white,
4 | * ... subsequently changing the values of the adjacent inputs as the user is dragging the range slider
5 | * but since this may not be the indended final change (as the user may still be dragging the slider),
6 | * the adjacent values "spring back" to their original values if the slider does not officially end on a "global change" point (black or white)
7 | * For example, changing "lightness" to "0" or "100" would subsequently change the "h" and "l" values to "0",
8 | * but if the user drags the "lightness" back to a non black/white value (anything between 0 and 100, and BEFORE mousing up on the slider),
9 | * the "h" and "l" values would be returned to their original values, whatever they were before the slider dragging began.
10 | * this essentially prevents the user from "losing" their original color by accident as the hsl values are adjusted via the range slider
11 | */
12 |
13 | import React from 'react';
14 | import SelectBox from '../select-box';
15 | import RangeSlider from '../sliders/range-slider';
16 | import InputWrap from '../wrappers/input-wrap';
17 | import RangeButtons from '../buttons/range-buttons';
18 | import Row from '../wrappers/row';
19 |
20 | import {
21 | AppContext,
22 | } from '../../context';
23 |
24 | import {
25 | isValidInput,
26 | } from '../../utils/editor';
27 |
28 | const {
29 | PureComponent,
30 | } = React;
31 |
32 | const defaultIcons = [
33 | 'arrow_drop_up',
34 | 'arrow_drop_down'
35 | ];
36 |
37 | const unitList = {
38 | '%': { label: '%' },
39 | 'px': { label: 'px' },
40 | };
41 |
42 | /*
43 | * @desc used to manage the state of all number-based inputs
44 | * as input could be completely empty, the official value is only changed when the value is valid
45 | * @since 1.0.0
46 | */
47 | class NumberInput extends PureComponent {
48 | constructor() {
49 | super(...arguments);
50 | const { value } = this.props;
51 | this.state = { origValue: value };
52 | }
53 |
54 | /*
55 | * @desc cache the original value before changes begin
56 | * @since 1.0.0
57 | */
58 | onFocus = e => {
59 | const { target } = e;
60 | const { value } = target;
61 | this.setState({ origValue: parseInt(value, 10) });
62 | };
63 |
64 | /*
65 | * @desc reset the input to its original value if the changes are invalid
66 | * @since 1.0.0
67 | */
68 | onBlur = e => {
69 | const {
70 | min = 0,
71 | max = 100,
72 | } = this.props;
73 |
74 | const { target } = e;
75 | const { value } = target;
76 |
77 | if (!isValidInput(value, min, max)) {
78 | const {
79 | onChange,
80 | channel = 0,
81 | } = this.props;
82 |
83 | const { origValue } = this.state;
84 | onChange(origValue, false, channel);
85 | }
86 | };
87 |
88 | /*
89 | * @desc the up/down arrows have been pressed
90 | * @since 1.0.0
91 | */
92 | onMouseDownBtn = up => {
93 | let run;
94 | this.updating = true;
95 | this.increment(up);
96 |
97 | this.timeout = setTimeout(() => {
98 | this.timer = setInterval(() => {
99 | run = this.increment(up);
100 | if (!run) {
101 | this.onMouseUpBtn();
102 | }
103 | }, 50);
104 | }, 100);
105 | };
106 |
107 | /*
108 | * @desc stop incrementing if the arrows are no longer hovered
109 | * @since 1.0.0
110 | */
111 | onMouseLeave = () => {
112 | this.updating = false;
113 | this.clearTimers();
114 | }
115 |
116 | /*
117 | * @desc stop incrementing if the arrows are no longer pressed
118 | * also called each time the increment timer runs
119 | * @since 1.0.0
120 | */
121 | onMouseUpBtn = () => {
122 | this.updating = false;
123 | this.clearTimers();
124 | const { value } = this.props;
125 | this.onChange({ target: { value } });
126 | };
127 |
128 | /*
129 | * @desc increment the value up
130 | * @since 1.0.0
131 | */
132 | arrowUpClick = () => {
133 | this.onMouseDownBtn(true);
134 | }
135 |
136 | /*
137 | * @desc increment the value down
138 | * @since 1.0.0
139 | */
140 | arrowDownClick = () => {
141 | this.onMouseDownBtn();
142 | }
143 |
144 | /*
145 | * @desc input slider changes potentially starting
146 | * see "Records" note above
147 | * @since 1.0.0
148 | */
149 | onSliderDown = () => {
150 | const { fetchRecords } = this.props;
151 | if (fetchRecords) {
152 | this.fetch = true;
153 | }
154 | this.updating = true;
155 | }
156 |
157 | /*
158 | * @desc user has finished dragging the range slider control
159 | * @since 1.0.0
160 | */
161 | onSliderUp = e => {
162 | this.fetch = false;
163 | this.updating = false;
164 | this.onChange(e);
165 | }
166 |
167 |
168 | /*
169 | * @desc clears number input incrementing timers when changing is complete
170 | * @since 1.0.0
171 | */
172 | clearTimers() {
173 | clearTimeout(this.timeout);
174 | clearInterval(this.timer);
175 | }
176 |
177 | componentWillUnmount() {
178 | this.clearTimers();
179 | }
180 |
181 | /*
182 | * @desc increment the value up or down as the fake input number buttons are clicked
183 | * @since 1.0.0
184 | */
185 | increment = up => {
186 | const {
187 | value,
188 | min = 0,
189 | max = 100,
190 | } = this.props;
191 |
192 | let newValue = up ? value + 1 : value - 1;
193 | newValue = Math.round(newValue);
194 |
195 | if (newValue >= min && newValue <= max) {
196 | this.onChange({ target: { value: newValue } });
197 | return true;
198 | }
199 |
200 | return false;
201 | };
202 |
203 | /*
204 | * @desc gets a copy of the current values for the hsl inputs
205 | * see "Records" note above
206 | * @since 1.0.0
207 | */
208 | getRecords() {
209 | const { sliderRecord } = this.props;
210 | const { records } = sliderRecord;
211 | const passedRecords = [];
212 |
213 | records.forEach(index => {
214 | passedRecords.push({
215 | index,
216 | value: parseInt(this.records[index], 10),
217 | });
218 | });
219 |
220 | this.records = null;
221 | return passedRecords;
222 | }
223 |
224 | /*
225 | * @desc creates a copy of the current values for the hsl inputs
226 | * see "Records" note above
227 | * @since 1.0.0
228 | */
229 | setRecords(value) {
230 | const { sliderRecord } = this.props;
231 | const { minMax } = sliderRecord;
232 |
233 | if (minMax.includes(value)) {
234 | const { allValues } = this.props;
235 | const { records } = sliderRecord;
236 | const recordValues = [];
237 |
238 | records.forEach(index => {
239 | recordValues[index] = allValues[index];
240 | });
241 |
242 | this.records = recordValues;
243 | }
244 | }
245 |
246 | /*
247 | * @desc checks if the records need to be reset after a change happens
248 | * see "Records" note above
249 | * @since 1.0.0
250 | */
251 | checkRecords(value) {
252 | if (!this.records) {
253 | this.setRecords(value);
254 | } else {
255 | return this.getRecords();
256 | }
257 |
258 | return null;
259 | }
260 |
261 | /*
262 | * @desc fires after the slider has been dragged, an ioncrement arrow is clicked or via direct input
263 | * @since 1.0.0
264 | */
265 | onChange = e => {
266 | const {
267 | onChange,
268 | channel = 0,
269 | } = this.props;
270 |
271 | let records;
272 | const { target } = e;
273 | const { value: newValue } = target;
274 | const clampedValue = newValue !== '' ? parseInt(newValue, 10) : newValue;
275 |
276 | if (this.fetch) {
277 | records = this.checkRecords(clampedValue);
278 | }
279 |
280 | onChange(clampedValue, this.updating, channel, records);
281 | };
282 |
283 | render() {
284 | const {
285 | slug,
286 | unit,
287 | label,
288 | value,
289 | slider,
290 | disabled,
291 | inputRef,
292 | onChangeUnit,
293 | onMouseLeave,
294 | onMouseEnter,
295 | min = 0,
296 | max = 100,
297 | channel = 0,
298 | numbers = true,
299 | buttons = true,
300 | className = '',
301 | arrowIcons = defaultIcons,
302 | } = this.props;
303 |
304 | const { namespace } = this.context;
305 | const id = `${namespace}-${slug}-input-${channel}`;
306 |
307 | // if "disabled" exists it could be a number which could be 0
308 | const isDisabled = disabled !== undefined && disabled !== null && disabled !== false;
309 |
310 | let extraClass = !className ? '' : `${className}`;
311 | if (isDisabled) {
312 | if (extraClass) {
313 | extraClass += ' ';
314 | }
315 | extraClass += `${namespace}-disabled`;
316 | }
317 |
318 | let parsed = value;
319 | if (value !== '') {
320 | parsed = Math.round(parseFloat(value));
321 | } else {
322 | parsed = value;
323 | }
324 |
325 | return (
326 |
332 | {numbers && (
333 |
334 |
346 | {buttons && (
347 |
354 | )}
355 |
356 | )}
357 | {slider && (
358 |
367 | )}
368 | {label && (
369 |
373 | )}
374 | {unit && (
375 |
382 | )}
383 |
384 | );
385 | }
386 | }
387 |
388 | NumberInput.contextType = AppContext;
389 |
390 | export default NumberInput;
391 |
--------------------------------------------------------------------------------
/src/js/components/radio-menu.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import {
4 | AppContext,
5 | } from '../context';
6 |
7 | const {
8 | memo,
9 | useContext,
10 | } = React;
11 |
12 | /*
13 | * @desc creates a menu of radio inputs
14 | * @since 1.0.0
15 | */
16 | const RadioMenu = ({ active, list, type, className, onChange }) => {
17 | const locale = useContext(AppContext);
18 | const { namespace } = locale;
19 | const extraClass = !className ? '' : ` ${className}`;
20 |
21 | let ids = type || '';
22 | if (ids) {
23 | ids = `-${ids}-`;
24 | }
25 |
26 | return (
27 |
28 | {
29 | Object.keys(list).map(value => {
30 | const label = list[value];
31 | return (
32 |
36 | onChange(e.target.value, type)}
43 | />
44 |
50 |
51 | );
52 | })
53 | }
54 |
55 | );
56 | };
57 |
58 | export default memo(RadioMenu);
59 |
--------------------------------------------------------------------------------
/src/js/components/select-box/select-color.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | /*
4 | * @desc just used to reduce duplication in the select box component
5 | * @since 1.0.0
6 | */
7 | const SelectColor = ({ namespace, style, className = '' }) => {
8 | return (
9 |
10 |
11 |
12 |
16 |
17 |
18 |
19 |
20 | );
21 | };
22 |
23 | export default SelectColor;
--------------------------------------------------------------------------------
/src/js/components/sliders/drag-slider.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import {
4 | AppContext,
5 | } from '../../context';
6 |
7 | const {
8 | PureComponent,
9 | createRef,
10 | } = React;
11 |
12 | /*
13 | * @desc essentially creates a vertical range slider
14 | * @since 1.0.0
15 | */
16 | class DragSlider extends PureComponent {
17 | constructor() {
18 | super(...arguments);
19 | this.stripRef = createRef();
20 | }
21 |
22 | /*
23 | * @desc user has "moused down" on the slider at any given point
24 | * @since 1.0.0
25 | */
26 | onMouseDown = e => {
27 | e.preventDefault();
28 | const { setCursor } = this.context;
29 | setCursor('vertical');
30 |
31 | document.addEventListener('mousemove', this.onMouseMove);
32 | document.addEventListener('mouseleave', this.onMouseUp);
33 | document.addEventListener('mouseup', this.onMouseUp);
34 |
35 | this.onMouseMove(e);
36 | }
37 |
38 | /*
39 | * @desc user is "dragging" the slider handle
40 | * @since 1.0.0
41 | */
42 | onMouseMove = e => {
43 | const {
44 | prop,
45 | reverse,
46 | } = this.props;
47 |
48 | const { pageY } = e;
49 | const { onChange } = this.props;
50 | const { current: strip } = this.stripRef;
51 |
52 | const rect = strip.getBoundingClientRect();
53 | const { height, top } = rect;
54 |
55 | const perc = Math.max(0,
56 | Math.min(
57 | pageY - window.scrollY - top,
58 | height
59 | )
60 | );
61 | const value = (perc / height) * 100;
62 | const newValue = reverse ? 100 - value : value;
63 |
64 | onChange(newValue, prop, true);
65 | }
66 |
67 | /*
68 | * @desc cleanup after "dragging" is complete
69 | * @since 1.0.0
70 | */
71 | removeListeners() {
72 | document.removeEventListener('mousemove', this.onMouseMove);
73 | document.removeEventListener('mouseleave', this.onMouseUp);
74 | document.removeEventListener('mouseup', this.onMouseUp);
75 | }
76 |
77 | /*
78 | * @desc user has finished "dragging"
79 | * @since 1.0.0
80 | */
81 | onMouseUp = () => {
82 | this.removeListeners();
83 | const { setCursor } = this.context;
84 | setCursor();
85 | }
86 |
87 | componentWillUnmount() {
88 | this.removeListeners();
89 | this.stripRef = null;
90 | }
91 |
92 | render() {
93 | const {
94 | value,
95 | className,
96 | } = this.props;
97 |
98 | const { namespace } = this.context;
99 | const extraClass = className ? ` ${className}` : '';
100 | const style = { top: `${value}%` };
101 |
102 | return (
103 |
111 | );
112 | }
113 | }
114 |
115 | DragSlider.contextType = AppContext;
116 |
117 | export default DragSlider;
118 |
--------------------------------------------------------------------------------
/src/js/components/sliders/range-slider.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import {
4 | AppContext,
5 | } from '../../context';
6 |
7 | const {
8 | memo,
9 | useContext,
10 | } = React;
11 |
12 | /*
13 | * @desc creates a range slider inside a wrapper along with a track
14 | * @since 1.0.0
15 | */
16 | const RangeSlider = ({
17 | min,
18 | max,
19 | value,
20 | disabled,
21 | onMouseDown,
22 | onMouseUp,
23 | onChange,
24 | step = 0.5,
25 | }) => {
26 | const locale = useContext(AppContext);
27 | const { namespace } = locale;
28 |
29 | return (
30 |
31 |
32 |
44 |
45 | );
46 | };
47 |
48 | export default memo(RangeSlider);
49 |
--------------------------------------------------------------------------------
/src/js/components/toggle.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import {
4 | AppContext,
5 | } from '../context';
6 |
7 | const {
8 | memo,
9 | useContext,
10 | } = React;
11 |
12 | /*
13 | * @desc creates an "on/off" toggle button, mimicing radio inputs
14 | * @since 1.0.0
15 | */
16 | const Toggle = ({ value, onChange, type, label }) => {
17 | const locale = useContext(AppContext);
18 | const { namespace } = locale;
19 | const activeClass = !value ? '' : ` ${namespace}-toggle-active`
20 |
21 | return (
22 | <>
23 | onChange(!value, type)}
26 | >
27 |
28 |
29 | ON
30 |
31 |
32 | OFF
33 |
34 |
35 |
36 | {label && {label}}
37 | >
38 | );
39 | };
40 |
41 | export default memo(Toggle);
--------------------------------------------------------------------------------
/src/js/components/wheel.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import InputField from './inputs/input-field';
3 |
4 | import {
5 | AppContext,
6 | } from '../context';
7 |
8 | const {
9 | PureComponent,
10 | } = React;
11 |
12 | /*
13 | * @desc used to create the "radius" wheel for linear and conic gradients
14 | * @since 1.0.0
15 | */
16 | class Wheel extends PureComponent {
17 | constructor() {
18 | super(...arguments);
19 | }
20 |
21 | /*
22 | * @desc user has "mouse downed" somewhere on the wheel
23 | * @since 1.0.0
24 | */
25 | onMouseDown = e => {
26 | e.preventDefault();
27 |
28 | const rect = this.ref.getBoundingClientRect();
29 | const { top, left, width } = rect;
30 | const { setCursor } = this.context;
31 |
32 | this.mouseValues = {
33 | wheelCenter: (width * 0.5) + 5,
34 | moveX: left,
35 | moveY: top,
36 | }
37 |
38 | setCursor('wheel');
39 | this.updating = true;
40 | this.onMouseMove(e);
41 |
42 | document.addEventListener('mousemove', this.onMouseMove);
43 | document.addEventListener('mouseleave', this.onMouseUp);
44 | document.addEventListener('mouseup', this.onMouseUp);
45 | };
46 |
47 | /*
48 | * @desc user is dragging their mouse around the wheel
49 | * @since 1.0.0
50 | */
51 | onMouseMove = e => {
52 | const { pageX, pageY } = e;
53 | const { moveX, moveY, wheelCenter } = this.mouseValues;
54 | const posX = pageX - moveX - window.scrollX;
55 | const posY = pageY - moveY - window.scrollY;
56 |
57 | let value = Math.atan2(posY - wheelCenter, posX - wheelCenter) * (180 / Math.PI) + 90;
58 | if (value < 0) value += 360;
59 | value = Math.max(0, Math.min(360, Math.round(value)));
60 |
61 | const { onChange, type } = this.props;
62 | onChange(value, type, this.updating);
63 | };
64 |
65 | /*
66 | * @desc cleanup after dragging has ended
67 | * @since 1.0.0
68 | */
69 | removeListeners() {
70 | document.removeEventListener('mousemove', this.onMouseMove);
71 | document.removeEventListener('mouseleave', this.onMouseUp);
72 | document.removeEventListener('mouseup', this.onMouseUp);
73 | }
74 |
75 | /*
76 | * @desc user has finished "dragging" around the wheel
77 | * @since 1.0.0
78 | */
79 | onMouseUp = e => {
80 | this.removeListeners();
81 | const { setCursor } = this.context;
82 |
83 | setCursor();
84 | this.updating = false;
85 | this.onMouseMove(e);
86 | }
87 |
88 | /*
89 | * @desc the wheel's value has changed either from mouse dragging or from direct input
90 | * @since 1.0.0
91 | */
92 | onChange = (value, type, updating) => {
93 | const { onChange } = this.props;
94 | onChange(value, type, updating);
95 | }
96 |
97 | componentWillUnmount() {
98 | this.removeListeners();
99 | }
100 |
101 | render() {
102 | const { value } = this.props;
103 | const { namespace } = this.context;
104 |
105 | return (
106 | <>
107 |
115 | (this.ref = ref)}
119 | >
120 |
121 |
125 |
126 |
127 | >
128 | );
129 | }
130 | }
131 |
132 | Wheel.contextType = AppContext;
133 |
134 | export default (Wheel);
135 |
--------------------------------------------------------------------------------
/src/js/components/wrappers/input-wrap.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const {
4 | memo,
5 | useContext,
6 | } = React;
7 |
8 | import {
9 | AppContext,
10 | } from '../../context';
11 |
12 | /*
13 | * @desc the wrapper for inputs which may or may not have an additional class name
14 | * @since 1.0.0
15 | */
16 | const InputWrap = ({ className, children }) => {
17 | const locale = useContext(AppContext);
18 | const { namespace } = locale;
19 | const extraClass = !className ? '' : ` ${className}`;
20 |
21 | return (
22 | {children}
23 | );
24 | };
25 |
26 | export default memo(InputWrap);
--------------------------------------------------------------------------------
/src/js/components/wrappers/panel.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const {
4 | memo,
5 | useContext,
6 | } = React;
7 |
8 | import {
9 | AppContext,
10 | } from '../../context';
11 |
12 | /*
13 | * @desc represents a controls panel which may or may not have an additional class name
14 | * @since 1.0.0
15 | */
16 | const Panel = ({ className, children }) => {
17 | const locale = useContext(AppContext);
18 | const { namespace } = locale;
19 | const extraClass = !className ? '' : ` ${className}`;
20 |
21 | return (
22 | {children}
23 | );
24 | };
25 |
26 | export default memo(Panel);
--------------------------------------------------------------------------------
/src/js/components/wrappers/row.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import {
4 | AppContext,
5 | } from '../../context';
6 |
7 | const {
8 | memo,
9 | forwardRef,
10 | useContext,
11 | } = React;
12 |
13 | /*
14 | * @desc represents a row of controls which may or may not have additional classes or events
15 | * @since 1.0.0
16 | */
17 | const Row = forwardRef(({ className, onMouseEnter, onMouseLeave, children }, ref) => {
18 | const locale = useContext(AppContext);
19 | const { namespace } = locale;
20 | const extraClass = !className ? '' : ` ${className}`;
21 |
22 | return (
23 | {children}
29 | );
30 | });
31 |
32 | export default memo(Row);
--------------------------------------------------------------------------------
/src/js/components/wrappers/sortable.js:
--------------------------------------------------------------------------------
1 | /*
2 | * the wrappers used for conditionally sortable content
3 | */
4 |
5 | import {
6 | SortableHandle,
7 | SortableElement,
8 | SortableContainer,
9 | } from 'react-sortable-hoc';
10 |
11 | const SortableWrapper = SortableContainer(({ children }) => children);
12 | const SortableHandler = SortableHandle(({ children }) => children);
13 | const SortableItem = SortableElement(({ children }) => children);
14 |
15 | export {
16 | SortableWrapper,
17 | SortableHandler,
18 | SortableItem,
19 | };
--------------------------------------------------------------------------------
/src/js/components/wrappers/wrapper.js:
--------------------------------------------------------------------------------
1 | /*
2 | * just a generic conditional wrapper, mostly used for potentially scrollable and sortable containers
3 | */
4 | const Wrapper = ({ wrapIt, wrapper, children }) => wrapIt ? wrapper(children) : children;
5 |
6 | export default Wrapper;
--------------------------------------------------------------------------------
/src/js/context.js:
--------------------------------------------------------------------------------
1 | /*
2 | * the two main context points used by the App
3 | */
4 |
5 | import React from 'react';
6 |
7 | const {
8 | createContext,
9 | } = React;
10 |
11 | const AppContext = createContext(); // Provider: index.js
12 | const EditorContext = createContext(); // Provider: module/editor.js
13 |
14 | export {
15 | AppContext,
16 | EditorContext,
17 | }
--------------------------------------------------------------------------------
/src/js/data/color-names.js:
--------------------------------------------------------------------------------
1 | /*
2 | * all the possible CSS color names that could exist with their respective hex values
3 | */
4 |
5 | const colorNames = {
6 | aliceblue: '#F0F8FF',
7 | antiquewhite: '#FAEBD7',
8 | aqua: '#00FFFF',
9 | aquamarine: '#7FFFD4',
10 | azure: '#F0FFFF',
11 | beige: '#F5F5DC',
12 | bisque: '#FFE4C4',
13 | black: '#000000',
14 | blanchedalmond: '#FFEBCD',
15 | blue: '#0000FF',
16 | blueviolet: '#8A2BE2',
17 | brown: '#A52A2A',
18 | burlywood: '#DEB887',
19 | cadetblue: '#5F9EA0',
20 | chartreuse: '#7FFF00',
21 | chocolate: '#D2691E',
22 | coral: '#FF7F50',
23 | cornflowerblue: '#6495ED',
24 | cornsilk: '#FFF8DC',
25 | crimson: '#DC143C',
26 | cyan: '#00FFFF',
27 | darkblue: '#00008B',
28 | darkcyan: '#008B8B',
29 | darkgoldenrod: '#B8860B',
30 | darkgray: '#A9A9A9',
31 | darkgrey: '#A9A9A9',
32 | darkgreen: '#006400',
33 | darkkhaki: '#BDB76B',
34 | darkmagenta: '#8B008B',
35 | darkolivegreen: '#556B2F',
36 | darkorange: '#FF8C00',
37 | darkorchid: '#9932CC',
38 | darkred: '#8B0000',
39 | darksalmon: '#E9967A',
40 | darkseagreen: '#8FBC8F',
41 | darkslateblue: '#483D8B',
42 | darkslategray: '#2F4F4F',
43 | darkslategrey: '#2F4F4F',
44 | darkturquoise: '#00CED1',
45 | darkviolet: '#9400D3',
46 | deeppink: '#FF1493',
47 | deepskyblue: '#00BFFF',
48 | dimgray: '#696969',
49 | dimgrey: '#696969',
50 | dodgerblue: '#1E90FF',
51 | firebrick: '#B22222',
52 | floralwhite: '#FFFAF0',
53 | forestgreen: '#228B22',
54 | fuchsia: '#FF00FF',
55 | gainsboro: '#DCDCDC',
56 | ghostwhite: '#F8F8FF',
57 | gold: '#FFD700',
58 | goldenrod: '#DAA520',
59 | gray: '#808080',
60 | grey: '#808080',
61 | green: '#008000',
62 | greenyellow: '#ADFF2F',
63 | honeydew: '#F0FFF0',
64 | hotpink: '#FF69B4',
65 | indianred: '#CD5C5C',
66 | indigo: '#4B0082',
67 | ivory: '#FFFFF0',
68 | khaki: '#F0E68C',
69 | lavender: '#E6E6FA',
70 | lavenderblush: '#FFF0F5',
71 | lawngreen: '#7CFC00',
72 | lemonchiffon: '#FFFACD',
73 | lightblue: '#ADD8E6',
74 | lightcoral: '#F08080',
75 | lightcyan: '#E0FFFF',
76 | lightgoldenrodyellow: '#FAFAD2',
77 | lightgray: '#D3D3D3',
78 | lightgrey: '#D3D3D3',
79 | lightgreen: '#90EE90',
80 | lightpink: '#FFB6C1',
81 | lightsalmon: '#FFA07A',
82 | lightseagreen: '#20B2AA',
83 | lightskyblue: '#87CEFA',
84 | lightslategray: '#778899',
85 | lightslategrey: '#778899',
86 | lightsteelblue: '#B0C4DE',
87 | lightyellow: '#FFFFE0',
88 | lime: '#00FF00',
89 | limegreen: '#32CD32',
90 | linen: '#FAF0E6',
91 | magenta: '#FF00FF',
92 | maroon: '#800000',
93 | mediumaquamarine: '#66CDAA',
94 | mediumblue: '#0000CD',
95 | mediumorchid: '#BA55D3',
96 | mediumpurple: '#9370DB',
97 | mediumseagreen: '#3CB371',
98 | mediumslateblue: '#7B68EE',
99 | mediumspringgreen: '#00FA9A',
100 | mediumturquoise: '#48D1CC',
101 | mediumvioletred: '#C71585',
102 | midnightblue: '#191970',
103 | mintcream: '#F5FFFA',
104 | mistyrose: '#FFE4E1',
105 | moccasin: '#FFE4B5',
106 | navajowhite: '#FFDEAD',
107 | navy: '#000080',
108 | oldlace: '#FDF5E6',
109 | olive: '#808000',
110 | olivedrab: '#6B8E23',
111 | orange: '#FFA500',
112 | orangered: '#FF4500',
113 | orchid: '#DA70D6',
114 | palegoldenrod: '#EEE8AA',
115 | palegreen: '#98FB98',
116 | paleturquoise: '#AFEEEE',
117 | palevioletred: '#DB7093',
118 | papayawhip: '#FFEFD5',
119 | peachpuff: '#FFDAB9',
120 | peru: '#CD853F',
121 | pink: '#FFC0CB',
122 | plum: '#DDA0DD',
123 | powderblue: '#B0E0E6',
124 | purple: '#800080',
125 | rebeccapurple: '#663399',
126 | red: '#FF0000',
127 | rosybrown: '#BC8F8F',
128 | royalblue: '#4169E1',
129 | saddlebrown: '#8B4513',
130 | salmon: '#FA8072',
131 | sandybrown: '#F4A460',
132 | seagreen: '#2E8B57',
133 | seashell: '#FFF5EE',
134 | sienna: '#A0522D',
135 | silver: '#C0C0C0',
136 | skyblue: '#87CEEB',
137 | slateblue: '#6A5ACD',
138 | slategray: '#708090',
139 | slategrey: '#708090',
140 | snow: '#FFFAFA',
141 | springgreen: '#00FF7F',
142 | steelblue: '#4682B4',
143 | tan: '#D2B48C',
144 | teal: '#008080',
145 | thistle: '#D8BFD8',
146 | tomato: '#FF6347',
147 | turquoise: '#40E0D0',
148 | violet: '#EE82EE',
149 | wheat: '#F5DEB3',
150 | white: '#FFFFFF',
151 | whitesmoke: '#F5F5F5',
152 | yellow: '#FFFF00',
153 | yellowgreen: '#9ACD32',
154 | };
155 |
156 | const colorNameKeys = Object.keys(colorNames);
157 | const colorNameReg = colorNameKeys.slice().map(key => `|${key}`).join('');
158 | const colorRegExp = new RegExp(`^(#|rgb|hsl${colorNameReg})`);
159 |
160 | export {
161 | colorNames,
162 | colorRegExp,
163 | colorNameKeys,
164 | };
165 |
--------------------------------------------------------------------------------
/src/js/data/defaults.js:
--------------------------------------------------------------------------------
1 | // this number needs to equal the exact width of the preview strip
2 | // a max is needed to translate pixel values visually in the editor's main strip
3 | const maxPositionPixels = 800;
4 |
5 | /*
6 | * @desc used to create two default color objects for newly added gradients
7 | * @since 1.0.0
8 | */
9 | const defGradientColors = () => {
10 | return [
11 | {
12 | unit: '%',
13 | opacity: 0,
14 | position: 0,
15 | hex: '#000',
16 | color: 'rgba(0, 0, 0, 0)',
17 | rgb: [0, 0, 0],
18 | value: [0, 0, 0, 0],
19 | preview: { background: 'transparent' },
20 | },
21 | {
22 | unit: '%',
23 | opacity: 1,
24 | position: 100,
25 | hex: '#000',
26 | color: '#000000',
27 | rgb: [0, 0, 0],
28 | value: [0, 0, 0, 1],
29 | preview: { background: '#000' },
30 | }
31 | ];
32 | };
33 |
34 | /*
35 | * @desc the default gradient data excluding colors and hints
36 | * @since 1.0.0
37 | */
38 | const defaultGradient = () => {
39 | return {
40 | type: 'linear',
41 | angle: 180,
42 | shape: 'ellipse',
43 | extent: 'farthest-corner',
44 | repeating: false,
45 | hints: [],
46 | positions: {
47 | x: { value: 50, unit: '%' },
48 | y: { value: 50, unit: '%' },
49 | },
50 | sizes: {
51 | x: { value: 75, unit: '%' },
52 | y: { value: 75, unit: '%' },
53 | },
54 | }
55 | };
56 |
57 | /*
58 | * @desc used to create a default gradient object,
59 | * new gradient data will then be pushed onto this object
60 | * @since 1.0.0
61 | */
62 | const defaultEditorGradient = () => {
63 | return {
64 | ...defaultGradient(),
65 | colors: defGradientColors(),
66 | hints: [{ position: 50, percentage: 50 }],
67 | }
68 | };
69 |
70 | export {
71 | defaultGradient,
72 | defGradientColors,
73 | defaultEditorGradient,
74 | maxPositionPixels,
75 | }
--------------------------------------------------------------------------------
/src/js/data/presets.js:
--------------------------------------------------------------------------------
1 | /*
2 | * the built-in default presets for the App
3 | */
4 |
5 | let defColors = [];
6 | let defGradients = [];
7 |
8 | const colorPresets = {
9 | defaults: [...defColors, ...[
10 | '#FFFFFF', '#000000', '#C7C7CC', '#8E8E93', '#575757',
11 | '#FF3A2D', '#009933', '#007AFF', '#FFCC00', '#ff9500',
12 | '#FFD3E0', '#FF69B4', '#ad62aa', '#fa114f', '#800000',
13 | '#2e99b0', '#5893d4', '#5856D6', '#6900ff', '#20366b',
14 | '#f9d5bb', '#fcd77f', '#ef7b7b', '#8f4426', '#396362',
15 | '#011f4b', '#03396c', '#005b96', '#6497b1', '#b3cde0',
16 | '#a8e6cf', '#dcedc1', '#ffd3b6', '#ffaaa5', '#ff8b94',
17 | '#d11141', '#00b159', '#00aedb', '#f37735', '#ffc425',
18 | '#ebf4f6', '#bdeaee', '#76b4bd', '#58668b', '#5e5656',
19 | '#edc951', '#eb6841', '#cc2a36', '#4f372d', '#00a0b0',
20 | ]],
21 | custom: [],
22 | };
23 |
24 | const gradientPresets = {
25 | defaults: [...defGradients, ...[
26 | 'linear-gradient(#f7f7f7, #d7d7d7)',
27 | 'linear-gradient(#4a4a4a, #2b2b2b)',
28 | 'linear-gradient(#dbddde, #898c90)',
29 | 'linear-gradient(#1ad6fd, #1d62f0)',
30 | 'linear-gradient(#c644fc, #5856d6)',
31 | 'linear-gradient(#ff5e3a, #ff2a68)',
32 | 'linear-gradient(#e4ddca, #d6cec3)',
33 | 'linear-gradient(#ffdb4c, #ffcd02)',
34 | 'linear-gradient(#ff9500, #ff5e3a)',
35 | 'linear-gradient(#52edc7, #5ac8fb)',
36 | 'linear-gradient(#e4b7f0, #c86edf)',
37 | 'linear-gradient(#87fc70, #0bd318)',
38 | 'linear-gradient(#3d4e81, #5753c9, #6e7ff3)',
39 | 'linear-gradient(160deg, #231557, #44107a 29%, #ff1361 67%, #fff800)',
40 | 'linear-gradient(160deg, #69eacb, #eaccf8, #6654f1)',
41 | 'linear-gradient(160deg, #ff057c, #7c64d5, #4cc3ff)',
42 | 'linear-gradient(160deg, #ff057c, #8d0b93, #321575)',
43 | 'linear-gradient(160deg, #a445b2, #d41872, #f06)',
44 | 'linear-gradient(160deg, #9efbd3, #57e9f2, #45d4fb)',
45 | 'linear-gradient(160deg, #ac32e4, #7918f2, #4801ff)',
46 | 'linear-gradient(160deg, #7085b6, #87a7d9, #def3f8)',
47 | 'linear-gradient(160deg, #22e1ff, #1d8fe1, #625eb1)',
48 | 'linear-gradient(160deg, #2cd8d5, #6b8dd6, #8e37d7)',
49 | 'linear-gradient(160deg, #2cd8d5, #c5c1ff 56%, #ffbac3)',
50 | 'linear-gradient(#bfd9fe, #df89b5)',
51 | 'linear-gradient(340deg, #616161, #9bc5c3)',
52 | 'linear-gradient(90deg, #243949, #517fa4)',
53 | 'linear-gradient(#eacda3, #e6b980)',
54 | 'linear-gradient(45deg, #ee9ca7, #ffdde1)',
55 | 'linear-gradient(340deg, #f794a4, #fdd6bd)',
56 | 'linear-gradient(45deg, #874da2, #c43a30)',
57 | 'linear-gradient(#f3e7e9, #dad4ec)',
58 | 'linear-gradient(320deg, #2b5876, #4e4376)',
59 | 'linear-gradient(60deg, #29323c, #485563)',
60 | 'linear-gradient(#e9e9e7, #efeeec 25%, #eee 70%, #d5d4d0)',
61 | 'linear-gradient(#fbc8d4, #9795f0)',
62 | 'linear-gradient(#FC466B, #3F5EFB)',
63 | 'linear-gradient(#3F2B96, #A8C0FF)',
64 | 'linear-gradient(#efd5ff, #515ada)',
65 | 'linear-gradient(#4b6cb7, #182848)',
66 | 'linear-gradient(#e3ffe7, #d9e7ff)',
67 | 'linear-gradient(135deg, #1CB5E0, #000851)',
68 | 'linear-gradient(#00d2ff, #3a47d5)',
69 | 'linear-gradient(135deg, #03001e, #7303c0, #ec38bc, #fdeff9)',
70 | 'linear-gradient(#00C9FF, #92FE9D)',
71 | 'linear-gradient(#f8ff00, #3ad59f)',
72 | 'linear-gradient(#9ebd13, #008552)',
73 | 'linear-gradient(135deg, #0700b8, #00ff88)',
74 | 'linear-gradient(#FDBB2D, #3A1C71)',
75 | 'linear-gradient(#fcff9e, #c67700)',
76 | 'linear-gradient(#FDBB2D, #22C1C3)',
77 | 'linear-gradient(#d53369, #daae51)',
78 |
79 | ]],
80 | custom: [],
81 | };
82 |
83 | export {
84 | colorPresets as coreColors,
85 | gradientPresets as coreGradients,
86 | }
--------------------------------------------------------------------------------
/src/js/error.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import {
4 | AppContext,
5 | } from './context';
6 |
7 | const {
8 | Component,
9 | } = React;
10 |
11 | /*
12 | * @desc the top-level ErrorBoundary to catch any errors that may occur
13 | * @since 1.0.0
14 | */
15 | class ErrorBoundary extends Component {
16 | constructor() {
17 | super();
18 | this.state = { hasError: false };
19 | }
20 |
21 | static getDerivedStateFromError(error) {
22 | return { hasError: error };
23 | }
24 |
25 | render() {
26 | const { hasError } = this.state;
27 |
28 | if (hasError) {
29 | const { namespace } = this.context;
30 | const { onClose } = this.props;
31 |
32 | return (
33 |
34 |
35 | Something went wrong. Please report the error below.
36 |
37 |
{hasError.toString()}
38 |
39 |
40 |
41 |
42 | );
43 | }
44 |
45 | const { children } = this.props;
46 | return children;
47 | }
48 | }
49 |
50 | ErrorBoundary.contextType = AppContext;
51 |
52 | export default ErrorBoundary;
53 |
--------------------------------------------------------------------------------
/src/js/hoc/draggable.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import {
4 | EditorContext,
5 | } from '../context';
6 |
7 | import {
8 | withAppContext,
9 | } from './with-context';
10 |
11 | const {
12 | Component,
13 | Children,
14 | cloneElement,
15 | } = React;
16 |
17 | /*
18 | * @desc adds dragging capability to any given container of content
19 | * @since 1.0.0
20 | */
21 | class Draggable extends Component {
22 | constructor() {
23 | super(...arguments);
24 | }
25 |
26 | /*
27 | * @desc user has "mouse downed" the content where dragging can begin
28 | * @since 1.0.0
29 | */
30 | onMouseDown = e => {
31 | e.preventDefault();
32 |
33 | const { pageX, pageY } = e;
34 | const { appContext } = this.props;
35 | const { containerRef } = this.context;
36 | const { current: container } = containerRef;
37 | const { outputBar, setCursor } = appContext;
38 |
39 | let outputBarHeight;
40 | if (outputBar) {
41 | const { outputBarRef } = this.props;
42 | const { current: currentBar } = outputBarRef;
43 | if (currentBar) {
44 | const outputBarRect = currentBar.getBoundingClientRect();
45 | const { height: barHeight } = outputBarRect;
46 | outputBarHeight = barHeight;
47 | }
48 | } else {
49 | outputBarHeight = 0;
50 | }
51 |
52 | const rect = container.getBoundingClientRect();
53 | const { width, height, left, top } = rect;
54 |
55 | this.dragValues = {
56 | maxWidth: window.innerWidth - width,
57 | maxHeight: window.innerHeight - height - outputBarHeight,
58 | moveX: Math.max(0, Math.min(pageX - left, width)),
59 | moveY: Math.max(0, Math.min(pageY - top, height)),
60 | };
61 |
62 | setCursor('move');
63 | document.addEventListener('mousemove', this.onMouseMove);
64 | document.addEventListener('mouseup', this.onMouseUp);
65 | document.addEventListener('mouseleave', this.onMouseUp);
66 | };
67 |
68 | /*
69 | * @desc user is dragging the content
70 | * @since 1.0.0
71 | */
72 | onMouseMove = e => {
73 | const { containerRef } = this.context;
74 | const { current: container } = containerRef;
75 | const { pageX, pageY } = e;
76 |
77 | const {
78 | moveX,
79 | moveY,
80 | maxWidth,
81 | maxHeight,
82 | } = this.dragValues;
83 |
84 | const x = Math.max(0, Math.min(pageX - moveX, maxWidth));
85 | const y = Math.max(0, Math.min(pageY - moveY, maxHeight));
86 |
87 | container.style.left = `${x}px`;
88 | container.style.top = `${y}px`;
89 | }
90 |
91 | /*
92 | * @desc user has finished dragging the content
93 | * @since 1.0.0
94 | */
95 | onMouseUp = () => {
96 | document.removeEventListener('mousemove', this.onMouseMove);
97 | document.removeEventListener('mouseup', this.onMouseUp);
98 | document.removeEventListener('mouseleave', this.onMouseUp);
99 |
100 | const { appContext } = this.props;
101 | const { setCursor } = appContext;
102 |
103 | setCursor();
104 | this.dragValues = null;
105 | }
106 |
107 | /*
108 | * @desc cleanup if the content no longer exists
109 | * @since 1.0.0
110 | */
111 | componentWillUnmount() {
112 | this.onMouseUp();
113 | }
114 |
115 | render() {
116 | const { children } = this.props;
117 | return Children.map(
118 | children,
119 | child => cloneElement(
120 | child,
121 | { draggable: { onMouseDown: this.onMouseDown } }
122 | )
123 | );
124 | }
125 | }
126 |
127 | Draggable.contextType = EditorContext;
128 |
129 | export default withAppContext(Draggable);
130 |
--------------------------------------------------------------------------------
/src/js/hoc/scrollable.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import {
4 | AppContext,
5 | } from '../context';
6 |
7 | const {
8 | Component,
9 | } = React;
10 |
11 | /*
12 | * @desc adds scrolling capability to any given container of content
13 | * @since 1.0.0
14 | */
15 | class Scrollable extends Component {
16 | constructor() {
17 | super(...arguments);
18 | }
19 |
20 | /*
21 | * @desc user is scrolling content with the mouse-wheel
22 | * @since 1.0.0
23 | */
24 | onScroll = e => {
25 | e.preventDefault();
26 |
27 | const { target } = e;
28 | const { scrollTop } = target;
29 |
30 | const handleY = ((scrollTop / this.containerHeight) * this.handleHeight);
31 | this.handleY = Math.max(0, Math.min(this.handleDif, handleY));
32 |
33 | window.cancelAnimationFrame(this.requestAnime);
34 | this.requestAnime = window.requestAnimationFrame(this.updateScroll);
35 | };
36 |
37 | /*
38 | * @desc respositions the scroll handle after the mouse-wheel is scrolled
39 | * @since 1.0.0
40 | */
41 | updateScroll = () => {
42 | this.handle.style.marginTop = `${this.handleY}px`;
43 | };
44 |
45 | /*
46 | * @desc sets the scroll handle height
47 | * and also does some calculations in preparation of potential scrolling
48 | * @since 1.0.0
49 | */
50 | onMouseEnter = () => {
51 | const containerRect = this.container.getBoundingClientRect();
52 | const contentRect = this.content.getBoundingClientRect();
53 |
54 | const { height: containerHeight } = containerRect;
55 | const { height: contentHeight } = contentRect;
56 |
57 | const handleHeight = (containerHeight / contentHeight) * containerHeight;
58 | this.handleHeight = handleHeight;
59 |
60 | this.containerHeight = containerHeight;
61 | this.handleDif = containerHeight - handleHeight;
62 | this.contentDif = contentHeight - containerHeight;
63 | this.handle.style.height = `${handleHeight}px`;
64 | };
65 |
66 | /*
67 | * @desc user has "mouse downed" the scroll handle
68 | * @since 1.0.0
69 | */
70 | onMouseDown = e => {
71 | e.preventDefault();
72 | const { pageY } = e;
73 |
74 | this.pageY = pageY;
75 | this.startY = this.handle.offsetTop;
76 |
77 | const { namespace, setCursor } = this.context;
78 | this.handle.classList.add(`${namespace}-scrolling`);
79 | setCursor('default');
80 |
81 | document.addEventListener('mousemove', this.onMouseMove);
82 | document.addEventListener('mouseup', this.onMouseUp);
83 | document.addEventListener('mouseleave', this.onMouseUp);
84 | };
85 |
86 | /*
87 | * @desc user is scrolling content with the scroll handle
88 | * @since 1.0.0
89 | */
90 | onMouseMove = e => {
91 | const { pageY } = e;
92 | const scrollY = Math.max(0,
93 | Math.min(
94 | pageY - this.pageY + this.startY,
95 | this.handleDif
96 | )
97 | );
98 | this.container.scrollTop = Math.max(0,
99 | Math.min(
100 | (scrollY / this.handleDif) * this.contentDif,
101 | this.contentDif
102 | )
103 | );
104 | this.handle.style.marginTop = `${scrollY}px`;
105 | };
106 |
107 | /*
108 | * @desc user was scrolling content with the scroll handle and has finished
109 | * @since 1.0.0
110 | */
111 | onMouseUp = () => {
112 | this.removeListeners();
113 | const { namespace, setCursor } = this.context;
114 | this.handle.classList.remove(`${namespace}-scrolling`);
115 | setCursor();
116 | }
117 |
118 | /*
119 | * @desc cleanup after scrolling has ended
120 | * @since 1.0.0
121 | */
122 | removeListeners() {
123 | window.cancelAnimationFrame(this.requestAnime);
124 | document.removeEventListener('mousemove', this.onMouseMove);
125 | document.removeEventListener('mouseup', this.onMouseUp);
126 | document.removeEventListener('mouseleave', this.onMouseUp);
127 | }
128 |
129 | /*
130 | * @desc reset the scroll position of the container as browser's like to cache this
131 | * @since 1.0.0
132 | */
133 | componentDidMount() {
134 | this.container.scrollTop = 0;
135 | }
136 |
137 | /*
138 | * @desc cleanup when the component is no longer present
139 | * @since 1.0.0
140 | */
141 | componentWillUnmount() {
142 | this.removeListeners();
143 | this.container = null;
144 | this.content = null;
145 | this.handle = null;
146 | }
147 |
148 | render() {
149 | const { children } = this.props;
150 | const { namespace } = this.context;
151 |
152 | return (
153 |
154 |
(this.container = container)}
157 | onMouseEnter={this.onMouseEnter}
158 | onScroll={this.onScroll}
159 | >
160 |
(this.content = content)}
163 | >{children}
164 |
165 |
166 | (this.handle = handle)}
169 | onMouseDown={this.onMouseDown}
170 | >
171 |
172 |
173 |
174 |
175 | );
176 | }
177 | }
178 |
179 | Scrollable.contextType = AppContext;
180 |
181 | export default Scrollable;
182 |
--------------------------------------------------------------------------------
/src/js/hoc/with-context.js:
--------------------------------------------------------------------------------
1 | /*
2 | * used by component classes that need to hook into multiple contexts
3 | */
4 |
5 | import React from 'react';
6 |
7 | import {
8 | AppContext,
9 | EditorContext,
10 | } from '../context';
11 |
12 | export const withAppContext = Component => (props => (
13 |
14 | {context => }
15 |
16 | ));
17 |
18 | export const withEditorContext = Component => (props => (
19 |
20 | {context => }
21 |
22 | ));
--------------------------------------------------------------------------------
/src/js/loader.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import {
4 | AppContext,
5 | } from './context';
6 |
7 | const {
8 | Component,
9 | } = React;
10 |
11 | /*
12 | * @desc loads the additional .js chunks when the widget is first opened
13 | * @since 1.0.0
14 | */
15 | class Loader extends Component {
16 | constructor() {
17 | super(...arguments);
18 | }
19 |
20 | state = {
21 | Module: null,
22 | }
23 |
24 | async componentDidMount() {
25 | const { resolve } = this.props;
26 | const { default: Module } = await resolve();
27 |
28 | this.setState({ Module });
29 | }
30 |
31 | render() {
32 | const { namespace } = this.context;
33 | const { Module } = this.state;
34 |
35 | if (!Module) {
36 | return cache;
37 | }
38 |
39 | return ;
40 | }
41 | }
42 |
43 | Loader.contextType = AppContext;
44 |
45 | export default Loader;
46 |
--------------------------------------------------------------------------------
/src/js/module.js:
--------------------------------------------------------------------------------
1 | /*
2 | * this is the main entry point for the lazy-load chunks
3 | */
4 | require('../scss/editor.scss');
5 |
6 | import React from 'react';
7 | import Editor from './module/editor';
8 |
9 | const Module = props => ;
10 |
11 | export default Module;
12 |
13 |
--------------------------------------------------------------------------------
/src/js/module/container.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import FullPreview from './full-preview';
3 | import Draggable from '../hoc/draggable';
4 |
5 | import {
6 | EditorContext
7 | } from '../context';
8 |
9 | import {
10 | withAppContext,
11 | } from '../hoc/with-context';
12 |
13 | const {
14 | Component,
15 | createRef,
16 | forwardRef,
17 | } = React;
18 |
19 | const posMarginTop = 0.15;
20 |
21 | /*
22 | * @desc used to inherit the "draggable" events from the Draggable HOC
23 | * @since 1.0.0
24 | */
25 | const MainContainer = forwardRef(({ namespace, children, draggable }, ref) => (
26 |
27 |
28 | {children}
29 |
30 | ));
31 |
32 | /*
33 | * @desc the main editor container which positions and repositions itself on resize
34 | * @since 1.0.0
35 | */
36 | class Container extends Component {
37 | constructor() {
38 | super(...arguments);
39 | this.outputBarRef = createRef();
40 | }
41 |
42 | state = {
43 | previewSize: 'full',
44 | };
45 |
46 | /*
47 | * @desc positions the container on first mount and also after a window resize event
48 | * and also determines if the widget should be scrollable if the user's screen height is too small
49 | * @since 1.0.0
50 | */
51 | positionContainer = () => {
52 | const { containerRef } = this.context;
53 | const { current: container } = containerRef;
54 | const { current: outputBar } = this.outputBarRef;
55 | const { width, height } = container.getBoundingClientRect();
56 | const { appContext } = this.props;
57 | const { namespace, root } = appContext;
58 | const { innerWidth: winWidth, innerHeight: winHeight } = window;
59 |
60 | let containerHeight = height;
61 | if (outputBar) {
62 | const outputBarRect = outputBar.getBoundingClientRect();
63 | const { height: barHeight } = outputBarRect;
64 | containerHeight += barHeight;
65 | }
66 |
67 | let posTop = Math.max((winHeight * 0.5) - (containerHeight * 0.5), 0);
68 | let posLeft = Math.max((winWidth * 0.5) - (width * 0.5), 0);
69 |
70 | posTop -= Math.abs((winHeight - containerHeight)) * posMarginTop;
71 | posTop = Math.max(posTop, 0);
72 | posLeft = Math.max(posLeft, 0);
73 |
74 | if (posTop > 0) {
75 | if (this.overflowY) {
76 | this.overflowY = false;
77 | root.classList.remove(`${namespace}-overflow-y`);
78 | }
79 | } else if (!this.overflowY) {
80 | this.overflowY = true;
81 | root.classList.add(`${namespace}-overflow-y`);
82 | }
83 |
84 | if (posLeft > 0) {
85 | if (this.overflowX) {
86 | this.overflowX = false;
87 | root.classList.remove(`${namespace}-overflow-x`);
88 | }
89 | } else if (!this.overflowX) {
90 | this.overflowX = true;
91 | root.classList.add(`${namespace}-overflow-x`);
92 | }
93 |
94 | container.style.top = `${posTop}px`;
95 | container.style.left = `${posLeft}px`;
96 | }
97 |
98 | /*
99 | * @desc large preview has been closed
100 | * @since 1.0.0
101 | */
102 | onClosePreview = () => {
103 | const { showHidePreview } = this.context;
104 | showHidePreview();
105 | };
106 |
107 | /*
108 | * @desc large preview size has been changed
109 | * @since 1.0.0
110 | */
111 | onChangePreviewSize = (e, previewSize) => {
112 | e.stopPropagation();
113 | this.setState({ previewSize });
114 | };
115 |
116 | /*
117 | * @desc add/remove overflow class allowing for the large preview to be scrolled
118 | * @since 1.0.0
119 | */
120 | addRemoveOverflow(add) {
121 | const { appContext } = this.props;
122 | const { root, namespace } = appContext;
123 | if (add) {
124 | this.previewActivated = true;
125 | root.classList.add(`${namespace}-no-overflow`);
126 | } else {
127 | this.previewActivated = false;
128 | root.classList.remove(`${namespace}-no-overflow`);
129 | }
130 | }
131 |
132 | /*
133 | * @desc position the widget initially and add the resize listener
134 | * @since 1.0.0
135 | */
136 | componentDidMount() {
137 | const { appContext } = this.props;
138 | const { namespace, root } = appContext;
139 |
140 | this.positionContainer();
141 | root.classList.add(`${namespace}-mounted`);
142 | window.addEventListener('resize', this.positionContainer);
143 | }
144 |
145 | /*
146 | * @desc add/remove overflow class whenever the full preview is activated/deactivated
147 | * @since 1.0.0
148 | */
149 | componentDidUpdate() {
150 | const { previewActive } = this.context;
151 | if (previewActive) {
152 | if (!this.previewActivated) {
153 | this.addRemoveOverflow(true);
154 | }
155 | } else if (this.previewActivated) {
156 | this.addRemoveOverflow();
157 | }
158 | }
159 |
160 | /*
161 | * @desc cleanup after the widget has been closed
162 | * @since 1.0.0
163 | */
164 | componentWillUnmount() {
165 | window.removeEventListener('resize', this.positionContainer);
166 | this.outputBarRef = null;
167 | }
168 |
169 | render() {
170 | const {
171 | output,
172 | preview,
173 | currentMode,
174 | containerRef,
175 | previewActive,
176 | } = this.context;
177 |
178 | const { children, appContext } = this.props;
179 | const { namespace, outputBar } = appContext;
180 | const { previewSize } = this.state;
181 |
182 | let activeClass;
183 | let cancelEvent;
184 | let textOutput = output;
185 |
186 | if (!previewActive) {
187 | activeClass = '';
188 | } else {
189 | activeClass = ` ${namespace}-bg-active`;
190 | cancelEvent = this.onClosePreview;
191 | }
192 |
193 | if (currentMode === 'color' && output.charAt(0) === '#') {
194 | textOutput = output.toUpperCase();
195 | }
196 |
197 | return (
198 | <>
199 |
204 | {previewActive && (
205 |
211 | )}
212 |
213 |
214 | {children}
218 |
219 | {outputBar && !previewActive && (
220 |
221 | {textOutput}
222 |
223 | )}
224 | >
225 | );
226 | }
227 | }
228 |
229 | Container.contextType = EditorContext;
230 |
231 | export default withAppContext(Container);
--------------------------------------------------------------------------------
/src/js/module/editor/controls.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ColorControls from './controls/color-controls';
3 | import GradientControls from './controls/gradient-controls';
4 |
5 | import {
6 | AppContext,
7 | } from '../../context';
8 |
9 | const {
10 | memo,
11 | useContext,
12 | } = React;
13 |
14 | /*
15 | * @desc the main wrapper for the core controls which includes the color (plus palette) and gradient controls
16 | * @since 1.0.0
17 | */
18 | const Controls = ({ className }) => {
19 | const appContext = useContext(AppContext);
20 | const { namespace, colorMode } = appContext;
21 |
22 | return (
23 |
24 |
25 |
26 | {colorMode !== 'single' && }
27 |
28 |
29 | );
30 | };
31 |
32 | export default memo(Controls);
--------------------------------------------------------------------------------
/src/js/module/editor/controls/color-controls.js:
--------------------------------------------------------------------------------
1 | /*
2 | * the main entry point for all the color controls, including the color picker rainbow
3 | */
4 |
5 | import React from 'react';
6 | import ColorPalette from './color-controls/color-palette';
7 | import ColorFields from './color-controls/color-fields';
8 | import ColorList from './color-controls/color-list';
9 | import ColorButtons from './color-controls/color-buttons';
10 | import ColorPresets from './color-controls/color-presets';
11 | import DragSlider from '../../../components/sliders/drag-slider';
12 | import InputField from '../../../components/inputs/input-field';
13 | import Panel from '../../../components/wrappers/panel';
14 |
15 | import {
16 | AppContext,
17 | } from '../../../context';
18 |
19 | import {
20 | withEditorContext,
21 | } from '../../../hoc/with-context';
22 |
23 | import {
24 | shallowClone,
25 | } from '../../../utils/editor';
26 |
27 | import {
28 | sanitizeAlpha,
29 | } from '../../../utils/colors';
30 |
31 | import {
32 | toHsl,
33 | hslToRgb,
34 | } from '../../../utils/hsl';
35 |
36 | import {
37 | maxPositionPixels,
38 | } from '../../../data/defaults';
39 |
40 | const {
41 | Component,
42 | } = React;
43 |
44 | /*
45 | * @desc the main class that hosts all of the main color controls
46 | * @since 1.0.0
47 | */
48 | class ColorControls extends Component {
49 | constructor() {
50 | super(...arguments);
51 |
52 | const { editorContext } = this.props;
53 | const { currentColor } = editorContext;
54 | const { rgb: rgbData, opacity, position } = currentColor;
55 |
56 | this.state = {
57 | opacity,
58 | rgbData,
59 | position,
60 | colorMenu: 'rgb',
61 | };
62 | }
63 |
64 | /*
65 | * @desc fires after the internal state has been changed,
66 | * kicking up the color change to the editor level
67 | * @since 1.0.0
68 | */
69 | onUpdate = () => {
70 | const {
71 | rgbData,
72 | opacity,
73 | } = this.state;
74 |
75 | const value = rgbData.slice();
76 | value.length = 4;
77 | value[3] = opacity;
78 |
79 | const { editorContext } = this.props;
80 | const { onChangeColor } = editorContext;
81 | onChangeColor(value);
82 | };
83 |
84 | /*
85 | * @desc fires after the internal state has been changed,
86 | * kicking up the position change to the editor level
87 | * @since 1.0.0
88 | */
89 | onUpdatePosition() {
90 | const { position } = this.state;
91 | const { editorContext } = this.props;
92 | const { onChangePosition } = editorContext;
93 | onChangePosition(position, true);
94 | }
95 |
96 | /*
97 | * @desc when state is set from below and kicked back up,
98 | * and then state is set here, there's no need for an additional render below
99 | * @since 1.0.0
100 | */
101 | shouldComponentUpdate() {
102 | if (this.bounce) {
103 | this.bounce = false;
104 | return false;
105 | }
106 |
107 | return true;
108 | }
109 |
110 | /*
111 | * @desc all state for the colors is internally managed,
112 | * but the state need to be overridden if a preset is selected, on user input or if
113 | * a position change occurs (coming from the controls when hovering over the mini preview)
114 | * @since 1.0.0
115 | */
116 | static getDerivedStateFromProps(props, state) {
117 | const { bounce } = state;
118 | if (bounce) {
119 | return { bounce: false };
120 | }
121 |
122 | const { editorContext } = props;
123 | const { editorUpdate, positionChange } = editorContext;
124 |
125 | if (editorUpdate || positionChange) {
126 | const { currentColor } = editorContext;
127 | const newState = {};
128 |
129 | if (editorUpdate) {
130 | const { rgb, opacity } = currentColor;
131 | newState.opacity = opacity;
132 | newState.rgbData = rgb;
133 | }
134 | if (positionChange) {
135 | const { position } = currentColor;
136 | newState.position = position;
137 | }
138 | newState.updatePalette = editorUpdate;
139 | return newState;
140 | }
141 |
142 | return null;
143 | }
144 |
145 | /*
146 | * @desc fires whenever one of the color controls is changed
147 | * @param string|array val - the changed value
148 | * @param string type - the slug for the control that was changed
149 | * @since 1.0.0
150 | */
151 | onChange = (val, type) => {
152 | const newState = { bounce: true };
153 | const newVal = shallowClone(val);
154 |
155 | let colorChanged;
156 | let updatePalette;
157 | let opacity;
158 |
159 | switch (type) {
160 | case 'opacity':
161 | opacity = sanitizeAlpha(newVal * 0.01);
162 | newState.opacity = opacity;
163 | updatePalette = false;
164 | break;
165 | case 'rgb':
166 | newState.rgbData = newVal;
167 | updatePalette = true;
168 | colorChanged = true;
169 | break;
170 | case 'hsl':
171 | newState.rgbData = hslToRgb(...newVal);
172 | updatePalette = true;
173 | colorChanged = true;
174 | break;
175 | case 'clear':
176 | newState.opacity = 0;
177 | updatePalette = false;
178 | break;
179 | case 'position':
180 | newState.position = val;
181 | updatePalette = false;
182 | break;
183 | default:
184 | newState.rgbData = newVal;
185 | updatePalette = false;
186 | colorChanged = true;
187 | }
188 |
189 | // if the change came from the color palette, which maintain its own state,
190 | // this will allow the color palette class to avoid the extra unneeded render
191 | newState.updatePalette = updatePalette;
192 |
193 | this.setState(prevState => {
194 | const { opacity } = prevState;
195 | if (colorChanged && opacity === 0) {
196 | newState.opacity = 1;
197 | }
198 | return newState;
199 | }, () => {
200 | this.bounce = true;
201 | if (type !== 'position') {
202 | this.onUpdate();
203 | } else {
204 | this.onUpdatePosition();
205 | }
206 | });
207 | };
208 |
209 | /*
210 | * @desc changes the view between the rgb and hsl controls
211 | * @param string colorMenu - "rgb" or "hsl"
212 | * @since 1.0.0
213 | */
214 | setColorMenu = colorMenu => {
215 | this.setState({
216 | colorMenu,
217 | updatePalette: false,
218 | });
219 | };
220 |
221 | /*
222 | * @desc changes the unit of the currently selected color
223 | * @param string value - "%" or "px"
224 | * @since 1.0.0
225 | */
226 | changeUnit = value => {
227 | const { editorContext } = this.props;
228 | const { onChangeColorUnit } = editorContext;
229 | onChangeColorUnit(value);
230 | };
231 |
232 | render() {
233 | const { namespace, colorMode } = this.context;
234 | const { editorContext } = this.props;
235 |
236 | const {
237 | rgbData,
238 | opacity,
239 | position,
240 | colorMenu,
241 | updatePalette,
242 | } = this.state;
243 |
244 | const {
245 | colors,
246 | onAddColor,
247 | currentColor,
248 | onSavePreset,
249 | onDeleteColor,
250 | } = editorContext;
251 |
252 | const hslData = colorMenu === 'rgb' ? null : toHsl(...rgbData);
253 | const opacityHighlighted = opacity !== 0 ? '' : `${namespace}-highlighted`;
254 | const addColorClass = colors.length > 1 ? null : `${namespace}-btn-green-active`;
255 | const { color, unit: colorUnit } = currentColor;
256 |
257 | return (
258 | <>
259 |
260 |
261 |
268 |
275 |
276 | 1}
285 | opacityActive={opacity === 0}
286 | />
287 |
288 |
289 |
293 |
301 | {colorMode !== 'single' && (
302 |
312 | )}
313 |
320 |
321 | {colorMode === 'single' && (
322 |
323 |
324 |
325 | )}
326 | >
327 | );
328 | }
329 | }
330 |
331 | ColorControls.contextType = AppContext;
332 |
333 | export default withEditorContext(ColorControls);
334 |
335 |
336 |
--------------------------------------------------------------------------------
/src/js/module/editor/controls/color-controls/color-buttons.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import SaveBtn from '../../../../components/buttons/save-btn';
3 | import CopyBtn from '../../../../components/buttons/copy-btn';
4 | import Button from '../../../../components/buttons/button';
5 | import Row from '../../../../components/wrappers/row';
6 |
7 | import {
8 | AppContext,
9 | } from '../../../../context';
10 |
11 | const {
12 | memo,
13 | useContext,
14 | } = React;
15 |
16 | /*
17 | * @desc the action buttons placed below the palette:
18 | * 1. save preset
19 | * 2. set color to transparent
20 | * 3. add new color to gradient
21 | * 4. delete currently selected color from gradient
22 | * @since 1.0.0
23 | */
24 | const ColorButtons = ({
25 | onChange,
26 | colorMode,
27 | canDelete,
28 | onAddColor,
29 | onSavePreset,
30 | onDeleteColor,
31 | opacityActive,
32 | addColorClass,
33 | currentOutput,
34 | }) => {
35 | const locale = useContext(AppContext);
36 | const { namespace } = locale;
37 |
38 | return (
39 |
40 | onSavePreset('color')}
46 | />
47 | {colorMode === 'single' && (
48 |
52 | )}
53 |
77 | );
78 | }
79 |
80 | export default memo(ColorButtons);
--------------------------------------------------------------------------------
/src/js/module/editor/controls/color-controls/color-fields.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ColorInputs from './color-fields/color-inputs';
3 | import RadioMenu from '../../../../components/radio-menu';
4 |
5 | const menuList = {
6 | rgb: 'RGB',
7 | hsl: 'HSL',
8 | };
9 |
10 | /*
11 | * @desc the rgb and hls inputs as well as the menu that switches between them
12 | * @since 1.0.0
13 | */
14 | const ColorFields = ({ hsl, rgb, onChange, colorMenu, setColorMenu }) => {
15 | return (
16 | <>
17 |
22 | {colorMenu === 'rgb' && (
23 |
29 | )}
30 | {colorMenu === 'hsl' && (
31 |
36 | )}
37 | >
38 | );
39 | }
40 |
41 | export default ColorFields;
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/src/js/module/editor/controls/color-controls/color-fields/color-inputs.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import InputField from '../../../../../components/inputs/input-field';
3 |
4 | import {
5 | AppContext,
6 | } from '../../../../../context';
7 |
8 | const {
9 | Component,
10 | } = React;
11 |
12 | // rgb and hsl labels and max values (100 is default for max on imputs)
13 | const inputs = {
14 | rgb: [
15 | { label: 'Red', max: 255 },
16 | { label: 'Green', max: 255 },
17 | { label: 'Blue', max: 255 },
18 | ],
19 | hsl: [
20 | { label: 'Hue', max: 359 },
21 | { label: 'Saturation' },
22 | { label: 'Lightness' },
23 | ],
24 | }
25 |
26 | const lightnessRecord = {
27 | minMax: [0, 100],
28 | records: [0, 1],
29 | };
30 |
31 | const saturationRecord = {
32 | minMax: [0, 100],
33 | records: [0],
34 | };
35 |
36 | const rgbDisabled = [false, false, false];
37 |
38 | /*
39 | * @desc used to determine if the "h" or "s" inputs in the hsl controls
40 | * should be disabled depending on its adjacent value
41 | * @since 1.0.0
42 | */
43 | const updateRecords = (slicedValue, records) => {
44 | records.forEach(record => {
45 | const { index, value } = record;
46 | slicedValue[index] = value;
47 | });
48 | };
49 |
50 | /*
51 | * @desc used to manage the rgb and hsl controls
52 | * @since 1.0.0
53 | */
54 | class ColorInputs extends Component {
55 | constructor() {
56 | super(...arguments);
57 | }
58 |
59 | /*
60 | * @desc fires when an rgb or hsl value has changed
61 | * @since 1.0.0
62 | */
63 | onChange = (newValue, prop, updating, channel, records) => {
64 | const {
65 | type,
66 | value,
67 | onChange,
68 | } = this.props;
69 |
70 | const slicedValue = value.slice();
71 |
72 | if (records) {
73 | updateRecords(slicedValue, records);
74 | }
75 |
76 | slicedValue[channel] = newValue;
77 | onChange(slicedValue, type, updating);
78 | };
79 |
80 | render() {
81 | const {
82 | type,
83 | value,
84 | } = this.props;
85 |
86 | let disabled;
87 | if (type === 'rgb') {
88 | disabled = rgbDisabled;
89 | } else {
90 | disabled = [value[1] === 0, value[2] === 0 || value[2] === 100, false];
91 | if (Math.round(value[0]) === 360) {
92 | value[0] = 359;
93 | }
94 | }
95 |
96 | return (
97 | inputs[type].map((input, index) => {
98 | const { label, min, max } = input;
99 | const isDisabled = disabled[index];
100 |
101 | let fetchRecords;
102 | let sliderMinMax;
103 |
104 | if (type === 'hsl' && index > 0) {
105 | fetchRecords = true;
106 | sliderMinMax = index === 1 ? saturationRecord : lightnessRecord;
107 | } else {
108 | fetchRecords = false;
109 | }
110 |
111 | return (
112 |
127 | );
128 | })
129 | );
130 | }
131 | }
132 |
133 | ColorInputs.contextType = AppContext;
134 |
135 | export default ColorInputs;
--------------------------------------------------------------------------------
/src/js/module/editor/controls/color-controls/color-list.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import SelectBox from '../../../../components/select-box';
3 | import Row from '../../../../components/wrappers/row';
4 |
5 | import {
6 | EditorContext,
7 | } from '../../../../context';
8 |
9 | import {
10 | withAppContext,
11 | } from '../../../../hoc/with-context';
12 |
13 | import {
14 | rgbToHex,
15 | hexToRGB,
16 | toFullHex,
17 | isValidHex,
18 | isValidRGB,
19 | } from '../../../../utils/colors';
20 |
21 | const {
22 | Component,
23 | } = React;
24 |
25 | /*
26 | * @desc used to create the main dropdown for the current gradient colors
27 | * @since 1.0.0
28 | */
29 | class ColorList extends Component {
30 | constructor() {
31 | super(...arguments);
32 |
33 | const { value } = this.props;
34 | const hex = rgbToHex(...value);
35 |
36 | this.state = {
37 | value: hex,
38 | isValid: true,
39 | };
40 | }
41 |
42 | /*
43 | * @desc cache the original value and auto-select the input field
44 | * @since 1.0.0
45 | */
46 | onFocus = e => {
47 | const { target } = e;
48 | const { value } = target;
49 |
50 | this.setState({
51 | blurred: false,
52 | origValue: value,
53 | }, () => {
54 | target.select();
55 | });
56 | };
57 |
58 | /*
59 | * @desc possibly restore the cached value if the current value is invalid
60 | * @since 1.0.0
61 | */
62 | onBlur = () => {
63 | let callback;
64 | let callbackValue;
65 |
66 | this.setState(prevState => {
67 | const { isValid } = prevState;
68 | if (isValid) {
69 | return null;
70 | }
71 |
72 | const newState = { blurred: true, isValid: true };
73 | let { value } = prevState;
74 |
75 | value = `#${value.replace('#', '')}`;
76 | callback = true;
77 |
78 | if (!isValidHex(value, true)) {
79 | const { origValue } = prevState;
80 | newState.value = origValue;
81 | callbackValue = origValue;
82 | } else {
83 | callbackValue = value;
84 | }
85 |
86 | return newState;
87 | }, () => {
88 | if (callback) {
89 | const { onChange } = this.props;
90 | onChange(hexToRGB(callbackValue));
91 | }
92 | });
93 | };
94 |
95 | /*
96 | * @desc user has changed the input field for the hex value,
97 | * set the new state and trigger the callback if its valid
98 | * @since 1.0.0
99 | */
100 | onInputChange = e => {
101 | const { target } = e;
102 | const { value } = target;
103 |
104 | if (! /^[#0-9A-F]*$/i.test(value)) {
105 | return;
106 | }
107 |
108 | const isValid = isValidHex(value);
109 | this.setState({ value, isValid }, () => {
110 | if (isValid) {
111 | const { onChange } = this.props;
112 | onChange(hexToRGB(value), 'rgb');
113 | }
114 | });
115 | };
116 |
117 | /*
118 | * @desc user has selected a new color from the dropdown
119 | * @since 1.0.0
120 | */
121 | onChangeSelectBox = (value, prop, index) => {
122 | const { colors } = this.context;
123 |
124 | this.setState({
125 | value: colors[index],
126 | isValid: true,
127 | }, () => {
128 | const { onActivateColor } = this.context;
129 | onActivateColor(index);
130 | });
131 | }
132 |
133 | /*
134 | * @desc always use the internal state value if the current internal value is invalid,
135 | * or else pull the value from the prop
136 | * @since 1.0.0
137 | */
138 | static getDerivedStateFromProps(props, state) {
139 | const {
140 | isValid,
141 | blurred,
142 | value: stateValue,
143 | } = state;
144 |
145 | const { value: propValue } = props;
146 | const hex = isValidRGB(Array.from(propValue)) ? rgbToHex(...propValue, false) : stateValue;
147 |
148 | let value;
149 | if (!blurred) {
150 | value = isValid ? hex : stateValue;
151 | } else {
152 | value = hex;
153 | }
154 |
155 | return { value };
156 | }
157 |
158 | render() {
159 | const { appContext, className } = this.props;
160 | const { namespace, colorMode } = appContext;
161 | const { value: stateValue } = this.state;
162 | const { colors, selectedColor, onSwapColor } = this.context;
163 | const parsedValue = stateValue !== '' ? `#${stateValue.replace('#', '')}` : '';
164 |
165 | // create a list of colors to display in the dropdown when activated
166 | // TODO: lift this up to the editor level for performance?
167 | const list = {};
168 | colors.forEach((color, index) => {
169 | const { hex } = color;
170 | list[index] = {
171 | label: toFullHex(hex),
172 | preview: hex,
173 | }
174 | });
175 |
176 | return (
177 |
178 |
197 |
198 | );
199 | }
200 | }
201 |
202 | ColorList.contextType = EditorContext;
203 |
204 | export default withAppContext(ColorList);
205 |
--------------------------------------------------------------------------------
/src/js/module/editor/controls/color-controls/color-presets.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Presets from '../../presets';
3 |
4 | import {
5 | AppContext,
6 | } from '../../../../context';
7 |
8 | const {
9 | memo,
10 | useContext,
11 | } = React;
12 |
13 | /*
14 | * @desc displays a presets panel when the editor is in "single" mode (colors only, no gradients)
15 | * @since 1.0.0
16 | */
17 | const ColorPresets = () => {
18 | const locale = useContext(AppContext);
19 | const { namespace } = locale;
20 |
21 | return (
22 |
25 | );
26 | };
27 |
28 | export default memo(ColorPresets);
--------------------------------------------------------------------------------
/src/js/module/editor/controls/gradient-controls.js:
--------------------------------------------------------------------------------
1 | /*
2 | * the main entry point for all the gradient controls (not including colors of hints)
3 | */
4 |
5 | import React from 'react';
6 | import GradientSwitcher from './gradient-controls/gradient-switcher';
7 | import RadialControls from './gradient-controls/radial-controls';
8 | import RadioMenu from '../../../components/radio-menu';
9 | import SelectBox from '../../../components/select-box';
10 | import Toggle from '../../../components/toggle';
11 | import Wheel from '../../../components/wheel';
12 | import Row from '../../../components/wrappers/row';
13 | import Panel from '../../../components/wrappers/panel';
14 |
15 | import {
16 | AppContext,
17 | } from '../../../context';
18 |
19 | import {
20 | withEditorContext,
21 | } from '../../../hoc/with-context';
22 |
23 | import {
24 | getDirection,
25 | } from '../../../utils/editor';
26 |
27 | const {
28 | Component,
29 | } = React;
30 |
31 | // gradient type switcher values
32 | const gradientTypes = {
33 | linear: 'Linear',
34 | radial: 'Radial',
35 | conic: 'Conic',
36 | };
37 |
38 | // gradient type switcher values without conic option
39 | const gradientsNoConic = {
40 | linear: 'Linear',
41 | radial: 'Radial',
42 | };
43 |
44 | // radial shape switcher values
45 | const radialShapes = {
46 | ellipse: 'Ellipse',
47 | circle: 'Circle',
48 | size: 'Size',
49 | };
50 |
51 | // radial extent values for dropdown
52 | const radialExtents = {
53 | 'closest-side': { label: 'Closest Side' },
54 | 'closest-corner': { label: 'Closest Corner' },
55 | 'farthest-side': { label: 'Farthest Side' },
56 | 'farthest-corner': { label: 'Farthest Corner' },
57 | };
58 |
59 | // linear direction values for dropdown
60 | const linearDirections = {
61 | 'degree': {
62 | label: 'Degrees',
63 | icon: 'rotate_left',
64 | iconStyle: { top: '1px' },
65 | },
66 | 'right': {
67 | label: 'To Right',
68 | icon: 'arrow_forward',
69 | },
70 | 'bottom': {
71 | label: 'To Bottom',
72 | icon: 'arrow_downward',
73 | },
74 | 'left': {
75 | label: 'To Left',
76 | icon: 'arrow_back',
77 | },
78 | 'top': {
79 | label: 'To Top',
80 | icon: 'arrow_upward',
81 | },
82 | 'right_bottom': {
83 | label: 'To Right Bottom',
84 | icon: 'arrow_downward',
85 | iconStyle: { transform: 'rotate(-45deg)' },
86 | },
87 | 'left_bottom': {
88 | label: 'To Left Bottom',
89 | icon: 'arrow_downward',
90 | iconStyle: { transform: 'rotate(45deg)' },
91 | },
92 | 'right_top': {
93 | label: 'To Right Top',
94 | icon: 'arrow_upward',
95 | iconStyle: { transform: 'rotate(45deg)' },
96 | },
97 | 'left_top': {
98 | label: 'To Left Top',
99 | icon: 'arrow_upward',
100 | iconStyle: { transform: 'rotate(-45deg)' },
101 | },
102 | };
103 |
104 | // labels for gradient positioning and radial custom sizes
105 | const positionLabels = ['Left', 'Top'];
106 | const sizeLabels = ['Width', 'Height'];
107 |
108 |
109 | /*
110 | * @desc the main class that hosts all of the main gradient controls
111 | * @since 1.0.0
112 | */
113 | class GradientControls extends Component {
114 | constructor() {
115 | super(...arguments);
116 | }
117 |
118 | state = {
119 | angleChanging: false,
120 | };
121 |
122 | /*
123 | * @desc when state is set from below and kicked back up,
124 | * and then state is set here, there's no need for an additional render below
125 | * @since 1.0.0
126 | */
127 | shouldComponentUpdate() {
128 | if (this.bounce) {
129 | this.bounce = false;
130 | return false;
131 | }
132 |
133 | return true;
134 | }
135 |
136 | /*
137 | * @desc used to only change the linear direction dropdown selection after
138 | * the user has officially finished changing the angle (i.e. on mouseup)
139 | * @since 1.0.0
140 | */
141 | static getDerivedStateFromProps(props, state) {
142 | const { angleChanging } = state;
143 | if (!angleChanging) {
144 | const { editorContext } = props;
145 | const { currentGradient } = editorContext;
146 | const { angle } = currentGradient;
147 | const curDirection = getDirection(angle);
148 | const { direction } = curDirection;
149 | return { direction };
150 | }
151 |
152 | return null;
153 | }
154 |
155 | /*
156 | * @desc fires whenever a gradient value has been changed
157 | * @param string|number value - new value to be applied to the gradient
158 | * @param string opt - the name of the option that changed
159 | * @param boolean angleChanging - if the angle is being changed or not (see above)
160 | * @since 1.0.0
161 | */
162 | onChange = (value, opt, angleChanging) => {
163 | const { editorContext } = this.props;
164 | const { onChangeGradient } = editorContext;
165 |
166 | if (opt !== 'angle') {
167 | onChangeGradient(opt, value);
168 | } else {
169 | this.bounce = true;
170 | this.setState({ angleChanging }, () => {
171 | onChangeGradient(opt, value);
172 | });
173 | }
174 | };
175 |
176 | /*
177 | * @desc fires when the angle/direction has changed
178 | * @param string|number value - new angle number or direction selection
179 | * @since 1.0.0
180 | */
181 | onChangeDirection = value => {
182 | const direction = getDirection(value);
183 | const { value: angle } = direction;
184 | const { editorContext } = this.props;
185 | const { onChangeGradient } = editorContext;
186 | onChangeGradient('angle', angle);
187 | };
188 |
189 | render() {
190 | const { direction } = this.state;
191 | const { editorContext } = this.props;
192 | const { namespace, allowConic, conicNote } = this.context;
193 |
194 | const {
195 | currentMode,
196 | currentGradient,
197 | } = editorContext;
198 |
199 | const {
200 | angle,
201 | shape,
202 | sizes,
203 | extent,
204 | repeating,
205 | positions,
206 | type: gradientType,
207 | } = currentGradient;
208 |
209 | const disabledClass = currentMode !== 'color' ? '' : ` ${namespace}-disabled`;
210 | const dataClassName = shape !== 'size' ? '' : `${namespace}-sizes`;
211 |
212 | return (
213 |
214 |
215 |
216 |
217 |
218 |
224 |
225 | {gradientType !== 'radial' && (
226 |
227 |
232 |
233 | )}
234 | {gradientType === 'linear' && (
235 |
236 |
245 |
246 | )}
247 | {gradientType === 'radial' && (
248 | <>
249 |
250 |
257 |
258 | {shape === 'size' && (
259 |
266 | )}
267 | >
268 | )}
269 | {gradientType !== 'linear' && (
270 |
277 | )}
278 | {gradientType === 'radial' && shape !== 'size' && (
279 |
280 |
289 |
290 | )}
291 |
292 |
298 |
299 | {gradientType === 'conic' && conicNote && (
300 |
301 | Browser Support for Conic Gradients varies
302 |
303 | )}
304 |
305 | );
306 | }
307 | }
308 |
309 | GradientControls.contextType = AppContext;
310 |
311 | export default withEditorContext(GradientControls);
--------------------------------------------------------------------------------
/src/js/module/editor/controls/gradient-controls/gradient-switcher.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import SelectBox from '../../../../components/select-box';
3 | import SaveBtn from '../../../../components/buttons/save-btn';
4 | import Button from '../../../../components/buttons/button';
5 |
6 | import {
7 | AppContext,
8 | EditorContext,
9 | } from '../../../../context';
10 |
11 | const {
12 | useContext,
13 | } = React;
14 |
15 | /*
16 | * @desc displays the dropdown to switch between gradients
17 | * and also the mini actions buttons:
18 | * 1. add new gradient to the stack
19 | * 2. save currently selected gradient as preset
20 | * 3. deleted currently selected gradient from stack
21 | * @since 1.0.0
22 | */
23 | const GradientSwitcher = () => {
24 | const appContext = useContext(AppContext);
25 | const editorContext = useContext(EditorContext);
26 |
27 | const {
28 | onSavePreset,
29 | onAddGradient,
30 | onSwapGradient,
31 | onDeleteGradient,
32 | onSwitchGradient,
33 | currentPreview,
34 | currentOutput,
35 | selectedGradient,
36 | value: editorValue,
37 | } = editorContext;
38 |
39 | const list = {};
40 | const { length: editorLength } = editorValue;
41 | const { namespace } = appContext;
42 |
43 | let i = editorLength;
44 | let currentLabel;
45 | let count = 0;
46 |
47 | while (i--) {
48 | list[i] = {
49 | index: i + 1,
50 | label: `Gradient #${i + 1}`,
51 | preview: editorValue[count++],
52 | };
53 | }
54 |
55 | const currentIndex = Math.abs(selectedGradient - (editorLength - 1));
56 | if (editorLength > 1) {
57 | const selectedItm = list[currentIndex];
58 | const { index } = selectedItm;
59 | currentLabel = `Gradient ${index}/${editorLength}`;
60 | }
61 |
62 | return (
63 | <>
64 |
76 |
81 | onSavePreset('gradient')}
86 | />
87 |
94 | >
95 | );
96 | };
97 |
98 | export default GradientSwitcher;
99 |
--------------------------------------------------------------------------------
/src/js/module/editor/controls/gradient-controls/radial-controls.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import InputField from '../../../../components/inputs/input-field';
3 | import Row from '../../../../components/wrappers/row';
4 |
5 | import {
6 | AppContext,
7 | } from '../../../../context';
8 |
9 | import {
10 | maxPositionPixels,
11 | } from '../../../../data/defaults';
12 |
13 | const {
14 | memo,
15 | useContext,
16 | } = React;
17 |
18 | /*
19 | * @desc displays the "width" and "height" controls for
20 | * when "size" is selected as the shape for a radial gradient
21 | * @since 1.0.0
22 | */
23 | const RadialControls = ({
24 | labels,
25 | className,
26 | value: data,
27 | prop: gradientProp,
28 | onChange: callback,
29 | }) => {
30 | const appContext = useContext(AppContext);
31 | const { namespace } = appContext;
32 |
33 | const { x, y } = data;
34 | const { value: valueX, unit: unitX } = x;
35 | const { value: valueY, unit: unitY } = y;
36 |
37 | const maxX = unitX === '%' ? 100 : maxPositionPixels;
38 | const maxY = unitY === '%' ? 100 : maxPositionPixels;
39 |
40 | const onChangeValue = (value, prop) => {
41 | data[prop].value = value;
42 | callback({ ...data }, gradientProp);
43 | };
44 |
45 | const onChangeUnit = (value, prop) => {
46 | data[prop].unit = value;
47 | callback({ ...data }, gradientProp);
48 | };
49 |
50 | return (
51 |
52 |
53 |
63 |
64 |
65 |
75 |
76 |
77 | );
78 | };
79 |
80 | export default memo(RadialControls);
--------------------------------------------------------------------------------
/src/js/module/editor/footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Button from '../../components/buttons/button';
3 | import UserInput from './footer/user-input';
4 |
5 | import {
6 | AppContext,
7 | } from '../../context';
8 |
9 | const {
10 | memo,
11 | useContext,
12 | } = React;
13 |
14 | /*
15 | * @desc the main footer for the editor which includes the save button and also the user input field
16 | * @since 1.0.0
17 | */
18 | const Footer = ({ clearLayers, onClearGradient }) => {
19 | const locale = useContext(AppContext);
20 | const { namespace, onSave } = locale;
21 |
22 | return (
23 |
24 |
29 |
30 |
37 | {clearLayers && (
38 |
44 | )}
45 |
46 | );
47 | };
48 |
49 | export default memo(Footer);
50 |
--------------------------------------------------------------------------------
/src/js/module/editor/footer/user-input.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import getColorData from '../../../utils/data';
3 |
4 | import {
5 | EditorContext,
6 | } from '../../../context';
7 |
8 | import {
9 | withAppContext,
10 | } from '../../../hoc/with-context';
11 |
12 | import {
13 | isValidColor,
14 | } from '../../../utils/colors';
15 |
16 | const {
17 | Component,
18 | createRef,
19 | } = React;
20 |
21 | /*
22 | * @desc this manages the user input field in the footer,
23 | * updating the editor controls when valid input is entered
24 | * @since 1.0.0
25 | */
26 | class UserInput extends Component {
27 | constructor() {
28 | super(...arguments);
29 | this.inputRef = createRef();
30 | this.input = '';
31 | }
32 |
33 | /*
34 | * @desc user input has changed, sanitize the input and make a change if its valid
35 | * @since 1.0.0
36 | */
37 | onChange = () => {
38 | const { current: inputRef } = this.inputRef;
39 | const { value: inputValue } = inputRef;
40 | const { appContext } = this.props;
41 | const { colorMode, allowConic, regGradient } = appContext;
42 |
43 | let typedText = inputValue.toLowerCase()
44 | .replace(/;|\:|\{|\}/g, '')
45 | .replace(/ +/g, ' ')
46 | .replace(/-webkit-|-moz-/, '')
47 | .replace(/\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/g, '')
48 | .replace('background-color', '')
49 | .replace('background-image', '')
50 | .replace('background', '')
51 | .trim();
52 |
53 | while (typedText.charAt(typedText.length - 1) === ',') {
54 | typedText = typedText.slice(0, -1);
55 | }
56 |
57 | if (typedText !== 'transparent') {
58 | let isValidGradient;
59 | if (colorMode !== 'single') {
60 | isValidGradient = typedText.length > 23 && regGradient.test(typedText);
61 | }
62 | if (!isValidColor(typedText, true) && !isValidGradient) {
63 | this.data = null;
64 | return;
65 | }
66 | }
67 |
68 | const data = getColorData(typedText, allowConic);
69 | const { gradient } = data;
70 | const type = !gradient ? 'color' : 'gradient';
71 | const { setColorByRecord } = this.context;
72 |
73 | this.data = data;
74 | setColorByRecord(data, type, true);
75 | }
76 |
77 | /*
78 | * @desc reset the user input value if a change has been made elsewhere in the App
79 | * and the input value no longer matches the editor's value
80 | * @since 1.0.0
81 | */
82 | componentDidUpdate() {
83 | if (this.data) {
84 | const { gradient } = this.data;
85 | if (!gradient) {
86 | const { hex } = this.data;
87 | const { currentColor } = this.context;
88 | const { hex: editorHex } = currentColor;
89 |
90 | if (hex !== editorHex) {
91 | this.resetInput();
92 | }
93 | } else {
94 | let { output } = this.data;
95 | const { output: editorOutput } = this.context;
96 |
97 | if (output) {
98 | output = output.toLowerCase();
99 | }
100 | if (output !== editorOutput.toLowerCase()) {
101 | this.resetInput();
102 | }
103 | }
104 | }
105 | }
106 |
107 | /*
108 | * @desc reset the user input value
109 | * @since 1.0.0
110 | */
111 | resetInput() {
112 | const { current: inputRef } = this.inputRef;
113 | this.data = null;
114 | inputRef.value = '';
115 | }
116 |
117 | componentWillUnmount() {
118 | this.data = null;
119 | this.inputRef = null;
120 | }
121 |
122 | render() {
123 | const { appContext } = this.props;
124 | const { namespace, colorMode } = appContext;
125 | const placeholder = colorMode !== 'single' ? 'Enter a Color or Gradient' : 'Enter a Color';
126 |
127 | return (
128 |
140 | );
141 | }
142 | }
143 |
144 | UserInput.contextType = EditorContext;
145 |
146 | export default withAppContext(UserInput);
--------------------------------------------------------------------------------
/src/js/module/editor/header.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Strip from './header/strip';
3 | import Button from '../../components/buttons/button';
4 |
5 | import {
6 | AppContext,
7 | } from '../../context';
8 |
9 | const {
10 | memo,
11 | useContext,
12 | } = React;
13 |
14 | /*
15 | * @desc the main header for the editor which includes the close button and also the preview strip
16 | * @since 1.0.0
17 | */
18 | const Header = ({ showHidePreview }) => {
19 | const locale = useContext(AppContext);
20 | const { namespace, colorMode, onCancel } = locale;
21 |
22 | return (
23 |
24 |
30 |
31 | {colorMode !== 'single' && (
32 | showHidePreview(true)}
36 | />
37 | )}
38 | {colorMode === 'single' && (
39 |
44 | )}
45 |
46 | );
47 | };
48 |
49 | export default memo(Header);
--------------------------------------------------------------------------------
/src/js/module/editor/header/strip.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ColorPoint from './strip/color-point';
3 | import HintPoint from './strip/hint-point';
4 |
5 | import {
6 | EditorContext,
7 | } from '../../../context';
8 |
9 | import {
10 | withAppContext,
11 | } from '../../../hoc/with-context';
12 |
13 | const {
14 | Component,
15 | createRef,
16 | } = React;
17 |
18 | /*
19 | * @desc the horizontal linear-gradient strip used to manage color point positions and also show hint tooltip percentages
20 | * @since 1.0.0
21 | */
22 | class Strip extends Component {
23 | constructor() {
24 | super(...arguments);
25 | this.stripRef = createRef();
26 | }
27 |
28 | /*
29 | * @desc add a new color to the current gradient and position it where the strip was clicked
30 | * @since 1.0.0
31 | */
32 | onClick = e => {
33 | const { pageX } = e;
34 | const { current: strip } = this.stripRef;
35 |
36 | const rect = strip.getBoundingClientRect();
37 | const { width, left } = rect;
38 |
39 | const perc = Math.max(0, Math.min(pageX - window.scrollX - left, width));
40 | const position = (perc / width) * 100;
41 |
42 | const { onAddColor } = this.context;
43 | onAddColor(position);
44 | };
45 |
46 | render() {
47 | const { appContext } = this.props;
48 | const { namespace, colorMode } = appContext;
49 |
50 | const {
51 | strip,
52 | hints,
53 | colors,
54 | activeHint,
55 | currentMode,
56 | currentColor,
57 | selectedColor,
58 | onDeleteColor,
59 | onActivateColor,
60 | onChangePosition,
61 | } = this.context;
62 |
63 | let currentColors;
64 | let colorSelected;
65 |
66 | if (colorMode !== 'single') {
67 | if (currentMode !== 'color') {
68 | currentColors = colors;
69 | colorSelected = selectedColor;
70 | } else {
71 | // only display one color point when in "single color mode"
72 | currentColors = [colors[selectedColor]];
73 | colorSelected = 0;
74 | }
75 | }
76 |
77 | const { unit: activeUnit } = currentColor;
78 |
79 | return (
80 |
81 |
82 | {activeHint !== -1 && (
83 |
87 | )}
88 |
92 | {colorMode !== 'single' && (
93 | <>
94 |
99 |
100 | {currentColors.map((color, index) => {
101 | return (
102 | 1}
113 | index={index}
114 | />
115 | );
116 | })}
117 |
118 | >
119 | )}
120 |
121 |
122 | );
123 | }
124 | }
125 |
126 | Strip.contextType = EditorContext;
127 |
128 | export default withAppContext(Strip);
--------------------------------------------------------------------------------
/src/js/module/editor/header/strip/color-point.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Icon from '../../../../components/icon';
3 |
4 | import {
5 | AppContext,
6 | } from '../../../../context';
7 |
8 | import {
9 | maxPositionPixels,
10 | } from '../../../../data/defaults';
11 |
12 | import {
13 | convertPositionUnit,
14 | } from '../../../../utils/utilities';
15 |
16 | const {
17 | Component,
18 | } = React;
19 |
20 | /*
21 | * @desc represents a single color point in the horizontal linear-gradient strip
22 | * @since 1.0.0
23 | */
24 | class ColorPoint extends Component {
25 | constructor() {
26 | super(...arguments);
27 | }
28 |
29 | /*
30 | * @desc color point has been clicked, activate it if it isn't currently active
31 | * and then also prepare the point for dragging/repositioning
32 | * @since 1.0.0
33 | */
34 | onMouseDown = e => {
35 | e.preventDefault();
36 | const { pageX, pageY } = e;
37 | const { stripRef } = this.props;
38 | const { current: strip } = stripRef;
39 | const stripRect = strip.getBoundingClientRect();
40 | const pointRect = this.pointRef.getBoundingClientRect();
41 |
42 | const {
43 | left: stripLeft,
44 | width: stripWidth,
45 | } = stripRect;
46 |
47 | const {
48 | height,
49 | top: pointTop,
50 | left: pointLeft,
51 | width: pointWidth,
52 | } = pointRect;
53 |
54 | const pointHeight = height * 1.5;
55 |
56 | this.mouseValues = {
57 | pointTop,
58 | pointHeight,
59 | stripLeft,
60 | stripWidth,
61 | moveX: Math.max(0,
62 | Math.min(
63 | pageX - pointLeft - (pointWidth * 0.5),
64 | pointWidth
65 | )
66 | ),
67 | moveY: Math.max(0,
68 | Math.min(
69 | pageY - pointTop,
70 | pointHeight
71 | )
72 | ),
73 | };
74 |
75 | const { active } = this.props;
76 | if (!active) {
77 | const { onActivate, index } = this.props;
78 | onActivate(index);
79 | }
80 |
81 | const { setCursor } = this.context;
82 | setCursor('default');
83 |
84 | document.addEventListener('mousemove', this.onMouseMove);
85 | document.addEventListener('mouseleave', this.onMouseUp);
86 | document.addEventListener('mouseup', this.onMouseUp);
87 | }
88 |
89 | /*
90 | * @desc color point is being dragged, update the current ref if it no longer corresponds to this class
91 | * (can change if the point is dragged beyond another point's position)
92 | * and then update the editor with its new position,
93 | * and also display the "delete" icon if the point is pulled down a certain distance
94 | * @since 1.0.0
95 | */
96 | onMouseMove = e => {
97 | const { namespace } = this.context;
98 | const { pageX, pageY } = e;
99 | const { canDelete, activeIndex, index, activeUnit } = this.props;
100 |
101 | if (activeIndex !== index) {
102 | if (this.pulled) {
103 | this.pointRef.classList.remove(`${namespace}-gradient-point-pulled`);
104 | }
105 | this.pointRef = [...this.pointRef.parentElement.children][activeIndex];
106 | }
107 |
108 | const {
109 | moveX,
110 | moveY,
111 | pointTop,
112 | pointHeight,
113 | stripWidth,
114 | stripLeft,
115 | } = this.mouseValues;
116 |
117 | const perc = Math.max(0,
118 | Math.min(
119 | pageX - moveX - window.scrollX - stripLeft,
120 | stripWidth
121 | )
122 | );
123 |
124 | let update = true;
125 | if (canDelete) {
126 | const pull = Math.max(0,
127 | Math.min(
128 | pageY - moveY - window.scrollY - pointTop,
129 | pointHeight
130 | )
131 | );
132 | if (pull > pointHeight * 0.75) {
133 | this.pullPoint();
134 | update = false;
135 | } else if (this.pointPulled) {
136 | this.releasePoint();
137 | }
138 | }
139 |
140 | if (update) {
141 | const value = perc / stripWidth;
142 | const times = activeUnit === '%' ? 100 : maxPositionPixels;
143 | const { onChange } = this.props;
144 | onChange(value * times);
145 | }
146 | }
147 |
148 | /*
149 | * @desc display the "delete" icon if the current point has been pulled down
150 | * @since 1.0.0
151 | */
152 | pullPoint() {
153 | const { namespace } = this.context;
154 | this.pointRef.classList.add(`${namespace}-gradient-point-pulled`);
155 | this.pointPulled = true;
156 | }
157 |
158 | /*
159 | * @desc hide the "delete" icon if the current point is no longer being pulled down
160 | * @since 1.0.0
161 | */
162 | releasePoint() {
163 | const { namespace } = this.context;
164 | this.pointRef.classList.remove(`${namespace}-gradient-point-pulled`);
165 | this.pointPulled = false;
166 | }
167 |
168 | /*
169 | * @desc cleanup after dragging has finished
170 | * @since 1.0.0
171 | */
172 | removeListeners() {
173 | document.removeEventListener('mousemove', this.onMouseMove);
174 | document.removeEventListener('mouseleave', this.onMouseUp);
175 | document.removeEventListener('mouseup', this.onMouseUp);
176 | }
177 |
178 | /*
179 | * @desc the user is no longer dragging the point
180 | * possibly delete the point if the point has been pulled down (where "delete" icon would be visible)
181 | * @since 1.0.0
182 | */
183 | onMouseUp = () => {
184 | this.removeListeners();
185 | this.mouseValues = null;
186 |
187 | if (this.pointPulled) {
188 | this.releasePoint();
189 | const { onDelete } = this.props;
190 | onDelete();
191 | }
192 |
193 | const { setCursor } = this.context;
194 | setCursor();
195 | }
196 |
197 | /*
198 | * @desc cleanup when the point no longer exists in the editor
199 | * @since 1.0.0
200 | */
201 | componentWillUnmount() {
202 | this.removeListeners();
203 | this.pointRef = null;
204 | this.mouseValues = null;
205 | }
206 |
207 | render() {
208 | const { namespace } = this.context;
209 | const { color: colorPoint, active, canDelete } = this.props;
210 | const { position, preview, unit } = colorPoint;
211 |
212 | const activeClass = !active ? '' : ` ${namespace}-gradient-point-active`;
213 | const pos = unit === '%' ? position : convertPositionUnit(position);
214 |
215 | return (
216 | (this.pointRef = point)}
221 | >
222 |
223 |
227 | {canDelete && (
228 |
229 |
230 |
231 | )}
232 |
233 |
234 | );
235 | }
236 | }
237 |
238 | ColorPoint.contextType = AppContext;
239 |
240 | export default ColorPoint;
241 |
--------------------------------------------------------------------------------
/src/js/module/editor/header/strip/hint-point.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import {
4 | AppContext,
5 | } from '../../../../context';
6 |
7 | const {
8 | Component,
9 | } = React;
10 |
11 | /*
12 | * @desc displays the hint percentage tooltip when mousing over the hint pair controls
13 | * @since 1.0.0
14 | */
15 | class HintPoint extends Component {
16 | constructor() {
17 | super(...arguments);
18 | const { index } = this.props;
19 | this.index = index;
20 | }
21 |
22 | /*
23 | * @desc fades in the tooltip if a new hint-pair is hovered for a smoother visual
24 | * @since 1.0.0
25 | */
26 | componentDidUpdate() {
27 | const { index } = this.props;
28 | if (this.index !== index) {
29 | const { namespace } = this.context;
30 | this.index = index;
31 |
32 | this.ref.classList.remove(`${namespace}-fade-in`);
33 | void this.ref.offsetWidth;
34 | this.ref.classList.add(`${namespace}-fade-in`);
35 | }
36 | }
37 |
38 | render() {
39 | const { namespace } = this.context;
40 | const { hint } = this.props;
41 | const { position, percentage } = hint;
42 |
43 | return (
44 |
45 | (this.ref = ref)}
49 | >
50 |
51 |
52 | {`${Math.round(percentage)}%`}
53 |
54 |
55 |
56 |
57 |
58 | );
59 | }
60 | }
61 |
62 | HintPoint.contextType = AppContext;
63 |
64 | export default HintPoint;
65 |
--------------------------------------------------------------------------------
/src/js/module/editor/presets.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PresetItems from './presets/preset-items';
3 | import RadioMenu from '../../components/radio-menu';
4 |
5 | import {
6 | EditorContext,
7 | } from '../../context';
8 |
9 | const {
10 | useContext,
11 | } = React;
12 |
13 | const menuList = {
14 | defaults: 'Defaults',
15 | custom: 'Custom',
16 | };
17 |
18 | /*
19 | * @desc represents a preset group, showing either a "defaults" view or a "custom" view
20 | * @since 1.0.0
21 | */
22 | const Presets = ({ type, isSingle, columns = 4, minRows = 4 }) => {
23 | const editorContext = useContext(EditorContext);
24 |
25 | const {
26 | colorPresetMenu,
27 | gradPresetMenu,
28 | onChangePresetMenu,
29 | } = editorContext;
30 |
31 | let rows;
32 | const menu = type === 'color' ? colorPresetMenu : gradPresetMenu;
33 |
34 | if (menu === 'defaults' || isSingle) {
35 | rows = minRows;
36 | } else {
37 | rows = type === 'gradient' ? minRows - 1 : minRows - 2;
38 | }
39 |
40 | return (
41 | <>
42 |
48 |
54 | >
55 | );
56 | };
57 |
58 | export default Presets;
--------------------------------------------------------------------------------
/src/js/module/editor/presets/preset-items.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Preset from './preset-items/preset';
3 | import DeletePreset from './preset-items/delete-preset';
4 | import Wrapper from '../../../components/wrappers/wrapper';
5 | import Scrollable from '../../../hoc/scrollable';
6 |
7 | import {
8 | AppContext,
9 | EditorContext,
10 | } from '../../../context';
11 |
12 | const {
13 | useContext,
14 | } = React;
15 |
16 | /*
17 | * @desc displays a list of presets depending on its type (colors or gradients) and group (defaults or custom)
18 | * @since 1.0.0
19 | */
20 | const PresetItems = ({ type, menu, columns, minRows }) => {
21 | const appContext = useContext(AppContext);
22 | const editorContext = useContext(EditorContext);
23 |
24 | const {
25 | output,
26 | presets,
27 | currentColor,
28 | currentOutput,
29 | onDeletePreset,
30 | setColorByRecord,
31 | value: editorValue,
32 | } = editorContext;
33 |
34 | const colorValue = type === 'color' ? currentColor : editorValue;
35 | const { color, opacity } = colorValue;
36 | let { hex: editorHex } = colorValue;
37 | let editorOutput;
38 |
39 | // sanitize the current editor output so it can be compared to a preset's value
40 | // this then helps to determine if the preset can be shown as "selected"
41 | if (color) {
42 | if (opacity > 0) {
43 | editorOutput = color.toLowerCase();
44 | } else {
45 | editorOutput = 'transparent';
46 | editorHex = '';
47 | }
48 | } else {
49 | editorOutput = output.toLowerCase();
50 | }
51 |
52 | const items = presets[type][menu];
53 | const numRows = items.length ? Math.ceil(items.length / columns) : minRows;
54 | const rows = new Array(numRows).fill(0);
55 |
56 | while (rows.length < minRows) {
57 | rows.push(0);
58 | }
59 |
60 | const { length: rowLength } = rows;
61 | const lastRow = rowLength - 1;
62 |
63 | const { namespace, colorMode } = appContext;
64 | const customClass = menu === 'defaults' ? '' : ` ${namespace}-presets-custom`;
65 | const hasDeleteBtn = menu === 'custom' || colorMode === 'single';
66 | const isSingleDefault = menu === 'defaults' && colorMode === 'single';
67 |
68 | let showDeleteBtn;
69 | let presetIndex = 0;
70 | let deleteIndex;
71 |
72 | return (
73 | <>
74 |
75 |
minRows}
77 | wrapper={children => {children}}
78 | >
79 | {
80 | rows.map((itm, row) => {
81 | const className = row < lastRow ? '' : `${namespace}-preset-row-last`;
82 | const point = row * columns;
83 | const presets = items.slice(point, point + columns);
84 |
85 | const presetItems = presets.map(preset => {
86 | return { preset, extraClass: '' };
87 | });
88 |
89 | const extraClass = ` ${namespace}-preset-blank`;
90 | while (presetItems.length < columns) {
91 | presetItems.push({ extraClass });
92 | }
93 |
94 | return presetItems.map((itm, index) => {
95 | const { preset, extraClass: blankClass } = itm;
96 | let { hex, output, preview, gradient } = preset || {};
97 |
98 | if (hex) {
99 | hex = hex.toLowerCase();
100 | }
101 | if (output) {
102 | output = output.toLowerCase();
103 | }
104 |
105 | let active;
106 | if (!blankClass) {
107 | if (!gradient) {
108 | active = output === editorOutput ||
109 | hex === editorOutput ||
110 | hex === editorHex.toLowerCase();
111 | } else {
112 | active = output === editorOutput ||
113 | output === currentOutput.toLowerCase();
114 | }
115 | }
116 |
117 | if (active && !showDeleteBtn && menu === 'custom') {
118 | showDeleteBtn = true;
119 | deleteIndex = presetIndex;
120 | }
121 |
122 | const mainClass = `${className}${blankClass}`;
123 | const extraClass = !mainClass ? '' : ` ${mainClass}`;
124 | const activeClass = !active ? '' : ` ${namespace}-preset-active`;
125 | presetIndex++;
126 |
127 | return (
128 |
136 | );
137 | })
138 | })
139 | }
140 |
141 |
142 | {hasDeleteBtn && (
143 |
150 | )}
151 | >
152 | );
153 | }
154 |
155 | export default PresetItems;
--------------------------------------------------------------------------------
/src/js/module/editor/presets/preset-items/delete-preset.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Button from '../../../../components/buttons/button';
3 |
4 | const {
5 | memo,
6 | } = React;
7 |
8 | /*
9 | * @desc the "delete preset button" displayed underneath the custom presets
10 | * @since 1.0.0
11 | */
12 | const DeletePreset = ({ index, type, disabled, callback }) => {
13 | const onDelete = () => {
14 | const view = type.charAt(0).toUpperCase() + type.slice(1);
15 | if (window.confirm(`Delete this Saved ${view}?`)) {
16 | callback(type, index);
17 | }
18 | }
19 |
20 | return (
21 |
27 |
28 | );
29 | };
30 |
31 | export default memo(DeletePreset);
--------------------------------------------------------------------------------
/src/js/module/editor/presets/preset-items/preset.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import {
4 | AppContext,
5 | } from '../../../../context';
6 |
7 | const {
8 | memo,
9 | forwardRef,
10 | useContext,
11 | } = React;
12 |
13 | /*
14 | * @desc represents a preset item that can then be selected
15 | * @since 1.0.0
16 | */
17 | const Preset = forwardRef(({ preset, className, style, type, setColorByRecord }, ref) => {
18 | const locale = useContext(AppContext);
19 | const { namespace } = locale;
20 |
21 | let presetClass = className.replace(/\s\s+/g, ' ');
22 | if (presetClass === ' ') {
23 | presetClass = '';
24 | }
25 |
26 | return (
27 | setColorByRecord(preset, type)}
31 | >
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | );
40 | });
41 |
42 | export default memo(Preset);
--------------------------------------------------------------------------------
/src/js/module/editor/sidepanels.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Presets from './presets';
3 | import Preview from './sidepanels/preview';
4 | import Hints from './sidepanels/hints';
5 |
6 | import {
7 | AppContext,
8 | } from '../../context';
9 |
10 | const {
11 | memo,
12 | useContext,
13 | } = React;
14 |
15 | /*
16 | * @desc holder for the editor's 4 sidepanels, only shown when the editor is in "full" mode (colors + gradients)
17 | * @since 1.0.0
18 | */
19 | const SidePanels = () => {
20 | const locale = useContext(AppContext);
21 | const { namespace } = locale;
22 |
23 | return (
24 | <>
25 |
33 |
41 | >
42 | );
43 | };
44 |
45 | export default memo(SidePanels);
--------------------------------------------------------------------------------
/src/js/module/editor/sidepanels/hints.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import HintPair from './hints/hint-pair';
3 | import InputField from '../../../components/inputs/input-field';
4 | import Button from '../../../components/buttons/button';
5 | import Wrapper from '../../../components/wrappers/wrapper';
6 | import Scrollable from '../../../hoc/scrollable';
7 |
8 | import {
9 | EditorContext,
10 | } from '../../../context';
11 |
12 | import {
13 | withAppContext,
14 | } from '../../../hoc/with-context';
15 |
16 | const {
17 | Component,
18 | } = React;
19 |
20 | /*
21 | * @desc displays the "hint controls" that change/update hint percentages between colors
22 | * and also includes the "reverse positions" used to reverse all of the colors in the gradient
23 | * @since 1.0.0
24 | */
25 | class Hints extends Component {
26 | constructor() {
27 | super(...arguments);
28 | }
29 |
30 | /*
31 | * @desc range slider has been used, notify the editor of the change
32 | * @since 1.0.0
33 | */
34 | onChange = (value, index, updating) => {
35 | this.updating = updating;
36 | const { onChangeHintPercentage } = this.context;
37 | onChangeHintPercentage(value, index, updating);
38 | }
39 |
40 | /*
41 | * @desc hide the hint percentage tooltip in the editor on mouseout (as long as the range slider is not being moved)
42 | * @since 1.0.0
43 | */
44 | hideHint = () => {
45 | if (!this.updating) {
46 | const { showHideHint } = this.context;
47 | showHideHint(-1);
48 | }
49 | };
50 |
51 | render() {
52 | const {
53 | colors,
54 | currentMode,
55 | showHideHint,
56 | onReverseGradient,
57 | hints: colorHints,
58 | } = this.context;
59 |
60 | const { appContext, minPairs = 3 } = this.props;
61 | const { namespace } = appContext;
62 |
63 | const allHints = currentMode !== 'color' ? colorHints : [];
64 | const hints = allHints.slice();
65 |
66 | while (hints.length < minPairs) {
67 | hints.push(null);
68 | }
69 |
70 | const { length: pairLength } = hints;
71 | const scrollable = pairLength > minPairs;
72 | const reverseDisabled = currentMode === 'color';
73 |
74 | return (
75 | <>
76 |
77 |
{children}}
80 | >
81 | {
82 | hints.map((hint, index) => {
83 | let blank;
84 | let startStyle;
85 | let endStyle;
86 | let percentage;
87 | let onMouseLeave;
88 | let className = '';
89 | let disabledClass = '';
90 |
91 | const lastRow = index === pairLength - 1;
92 | let extraClass = !lastRow ? '' : ` ${namespace}-pair-last`;
93 |
94 | if (hint !== null) {
95 | const start = colors[index];
96 | const end = colors[index + 1];
97 |
98 | const {
99 | unit: startUnit,
100 | color: startColor,
101 | position: startPos,
102 | preview: startPreview,
103 | } = start;
104 |
105 | const {
106 | unit: endUnit,
107 | color: endColor,
108 | position: endPos,
109 | preview: endPreview,
110 | } = end;
111 |
112 | const posEquals = startPos === endPos && startUnit === endUnit;
113 | if (posEquals || startColor === endColor) {
114 | disabledClass = ` ${namespace}-disabled`;
115 | onMouseLeave = this.hideHint;
116 | }
117 |
118 | const currentHint = hints[index];
119 | const { percentage: hintPercentage } = currentHint;
120 |
121 | percentage = hintPercentage;
122 | startStyle = startPreview;
123 | endStyle = endPreview;
124 |
125 | } else {
126 | blank = true;
127 | disabledClass = ` ${namespace}-disabled`;
128 | className = ` ${namespace}-preset-blank`;
129 | onMouseLeave = this.hideHint;
130 | percentage = 50;
131 | }
132 |
133 | return (
134 |
139 |
showHideHint(index)}
142 | >
143 |
148 |
155 |
160 |
161 |
162 | );
163 | })
164 | }
165 |
166 |
167 |
168 |
174 |
175 | >
176 | );
177 | }
178 | }
179 |
180 | Hints.contextType = EditorContext;
181 |
182 | export default withAppContext(Hints);
183 |
--------------------------------------------------------------------------------
/src/js/module/editor/sidepanels/hints/hint-pair.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import {
4 | AppContext,
5 | } from '../../../../context';
6 |
7 | const {
8 | memo,
9 | useContext,
10 | } = React;
11 |
12 | /*
13 | * @desc represents a hint pair item, reusing preset item class names as the display is similar
14 | * @since 1.0.0
15 | */
16 | const HintPair = ({ className, style, blank }) => {
17 | const locale = useContext(AppContext);
18 | const { namespace } = locale;
19 |
20 | return (
21 |
22 |
23 |
24 |
28 | {!blank && }
29 |
30 |
31 |
32 | );
33 | };
34 |
35 | export default memo(HintPair);
--------------------------------------------------------------------------------
/src/js/module/full-preview.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Button from '../components/buttons/button';
3 |
4 | import {
5 | AppContext,
6 | } from '../context';
7 |
8 | const {
9 | useContext,
10 | } = React;
11 |
12 | /*
13 | * @desc the icons used for each viewport button
14 | * @since 1.0.0
15 | */
16 | const previewSizes = {
17 | full: 'crop_free',
18 | desktop: 'desktop_mac',
19 | laptop: 'laptop_mac',
20 | tablet: 'tablet_mac',
21 | phone: 'phone_iphone',
22 | };
23 |
24 | /*
25 | * @desc the large preview that displays when clicking the "open modal" button
26 | * @since 1.0.0
27 | */
28 | const FullPreview = ({
29 | preview,
30 | previewSize,
31 | onClosePreview,
32 | onChangePreviewSize,
33 | }) => {
34 | const locale = useContext(AppContext);
35 | const { namespace } = locale;
36 |
37 | return (
38 | <>
39 |
42 |
43 | {
44 | Object.keys(previewSizes).map(size => {
45 | const icon = previewSizes[size];
46 |
47 | return (
48 | onChangePreviewSize(e, size)}
53 | activeState={size === previewSize}
54 | />
55 | );
56 | })
57 | }
58 |
63 |
64 | >
65 | );
66 |
67 | };
68 |
69 | export default FullPreview;
--------------------------------------------------------------------------------
/src/js/settings.js:
--------------------------------------------------------------------------------
1 | /*
2 | * the default settings which are then overwritten by any possible admin settings
3 | */
4 |
5 | let defaultSettings = {
6 | size: 24,
7 | skin: 'classic',
8 | mode: 'full',
9 | conic: true,
10 | conicNote: false,
11 | outputBar: false,
12 | multiStops: true,
13 | className: null,
14 | modalBgColor: 'rgba(0,0,0,0.5)',
15 | };
16 |
17 | const updateDefaults = settings => {
18 | defaultSettings = { ...defaultSettings, ...settings };
19 | };
20 |
21 | export {
22 | defaultSettings,
23 | updateDefaults,
24 | }
--------------------------------------------------------------------------------
/src/js/utils/colors.js:
--------------------------------------------------------------------------------
1 | /*
2 | * The functions in this file are used for converting CSS color strings into data and vice versa
3 | */
4 |
5 | import {
6 | colorNameKeys,
7 | } from '../data/color-names';
8 |
9 | import {
10 | regRgb,
11 | regRgba,
12 | regHsl,
13 | regHsla,
14 | } from './regexp';
15 |
16 | /*
17 | * @desc formats opacity values into a number with max 2 decimals
18 | * @param string|number alpha - the opacity value to process
19 | * @returns number
20 | * @since 1.0.0
21 | */
22 | const sanitizeAlpha = alpha => {
23 | return parseFloat(
24 | Math.max(
25 | Math.min(
26 | parseFloat(alpha), 1
27 | ), 0
28 | ).toFixed(2).replace(/\.?0*$/, '')
29 | );
30 | };
31 |
32 | /*
33 | * @desc converts rgba/hsla data into an rgba/hsla CSS color
34 | * @param string type - "rgb" or "hsl"
35 | * @returns string
36 | * @since 1.0.0
37 | */
38 | const rgbHslString = (rr, gg, bb, aa, type) => {
39 | const r = Math.round(rr);
40 | const g = Math.round(gg);
41 | const b = Math.round(bb);
42 | const a = sanitizeAlpha(aa);
43 |
44 | if (type) {
45 | const perc = type === 'rgb' ? '' : '%';
46 | if (a === 1) {
47 | return `${type}(${r}, ${g}${perc}, ${b}${perc})`;
48 | }
49 | return `${type}a(${r}, ${g}${perc}, ${b}${perc}, ${a})`;
50 | }
51 | return `rgba(${r},${g},${b},${a})`;
52 | };
53 |
54 | /*
55 | * @desc converts rgb/hsl value into a hex value
56 | * @returns string
57 | * @since 1.0.0
58 | */
59 | const toHex = c => {
60 | return ('0' + parseInt(c, 10).toString(16)).slice(-2).toUpperCase();
61 | };
62 |
63 | /*
64 | * @desc converts rgb data into a CSS hex color
65 | * @param string reduce - final output will attempt to reduce 6 digit hex to 3
66 | * @returns string
67 | * @since 1.0.0
68 | */
69 | const rgbToHex = (r, g, b, reduce = true) => {
70 | let hex = `#${toHex(r)}${toHex(g)}${toHex(b)}`;
71 | if (reduce) {
72 | hex = reduceHex(hex);
73 | }
74 | return hex;
75 | };
76 |
77 | /*
78 | * @desc tests if a string is a valid rgb/rgba/hsl/hsla CSS color
79 | * @returns boolean
80 | * @since 1.0.0
81 | */
82 | const isValidRgbHsl = color => {
83 | return regRgb.test(color) ||
84 | regRgba.test(color) ||
85 | regHsl.test(color) ||
86 | regHsla.test(color);
87 | };
88 |
89 | /*
90 | * @desc tests if a string is a valid CSS color
91 | * @returns boolean
92 | * @since 1.0.0
93 | */
94 | const isValidColor = (color, varyingLength) => {
95 | return isValidHex(color, varyingLength) ||
96 | colorNameKeys.includes(color) ||
97 | isValidRgbHsl(color.replace(/\s/g, ''));
98 | };
99 |
100 | /*
101 | * @desc converts a 3 digit hex color into 6 digits
102 | * @returns string
103 | * @since 1.0.0
104 | */
105 | const fullHex = hex => {
106 | const a = hex.charAt(0);
107 | const b = hex.charAt(1);
108 | const c = hex.charAt(2);
109 |
110 | return `${a}${a}${b}${b}${c}${c}`;
111 | };
112 |
113 | /*
114 | * @desc attempts to convert a 3 digit hex color into 6 digits
115 | * @returns string
116 | * @since 1.0.0
117 | */
118 | const toFullHex = color => {
119 | let hex = color.replace('#', '');
120 | if (hex.length === 3) {
121 | hex = fullHex(hex);
122 | }
123 | return `#${hex.toUpperCase()}`;
124 | };
125 |
126 | /*
127 | * @desc attempts to convert a 6 digit hex color into 3 digits
128 | * @returns string
129 | * @since 1.0.0
130 | */
131 | const reduceHex = (clr, fromOutput) => {
132 | let color = clr.toLowerCase();
133 | if (color.charAt(0) === '#') {
134 | if (fromOutput) {
135 | color = color.toUpperCase();
136 | }
137 | if (color.charAt(1) === color.charAt(2) &&
138 | color.charAt(3) === color.charAt(4) &&
139 | color.charAt(5) === color.charAt(6)) {
140 | return `#${color.charAt(1)}${color.charAt(3)}${color.charAt(5)}`;
141 | }
142 | }
143 |
144 | return color;
145 | };
146 |
147 | /*
148 | * @desc converts a hex color and opacity value into rgba data
149 | * @returns array
150 | * @since 1.0.0
151 | */
152 | const hexToRGB = (color, opacity) => {
153 | let hex = color.replace('#', '');
154 | if (hex.length === 3) {
155 | hex = fullHex(hex);
156 | }
157 |
158 | const newColor = [
159 | parseInt(hex.substring(0, 2), 16),
160 | parseInt(hex.substring(2, 4), 16),
161 | parseInt(hex.substring(4, 6), 16),
162 | ];
163 |
164 | if (opacity) {
165 | newColor[3] = 1;
166 | }
167 |
168 | return newColor;
169 | };
170 |
171 | /*
172 | * @desc converts a valid rgb/rgba/hsl/hsla string into data values
173 | * @returns array
174 | * @since 1.0.0
175 | */
176 | const getRgbHslValues = (color, hsl) => {
177 | let clr = color.replace(/\s/g, '');
178 | if (clr.search(/,\)/) !== -1) {
179 | clr = `${clr.split(',)')[0]},1)`;
180 | }
181 |
182 | const values = clr.substring(
183 | clr.indexOf('(') + 1,
184 | clr.lastIndexOf(')')
185 | ).split(',');
186 |
187 | while (values.length < 3) {
188 | values.push(0);
189 | }
190 | if (values.length === 3) {
191 | values.push(1);
192 | }
193 |
194 | for (let i = 0; i < 4; i++) {
195 | if (i < 3) {
196 | const max = !hsl ? 255 : i > 0 ? 100 : 360;
197 | values[i] = Math.min(max,
198 | Math.max(0,
199 | parseInt(values[i], 10)
200 | )
201 | );
202 | } else {
203 | values[i] = Math.min(1,
204 | Math.max(0,
205 | parseFloat(values[i])
206 | )
207 | );
208 | }
209 | }
210 |
211 | return values;
212 | };
213 |
214 | /*
215 | * @desc converts a valid hex string into rgba data
216 | * @returns array
217 | * @since 1.0.0
218 | */
219 | const getValuesFromHex = color => {
220 | let hex;
221 | if (isValidHex(color, true)) {
222 | hex = color.replace('#', '').trim();
223 | } else {
224 | hex = '000000';
225 | }
226 |
227 | const rgba = hexToRGB(hex);
228 | rgba[3] = 1;
229 | return rgba;
230 | };
231 |
232 | /*
233 | * @desc checks if a string is a valid hex
234 | * @param string varyingNumbers - if the string can be 3 digits and also 6 digits
235 | * @returns boolean
236 | * @since 1.0.0
237 | */
238 | const isValidHex = (color, varyingNumbers) => {
239 | if (!varyingNumbers) {
240 | return /(^#[0-9A-F]{6}$)/i.test(color);
241 | }
242 |
243 | return /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(color);
244 | }
245 |
246 | /*
247 | * @desc checks if a data value exists as an rgba data value
248 | * @returns boolean
249 | * @since 1.0.0
250 | */
251 | const isValidRGB = rgb => {
252 | if (!Array.isArray(rgb) || rgb.length !== 3) {
253 | return false;
254 | }
255 |
256 | for (let i = 0; i < 3; i++) {
257 | const value = rgb[i];
258 | if (isNaN(value) || value < 0 || value > 255) {
259 | return false;
260 | }
261 | }
262 |
263 | return true;
264 | };
265 |
266 | /*
267 | * @desc converts a string into a valid CSS color for the editor
268 | * @returns string
269 | * @since 1.0.0
270 | */
271 | const cssColor = color => {
272 | if (typeof color === 'string') {
273 | return reduceHex(color);
274 | }
275 | if (Array.isArray(color)) {
276 | if (color[3] === 1) {
277 | return rgbToHex(...color);
278 | }
279 | return rgbHslString(...color);
280 | }
281 |
282 | return rgbToHex(color);
283 | };
284 |
285 | /*
286 | * @desc converts rgb data into a hex for the editor
287 | * @returns string
288 | * @since 1.0.0
289 | */
290 | const getRawHex = value => {
291 | if (Array.isArray(value)) {
292 | const rgb = value.slice(0, 3);
293 | rgb[3] = 1;
294 | return cssColor(rgb);
295 | }
296 |
297 | return '#000';
298 | };
299 |
300 | /*
301 | * @desc verifies that an incoming input value is valid
302 | * @param string mode - the editor mode ("single color" or "gradients")
303 | * @param RegExpr regGradient - the predefined RegExpr used to test gradient strings (conic or no conic)
304 | * @param boolean allowConic - if the editor supports "conic mode" or not
305 | * @returns string - the original color or "transparent" for invalid values
306 | * @since 1.0.0
307 | */
308 | const verifyColorBySettings = (clr, mode, regGradient, allowConic) => {
309 | if (mode !== 'single') {
310 | if (!allowConic && clr.search('conic') !== -1) {
311 | return 'transparent';
312 | }
313 | return clr;
314 | }
315 |
316 | const color = clr.replace(/;/g, '').replace(/-webkit-|-moz-/, '').toLowerCase();
317 | if (regGradient.test(color)) {
318 | return 'transparent';
319 | }
320 |
321 | return clr;
322 | };
323 |
324 | export {
325 | cssColor,
326 | hexToRGB,
327 | rgbToHex,
328 | toFullHex,
329 | reduceHex,
330 | getRawHex,
331 | isValidHex,
332 | isValidRGB,
333 | rgbHslString,
334 | isValidRgbHsl,
335 | isValidColor,
336 | sanitizeAlpha,
337 | getValuesFromHex,
338 | getRgbHslValues,
339 | verifyColorBySettings,
340 | };
--------------------------------------------------------------------------------
/src/js/utils/data.js:
--------------------------------------------------------------------------------
1 | /*
2 | * The functions in this file are used for converting swatch input values into editor data
3 | */
4 |
5 | import {
6 | writeGradientColor,
7 | getGradientObject,
8 | } from './gradients';
9 |
10 | import {
11 | cssGradient,
12 | buildGradientStrip,
13 | } from '../utils/output';
14 |
15 | import {
16 | cssColor,
17 | getRawHex,
18 | getRgbHslValues,
19 | getValuesFromHex,
20 | } from './colors';
21 |
22 | import {
23 | getValuesFromHsl,
24 | } from './hsl';
25 |
26 | import {
27 | regHex,
28 | regHsl,
29 | regHsla,
30 | regRgb,
31 | regRgba,
32 | regGradient,
33 | regGradientNoConic,
34 | } from './regexp';
35 |
36 | import {
37 | defaultGradient,
38 | } from '../data/defaults';
39 |
40 | import {
41 | colorNames,
42 | colorNameKeys,
43 | } from '../data/color-names';
44 |
45 | /*
46 | * @desc creates a default gradient data object
47 | * @returns object
48 | * @since 1.0.0
49 | */
50 | const defaultValue = () => {
51 | const defGradient = defaultGradient();
52 | defGradient.colors = [writeGradientColor([0, 0, 0, 0], 0)];
53 |
54 | return {
55 | hex: '#000',
56 | rgba: [0, 0, 0, 0],
57 | value: [defGradient],
58 | output: 'transparent',
59 | preview: { background: 'transparent' },
60 | strip: { background: 'transparent' },
61 | };
62 | };
63 |
64 | /*
65 | * @desc converts all incoming input values into editor data
66 | * @param boolean conic - if the editor supports "conic mode" or not
67 | * @returns object - the master editor data
68 | * @since 1.0.0
69 | */
70 | const getColorData = (clr, conic = true) => {
71 | if (!clr || typeof clr !== 'string') {
72 | return defaultValue();
73 | }
74 |
75 | let rawColor = clr.replace(/;|\:|\{|\}/g, '')
76 | .replace(/-webkit-|-moz-/, '')
77 | .replace('background-color', '')
78 | .replace('background-image', '')
79 | .replace('background', '')
80 | .replace('color', '')
81 | .toLowerCase()
82 | .trim();
83 |
84 | if (rawColor === 'transparent') {
85 | return defaultValue();
86 | }
87 |
88 | const regExpGradient = conic ? regGradient : regGradientNoConic;
89 | if (regExpGradient.test(rawColor)) {
90 | const value = getGradientObject(rawColor);
91 | const output = cssGradient(value);
92 | const gradient = value[value.length - 1];
93 | const { colors, hints } = gradient;
94 |
95 | return {
96 | value,
97 | output,
98 | gradient: true,
99 | preview: { background: output },
100 | strip: { background: buildGradientStrip(colors, hints) },
101 | };
102 | }
103 |
104 | let colorName;
105 | rawColor = rawColor.replace(/\s/g, '');
106 |
107 | if (colorNameKeys.includes(rawColor)) {
108 | colorName = true;
109 | rawColor = colorNames[rawColor];
110 | }
111 |
112 | if (colorName || regHex.test(rawColor)) {
113 | const colorValue = getValuesFromHex(rawColor);
114 | const color = cssColor(colorValue);
115 |
116 | const defGradient = defaultGradient();
117 | defGradient.colors = [writeGradientColor(colorValue, 0)];
118 |
119 | return {
120 | value: [defGradient],
121 | output: color,
122 | rgba: colorValue,
123 | hex: getRawHex(colorValue),
124 | preview: { background: color },
125 | strip: { background: color },
126 | };
127 | }
128 |
129 | const rgb = regRgb.test(rawColor);
130 | const rgba = regRgba.test(rawColor);
131 | let hsl;
132 | let hsla;
133 |
134 | if (!rgb && !rgba) {
135 | hsl = regHsl.test(rawColor);
136 | hsla = regHsla.test(rawColor);
137 | }
138 |
139 | if (rgb || rgba || hsl || hsla) {
140 | let colorValue;
141 | if (rgb || rgba) {
142 | colorValue = getRgbHslValues(rawColor);
143 | } else {
144 | colorValue = getValuesFromHsl(rawColor);
145 | }
146 |
147 | const color = cssColor(colorValue);
148 | const defGradient = defaultGradient();
149 | defGradient.colors = [writeGradientColor(colorValue, 0)];
150 |
151 | return {
152 | output: color,
153 | rgba: colorValue,
154 | value: [defGradient],
155 | hex: getRawHex(colorValue),
156 | preview: { background: color },
157 | strip: { background: color },
158 | };
159 | }
160 |
161 | return defaultValue();
162 | };
163 |
164 | export default getColorData;
165 |
--------------------------------------------------------------------------------
/src/js/utils/editor.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file includes helper functions used in 'src/js/module/editor.js' (which will be packed in the chunks)
3 | */
4 |
5 | import {
6 | convertPositionUnit,
7 | } from './utilities';
8 |
9 | /*
10 | * @desc creates a deep clone of any given editor object/array
11 | * @returns object
12 | * @since 1.0.0
13 | */
14 | const deepClone = value => {
15 | const str = JSON.stringify(value);
16 | return JSON.parse(str);
17 | };
18 |
19 | /*
20 | * @desc creates a shallow clone of any given editor object/array
21 | * @returns copied array or object
22 | * @since 1.0.0
23 | */
24 | const shallowClone = value => {
25 | if (Array.isArray(value)) {
26 | return Array.from(value);
27 | } else if (typeof value === 'object') {
28 | return { ...value };
29 | }
30 |
31 | return value;
32 | }
33 |
34 | /*
35 | * @desc checks if user input is valid for fields
36 | * @returns boolean
37 | * @since 1.0.0
38 | */
39 | const isValidInput = (value, min, max) => {
40 | if (value === '' || !(/^\d+$/.test(value))) {
41 | return false;
42 | }
43 |
44 | const newValue = parseInt(value, 10);
45 | if (newValue < min || newValue > max) {
46 | return false;
47 | }
48 |
49 | return true;
50 | };
51 |
52 | /*
53 | * @desc reverses color position stops for the current editor gradient
54 | * @since 1.0.0
55 | */
56 | const reversePositions = itm => {
57 | let { position, unit } = itm;
58 | if (unit === 'px') {
59 | position = convertPositionUnit(position);
60 | }
61 | if (position < 50) {
62 | position += (50 - position) * 2;
63 | } else {
64 | position -= (position - 50) * 2;
65 | }
66 | if (unit === 'px') {
67 | position = convertPositionUnit(position, true);
68 | }
69 |
70 | itm.position = position;
71 | };
72 |
73 | /*
74 | * @desc determines an initial position for newly added colors
75 | * @returns number
76 | * @since 1.0.0
77 | */
78 | const getNextPosition = (colors, index) => {
79 | const { length } = colors;
80 | const nextIndex = index + 1 < length ? index + 1 : length - 1;
81 | const endColor = colors[nextIndex];
82 |
83 | const { unit: endUnit, position: endPos } = endColor;
84 | const lastPos = endUnit === '%' ? endPos : convertPositionUnit(endPos);
85 |
86 | if (length < 2) {
87 | return lastPos < 100 ? 100 : 0;
88 | }
89 | if (nextIndex === length - 1 && lastPos < 100) {
90 | return 100;
91 | }
92 |
93 | const startIndex = index !== nextIndex ? index : index - 1;
94 | const startColor = colors[startIndex];
95 | const { unit: startUnit, position: startPos } = startColor;
96 | const firstPos = startUnit === '%' ? startPos : convertPositionUnit(startPos);
97 |
98 | return firstPos + ((lastPos - firstPos) * 0.5);
99 | };
100 |
101 | /*
102 | * @desc gets the linear angle data depending on which control was used
103 | * @returns object
104 | * @since 1.0.0
105 | */
106 | const getDirection = angle => {
107 | switch (angle) {
108 | case 0:
109 | case 360:
110 | case 'top':
111 | return { direction: 'top', value: 0 };
112 | case 180:
113 | case 'bottom':
114 | return { direction: 'bottom', value: 180 };
115 | case 270:
116 | case 'left':
117 | return { direction: 'left', value: 270 };
118 | case 90:
119 | case 'right':
120 | return { direction: 'right', value: 90 };
121 | case 45:
122 | case 'right_top':
123 | return { direction: 'right_top', value: 45 };
124 | case 135:
125 | case 'right_bottom':
126 | return { direction: 'right_bottom', value: 135 };
127 | case 225:
128 | case 'left_bottom':
129 | return { direction: 'left_bottom', value: 225 };
130 | case 315:
131 | case 'left_top':
132 | return { direction: 'left_top', value: 315 };
133 | }
134 |
135 | return {
136 | direction: 'degree',
137 | value: isNaN(angle) ? 125 : angle,
138 | };
139 | };
140 |
141 | /*
142 | * @desc adjusts hint positioning when the color positions have changed
143 | * @since 1.0.0
144 | */
145 | const writeHintData = (colors, hints, positions) => {
146 | if (colors.length < 2) {
147 | return;
148 | }
149 |
150 | hints.forEach((hint, index) => {
151 | let prevColor = colors[index];
152 | let nextColor = colors[index + 1];
153 |
154 | const { position: prevColorPos, unit: prevColorUnit } = prevColor;
155 | const { position: nextColorPos, unit: nextColorUnit } = nextColor;
156 |
157 | const posOne = prevColorUnit === '%' ? prevColorPos : convertPositionUnit(prevColorPos);
158 | const posTwo = nextColorUnit === '%' ? nextColorPos : convertPositionUnit(nextColorPos);
159 |
160 | const minPos = Math.min(posOne, posTwo);
161 | const maxPos = Math.max(posOne, posTwo);
162 | const difPos = maxPos - minPos;
163 |
164 | if (positions) {
165 | const { percentage } = hint;
166 | hint.position = minPos + (difPos * (percentage * 0.01));
167 | } else {
168 | const { position } = hint;
169 | let perc = ((position - minPos) / (maxPos - minPos)) * 100;
170 | if (isNaN(perc)) {
171 | perc = 50;
172 | }
173 | hint.percentage = perc;
174 | }
175 | });
176 | };
177 |
178 | export {
179 | deepClone,
180 | shallowClone,
181 | isValidInput,
182 | getDirection,
183 | writeHintData,
184 | getNextPosition,
185 | reversePositions,
186 | }
--------------------------------------------------------------------------------
/src/js/utils/global.js:
--------------------------------------------------------------------------------
1 | /*
2 | This file includes helper functions that need to be packed in the main index file (not the subsequent chunks)
3 | */
4 |
5 | import {
6 | maxPositionPixels,
7 | } from '../data/defaults';
8 |
9 | /*
10 | * @desc converts the current preset data to an Array of CSS colors to send back to admin
11 | * after a preset has been saved or deleted
12 | * @returns array
13 | * @since 1.0.0
14 | */
15 | const formatPresets = presets => {
16 | return presets.slice().map(preset => preset.output);
17 | };
18 |
19 | /*
20 | * @desc verifies an admin Boolean setting
21 | * @returns boolean
22 | * @since 1.0.0
23 | */
24 | const parseBoolean = val => {
25 | return val === true || val === 'true' || val === 1 || val === '1' || val === 'on';
26 | };
27 |
28 | /*
29 | * @desc verifies an admin Boolean setting checking the data-attr first and then the global settings
30 | * @returns boolean
31 | * @since 1.0.0
32 | */
33 | const booleanSetting = (global, defValue, local) => {
34 | if (local !== undefined) {
35 | return parseBoolean(local)
36 | }
37 | if (global !== undefined) {
38 | return parseBoolean(global)
39 | }
40 | return defValue;
41 | };
42 |
43 | /*
44 | * @desc creates a new RegExp to use to verify incoming settings
45 | * @returns RegExp
46 | * @since 1.0.0
47 | */
48 | const verifySettingRegExp = options => {
49 | let regString = '';
50 |
51 | options.forEach((opt, index) => {
52 | if (index > 0) regString += '|';
53 | regString += `^${opt}$`;
54 | });
55 |
56 | return new RegExp(`${regString}`);
57 | }
58 |
59 | /*
60 | * @desc widget settings that exist as strings that need to be verified
61 | * @since 1.0.0
62 | */
63 | const verifiableSettings = {
64 | mode: {
65 | options: ['full', 'single'],
66 | defaultValue: 'full',
67 | },
68 | };
69 |
70 | /*
71 | * @desc verifies incoming settings that are intended to exist as strings
72 | * @since 1.0.0
73 | */
74 | const verifySetting = (local, global, setting) => {
75 | const localSetting = typeof local === 'string' ? local.toLowerCase().trim() : null;
76 | const globalSetting = typeof global === 'string' ? global.toLowerCase().trim() : null;
77 |
78 | const { options, defaultValue } = verifiableSettings[setting];
79 | const regex = verifySettingRegExp(options);
80 |
81 | if (regex.test(localSetting)) {
82 | return localSetting;
83 | }
84 | if (regex.test(globalSetting)) {
85 | return globalSetting;
86 | }
87 | return defaultValue;
88 | };
89 |
90 | /*
91 | * @desc sets the style of the color picker swatch based on the admin settings
92 | * @param HTMLElement swatch - the swatch's outermost wrapper
93 | * @param HTMLElement inner - the swatch's inner container which holds the background style
94 | * @param string swatchClass - the class name defined in index.js to add to the swatch
95 | * @param string|number inputSize - the potential "data-size" attribute for the swatch input field
96 | * @param string inputSkin - the potential "data-skin" attribute for the swatch input field
97 | * @param object settings - the current admin settings
98 | * @since 1.0.0
99 | */
100 | const setSwatchStyle = (
101 | swatch,
102 | inner,
103 | swatchClass,
104 | inputSize,
105 | inputSkin,
106 | settings
107 | ) => {
108 | const { size: settingsSize, skin: settingsSkin } = settings;
109 | const skin = inputSkin || settingsSkin;
110 |
111 | let size = inputSize || settingsSize || 24;
112 | let className = swatchClass;
113 |
114 | if (skin !== 'classic') {
115 | className += ` ${swatchClass}-${skin}`;
116 | }
117 | if (/^[0-9]+$/.test(size)) {
118 | const perc = size / maxPositionPixels;
119 | const scaled = `${maxPositionPixels}px`;
120 | inner.style.width = scaled;
121 | inner.style.height = scaled;
122 | inner.style.transform = `scale(${perc})`;
123 | size += 'px';
124 | } else {
125 | inner.style.width = '100%';
126 | inner.style.height = '100%';
127 | inner.style.transform = null;
128 | }
129 |
130 | swatch.className = className;
131 | swatch.style.width = size;
132 | swatch.style.height = size;
133 | };
134 |
135 | export {
136 | verifySetting,
137 | setSwatchStyle,
138 | booleanSetting,
139 | formatPresets,
140 | };
--------------------------------------------------------------------------------
/src/js/utils/hsl.js:
--------------------------------------------------------------------------------
1 | import {
2 | getRgbHslValues,
3 | } from './colors';
4 |
5 | /*
6 | * @desc converts hsl data to rgb
7 | * @since 1.0.0
8 | */
9 | // https://github.com/Qix-/color-convert/blob/master/conversions.js
10 | const hslToRgb = (hh, ss, ll) => {
11 | const h = hh / 360;
12 | const s = ss / 100;
13 | const l = ll / 100;
14 |
15 | let t2;
16 | let t3;
17 | let val;
18 |
19 | if (s === 0) {
20 | val = l * 255;
21 | return [val, val, val];
22 | }
23 | if (l < 0.5) {
24 | t2 = l * (1 + s);
25 | } else {
26 | t2 = l + s - l * s;
27 | }
28 |
29 | const t1 = 2 * l - t2;
30 | const rgb = [0, 0, 0];
31 |
32 | for (let i = 0; i < 3; i++) {
33 | t3 = h + 1 / 3 * -(i - 1);
34 | if (t3 < 0) t3++;
35 | if (t3 > 1) t3--;
36 |
37 | if (6 * t3 < 1) {
38 | val = t1 + (t2 - t1) * 6 * t3;
39 | } else if (2 * t3 < 1) {
40 | val = t2;
41 | } else if (3 * t3 < 2) {
42 | val = t1 + (t2 - t1) * (2 / 3 - t3) * 6;
43 | } else {
44 | val = t1;
45 | }
46 |
47 | rgb[i] = val * 255;
48 | }
49 |
50 | return rgb;
51 | };
52 |
53 | /*
54 | * @desc converts rgb data to hsl
55 | * @since 1.0.0
56 | */
57 | // https://github.com/Qix-/color-convert/blob/master/conversions.js
58 | const toHsl = (rr, gg, bb) => {
59 | const r = rr / 255;
60 | const g = gg / 255;
61 | const b = bb / 255;
62 |
63 | const min = Math.min(r, g, b);
64 | const max = Math.max(r, g, b);
65 | const delta = max - min;
66 |
67 | let h;
68 | let s;
69 | let l = (min + max) / 2;
70 |
71 | if (max === min) {
72 | h = 0;
73 | } else if (r === max) {
74 | h = (g - b) / delta;
75 | } else if (g === max) {
76 | h = 2 + (b - r) / delta;
77 | } else {
78 | h = 4 + (r - g) / delta;
79 | }
80 |
81 | h = Math.min(h * 60, 360);
82 | if (h < 0) h += 360;
83 |
84 | if (max === min) {
85 | s = 0;
86 | } else if (l <= 0.5) {
87 | s = delta / (max + min);
88 | } else {
89 | s = delta / (2 - max - min);
90 | }
91 |
92 | s *= 100;
93 | l *= 100;
94 |
95 | return [h, s, l];
96 | };
97 |
98 | /*
99 | * @desc converts rgb data to hsb
100 | * @since 1.0.0
101 | */
102 | const toHsb = (rr, gg, bb) => {
103 | const max = Math.max(rr, gg, bb);
104 | const delta = max - Math.min(rr, gg, bb);
105 |
106 | let h = 0;
107 | let s = max !== 0 ? 255 * delta / max : 0;
108 | let b = max;
109 |
110 | if (s !== 0) {
111 | if (rr === max) {
112 | h = (gg - bb) / delta;
113 | } else if (gg === max) {
114 | h = 2 + (bb - rr) / delta;
115 | } else {
116 | h = 4 + (rr - gg) / delta;
117 | }
118 | } else {
119 | h = -1;
120 | }
121 |
122 | h *= 60;
123 | s *= 100 / 255;
124 | b *= 100 / 255;
125 |
126 | if (h < 0) {
127 | h += 360;
128 | }
129 | if (h === 300 && s === 0) {
130 | h = 0;
131 | }
132 |
133 | return { h, s, b };
134 | };
135 |
136 | /*
137 | * @desc converts rgb data to hsb, used by the color picker rainbow
138 | * @since 1.0.0
139 | */
140 | const hsbaData = color => {
141 | const hsb = toHsb(...color);
142 | const { h, s, b } = hsb;
143 | return { h, b, alpha: (100 - s) / 100 };
144 | }
145 |
146 | /*
147 | * @desc converts an hsl CSS color to rgba data
148 | * @since 1.0.0
149 | */
150 | const getValuesFromHsl = clr => {
151 | const hsla = getRgbHslValues(clr, true);
152 | const opacity = hsla[3];
153 | hsla.pop();
154 |
155 | const color = hslToRgb(...hsla);
156 | color[3] = opacity;
157 | return color;
158 | };
159 |
160 | export {
161 | toHsl,
162 | hsbaData,
163 | hslToRgb,
164 | getValuesFromHsl,
165 | }
--------------------------------------------------------------------------------
/src/js/utils/presets.js:
--------------------------------------------------------------------------------
1 | import getColorData from './data';
2 | import {
3 | defaultSettings,
4 | } from '../settings';
5 |
6 | /*
7 | * @desc converts all pre-exisiting presets into Objects of data
8 | * @param array presets - the incoming Array of preset strings (colors/gradients)
9 | * @param boolean gradients - if the presets being processed are regular colors or gradients
10 | * @param boolean allowConic - if the editor supports "conic mode" or not
11 | * @returns a final Array of preset Objects that filter out ones that couldn't be processed
12 | * @since 1.0.0
13 | */
14 | const processPresets = (presets, gradients, allowConic) => {
15 | return Array.from(new Set(presets)).map(preset => {
16 | if (typeof preset !== 'string') {
17 | return preset;
18 | }
19 |
20 | const data = getColorData(preset, allowConic);
21 | const { gradient } = data;
22 |
23 | if (gradients && !gradient) {
24 | return null;
25 | }
26 |
27 | return data;
28 | }).filter(preset => preset !== null);
29 | };
30 |
31 | /*
32 | * @desc attempts to use presets from admin settings and then falls back to defaults
33 | * @param object settings - the current admin settings passed into the widget
34 | * @param object defaults - the editors default settings
35 | * @param boolean allowConic - if the editor supports "conic mode" or not
36 | * @param boolean gradients - if the current presets being processed are gradients
37 | * @returns the processed presets for its respective group (colors or gradients)
38 | * @since 1.0.0
39 | */
40 | const verifyPresets = (settings, defaults, allowConic, gradients) => {
41 | let defaultPresets;
42 | let customPresets;
43 | const { defaults: coreDefaults, custom: coreCustom } = defaults;
44 |
45 | if (typeof settings === 'object') {
46 | const { defaults: settingsDefaults, custom: settingsCustom } = settings;
47 |
48 | if (Array.isArray(settingsDefaults) && settingsDefaults.length) {
49 | defaultPresets = settingsDefaults;
50 | } else {
51 | defaultPresets = coreDefaults;
52 | }
53 | if (Array.isArray(settingsCustom)) {
54 | customPresets = settingsCustom;
55 | } else {
56 | customPresets = coreCustom;
57 | }
58 | } else {
59 | defaultPresets = coreDefaults;
60 | customPresets = coreCustom;
61 | }
62 |
63 | return {
64 | defaults: processPresets(defaultPresets, gradients, allowConic),
65 | custom: processPresets(customPresets, gradients, allowConic),
66 | };
67 | }
68 |
69 | /*
70 | * @desc the entry point when presets are first processed
71 | * @param object coreColors - the editors default preset colors
72 | * @param object coreGradients - the editors default preset gradients
73 | * @param boolean allowConic - if the editor supports "conic mode" or not
74 | * @returns the final presets to be used in the editor
75 | * @since 1.0.0
76 | */
77 | const initPresets = (coreColors, coreGradients, allowConic) => {
78 | const {
79 | colorPresets,
80 | gradientPresets,
81 | } = defaultSettings;
82 |
83 | const color = verifyPresets(colorPresets, coreColors, allowConic);
84 | const gradient = verifyPresets(gradientPresets, coreGradients, allowConic, true);
85 |
86 | defaultSettings.colorPresets = color;
87 | defaultSettings.gradientPresets = gradient;
88 |
89 | return { color, gradient };
90 | }
91 |
92 | export default initPresets;
93 |
--------------------------------------------------------------------------------
/src/js/utils/regexp.js:
--------------------------------------------------------------------------------
1 | /*
2 | * All the RegExp used to verify colors from user input
3 | */
4 |
5 | const regHex = /^#?([a-f\d]{3}|[a-f\d]{6})$/;
6 | const regHsl = /^hsl\((0|360|35\d|3[0-4]\d|[12]\d\d|0?\d?\d),(0|100|\d{1,2})%,(0|100|\d{1,2})%\)$/;
7 | const regHsla = /^hsla\((0|360|35\d|3[0-4]\d|[12]\d\d|0?\d?\d),(0|100|\d{1,2})%,(0|100|\d{1,2})%,0|(0?\.\d|1(\.0)?)\)$/;
8 | const regRgb = /^rgb\((0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d),(0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d),(0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d)\)$/;
9 | const regRgba = /^rgba\((0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d),(0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d),(0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d),0|(0?\.\d|1(\.0)?)\)$/;
10 | const regGradient = /^(linear\-gradient\(|radial\-gradient\(|conic\-gradient\(|repeating\-linear\-gradient\(|repeating\-radial\-gradient\(|repeating\-conic\-gradient\().*\)$/;
11 | const regGradientNoConic = /^(linear\-gradient\(|radial\-gradient\(|repeating\-linear\-gradient\(|repeating\-radial\-gradient\().*\)$/;
12 |
13 | export {
14 | regHex,
15 | regHsl,
16 | regHsla,
17 | regRgb,
18 | regRgba,
19 | regGradient,
20 | regGradientNoConic,
21 | };
--------------------------------------------------------------------------------
/src/js/utils/utilities.js:
--------------------------------------------------------------------------------
1 | import {
2 | maxPositionPixels,
3 | } from '../data/defaults';
4 |
5 | /*
6 | * @desc formats a floating number to only have 2 max decimal points
7 | * @since 1.0.0
8 | */
9 | const toFixed = num => {
10 | return parseFloat(num.toFixed(2));
11 | };
12 |
13 | /*
14 | * @desc clamps a number between two other numbers
15 | * @since 1.0.0
16 | */
17 | const minMax = (min, max, value) => {
18 | return Math.max(min, Math.min(value, max));
19 | };
20 |
21 | /*
22 | * @desc removes trailing commas from a string
23 | * @since 1.0.0
24 | */
25 | const trimComma = str => {
26 | let st = str.trim();
27 | if (st.slice(-1) === ',') {
28 | return st.slice(0, -1);
29 | }
30 | return st;
31 | };
32 |
33 | /*
34 | * @desc converts percentage-based positions to pixels and vice versa
35 | * @since 1.0.0
36 | */
37 | const convertPositionUnit = (value, px) => {
38 | if (!px) {
39 | return (value / maxPositionPixels) * 100;
40 | }
41 | return maxPositionPixels * (value * 0.01);
42 | };
43 |
44 | /*
45 | * @desc normalize sorting between browsers to account for differences when identical values exist to ensure that
46 | * consecutive colors with identical stop values maintain their respective order (which matters for CSS gradients)
47 | * https://jsfiddle.net/r7gmxf92/
48 | * @since 1.0.0
49 | */
50 | const sortCompareAlt = (() => {
51 | return [{ index: 0, val: 0 }, { index: 1, val: 0 }].sort((a, b) => a.val >= b.val)[0].index;
52 | })();
53 |
54 | const sortCompare = (a, b) => {
55 | return !sortCompareAlt ? a >= b ? 1 : -1 : a > b ? 1 : -1;
56 | };
57 |
58 | export {
59 | minMax,
60 | toFixed,
61 | trimComma,
62 | sortCompare,
63 | convertPositionUnit,
64 | }
--------------------------------------------------------------------------------
/src/scss/swatch.scss:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css?family=Roboto:400,500&display=swap');
2 |
3 | $namespace: cj-color;
4 | $swatch-class: cj-colorpicker-swatch;
5 | $swatch-inner-class: cj-colorpicker-swatch-inner;
6 |
7 | $modal-zindex: 99999999;
8 | $preloader-color: #FFF;
9 |
10 | $font-family: 'Roboto', sans-serif;
11 | $font-weight: 500;
12 |
13 | /* swatch styles */
14 | $triangle-color: #FFF;
15 | $border: 2px solid #FFF;
16 | $triangle-border: 1px solid rgba(0, 0, 0, 0.2);
17 | $box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2);
18 | $triangle-box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2);
19 |
20 | .#{$swatch-class},
21 | .#{$swatch-class} *,
22 | .#{$namespace},
23 | .#{$namespace} * {
24 | box-sizing: border-box;
25 | &::before,
26 | &::after {box-sizing: border-box}
27 | margin: 0;
28 | padding: 0;
29 | border: none;
30 | outline: none;
31 | box-shadow: none;
32 | background: transparent;
33 | -moz-user-select: none;
34 | -webkit-user-select: none;
35 | -ms-user-select: none;
36 | user-select: none;
37 | }
38 |
39 | .#{$swatch-class} {
40 | position: relative;
41 | display: inline-block;
42 | border: $border;
43 | box-shadow: $box-shadow;
44 | box-sizing: content-box;
45 | overflow: hidden;
46 | background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAKElEQVQoU2M8c+bMfwY0YGxsjC7EwDgUFP7//x/DM2fPnsX0zBBQCABDRTPmdTCmRAAAAABJRU5ErkJggg==');
47 | &-light {
48 | border-radius: 3px;
49 | &:after {
50 | display: none;
51 | }
52 | }
53 | &:before {
54 | content: "";
55 | position: absolute;
56 | top: 0;
57 | left: 0;
58 | width: 100%;
59 | height: 100%;
60 | border: $triangle-border;
61 | display: block;
62 | z-index: 1;
63 | }
64 | &:after {
65 | content: "";
66 | position: absolute;
67 | top: -3px;
68 | right: -8px;
69 | width: 0;
70 | height: 0;
71 | border-left: 8px solid transparent;
72 | border-right: 8px solid transparent;
73 | border-bottom: 8px solid $triangle-color;
74 | transform: rotate(45deg);
75 | box-shadow: $triangle-box-shadow;
76 | z-index: 2;
77 | }
78 | }
79 |
80 | .#{$swatch-inner-class}{
81 | display: block;
82 | transform-origin: top left;
83 | }
84 |
85 | .#{$namespace} {
86 | display: none;
87 | position: fixed;
88 | top: 0;
89 | left: 0;
90 | width: 100%;
91 | height: 100%;
92 | align-items: center;
93 | justify-content: center;
94 | z-index: $modal-zindex;
95 |
96 | &-preloader {
97 | font-family: $font-family;
98 | font-weight: $font-weight;
99 | display: inline-block;
100 | position: absolute;
101 | top: 50%;
102 | left: 50%;
103 | margin: -23px 0 0 -23px;
104 | transition: all 0.3s ease-out;
105 | transition-property: opacity, visibility;
106 | animation: #{$namespace}-preloading 1.2s linear infinite;
107 | &:after {
108 | content: " ";
109 | display: block;
110 | width: 46px;
111 | height: 46px;
112 | border-radius: 50%;
113 | border: 5px solid $preloader-color;
114 | border-color: $preloader-color transparent $preloader-color transparent;
115 | }
116 | span {
117 | position: absolute;
118 | opacity: 0;
119 | }
120 | }
121 | &-active {
122 | display: flex;
123 | }
124 | }
125 |
126 |
127 | @keyframes #{$namespace}-preloading {
128 | 0% {transform: rotate(0deg)}
129 | 100% {transform: rotate(360deg)}
130 | }
131 |
--------------------------------------------------------------------------------