From f7f68a66c54a38673691b0f6da15431a51f4c25d Mon Sep 17 00:00:00 2001 From: adriangroch Date: Tue, 26 Nov 2019 14:19:23 +1100 Subject: [PATCH 1/5] Added state to RTE actions --- src/rich_text_editor/index.js | 627 ++++++++++--------- src/rich_text_editor/model/RichTextEditor.js | 73 ++- src/styles/scss/_gjs_rte.scss | 9 +- 3 files changed, 394 insertions(+), 315 deletions(-) diff --git a/src/rich_text_editor/index.js b/src/rich_text_editor/index.js index 68a44d833..a23efe3d2 100644 --- a/src/rich_text_editor/index.js +++ b/src/rich_text_editor/index.js @@ -5,302 +5,331 @@ * You can customize the initial state of the module from the editor initialization, by passing the following [Configuration Object](https://github.com/artf/grapesjs/blob/master/src/rich_text_editor/config/config.js) * ```js * const editor = grapesjs.init({ - * richTextEditor: { - * // options - * } - * }) - * ``` - * - * Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance - * - * ```js - * const rte = editor.RichTextEditor; - * ``` - * - * * [add](#add) - * * [get](#get) - * * [getAll](#getall) - * * [remove](#remove) - * * [getToolbarEl](#gettoolbarel) - * - * @module RichTextEditor - */ - -import RichTextEditor from './model/RichTextEditor'; -import { on, off } from 'utils/mixins'; -import defaults from './config/config'; - -export default () => { - let config = {}; - let toolbar, actions, lastEl, globalRte; - - const hideToolbar = () => { - const style = toolbar.style; - const size = '-1000px'; - style.top = size; - style.left = size; - style.display = 'none'; - }; - - return { - customRte: null, - - /** - * Name of the module - * @type {String} - * @private - */ - name: 'RichTextEditor', - - getConfig() { - return config; - }, - - /** - * Initialize module. Automatically called with a new instance of the editor - * @param {Object} opts Options - * @private - */ - init(opts = {}) { - config = { - ...defaults, - ...opts - }; - const ppfx = config.pStylePrefix; - - if (ppfx) { - config.stylePrefix = ppfx + config.stylePrefix; - } - - this.pfx = config.stylePrefix; - actions = config.actions || []; - toolbar = document.createElement('div'); - toolbar.className = `${ppfx}rte-toolbar ${ppfx}one-bg`; - globalRte = this.initRte(document.createElement('div')); - - //Avoid closing on toolbar clicking - on(toolbar, 'mousedown', e => e.stopPropagation()); - return this; - }, - - /** - * Post render callback - * @param {View} ev - * @private - */ - postRender(ev) { - const canvas = ev.model.get('Canvas'); - toolbar.style.pointerEvents = 'all'; - hideToolbar(); - canvas.getToolsEl().appendChild(toolbar); - }, - - /** - * Init the built-in RTE - * @param {HTMLElement} el - * @return {RichTextEditor} - * @private - */ - initRte(el) { - const pfx = this.pfx; - const actionbarContainer = toolbar; - const actionbar = this.actionbar; - const actions = this.actions || config.actions; - const classes = { - actionbar: `${pfx}actionbar`, - button: `${pfx}action`, - active: `${pfx}active` - }; - const rte = new RichTextEditor({ - el, - classes, - actions, - actionbar, - actionbarContainer - }); - globalRte && globalRte.setEl(el); - - if (rte.actionbar) { - this.actionbar = rte.actionbar; - } - - if (rte.actions) { - this.actions = rte.actions; - } - - return rte; - }, - - /** - * Add a new action to the built-in RTE toolbar - * @param {string} name Action name - * @param {Object} action Action options - * @example - * rte.add('bold', { - * icon: 'B', - * attributes: {title: 'Bold'}, - * result: rte => rte.exec('bold') - * }); - * rte.add('link', { - * icon: document.getElementById('t'), - * attributes: {title: 'Link',} - * // Example on it's easy to wrap a selected content - * result: rte => rte.insertHTML(`${rte.selection()}`) - * }); - * // An example with fontSize - * rte.add('fontSize', { - * icon: ``, - * // Bind the 'result' on 'change' listener - * event: 'change', - * result: (rte, action) => rte.exec('fontSize', action.btn.firstChild.value), - * // Callback on any input change (mousedown, keydown, etc..) - * update: (rte, action) => { - * const value = rte.doc.queryCommandValue(action.name); - * if (value != 'false') { // value is a string - * action.btn.firstChild.value = value; - * } - * } - * }) - */ - add(name, action = {}) { - action.name = name; - globalRte.addAction(action, { sync: 1 }); - }, - - /** - * Get the action by its name - * @param {string} name Action name - * @return {Object} - * @example - * const action = rte.get('bold'); - * // {name: 'bold', ...} - */ - get(name) { - let result; - globalRte.getActions().forEach(action => { - if (action.name == name) { - result = action; - } - }); - return result; - }, - - /** - * Get all actions - * @return {Array} - */ - getAll() { - return globalRte.getActions(); - }, - - /** - * Remove the action from the toolbar - * @param {string} name - * @return {Object} Removed action - * @example - * const action = rte.remove('bold'); - * // {name: 'bold', ...} - */ - remove(name) { - const actions = this.getAll(); - const action = this.get(name); - - if (action) { - const btn = action.btn; - const index = actions.indexOf(action); - btn.parentNode.removeChild(btn); - actions.splice(index, 1); - } - - return action; - }, - - /** - * Get the toolbar element - * @return {HTMLElement} - */ - getToolbarEl() { - return toolbar; - }, - - /** - * Triggered when the offset of the editor is changed - * @private - */ - updatePosition() { - const un = 'px'; - const canvas = config.em.get('Canvas'); - const pos = canvas.getTargetToElementDim(toolbar, lastEl, { - event: 'rteToolbarPosUpdate' - }); - - if (pos) { - if (config.adjustToolbar) { - const frameOffset = canvas.getCanvasView().getFrameOffset(); - // Move the toolbar down when the top canvas edge is reached - if ( - pos.top <= pos.canvasTop && - !(pos.elementHeight + pos.targetHeight >= frameOffset.height) - ) { - pos.top = pos.elementTop + pos.elementHeight; - } - } - - const toolbarStyle = toolbar.style; - toolbarStyle.top = pos.top + un; - toolbarStyle.left = pos.left + un; - } - }, - - /** - * Enable rich text editor on the element - * @param {View} view Component view - * @param {Object} rte The instance of already defined RTE - * @private - * */ - enable(view, rte) { - lastEl = view.el; - const em = config.em; - const el = view.getChildrenContainer(); - const customRte = this.customRte; - - toolbar.style.display = ''; - rte = customRte ? customRte.enable(el, rte) : this.initRte(el).enable(); - - if (em) { - setTimeout(this.updatePosition.bind(this), 0); - const event = 'change:canvasOffset canvasScroll'; - em.off(event, this.updatePosition, this); - em.on(event, this.updatePosition, this); - em.trigger('rte:enable', view, rte); - } - - return rte; - }, - - /** - * Unbind rich text editor from the element - * @param {View} view - * @param {Object} rte The instance of already defined RTE - * @private - * */ - disable(view, rte) { - const em = config.em; - const customRte = this.customRte; - var el = view.getChildrenContainer(); - - if (customRte) { - customRte.disable(el, rte); - } else { - rte && rte.disable(); - } - - hideToolbar(); - em && em.trigger('rte:disable', view, rte); - } - }; -}; + * richTextEditor: { + * // options + * } + * }) + * ``` + * + * Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance + * + * ```js + * const rte = editor.RichTextEditor; + * ``` + * + * * [add](#add) + * * [get](#get) + * * [getAll](#getall) + * * [remove](#remove) + * * [getToolbarEl](#gettoolbarel) + * + * @module RichTextEditor + */ + + import RichTextEditor from './model/RichTextEditor'; + import { on, off } from 'utils/mixins'; + import defaults from './config/config'; + + export default () => { + let config = {}; + let toolbar, actions, lastEl, globalRte; + + const hideToolbar = () => { + const style = toolbar.style; + const size = '-1000px'; + style.top = size; + style.left = size; + style.display = 'none'; + }; + + return { + customRte: null, + + /** + * Name of the module + * @type {String} + * @private + */ + name: 'RichTextEditor', + + getConfig() { + return config; + }, + + /** + * Initialize module. Automatically called with a new instance of the editor + * @param {Object} opts Options + * @private + */ + init(opts = {}) { + config = { + ...defaults, + ...opts + }; + const ppfx = config.pStylePrefix; + + if (ppfx) { + config.stylePrefix = ppfx + config.stylePrefix; + } + + this.pfx = config.stylePrefix; + actions = config.actions || []; + toolbar = document.createElement('div'); + toolbar.className = `${ppfx}rte-toolbar ${ppfx}one-bg`; + globalRte = this.initRte(document.createElement('div')); + + //Avoid closing on toolbar clicking + on(toolbar, 'mousedown', e => e.stopPropagation()); + return this; + }, + + /** + * Post render callback + * @param {View} ev + * @private + */ + postRender(ev) { + const canvas = ev.model.get('Canvas'); + toolbar.style.pointerEvents = 'all'; + hideToolbar(); + canvas.getToolsEl().appendChild(toolbar); + }, + + /** + * Init the built-in RTE + * @param {HTMLElement} el + * @return {RichTextEditor} + * @private + */ + initRte(el) { + const pfx = this.pfx; + const actionbarContainer = toolbar; + const actionbar = this.actionbar; + const actions = this.actions || config.actions; + const classes = { + actionbar: `${pfx}actionbar`, + button: `${pfx}action`, + active: `${pfx}active`, + inactive: `${pfx}inactive`, + disabled: `${pfx}disabled` + }; + const rte = new RichTextEditor({ + el, + classes, + actions, + actionbar, + actionbarContainer + }); + globalRte && globalRte.setEl(el); + + if (rte.actionbar) { + this.actionbar = rte.actionbar; + } + + if (rte.actions) { + this.actions = rte.actions; + } + + return rte; + }, + + /** + * Add a new action to the built-in RTE toolbar + * @param {string} name Action name + * @param {Object} action Action options + * @example + * rte.add('bold', { + * icon: 'B', + * attributes: {title: 'Bold'}, + * result: rte => rte.exec('bold') + * }); + * rte.add('link', { + * icon: document.getElementById('t'), + * attributes: {title: 'Link',} + * // Example on it's easy to wrap a selected content + * result: rte => rte.insertHTML(`${rte.selection()}`) + * }); + * // An example with fontSize + * rte.add('fontSize', { + * icon: ``, + * // Bind the 'result' on 'change' listener + * event: 'change', + * result: (rte, action) => rte.exec('fontSize', action.btn.firstChild.value), + * // Callback on any input change (mousedown, keydown, etc..) + * update: (rte, action) => { + * const value = rte.doc.queryCommandValue(action.name); + * if (value != 'false') { // value is a string + * action.btn.firstChild.value = value; + * } + * } + * }) + * // An example with state + * const isValidAnchor = (rte) => { + * // a utility function to help determine if the selected is a valid anchor node + * const anchor = rte.selection().anchorNode; + * const parentNode = anchor && anchor.parentNode; + * const nextSibling = anchor && anchor.nextSibling; + * return (parentNode && parentNode.nodeName == 'A') || (nextSibling && nextSibling.nodeName == 'A') + * } + * rte.add('toggleAnchor', { + * icon: ``, + * state: (rte, doc) => { + * if (rte && rte.selection()) { + * // `btnState` is a integer, -1 for disabled, 0 for inactive, 1 for active + * return isValidAnchor(rte) ? btnState.ACTIVE : btnState.INACTIVE; + * } else { + * return btnState.INACTIVE; + * } + * }, + * result: (rte, action) => { + * if (isValidAnchor(rte)) { + * rte.exec('unlink'); + * } else { + * rte.insertHTML(`${rte.selection()}`); + * } + * } + * }) + */ + add(name, action = {}) { + action.name = name; + globalRte.addAction(action, { sync: 1 }); + }, + + /** + * Get the action by its name + * @param {string} name Action name + * @return {Object} + * @example + * const action = rte.get('bold'); + * // {name: 'bold', ...} + */ + get(name) { + let result; + globalRte.getActions().forEach(action => { + if (action.name == name) { + result = action; + } + }); + return result; + }, + + /** + * Get all actions + * @return {Array} + */ + getAll() { + return globalRte.getActions(); + }, + + /** + * Remove the action from the toolbar + * @param {string} name + * @return {Object} Removed action + * @example + * const action = rte.remove('bold'); + * // {name: 'bold', ...} + */ + remove(name) { + const actions = this.getAll(); + const action = this.get(name); + + if (action) { + const btn = action.btn; + const index = actions.indexOf(action); + btn.parentNode.removeChild(btn); + actions.splice(index, 1); + } + + return action; + }, + + /** + * Get the toolbar element + * @return {HTMLElement} + */ + getToolbarEl() { + return toolbar; + }, + + /** + * Triggered when the offset of the editor is changed + * @private + */ + updatePosition() { + const un = 'px'; + const canvas = config.em.get('Canvas'); + const pos = canvas.getTargetToElementDim(toolbar, lastEl, { + event: 'rteToolbarPosUpdate' + }); + + if (pos) { + if (config.adjustToolbar) { + const frameOffset = canvas.getCanvasView().getFrameOffset(); + // Move the toolbar down when the top canvas edge is reached + if ( + pos.top <= pos.canvasTop && + !(pos.elementHeight + pos.targetHeight >= frameOffset.height) + ) { + pos.top = pos.elementTop + pos.elementHeight; + } + } + + const toolbarStyle = toolbar.style; + toolbarStyle.top = pos.top + un; + toolbarStyle.left = pos.left + un; + } + }, + + /** + * Enable rich text editor on the element + * @param {View} view Component view + * @param {Object} rte The instance of already defined RTE + * @private + * */ + enable(view, rte) { + lastEl = view.el; + const em = config.em; + const el = view.getChildrenContainer(); + const customRte = this.customRte; + + toolbar.style.display = ''; + rte = customRte ? customRte.enable(el, rte) : this.initRte(el).enable(); + + if (em) { + setTimeout(this.updatePosition.bind(this), 0); + const event = 'change:canvasOffset canvasScroll'; + em.off(event, this.updatePosition, this); + em.on(event, this.updatePosition, this); + em.trigger('rte:enable', view, rte); + } + + return rte; + }, + + /** + * Unbind rich text editor from the element + * @param {View} view + * @param {Object} rte The instance of already defined RTE + * @private + * */ + disable(view, rte) { + const em = config.em; + const customRte = this.customRte; + var el = view.getChildrenContainer(); + + if (customRte) { + customRte.disable(el, rte); + } else { + rte && rte.disable(); + } + + hideToolbar(); + em && em.trigger('rte:disable', view, rte); + } + }; + }; + \ No newline at end of file diff --git a/src/rich_text_editor/model/RichTextEditor.js b/src/rich_text_editor/model/RichTextEditor.js index d2b076378..b7b44da42 100644 --- a/src/rich_text_editor/model/RichTextEditor.js +++ b/src/rich_text_editor/model/RichTextEditor.js @@ -5,6 +5,20 @@ import { on, off } from 'utils/mixins'; const RTE_KEY = '_rte'; +const btnState = { + ACTIVE: 1, + INACTIVE: 0, + DISABLED: -1 +}; +const isValidAnchor = rte => { + const anchor = rte.selection().anchorNode; + const parentNode = anchor && anchor.parentNode; + const nextSibling = anchor && anchor.nextSibling; + return ( + (parentNode && parentNode.nodeName == 'A') || + (nextSibling && nextSibling.nodeName == 'A') + ); +}; const defActions = { bold: { name: 'bold', @@ -37,10 +51,15 @@ const defActions = { style: 'font-size:1.4rem;padding:0 4px 2px;', title: 'Link' }, + state: (rte, doc) => { + if (rte && rte.selection()) { + return isValidAnchor(rte) ? btnState.ACTIVE : btnState.INACTIVE; + } else { + return btnState.INACTIVE; + } + }, result: rte => { - const anchor = rte.selection().anchorNode; - const nextSibling = anchor && anchor.nextSibling; - if (nextSibling && nextSibling.nodeName == 'A') { + if (isValidAnchor(rte)) { rte.exec('unlink'); } else { rte.insertHTML(`${rte.selection()}`); @@ -78,7 +97,9 @@ export default class RichTextEditor { ...{ actionbar: 'actionbar', button: 'action', - active: 'active' + active: 'active', + disabled: 'disabled', + inactive: 'inactive' }, ...settings.classes }; @@ -114,16 +135,36 @@ export default class RichTextEditor { this.getActions().forEach(action => { const btn = action.btn; const update = action.update; - const active = this.classes.active; + const { active, inactive, disabled } = { ...this.classes }; + const state = action.state; const name = action.name; const doc = this.doc; btn.className = btn.className.replace(active, '').trim(); - - // doc.queryCommandValue(name) != 'false' - if (doc.queryCommandSupported(name) && doc.queryCommandState(name)) { - btn.className += ` ${active}`; + btn.className = btn.className.replace(inactive, '').trim(); + btn.className = btn.className.replace(disabled, '').trim(); + + console.log(this); + + // if there is a state function, which depicts the state, + // i.e. `active`, `disabled`, then call it + if (state) { + switch (state(this, doc)) { + case btnState.ACTIVE: + btn.className += ` ${active}`; + break; + case btnState.INACTIVE: + btn.className += ` ${inactive}`; + break; + case btnState.DISABLED: + btn.className += ` ${disabled}`; + break; + } + } else { + // otherwise default to checking if the name command is supported & enabled + if (doc.queryCommandSupported(name) && doc.queryCommandState(name)) { + btn.className += ` ${active}`; + } } - update && update(this, action); }); } @@ -156,11 +197,13 @@ export default class RichTextEditor { */ syncActions() { this.getActions().forEach(action => { - const event = action.event || 'click'; - action.btn[`on${event}`] = e => { - action.result(this, action); - this.updateActiveActions(); - }; + if (!action.state || (action.state && action.state() >= 0)) { + const event = action.event || 'click'; + action.btn[`on${event}`] = e => { + action.result(this, action); + this.updateActiveActions(); + }; + } }); } diff --git a/src/styles/scss/_gjs_rte.scss b/src/styles/scss/_gjs_rte.scss index eefd4c183..6654e263a 100644 --- a/src/styles/scss/_gjs_rte.scss +++ b/src/styles/scss/_gjs_rte.scss @@ -33,6 +33,13 @@ } &active { - background-color: $mainDkColor; + background-color: $mainLhColor; + } + &disabled { + color: $mainLhColor; + cursor: not-allowed; + &:hover { + background-color: unset; + } } } From 5ddd3dadbb6f24e2fc0fcc3e949a45bfa9605577 Mon Sep 17 00:00:00 2001 From: adriangroch Date: Tue, 26 Nov 2019 14:23:11 +1100 Subject: [PATCH 2/5] reverted index.js linting --- src/rich_text_editor/index.js | 627 ++++++++++++++++------------------ 1 file changed, 299 insertions(+), 328 deletions(-) diff --git a/src/rich_text_editor/index.js b/src/rich_text_editor/index.js index a23efe3d2..68a44d833 100644 --- a/src/rich_text_editor/index.js +++ b/src/rich_text_editor/index.js @@ -5,331 +5,302 @@ * You can customize the initial state of the module from the editor initialization, by passing the following [Configuration Object](https://github.com/artf/grapesjs/blob/master/src/rich_text_editor/config/config.js) * ```js * const editor = grapesjs.init({ - * richTextEditor: { - * // options - * } - * }) - * ``` - * - * Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance - * - * ```js - * const rte = editor.RichTextEditor; - * ``` - * - * * [add](#add) - * * [get](#get) - * * [getAll](#getall) - * * [remove](#remove) - * * [getToolbarEl](#gettoolbarel) - * - * @module RichTextEditor - */ - - import RichTextEditor from './model/RichTextEditor'; - import { on, off } from 'utils/mixins'; - import defaults from './config/config'; - - export default () => { - let config = {}; - let toolbar, actions, lastEl, globalRte; - - const hideToolbar = () => { - const style = toolbar.style; - const size = '-1000px'; - style.top = size; - style.left = size; - style.display = 'none'; - }; - - return { - customRte: null, - - /** - * Name of the module - * @type {String} - * @private - */ - name: 'RichTextEditor', - - getConfig() { - return config; - }, - - /** - * Initialize module. Automatically called with a new instance of the editor - * @param {Object} opts Options - * @private - */ - init(opts = {}) { - config = { - ...defaults, - ...opts - }; - const ppfx = config.pStylePrefix; - - if (ppfx) { - config.stylePrefix = ppfx + config.stylePrefix; - } - - this.pfx = config.stylePrefix; - actions = config.actions || []; - toolbar = document.createElement('div'); - toolbar.className = `${ppfx}rte-toolbar ${ppfx}one-bg`; - globalRte = this.initRte(document.createElement('div')); - - //Avoid closing on toolbar clicking - on(toolbar, 'mousedown', e => e.stopPropagation()); - return this; - }, - - /** - * Post render callback - * @param {View} ev - * @private - */ - postRender(ev) { - const canvas = ev.model.get('Canvas'); - toolbar.style.pointerEvents = 'all'; - hideToolbar(); - canvas.getToolsEl().appendChild(toolbar); - }, - - /** - * Init the built-in RTE - * @param {HTMLElement} el - * @return {RichTextEditor} - * @private - */ - initRte(el) { - const pfx = this.pfx; - const actionbarContainer = toolbar; - const actionbar = this.actionbar; - const actions = this.actions || config.actions; - const classes = { - actionbar: `${pfx}actionbar`, - button: `${pfx}action`, - active: `${pfx}active`, - inactive: `${pfx}inactive`, - disabled: `${pfx}disabled` - }; - const rte = new RichTextEditor({ - el, - classes, - actions, - actionbar, - actionbarContainer - }); - globalRte && globalRte.setEl(el); - - if (rte.actionbar) { - this.actionbar = rte.actionbar; - } - - if (rte.actions) { - this.actions = rte.actions; - } - - return rte; - }, - - /** - * Add a new action to the built-in RTE toolbar - * @param {string} name Action name - * @param {Object} action Action options - * @example - * rte.add('bold', { - * icon: 'B', - * attributes: {title: 'Bold'}, - * result: rte => rte.exec('bold') - * }); - * rte.add('link', { - * icon: document.getElementById('t'), - * attributes: {title: 'Link',} - * // Example on it's easy to wrap a selected content - * result: rte => rte.insertHTML(`${rte.selection()}`) - * }); - * // An example with fontSize - * rte.add('fontSize', { - * icon: ``, - * // Bind the 'result' on 'change' listener - * event: 'change', - * result: (rte, action) => rte.exec('fontSize', action.btn.firstChild.value), - * // Callback on any input change (mousedown, keydown, etc..) - * update: (rte, action) => { - * const value = rte.doc.queryCommandValue(action.name); - * if (value != 'false') { // value is a string - * action.btn.firstChild.value = value; - * } - * } - * }) - * // An example with state - * const isValidAnchor = (rte) => { - * // a utility function to help determine if the selected is a valid anchor node - * const anchor = rte.selection().anchorNode; - * const parentNode = anchor && anchor.parentNode; - * const nextSibling = anchor && anchor.nextSibling; - * return (parentNode && parentNode.nodeName == 'A') || (nextSibling && nextSibling.nodeName == 'A') - * } - * rte.add('toggleAnchor', { - * icon: ``, - * state: (rte, doc) => { - * if (rte && rte.selection()) { - * // `btnState` is a integer, -1 for disabled, 0 for inactive, 1 for active - * return isValidAnchor(rte) ? btnState.ACTIVE : btnState.INACTIVE; - * } else { - * return btnState.INACTIVE; - * } - * }, - * result: (rte, action) => { - * if (isValidAnchor(rte)) { - * rte.exec('unlink'); - * } else { - * rte.insertHTML(`${rte.selection()}`); - * } - * } - * }) - */ - add(name, action = {}) { - action.name = name; - globalRte.addAction(action, { sync: 1 }); - }, - - /** - * Get the action by its name - * @param {string} name Action name - * @return {Object} - * @example - * const action = rte.get('bold'); - * // {name: 'bold', ...} - */ - get(name) { - let result; - globalRte.getActions().forEach(action => { - if (action.name == name) { - result = action; - } - }); - return result; - }, - - /** - * Get all actions - * @return {Array} - */ - getAll() { - return globalRte.getActions(); - }, - - /** - * Remove the action from the toolbar - * @param {string} name - * @return {Object} Removed action - * @example - * const action = rte.remove('bold'); - * // {name: 'bold', ...} - */ - remove(name) { - const actions = this.getAll(); - const action = this.get(name); - - if (action) { - const btn = action.btn; - const index = actions.indexOf(action); - btn.parentNode.removeChild(btn); - actions.splice(index, 1); - } - - return action; - }, - - /** - * Get the toolbar element - * @return {HTMLElement} - */ - getToolbarEl() { - return toolbar; - }, - - /** - * Triggered when the offset of the editor is changed - * @private - */ - updatePosition() { - const un = 'px'; - const canvas = config.em.get('Canvas'); - const pos = canvas.getTargetToElementDim(toolbar, lastEl, { - event: 'rteToolbarPosUpdate' - }); - - if (pos) { - if (config.adjustToolbar) { - const frameOffset = canvas.getCanvasView().getFrameOffset(); - // Move the toolbar down when the top canvas edge is reached - if ( - pos.top <= pos.canvasTop && - !(pos.elementHeight + pos.targetHeight >= frameOffset.height) - ) { - pos.top = pos.elementTop + pos.elementHeight; - } - } - - const toolbarStyle = toolbar.style; - toolbarStyle.top = pos.top + un; - toolbarStyle.left = pos.left + un; - } - }, - - /** - * Enable rich text editor on the element - * @param {View} view Component view - * @param {Object} rte The instance of already defined RTE - * @private - * */ - enable(view, rte) { - lastEl = view.el; - const em = config.em; - const el = view.getChildrenContainer(); - const customRte = this.customRte; - - toolbar.style.display = ''; - rte = customRte ? customRte.enable(el, rte) : this.initRte(el).enable(); - - if (em) { - setTimeout(this.updatePosition.bind(this), 0); - const event = 'change:canvasOffset canvasScroll'; - em.off(event, this.updatePosition, this); - em.on(event, this.updatePosition, this); - em.trigger('rte:enable', view, rte); - } - - return rte; - }, - - /** - * Unbind rich text editor from the element - * @param {View} view - * @param {Object} rte The instance of already defined RTE - * @private - * */ - disable(view, rte) { - const em = config.em; - const customRte = this.customRte; - var el = view.getChildrenContainer(); - - if (customRte) { - customRte.disable(el, rte); - } else { - rte && rte.disable(); - } - - hideToolbar(); - em && em.trigger('rte:disable', view, rte); - } - }; - }; - \ No newline at end of file + * richTextEditor: { + * // options + * } + * }) + * ``` + * + * Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance + * + * ```js + * const rte = editor.RichTextEditor; + * ``` + * + * * [add](#add) + * * [get](#get) + * * [getAll](#getall) + * * [remove](#remove) + * * [getToolbarEl](#gettoolbarel) + * + * @module RichTextEditor + */ + +import RichTextEditor from './model/RichTextEditor'; +import { on, off } from 'utils/mixins'; +import defaults from './config/config'; + +export default () => { + let config = {}; + let toolbar, actions, lastEl, globalRte; + + const hideToolbar = () => { + const style = toolbar.style; + const size = '-1000px'; + style.top = size; + style.left = size; + style.display = 'none'; + }; + + return { + customRte: null, + + /** + * Name of the module + * @type {String} + * @private + */ + name: 'RichTextEditor', + + getConfig() { + return config; + }, + + /** + * Initialize module. Automatically called with a new instance of the editor + * @param {Object} opts Options + * @private + */ + init(opts = {}) { + config = { + ...defaults, + ...opts + }; + const ppfx = config.pStylePrefix; + + if (ppfx) { + config.stylePrefix = ppfx + config.stylePrefix; + } + + this.pfx = config.stylePrefix; + actions = config.actions || []; + toolbar = document.createElement('div'); + toolbar.className = `${ppfx}rte-toolbar ${ppfx}one-bg`; + globalRte = this.initRte(document.createElement('div')); + + //Avoid closing on toolbar clicking + on(toolbar, 'mousedown', e => e.stopPropagation()); + return this; + }, + + /** + * Post render callback + * @param {View} ev + * @private + */ + postRender(ev) { + const canvas = ev.model.get('Canvas'); + toolbar.style.pointerEvents = 'all'; + hideToolbar(); + canvas.getToolsEl().appendChild(toolbar); + }, + + /** + * Init the built-in RTE + * @param {HTMLElement} el + * @return {RichTextEditor} + * @private + */ + initRte(el) { + const pfx = this.pfx; + const actionbarContainer = toolbar; + const actionbar = this.actionbar; + const actions = this.actions || config.actions; + const classes = { + actionbar: `${pfx}actionbar`, + button: `${pfx}action`, + active: `${pfx}active` + }; + const rte = new RichTextEditor({ + el, + classes, + actions, + actionbar, + actionbarContainer + }); + globalRte && globalRte.setEl(el); + + if (rte.actionbar) { + this.actionbar = rte.actionbar; + } + + if (rte.actions) { + this.actions = rte.actions; + } + + return rte; + }, + + /** + * Add a new action to the built-in RTE toolbar + * @param {string} name Action name + * @param {Object} action Action options + * @example + * rte.add('bold', { + * icon: 'B', + * attributes: {title: 'Bold'}, + * result: rte => rte.exec('bold') + * }); + * rte.add('link', { + * icon: document.getElementById('t'), + * attributes: {title: 'Link',} + * // Example on it's easy to wrap a selected content + * result: rte => rte.insertHTML(`${rte.selection()}`) + * }); + * // An example with fontSize + * rte.add('fontSize', { + * icon: ``, + * // Bind the 'result' on 'change' listener + * event: 'change', + * result: (rte, action) => rte.exec('fontSize', action.btn.firstChild.value), + * // Callback on any input change (mousedown, keydown, etc..) + * update: (rte, action) => { + * const value = rte.doc.queryCommandValue(action.name); + * if (value != 'false') { // value is a string + * action.btn.firstChild.value = value; + * } + * } + * }) + */ + add(name, action = {}) { + action.name = name; + globalRte.addAction(action, { sync: 1 }); + }, + + /** + * Get the action by its name + * @param {string} name Action name + * @return {Object} + * @example + * const action = rte.get('bold'); + * // {name: 'bold', ...} + */ + get(name) { + let result; + globalRte.getActions().forEach(action => { + if (action.name == name) { + result = action; + } + }); + return result; + }, + + /** + * Get all actions + * @return {Array} + */ + getAll() { + return globalRte.getActions(); + }, + + /** + * Remove the action from the toolbar + * @param {string} name + * @return {Object} Removed action + * @example + * const action = rte.remove('bold'); + * // {name: 'bold', ...} + */ + remove(name) { + const actions = this.getAll(); + const action = this.get(name); + + if (action) { + const btn = action.btn; + const index = actions.indexOf(action); + btn.parentNode.removeChild(btn); + actions.splice(index, 1); + } + + return action; + }, + + /** + * Get the toolbar element + * @return {HTMLElement} + */ + getToolbarEl() { + return toolbar; + }, + + /** + * Triggered when the offset of the editor is changed + * @private + */ + updatePosition() { + const un = 'px'; + const canvas = config.em.get('Canvas'); + const pos = canvas.getTargetToElementDim(toolbar, lastEl, { + event: 'rteToolbarPosUpdate' + }); + + if (pos) { + if (config.adjustToolbar) { + const frameOffset = canvas.getCanvasView().getFrameOffset(); + // Move the toolbar down when the top canvas edge is reached + if ( + pos.top <= pos.canvasTop && + !(pos.elementHeight + pos.targetHeight >= frameOffset.height) + ) { + pos.top = pos.elementTop + pos.elementHeight; + } + } + + const toolbarStyle = toolbar.style; + toolbarStyle.top = pos.top + un; + toolbarStyle.left = pos.left + un; + } + }, + + /** + * Enable rich text editor on the element + * @param {View} view Component view + * @param {Object} rte The instance of already defined RTE + * @private + * */ + enable(view, rte) { + lastEl = view.el; + const em = config.em; + const el = view.getChildrenContainer(); + const customRte = this.customRte; + + toolbar.style.display = ''; + rte = customRte ? customRte.enable(el, rte) : this.initRte(el).enable(); + + if (em) { + setTimeout(this.updatePosition.bind(this), 0); + const event = 'change:canvasOffset canvasScroll'; + em.off(event, this.updatePosition, this); + em.on(event, this.updatePosition, this); + em.trigger('rte:enable', view, rte); + } + + return rte; + }, + + /** + * Unbind rich text editor from the element + * @param {View} view + * @param {Object} rte The instance of already defined RTE + * @private + * */ + disable(view, rte) { + const em = config.em; + const customRte = this.customRte; + var el = view.getChildrenContainer(); + + if (customRte) { + customRte.disable(el, rte); + } else { + rte && rte.disable(); + } + + hideToolbar(); + em && em.trigger('rte:disable', view, rte); + } + }; +}; From 5f4491173ef1936d0e487c4680a3b5f6b1a575b6 Mon Sep 17 00:00:00 2001 From: adriangroch Date: Tue, 26 Nov 2019 14:27:48 +1100 Subject: [PATCH 3/5] Cleanup --- src/rich_text_editor/index.js | 30 +++++++++++++++++++- src/rich_text_editor/model/RichTextEditor.js | 2 -- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/rich_text_editor/index.js b/src/rich_text_editor/index.js index 68a44d833..42935d8b3 100644 --- a/src/rich_text_editor/index.js +++ b/src/rich_text_editor/index.js @@ -109,7 +109,9 @@ export default () => { const classes = { actionbar: `${pfx}actionbar`, button: `${pfx}action`, - active: `${pfx}active` + active: `${pfx}active`, + inactive: `${pfx}inactive`, + disabled: `${pfx}disabled` }; const rte = new RichTextEditor({ el, @@ -165,6 +167,32 @@ export default () => { * } * } * }) + * // An example with state + * const isValidAnchor = (rte) => { + * // a utility function to help determine if the selected is a valid anchor node + * const anchor = rte.selection().anchorNode; + * const parentNode = anchor && anchor.parentNode; + * const nextSibling = anchor && anchor.nextSibling; + * return (parentNode && parentNode.nodeName == 'A') || (nextSibling && nextSibling.nodeName == 'A') + * } + * rte.add('toggleAnchor', { + * icon: ``, + * state: (rte, doc) => { + * if (rte && rte.selection()) { + * // `btnState` is a integer, -1 for disabled, 0 for inactive, 1 for active + * return isValidAnchor(rte) ? btnState.ACTIVE : btnState.INACTIVE; + * } else { + * return btnState.INACTIVE; + * } + * }, + * result: (rte, action) => { + * if (isValidAnchor(rte)) { + * rte.exec('unlink'); + * } else { + * rte.insertHTML(`${rte.selection()}`); + * } + * } + * }) */ add(name, action = {}) { action.name = name; diff --git a/src/rich_text_editor/model/RichTextEditor.js b/src/rich_text_editor/model/RichTextEditor.js index b7b44da42..e6a63d96c 100644 --- a/src/rich_text_editor/model/RichTextEditor.js +++ b/src/rich_text_editor/model/RichTextEditor.js @@ -143,8 +143,6 @@ export default class RichTextEditor { btn.className = btn.className.replace(inactive, '').trim(); btn.className = btn.className.replace(disabled, '').trim(); - console.log(this); - // if there is a state function, which depicts the state, // i.e. `active`, `disabled`, then call it if (state) { From 4870282bc7ade2f90f3290102629a5eb47b5aa4d Mon Sep 17 00:00:00 2001 From: adriangroch Date: Tue, 10 Dec 2019 11:38:47 +1100 Subject: [PATCH 4/5] Request PR change --- src/rich_text_editor/model/RichTextEditor.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/rich_text_editor/model/RichTextEditor.js b/src/rich_text_editor/model/RichTextEditor.js index e6a63d96c..3fb22e043 100644 --- a/src/rich_text_editor/model/RichTextEditor.js +++ b/src/rich_text_editor/model/RichTextEditor.js @@ -195,7 +195,10 @@ export default class RichTextEditor { */ syncActions() { this.getActions().forEach(action => { - if (!action.state || (action.state && action.state() >= 0)) { + if ( + !action.state || + (action.state && action.state(this, this.doc) >= 0) + ) { const event = action.event || 'click'; action.btn[`on${event}`] = e => { action.result(this, action); From 93b3067a7430a7aa75072a15958d09d6da13c808 Mon Sep 17 00:00:00 2001 From: adriangroch Date: Tue, 10 Dec 2019 12:13:15 +1100 Subject: [PATCH 5/5] Added check to ensure acitonBar existed --- src/rich_text_editor/model/RichTextEditor.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/rich_text_editor/model/RichTextEditor.js b/src/rich_text_editor/model/RichTextEditor.js index 3fb22e043..597a3f787 100644 --- a/src/rich_text_editor/model/RichTextEditor.js +++ b/src/rich_text_editor/model/RichTextEditor.js @@ -195,15 +195,17 @@ export default class RichTextEditor { */ syncActions() { this.getActions().forEach(action => { - if ( - !action.state || - (action.state && action.state(this, this.doc) >= 0) - ) { - const event = action.event || 'click'; - action.btn[`on${event}`] = e => { - action.result(this, action); - this.updateActiveActions(); - }; + if (this.settings.actionbar) { + if ( + !action.state || + (action.state && action.state(this, this.doc) >= 0) + ) { + const event = action.event || 'click'; + action.btn[`on${event}`] = e => { + action.result(this, action); + this.updateActiveActions(); + }; + } } }); }