├── .gitignore ├── LICENSE ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── src ├── index.ts └── styles.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | private/ 3 | node_modules/ 4 | .eslintrc 5 | *.log 6 | _index.html 7 | dist/ 8 | .npmrc -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-current, Artur Arseniev 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | - Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | - Redistributions in binary form must reproduce the above copyright notice, this 10 | list of conditions and the following disclaimer in the documentation and/or 11 | other materials provided with the distribution. 12 | - Neither the name "GrapesJS" nor the names of its contributors may be 13 | used to endorse or promote products derived from this software without 14 | specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 23 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GrapesJS Style Gradient 2 | 3 | This plugins adds a new `gradient` property to the GrapesJS's StyleManager by using [Grapick](https://github.com/artf/grapick) 4 | 5 | > Requires GrapesJS v0.20.1 or higher 6 | 7 | [Demo](https://codepen.io/artf/full/bYwdQG/) 8 | 9 | ## Summary 10 | 11 | * Plugin name: `grapesjs-style-gradient` 12 | * Style type: `gradient` (the gradient picker input) 13 | * Built-in Style property: `background-image` (composite type with gradient picker and direction/type selectors) 14 | 15 | 16 | 17 | 18 | 19 | ## Options 20 | 21 | | Option | Description | Default | 22 | |-|-|- 23 | | `grapickOpts` | [Grapick options](https://github.com/artf/grapick#configurations). | `{}` | 24 | | `colorPicker` | Custom color picker, check [Grapick's repo](https://github.com/artf/grapick#add-custom-color-picker) to get more about it. | `undefined` | 25 | | `selectEdgeStops` | Select, by default, the edge color stops of the gradient picker. | `true` | 26 | | `styleType` | The id to assign for the gradient picker type. | `'gradient'` | 27 | | `builtInType` | Built-in property name to use for the composite type with the gradient picker and direction/type selectors. | `'background-image'` | 28 | 29 | 30 | 31 | 32 | 33 | ## Download 34 | 35 | * CDN 36 | * `https://unpkg.com/grapesjs-style-gradient` 37 | * NPM 38 | * `npm i grapesjs-style-gradient` 39 | * GIT 40 | * `git clone https://github.com/GrapesJS/style-gradient.git` 41 | 42 | 43 | 44 | 45 | 46 | ## Usage 47 | 48 | Directly in the browser. 49 | ```html 50 | 51 | 52 | 53 | 54 | 55 |
56 | 57 | 67 | ``` 68 | 69 | Modern javascript 70 | ```js 71 | import grapesjs from 'grapesjs'; 72 | import plugin from 'grapesjs-style-gradient'; 73 | 74 | const editor = grapesjs.init({ 75 | container : '#gjs', 76 | // ... 77 | plugins: [plugin], 78 | pluginsOpts: { 79 | [plugin]: { /* options */ } 80 | } 81 | }); 82 | 83 | // Usage via API 84 | 85 | // Add gradient picker as a single input 86 | editor.StyleManager.addProperty('decorations', { 87 | type: 'gradient', // <- new type 88 | name: 'Gradient', 89 | property: 'background-image', 90 | defaults: 'none' 91 | full: true, 92 | }); 93 | 94 | // Add the new background-image bulti-in type 95 | editor.StyleManager.addProperty('decorations', { 96 | extend: 'background-image', // <- extend the built-in type 97 | name: 'Gradient Background', 98 | }); 99 | ``` 100 | 101 | 102 | 103 | 104 | 105 | ## Development 106 | 107 | Clone the repository 108 | 109 | ```sh 110 | $ git clone https://github.com/GrapesJS/style-gradient.git 111 | $ cd style-gradient 112 | ``` 113 | 114 | Install dependencies 115 | 116 | ```sh 117 | $ npm i 118 | ``` 119 | 120 | Start the dev server 121 | 122 | ```sh 123 | $ npm start 124 | ``` 125 | 126 | 127 | 128 | ## License 129 | 130 | BSD 3-Clause 131 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | GrapesJS Style Gradient 6 | 7 | 8 | 9 | 37 | 38 | 39 |
40 |
48 |

Gradient

49 |
50 | This is a demo content from index.html. For the development, you shouldn't edit this file, instead you can 51 | copy and rename it to _index.html, on next server start the new file will be served, and it will be ignored by git. 52 |
53 |
54 |
55 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grapesjs-style-gradient", 3 | "version": "3.0.3", 4 | "description": "Add gradient input to the Style Manager in GrapesJS", 5 | "main": "dist/index.js", 6 | "files": [ 7 | "dist/" 8 | ], 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/GrapesJS/style-gradient.git" 12 | }, 13 | "scripts": { 14 | "start": "grapesjs-cli serve", 15 | "build": "grapesjs-cli build" 16 | }, 17 | "keywords": [ 18 | "grapesjs", 19 | "plugin", 20 | "style", 21 | "gradient" 22 | ], 23 | "author": "Artur Arseniev", 24 | "license": "BSD-3-Clause", 25 | "devDependencies": { 26 | "grapesjs": "^0.21.7", 27 | "grapesjs-cli": "^4.1.3" 28 | }, 29 | "dependencies": { 30 | "grapick": "^0.1.13" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from 'grapesjs'; 2 | import loadStyles from './styles'; 3 | 4 | export { parseGradient, toGradient, getValidDir, GRAD_DIRS, GRAD_TYPES } from './styles'; 5 | 6 | export type PluginOptions = { 7 | /** 8 | * Grapick options. 9 | * https://github.com/artf/grapick#configurations 10 | * @default {} 11 | */ 12 | grapickOpts?: Record, 13 | 14 | /** 15 | * Custom color picker, check Grapick's repo to get more about it. 16 | * If you leave it empty the native color picker will be used. 17 | * You can use 'default' string to get the one used 18 | * by Grapesjs (which is spectrum at the moment of writing). 19 | */ 20 | colorPicker?: 'default' | ((handler: any) => void), 21 | 22 | /** 23 | * Select, by default, the edge color stops of the gradient picker. 24 | * @default true 25 | */ 26 | selectEdgeStops?: boolean, 27 | 28 | /** 29 | * The id to assign for the gradient picker type. 30 | * @default 'gradient' 31 | */ 32 | styleType?: string, 33 | 34 | /** 35 | * Built-in property name to use for the composite type with the gradient 36 | * picker and direction/type selectors. 37 | * @default 'background-image' 38 | */ 39 | builtInType?: string | false, 40 | }; 41 | 42 | const plugin: Plugin = (editor, opts = {}) => { 43 | const options: PluginOptions = { 44 | grapickOpts: {}, 45 | selectEdgeStops: true, 46 | styleType: 'gradient', 47 | builtInType: 'background-image', 48 | ...opts, 49 | }; 50 | loadStyles(editor, options); 51 | }; 52 | 53 | export default plugin; 54 | -------------------------------------------------------------------------------- /src/styles.ts: -------------------------------------------------------------------------------- 1 | import type { Editor } from 'grapesjs'; 2 | // @ts-ignore 3 | import Grapick from 'grapick'; 4 | import { PluginOptions } from '.'; 5 | 6 | interface GradientStop { 7 | color: string; 8 | position: string; 9 | }; 10 | 11 | export interface GradientParseResult { 12 | direction: string; 13 | type: string; 14 | content: string; 15 | colors: string; 16 | stops: GradientStop[], 17 | } 18 | 19 | const getColor = (color: any) => { 20 | let cl = color.getAlpha() == 1 ? color.toHexString() : color.toRgbString(); 21 | return cl.replace(/ /g, ''); 22 | } 23 | 24 | const typeName = (name: string) => `${name}-gradient(`; 25 | 26 | /** 27 | * Parse CSS gradient value. 28 | */ 29 | export const parseGradient = (value: string): GradientParseResult => { 30 | const start = value.indexOf('(') + 1; 31 | const end = value.lastIndexOf(')'); 32 | const content = value.substring(start, end); 33 | const values = content.split(/,(?![^(]*\))/); 34 | const result: GradientParseResult = { 35 | direction: 'left', 36 | type: 'linear', 37 | content, 38 | colors: content, 39 | stops: [], 40 | }; 41 | 42 | if (!content) { 43 | return result; 44 | } 45 | 46 | if (values.length > 2) { 47 | result.direction = values.shift()!; 48 | result.colors = values.join(',').trim(); 49 | } 50 | 51 | let typeFound = false; 52 | const types = ['repeating-linear', 'repeating-radial', 'linear', 'radial']; 53 | types.forEach(name => { 54 | if (value.indexOf(typeName(name)) > -1 && !typeFound) { 55 | typeFound = true; 56 | result.type = name; 57 | } 58 | }); 59 | 60 | result.stops = values.map(value => { 61 | const parts = value.split(' '); 62 | const position = (parts.length > 1 ? parts.pop()! : '').trim(); 63 | const color = parts.join(' ').trim(); 64 | return { color, position }; 65 | }); 66 | 67 | return result; 68 | }; 69 | 70 | /** 71 | * Get CSS gradient value. 72 | */ 73 | export const toGradient = (type: string, angle: string, color: string): string => { 74 | const angles = [...GRAD_DIRS, 'center']; 75 | let angleValue = angle; 76 | 77 | if ( 78 | ['linear', 'repeating-linear'].indexOf(type) >= 0 79 | && angles.indexOf(angleValue) >= 0 80 | ) { 81 | angleValue = angleValue === 'center' ? 'to right' : `to ${angleValue}`; 82 | } 83 | 84 | if ( 85 | ['radial', 'repeating-radial'].indexOf(type) >= 0 86 | && angles.indexOf(angleValue) >= 0 87 | ) { 88 | angleValue = `circle at ${angleValue}`; 89 | } 90 | 91 | return color ? `${type}-gradient(${angleValue}, ${color})` : ''; 92 | }; 93 | 94 | export const getValidDir = (value: string) => { 95 | return GRAD_DIRS.filter(dir => value.indexOf(dir) > -1)[0]; 96 | } 97 | 98 | export const GRAD_DIRS = ['right', 'bottom', 'left', 'top']; 99 | export const GRAD_TYPES = ['linear', 'radial', 'repeating-linear', 'repeating-radial']; 100 | const defaultCpAttr = '[data-toggle="handler-color-wrap"]'; 101 | const PROP_GRADIENT = 'background-image-gradient'; 102 | const PROP_DIR = `${PROP_GRADIENT}-dir`; 103 | const PROP_TYPE = `${PROP_GRADIENT}-type`; 104 | 105 | export default (editor: Editor, config: PluginOptions = {}) => { 106 | const em = editor.getModel(); 107 | const { Styles } = editor; 108 | let { colorPicker, builtInType } = config; 109 | const styleTypeId = config.styleType; 110 | 111 | const clearHandler = (handler: any) => { 112 | const el = handler.getEl().querySelector(defaultCpAttr); 113 | const $el = editor.$(el); 114 | $el.spectrum && $el.spectrum('destroy'); 115 | }; 116 | 117 | styleTypeId && Styles.addType<{ gp?: any }>(styleTypeId, { 118 | create({ change }) { 119 | const el = document.createElement('div'); 120 | el.className = 'gp-container'; 121 | el.style.width = '100%'; 122 | const gp = new Grapick({ el, ...config.grapickOpts }); 123 | gp.on('change', (complete: boolean) => change({ value: gp.getValue(), partial: !complete })); 124 | this.gp = gp; 125 | 126 | // Add the custom color picker, if requested 127 | if (colorPicker === 'default') { 128 | colorPicker = handler => { 129 | const handlerEl = handler.getEl(); 130 | const el = handlerEl.querySelector(defaultCpAttr); 131 | const handlerInput = handlerEl.querySelector('input'); 132 | handlerInput?.parentNode.removeChild(handlerInput) 133 | const elStyle = el.style; 134 | elStyle.backgroundColor = handler.getColor(); 135 | const updateColor = (color: any, complete = 1) => { 136 | const cl = getColor(color); 137 | elStyle.backgroundColor = cl; 138 | handler.setColor(cl, complete); 139 | }; 140 | em.initBaseColorPicker(el, { 141 | color: handler.getColor(), 142 | change(color: any) { 143 | updateColor(color); 144 | }, 145 | move(color: any) { 146 | updateColor(color, 0); 147 | }, 148 | }); 149 | }; 150 | 151 | gp.on('handler:remove', clearHandler); 152 | } 153 | 154 | colorPicker && gp.setColorPicker(colorPicker); 155 | 156 | return el; 157 | }, 158 | emit({ updateStyle }, { partial, value }) { 159 | updateStyle(value, { partial }); 160 | }, 161 | update({ value }) { 162 | const { gp } = this; 163 | if (gp.getValue() === value) return; 164 | const handlers = gp.getHandlers(); 165 | handlers.map(clearHandler); 166 | gp.setValue(value, { silent: true }); 167 | 168 | if (config.selectEdgeStops) { 169 | [handlers[0], handlers[handlers.length - 1]].filter(Boolean) 170 | .map(h => h.select({ keepSelect: true })); 171 | } 172 | }, 173 | destroy() { 174 | this.gp?.destroy(); 175 | }, 176 | }); 177 | 178 | builtInType && Styles.addBuiltIn(builtInType, { 179 | type: 'composite', 180 | // @ts-ignore 181 | fromStyle(style: any, { name }: any) { 182 | const parsed = parseGradient(style[name] || ''); 183 | const gradType = parsed.type || GRAD_TYPES[0]; 184 | const gradDir = getValidDir(parsed.direction) || GRAD_DIRS[0]; 185 | const result = { 186 | [PROP_GRADIENT]: toGradient(gradType, gradDir, parsed.colors), 187 | [PROP_TYPE]: gradType, 188 | [PROP_DIR]: gradDir, 189 | }; 190 | return result; 191 | }, 192 | toStyle(values: any, { name }: any) { 193 | const gradValue = values[PROP_GRADIENT] || ''; 194 | const gradType = values[PROP_TYPE] || GRAD_TYPES[0]; 195 | const gradDir = values[PROP_DIR] || GRAD_DIRS[0]; 196 | const parsed = parseGradient(gradValue); 197 | const result = toGradient(gradType, gradDir, parsed.colors); 198 | return { [name]: result }; 199 | }, 200 | properties: [ 201 | { 202 | name: ' ', 203 | full: true, 204 | defaults: 'none', 205 | type: styleTypeId, 206 | property: PROP_GRADIENT, 207 | }, 208 | { 209 | name: 'Direction', 210 | type: 'select', 211 | defaults: 'right', 212 | property: PROP_DIR, 213 | options: GRAD_DIRS.map(value => ({ value })), 214 | }, 215 | { 216 | name: 'Type', 217 | type: 'select', 218 | defaults: 'linear', 219 | property: PROP_TYPE, 220 | options: GRAD_TYPES.map(value => ({ value })), 221 | } 222 | ] 223 | }); 224 | } 225 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/grapesjs-cli/dist/template/tsconfig.json", 3 | "include": ["src"] 4 | } --------------------------------------------------------------------------------