├── .gitignore
├── .npmignore
├── webpack.config.js
├── src
├── index.css
├── icons.js
├── picker
│ ├── utils
│ │ └── main.js
│ ├── index.js
│ └── components
│ │ ├── xy-button.js
│ │ └── xy-popover.js
└── index.js
├── package.json
├── LICENSE
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/*
2 | npm-debug.log
3 | .idea/*
4 | .DS_Store
5 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | assets/
3 | src/
4 | webpack.config.js
5 | yarn.lock
6 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | output: {
3 | path: __dirname + '/dist',
4 | publicPath: '/',
5 | filename: 'bundle.js',
6 | library: 'ColorPlugin',
7 | libraryTarget: 'umd'
8 | },
9 | mode: 'production',
10 | resolve: {
11 | extensions: ['.js'],
12 | },
13 | module: {
14 | rules: [
15 | {
16 | test: /\.css$/,
17 | use: [
18 | 'style-loader',
19 | 'css-loader'
20 | ]
21 | },
22 | {
23 | test: /\.(svg)$/,
24 | use: [
25 | {
26 | loader: 'raw-loader',
27 | }
28 | ]
29 | }
30 | ],
31 | },
32 | optimization: {
33 | minimize: true,
34 | },
35 | }
36 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | .picker_wrapper.popup {
2 | z-index: 99;
3 | width: 170px;
4 | margin: 0;
5 | box-shadow: 0 0 10px 1px #eaeaea;
6 | background: #ffffff;
7 | }
8 |
9 | .picker_arrow {
10 | display: none;
11 | }
12 |
13 | .layout_default .picker_slider, .layout_default .picker_selector {
14 | padding: 5px;
15 | }
16 |
17 | .colorPlugin.ce-inline-tool {
18 | width: 32px;
19 | border-radius: 3px;
20 | }
21 |
22 | .colorPlugin.ce-inline-tool--active svg {
23 | fill: #3c99ff;
24 | }
25 |
26 | #color-left-btn {
27 | height: 35px;
28 | width: 18px;
29 | font-weight: 600;
30 | display: flex;
31 | align-items: center;
32 | }
33 |
34 | #color-left-btn:hover {
35 | border-radius: 5px 0 0 5px;
36 | background: rgba(203, 203, 203, 0.49);
37 | }
38 |
39 | #color-text {
40 | padding: 0 4px;
41 | }
42 |
43 | #color-btn-text {
44 | height: 15px;
45 | }
46 |
47 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "editorjs-text-color-plugin",
3 | "version": "2.0.3",
4 | "description": "Text Color Tool for Editor.js",
5 | "main": "./dist/bundle.js",
6 | "scripts": {
7 | "build": "webpack --mode=production",
8 | "build:dev": "webpack --mode=development --watch"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/flaming-cl/editorjs-text-color-plugin.git"
13 | },
14 | "keywords": [
15 | "['editor'",
16 | "'editorjs'",
17 | "'text color picker']"
18 | ],
19 | "author": "flaming-cl",
20 | "license": "MIT",
21 | "bugs": {
22 | "url": "https://github.com/flaming-cl/editorjs-text-color-plugin/issues"
23 | },
24 | "homepage": "https://github.com/flaming-cl/editorjs-text-color-plugin#readme",
25 | "devDependencies": {
26 | "@babel/core": "^7.10.2",
27 | "@babel/preset-env": "^7.10.2",
28 | "babel-loader": "^8.1.0",
29 | "css-loader": "^3.5.3",
30 | "raw-loader": "^4.0.1",
31 | "style-loader": "^1.2.1",
32 | "webpack-cli": "^5.0.1"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 CodeX
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 |
--------------------------------------------------------------------------------
/src/icons.js:
--------------------------------------------------------------------------------
1 | const markerIcon = ``;
18 |
19 | const textIcon = ``;
20 |
21 | module.exports = {
22 | markerIcon,
23 | textIcon
24 | }
25 |
--------------------------------------------------------------------------------
/src/picker/utils/main.js:
--------------------------------------------------------------------------------
1 | const TEXT_COLOR_CACHE = 'editor-js-text-color-cache';
2 |
3 | /**
4 | * Convert CSS variables to color string.
5 | * @param colorValue original value provided by users
6 | * @returns string color string
7 | */
8 | export function handleCSSVariables(colorValue) {
9 | if (isColorVariable(colorValue)) {
10 | const variableName = extractVariableName(colorValue);
11 | return getCSSPropertyValue(variableName);
12 | }
13 | return colorValue;
14 | }
15 |
16 | function extractVariableName(colorValue) {
17 | const regexResult = /\((.*?)\)/.exec(colorValue);
18 | if (regexResult) return regexResult[1];
19 | }
20 |
21 | function getCSSPropertyValue(variableName) {
22 | return window.getComputedStyle(document.documentElement).getPropertyValue(variableName);
23 | }
24 |
25 | function isColorVariable(colorValue) {
26 | return isString(colorValue) && colorValue.includes('--');
27 | }
28 |
29 | function isString(stringInput) {
30 | return typeof stringInput === 'string' || stringInput instanceof String;
31 | }
32 |
33 | export function throttle(fn, delay) {
34 | let id;
35 | return (...args) => {
36 | if (!id) {
37 | id = setTimeout(() => {
38 | fn(...args);
39 | id = null;
40 | }, delay)
41 | }
42 | }
43 | }
44 |
45 | /**
46 | * Cache the latest text/marker color
47 | * @param defaultColor
48 | * @param pluginType
49 | * @returns defaultColor
50 | */
51 | export function setDefaultColorCache(defaultColor, pluginType) {
52 | sessionStorage.setItem(`${TEXT_COLOR_CACHE}-${pluginType}`, JSON.stringify(defaultColor));
53 | return defaultColor;
54 | }
55 |
56 | /**
57 | * Get cached text/marker color
58 | * @param defaultColor
59 | * @param pluginType
60 | * @returns string cachedDefaultColor/defaultColor
61 | */
62 | export function getDefaultColorCache(defaultColor, pluginType) {
63 | const cachedDefaultColor = sessionStorage.getItem(`${TEXT_COLOR_CACHE}-${pluginType}`);
64 | return cachedDefaultColor ? JSON.parse(cachedDefaultColor) : defaultColor;
65 | }
66 |
67 | /**
68 | * Cache custom color
69 | * @param customColor,
70 | * @param pluginType
71 | */
72 | export function setCustomColorCache(customColor, pluginType) {
73 | sessionStorage.setItem(`${TEXT_COLOR_CACHE}-${pluginType}-custom`, JSON.stringify(customColor));
74 | }
75 |
76 | /**
77 | * Get cached custom color
78 | * @param pluginType
79 | * @returns string cachedCustomColor
80 | */
81 | export function getCustomColorCache(pluginType) {
82 | const cachedCustomColor = sessionStorage.getItem(`${TEXT_COLOR_CACHE}-${pluginType}-custom`);
83 | return cachedCustomColor ? JSON.parse(cachedCustomColor) : null;
84 | }
85 |
86 | export const CONVERTER_BTN = 'ce-inline-toolbar__dropdown';
87 | export const CONVERTER_PANEL = 'ce-conversion-toolbar--showed';
88 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # Editor.js Text Color Tool
4 |
5 | A simple tool [Demo](https://flaming-cl.github.io/editorPlugin) to color text-fragments for [Editor.js](https://editorjs.io).
6 |
7 | 
8 |
9 | ## Installation
10 |
11 | ### Install via NPM
12 |
13 | Get the package
14 |
15 | ```shell
16 | npm i --save-dev editorjs-text-color-plugin
17 | ```
18 |
19 | Import the plugin
20 |
21 | ```javascript
22 | const ColorPlugin = require('editorjs-text-color-plugin');
23 | ```
24 |
25 | ### Load from CDN
26 | ```html
27 |
28 | ```
29 |
30 | ## Usage
31 |
32 | Add the plugin to Editor.js: editing the `tools` property in your Editor.js config.
33 |
34 | ```javascript
35 | var editor = EditorJS({
36 | ...
37 |
38 | tools: {
39 | ...
40 |
41 | Color: {
42 | class: ColorPlugin, // if load from CDN, please try: window.ColorPlugin
43 | config: {
44 | colorCollections: ['#EC7878','#9C27B0','#673AB7','#3F51B5','#0070FF','#03A9F4','#00BCD4','#4CAF50','#8BC34A','#CDDC39', '#FFF'],
45 | defaultColor: '#FF1300',
46 | type: 'text',
47 | customPicker: true // add a button to allow selecting any colour
48 | }
49 | },
50 | Marker: {
51 | class: ColorPlugin, // if load from CDN, please try: window.ColorPlugin
52 | config: {
53 | defaultColor: '#FFBF00',
54 | type: 'marker',
55 | icon: ``
56 | }
57 | },
58 | },
59 |
60 | ...
61 | });
62 | ```
63 |
64 | ## Config Params (optional)
65 |
66 | | Field | Type | Description |
67 | |------------------|-----------|----------------------------------------------------------------------------------------------------------------------------------------|
68 | | colorCollections | `array` | Colors available in the palette. CSS variables, for example var(--main-text-color), are supported |
69 | | defaultColor | `string` | Default color (if you do not set up a default color, it will be the first color of your color collections). CSS variables supported. |
70 | | type | `string` | Set the plugin as a marker or a text color tool. It will be set as a text color tool as default. |
71 | | customPicker | `boolean` | Turn on a random color picker in the palette, defaults to `false`. |
72 | | icon | `string` | SVG string to replace default button icons. |
73 |
74 | ## Output data
75 |
76 | Colored text will be wrapped with a `color` tag with an `color-plugin` class.
77 |
78 | ```json
79 | {
80 | "type" : "text",
81 | "data" : {
82 | "text" : "ColorPlugin."
83 | },
84 | }
85 | ```
86 |
87 | ## Recent Updates
88 | | Field | Type | Description |
89 | |---------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
90 | | V1.12.1 | Mar-25-2022 | CSS variable Support for colorCollection/defaultColor. This version supports the newest version of Editor.js (v2.23.2). Previous versions support Editor.js (v2.0) |
91 | | V1.13.1 | May-1-2022 | Thanks to HaoCherHong's contribution, we add a custom color picker in this version. |
92 | | V2.0.1 | Jan-20-2023 | New features: 1. clean applied text/marker color. When the left area of the plugin color turns blue, it means applied color can be cleaned now. 2. Allow customized icons |
93 | | V2.0.2 | Jan-23-2023 | Fix: 1. toggle conversion tool when opening inline color plugin 2. optimized picker initialization |
94 | | V2.0.4 | June-14-2023 | Fix: Chrome 114 popover conflicts. Credit to iwnow / dev1forma |
95 |
96 | ## Credits
97 | UI Built Based on https://github.com/XboxYan/xy-ui by XboxYan
98 |
99 | ## License
100 | [MIT](https://github.com/flaming-cl/editorjs-text-color-plugin/blob/master/LICENSE)
101 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Build styles
3 | */
4 | const Picker = require('./picker');
5 | const { markerIcon, textIcon } = require('./icons');
6 | const { getDefaultColorCache, handleCSSVariables } = require('./picker/utils/main');
7 | require('./index.css').toString();
8 |
9 | /**
10 | * Text Color Tool for Editor.js
11 | */
12 | class Color {
13 |
14 | /**
15 | * @param {{api: object}} - Editor.js API
16 | */
17 | constructor({ config, api }) {
18 | this.api = api;
19 | this.config = config;
20 | this.clickedOnLeft = false;
21 | this.pluginType = this.config.type || 'text';
22 | this.parentTag = this.pluginType === 'marker' ? 'MARK' : 'FONT';
23 | this.hasCustomPicker = this.config.customPicker || false;
24 | this.color = handleCSSVariables(
25 | getDefaultColorCache(this.config.defaultColor, this.pluginType)
26 | );
27 | this.picker = null;
28 | this.icon = null;
29 |
30 | /**
31 | * Toolbar Button
32 | *
33 | * @type {HTMLElement|null}
34 | */
35 | this.button = null;
36 |
37 | /**
38 | * CSS classes
39 | */
40 | this.iconClasses = {
41 | base: this.api.styles.inlineToolButton,
42 | active: this.api.styles.inlineToolButtonActive,
43 | };
44 | }
45 |
46 | /**
47 | * Specifies Tool as Inline Toolbar Tool
48 | *
49 | * @return {boolean}
50 | */
51 | static get isInline() {
52 | return true;
53 | }
54 |
55 | /**
56 | * Create button element for Toolbar
57 | *
58 | * @return {HTMLElement}
59 | */
60 | render() {
61 | this.button = document.createElement('button');
62 | this.button.type = 'button';
63 | this.button.classList.add('colorPlugin');
64 | this.button.classList.add(this.iconClasses.base);
65 | this.button.appendChild(this.createLeftButton());
66 | this.button.appendChild(this.createRightButton(this));
67 |
68 | return this.button;
69 | }
70 |
71 | /**
72 | * Create left part button
73 | *
74 | * @return {HTMLElement}
75 | */
76 | createLeftButton() {
77 | if (!this.icon) {
78 | this.icon = document.createElement('div');
79 | this.icon.id = 'color-left-btn';
80 | this.icon.appendChild(this.createButtonIcon());
81 | this.icon.addEventListener('click', () => this.clickedOnLeft = true);
82 | }
83 |
84 | return this.icon;
85 | }
86 |
87 | /**
88 | * Create button icon
89 | *
90 | * @return {HTMLElement}
91 | */
92 | createButtonIcon() {
93 | const buttonIcon = document.createElement('div');
94 | buttonIcon.id = 'color-btn-text';
95 | const defaultIcon = this.pluginType === 'marker' ? markerIcon : textIcon;
96 | buttonIcon.innerHTML = this.config.icon || defaultIcon;
97 | return buttonIcon;
98 | }
99 |
100 | /**
101 | * Create right part button
102 | *
103 | * @return {HTMLElement}
104 | */
105 | createRightButton(sharedScope) {
106 | if (!this.picker) {
107 | this.picker = new Picker.ColorPlugin({
108 | onColorPicked: function (value) {
109 | sharedScope.color = value;
110 | },
111 | hasCustomPicker: this.hasCustomPicker,
112 | defaultColor: this.config.defaultColor,
113 | colorCollections: this.config.colorCollections,
114 | type: this.pluginType
115 | });
116 | }
117 |
118 | return this.picker;
119 | }
120 |
121 | /**
122 | * handle selected fragment
123 | *
124 | * @param {Range} range - selected fragment
125 | */
126 | surround(range) {
127 | if (!range) {
128 | return
129 | }
130 |
131 | /**
132 | * clean legacy wrapper generated before editorjs-text-color-plugin v3.0
133 | */
134 | const legacySpanWrapper = this.api.selection.findParentTag("SPAN");
135 | if (legacySpanWrapper) this.unwrap(legacySpanWrapper);
136 |
137 | /**
138 | * If start or end of selection is in the highlighted block
139 | */
140 | const termWrapper = this.api.selection.findParentTag(this.parentTag);
141 |
142 | if (termWrapper) {
143 | this.unwrap(termWrapper);
144 | } else {
145 | this.wrap(range);
146 | }
147 |
148 | this.clickedOnLeft = false;
149 | }
150 |
151 | /**
152 | * Wrap selected fragment
153 | *
154 | * @param {Range} range - selected fragment
155 | */
156 | wrap(range) {
157 | const selectedText = range.extractContents();
158 | const newWrapper = document.createElement(this.parentTag);
159 |
160 | newWrapper.appendChild(selectedText);
161 | range.insertNode(newWrapper);
162 |
163 | if (this.pluginType === 'marker') {
164 | this.wrapMarker(newWrapper);
165 | } else {
166 | this.wrapTextColor(newWrapper);
167 | }
168 |
169 | this.api.selection.expandToTag(newWrapper);
170 | }
171 |
172 | /**
173 | * Wrap selected marker fragment
174 | *
175 | * @param newWrapper - wrapper for selected fragment
176 | */
177 | wrapMarker(newWrapper) {
178 | newWrapper.style.backgroundColor = this.color;
179 | const colorWrapper = this.api.selection.findParentTag('FONT');
180 | if (colorWrapper) newWrapper.style.color = colorWrapper.style.color;
181 | }
182 |
183 | /**
184 | * Wrap selected text color fragment
185 | *
186 | * @param {Range} newWrapper - wrapper for selected fragment
187 | */
188 | wrapTextColor(newWrapper) {
189 | newWrapper.style.color = this.color;
190 | }
191 |
192 | /**
193 | * Unwrap selected fragment
194 | *
195 | * @param {Range} termWrapper - parent of selected fragment
196 | */
197 | unwrap(termWrapper) {
198 | /**
199 | * Expand selection to all term-tag
200 | */
201 | this.api.selection.expandToTag(termWrapper)
202 |
203 | const sel = window.getSelection()
204 | const range = sel.getRangeAt(0)
205 |
206 | const unwrappedContent = range.extractContents()
207 |
208 | /**
209 | * Remove empty term-tag
210 | */
211 | if (this.clickedOnLeft) {
212 | this.removeWrapper(termWrapper);
213 | } else {
214 | this.updateWrapper(termWrapper);
215 | }
216 |
217 | /**
218 | * Insert extracted content
219 | */
220 | range.insertNode(unwrappedContent)
221 |
222 | /**
223 | * Restore selection
224 | */
225 | sel.removeAllRanges()
226 | sel.addRange(range)
227 | }
228 |
229 | /**
230 | * update color without create a new tag
231 | *
232 | * @param {Range} termWrapper - parent of selected fragment
233 | */
234 | updateWrapper(termWrapper) {
235 | if (this.pluginType === 'marker') {
236 | termWrapper.style.backgroundColor = this.color;
237 | } else {
238 | termWrapper.style.color = this.color;
239 | }
240 | }
241 |
242 | /**
243 | * remove wrapper
244 | *
245 | * @param {Range} termWrapper - parent of selected fragment
246 | */
247 | removeWrapper(termWrapper) {
248 | termWrapper.parentNode.removeChild(termWrapper);
249 | }
250 |
251 | /**
252 | * Check and change Term's state for current selection
253 | */
254 | checkState() {
255 | const legacyWrapper = this.api.selection.findParentTag("SPAN");
256 | const termTag = this.api.selection.findParentTag(this.parentTag);
257 | let isWrapped = legacyWrapper ? this.handleLegacyWrapper(legacyWrapper, termTag) : termTag;
258 | this.button.classList.toggle(this.iconClasses.active, !!isWrapped)
259 |
260 | return !!isWrapped;
261 | }
262 |
263 | /**
264 | * handle icon active state for legacy wrappers
265 | */
266 | handleLegacyWrapper(legacyWrapper, termTag) {
267 | return this.pluginType === 'marker' ? legacyWrapper : (termTag & legacyWrapper);
268 | }
269 |
270 | /**
271 | * Sanitizer rule
272 | * @return {{color: {class: string}}}
273 | */
274 | static get sanitize() {
275 | return {
276 | font: true,
277 | span: true,
278 | mark: true
279 | };
280 | }
281 |
282 | clear() {
283 | this.picker = null;
284 | this.icon = null;
285 | }
286 | }
287 |
288 | module.exports = Color;
289 |
--------------------------------------------------------------------------------
/src/picker/index.js:
--------------------------------------------------------------------------------
1 | import './components/xy-popover.js';
2 | import {
3 | handleCSSVariables,
4 | setDefaultColorCache,
5 | getDefaultColorCache,
6 | throttle,
7 | getCustomColorCache,
8 | setCustomColorCache,
9 | CONVERTER_BTN,
10 | CONVERTER_PANEL,
11 | } from './utils/main';
12 | const ColorCollections = ['#ff1300','#EC7878','#9C27B0','#673AB7','#3F51B5','#0070FF','#03A9F4','#00BCD4','#4CAF50','#8BC34A','#CDDC39','#FFE500','#FFBF00','#FF9800','#795548','#9E9E9E','#5A5A5A','#FFF'];
13 | class ColorPlugin extends HTMLElement {
14 |
15 | static get observedAttributes() { return ['disabled','dir'] }
16 |
17 | constructor(options) {
18 | super();
19 | const shadowRoot = this.attachShadow({ mode: 'open' });
20 | this.colorCollections = options.colorCollections || ColorCollections;
21 | this.onColorPicked = options.onColorPicked;
22 | this.defaulColor = handleCSSVariables(options.defaultColor || this.colorCollections[0]);
23 | this.pluginType = options.type;
24 | this.hasCustomPicker = options.hasCustomPicker;
25 | this.customColor = getCustomColorCache(this.pluginType);
26 |
27 | shadowRoot.innerHTML = `
28 |
155 |