diff --git a/src/rich_text_editor/index.js b/src/rich_text_editor/index.js deleted file mode 100644 index 1f54bf8f7..000000000 --- a/src/rich_text_editor/index.js +++ /dev/null @@ -1,350 +0,0 @@ -/** - * This module allows to customize the built-in toolbar of the Rich Text Editor and use commands from the [HTML Editing APIs](https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand). - * It's highly recommended to keep this toolbar as small as possible, especially from styling commands (eg. 'fontSize') and leave this task to the Style Manager - * - * 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 and listen to its events. Before using these methods, you should get the module from the instance. - * - * ```js - * // Listen to events - * editor.on('rte:enable', () => { ... }); - * - * // Use the API - * const rte = editor.RichTextEditor; - * rte.add(...); - * ``` - * - * ## Available Events - * * `rte:enable` - RTE enabled. The view, on which RTE is enabled, is passed as an argument - * * `rte:disable` - RTE disabled. The view, on which RTE is disabled, is passed as an argument - * - * ## Methods - * * [add](#add) - * * [get](#get) - * * [getAll](#getall) - * * [remove](#remove) - * * [getToolbarEl](#gettoolbarel) - * - * @module RichTextEditor - */ - -import { on, hasWin } from '../utils/mixins'; -import RichTextEditor from './model/RichTextEditor'; -import defaults from './config/config'; - -const eventsUp = 'change:canvasOffset frame:scroll component:update'; - -export default () => { - let toolbar; - - 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 this.config; - }, - - /** - * Initialize module. Automatically called with a new instance of the editor - * @param {Object} opts Options - * @private - */ - init(opts = {}) { - const config = { ...defaults, ...opts }; - const ppfx = config.pStylePrefix; - - if (ppfx) { - config.stylePrefix = ppfx + config.stylePrefix; - } - - this.config = config; - this.pfx = config.stylePrefix; - this.em = config.em; - this.actions = config.actions || []; - if (!hasWin()) return this; - toolbar = document.createElement('div'); - toolbar.className = `${ppfx}rte-toolbar ${ppfx}one-bg`; - this.initRte(document.createElement('div')); - - //Avoid closing on toolbar clicking - on(toolbar, 'mousedown', e => e.stopPropagation()); - return this; - }, - - destroy() { - this.globalRte?.destroy(); - this.customRte?.destroy?.(); - toolbar = 0; - ['actionbar', 'actions', 'em', 'config', 'globalRte', 'lastEl'].map(i => { - delete this[i]; - }); - }, - - /** - * 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) { - let { globalRte } = this; - const { em, pfx, actionbar, config } = this; - const actionbarContainer = toolbar; - const actions = this.actions || [...config.actions]; - const classes = { - actionbar: `${pfx}actionbar`, - button: `${pfx}action`, - active: `${pfx}active`, - inactive: `${pfx}inactive`, - disabled: `${pfx}disabled`, - }; - - if (!globalRte) { - globalRte = new RichTextEditor({ - em, - el, - classes, - actions, - actionbar, - actionbarContainer, - }); - this.globalRte = globalRte; - } else { - globalRte.em = em; - globalRte.setEl(el); - } - - if (globalRte.actionbar) { - this.actionbar = globalRte.actionbar; - } - - if (globalRte.actions) { - this.actions = globalRte.actions; - } - - return globalRte; - }, - - /** - * 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; - this.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; - this.globalRte?.getActions().forEach(action => { - if (action.name == name) { - result = action; - } - }); - return result; - }, - - /** - * Get all actions - * @return {Array} - */ - getAll() { - return this.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 { em } = this; - const un = 'px'; - const canvas = em.get('Canvas'); - const { style } = toolbar; - const pos = canvas.getTargetToElementFixed(this.lastEl, toolbar, { - event: 'rteToolbarPosUpdate', - left: 0, - }); - style.top = (pos.top || 0) + un; - style.left = (pos.left || 0) + un; - }, - - /** - * Enable rich text editor on the element - * @param {View} view Component view - * @param {Object} rte The instance of already defined RTE - * @private - * */ - async enable(view, rte, opts) { - this.lastEl = view.el; - const { customRte, em } = this; - const el = view.getChildrenContainer(); - - toolbar.style.display = ''; - const rteInst = await (customRte ? customRte.enable(el, rte) : this.initRte(el).enable(opts)); - - if (em) { - setTimeout(this.updatePosition.bind(this), 0); - em.off(eventsUp, this.updatePosition, this); - em.on(eventsUp, this.updatePosition, this); - em.trigger('rte:enable', view, rteInst); - } - - return rteInst; - }, - - /** - * 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 } = this; - const customRte = this.customRte; - var el = view.getChildrenContainer(); - - if (customRte) { - customRte.disable(el, rte); - } else { - rte && rte.disable(); - } - - hideToolbar(); - if (em) { - em.off(eventsUp, this.updatePosition, this); - em.trigger('rte:disable', view, rte); - } - }, - }; -}; diff --git a/src/rich_text_editor/index.ts b/src/rich_text_editor/index.ts new file mode 100644 index 000000000..d365c7a27 --- /dev/null +++ b/src/rich_text_editor/index.ts @@ -0,0 +1,345 @@ +/** + * This module allows to customize the built-in toolbar of the Rich Text Editor and use commands from the [HTML Editing APIs](https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand). + * It's highly recommended to keep this toolbar as small as possible, especially from styling commands (eg. 'fontSize') and leave this task to the Style Manager + * + * 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.ts) + * ```js + * const editor = grapesjs.init({ + * richTextEditor: { + * // options + * } + * }) + * ``` + * + * Once the editor is instantiated you can use its API and listen to its events. Before using these methods, you should get the module from the instance. + * + * ```js + * // Listen to events + * editor.on('rte:enable', () => { ... }); + * + * // Use the API + * const rte = editor.RichTextEditor; + * rte.add(...); + * ``` + * + * ## Available Events + * * `rte:enable` - RTE enabled. The view, on which RTE is enabled, is passed as an argument + * * `rte:disable` - RTE disabled. The view, on which RTE is disabled, is passed as an argument + * + * ## Methods + * * [add](#add) + * * [get](#get) + * * [getAll](#getall) + * * [remove](#remove) + * * [getToolbarEl](#gettoolbarel) + * + * @module RichTextEditor + */ + +import { on, hasWin } from '../utils/mixins'; +import RichTextEditor, { RichTextEditorAction } from './model/RichTextEditor'; +import defaults, { RichTextEditorConfig } from './config/config'; +import { Module } from '../abstract'; +import EditorModel from '../editor/model/Editor'; +import { removeEl } from '../utils/dom'; +import ComponentView from '../dom_components/view/ComponentView'; + +const eventsUp = 'change:canvasOffset frame:scroll component:update'; + +export interface CustomRTE { + enable: (el: HTMLElement, rte: T) => T; + disable: (el: HTMLElement, rte: T) => T; + destroy?: () => void; +} + +export default class RichTextEditorModule extends Module { + pfx: string; + toolbar!: HTMLElement; + globalRte?: RichTextEditor; + actionbar?: HTMLElement; + lastEl?: HTMLElement; + actions?: (RichTextEditorAction | string)[]; + customRte?: CustomRTE; + + getConfig() { + return this.config; + } + + constructor(em: EditorModel) { + super(em, 'RichTextEditor', defaults); + const { config } = this; + const ppfx = config.pStylePrefix; + // if (ppfx) { + // config.stylePrefix = ppfx + config.stylePrefix; + // } + + this.pfx = config.stylePrefix!; + this.actions = config.actions || []; + if (!hasWin()) return this; + const toolbar = document.createElement('div'); + toolbar.className = `${ppfx}rte-toolbar ${ppfx}one-bg`; + this.toolbar = toolbar; + this.initRte(document.createElement('div')); + + //Avoid closing on toolbar clicking + on(toolbar, 'mousedown', e => e.stopPropagation()); + } + + destroy() { + this.globalRte?.destroy(); + this.customRte?.destroy?.(); + removeEl(this.toolbar); + } + + /** + * Post render callback + * @param {View} ev + * @private + */ + postRender(ev: any) { + const canvas = ev.model.get('Canvas'); + this.toolbar.style.pointerEvents = 'all'; + this.hideToolbar(); + canvas.getToolsEl().appendChild(this.toolbar); + } + + /** + * Init the built-in RTE + * @param {HTMLElement} el + * @return {RichTextEditor} + * @private + */ + initRte(el: HTMLElement) { + let { globalRte } = this; + const { em, pfx, actionbar, config } = this; + const actions = this.actions || [...config.actions!]; + const classes = { + actionbar: `${pfx}actionbar`, + button: `${pfx}action`, + active: `${pfx}active`, + inactive: `${pfx}inactive`, + disabled: `${pfx}disabled`, + }; + + if (!globalRte) { + globalRte = new RichTextEditor(em, el, { + classes, + actions, + actionbar, + actionbarContainer: this.toolbar, + }); + this.globalRte = globalRte; + } else { + globalRte.em = em; + globalRte.setEl(el); + } + + if (globalRte.actionbar) { + this.actionbar = globalRte.actionbar; + } + + if (globalRte.actions) { + this.actions = globalRte.actions; + } + + return globalRte; + } + + /** + * 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: string, action: Partial = {}) { + action.name = name; + this.globalRte?.addAction(action as RichTextEditorAction, { sync: true }); + } + + /** + * Get the action by its name + * @param {string} name Action name + * @return {Object} + * @example + * const action = rte.get('bold'); + * // {name: 'bold', ...} + */ + get(name: string): RichTextEditorAction | undefined { + let result; + this.globalRte?.getActions().forEach(action => { + if (action.name == name) { + result = action; + } + }); + return result; + } + + /** + * Get all actions + * @return {Array} + */ + getAll() { + return this.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: string) { + 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 { em } = this; + const un = 'px'; + const canvas = em.get('Canvas'); + const { style } = this.toolbar; + const pos = canvas.getTargetToElementFixed(this.lastEl, toolbar, { + event: 'rteToolbarPosUpdate', + left: 0, + }); + style.top = (pos.top || 0) + un; + style.left = (pos.left || 0) + un; + } + + /** + * Enable rich text editor on the element + * @param {View} view Component view + * @param {Object} rte The instance of already defined RTE + * @private + * */ + async enable(view: ComponentView, rte: RichTextEditor, opts: any = {}) { + this.lastEl = view.el; + const { customRte, em } = this; + // @ts-ignore + const el = view.getChildrenContainer(); + + this.toolbar.style.display = ''; + const rteInst = await (customRte ? customRte.enable(el, rte) : this.initRte(el).enable(opts)); + + if (em) { + setTimeout(this.updatePosition.bind(this), 0); + em.off(eventsUp, this.updatePosition, this); + em.on(eventsUp, this.updatePosition, this); + em.trigger('rte:enable', view, rteInst); + } + + return rteInst; + } + + hideToolbar() { + const style = this.toolbar.style; + const size = '-1000px'; + style.top = size; + style.left = size; + style.display = 'none'; + } + + /** + * Unbind rich text editor from the element + * @param {View} view + * @param {Object} rte The instance of already defined RTE + * @private + * */ + disable(view: ComponentView, rte: RichTextEditor) { + const { em } = this; + const customRte = this.customRte; + // @ts-ignore + const el = view.getChildrenContainer(); + + if (customRte) { + customRte.disable(el, rte); + } else { + rte && rte.disable(); + } + + this.hideToolbar(); + + if (em) { + em.off(eventsUp, this.updatePosition, this); + em.trigger('rte:disable', view, rte); + } + } +} diff --git a/src/rich_text_editor/model/RichTextEditor.ts b/src/rich_text_editor/model/RichTextEditor.ts index 150c17638..70543ac11 100644 --- a/src/rich_text_editor/model/RichTextEditor.ts +++ b/src/rich_text_editor/model/RichTextEditor.ts @@ -5,7 +5,7 @@ import { isString } from 'underscore'; import EditorModel from '../../editor/model/Editor'; import { on, off, getPointerEvent, getModel } from '../../utils/mixins'; -interface RichTextEditorAction { +export interface RichTextEditorAction { name: string; icon: string; event?: string; @@ -16,6 +16,18 @@ interface RichTextEditorAction { btn?: HTMLElement; } +export interface RichTextEditorOptions { + actions?: (RichTextEditorAction | string)[]; + classes?: Record; + actionbar?: HTMLElement; + actionbarContainer?: HTMLElement; + styleWithCSS?: boolean; +} + +type EffectOptions = { + event?: Event; +}; + const RTE_KEY = '_rte'; const btnState = { @@ -97,18 +109,6 @@ const defActions: Record = { }, }; -export interface RichTextEditorOptions { - actions?: (RichTextEditorAction | string)[]; - classes?: Record; - actionbar?: HTMLElement; - actionbarContainer?: HTMLElement; - styleWithCSS?: boolean; -} - -type EffectOptions = { - event?: Event; -}; - export default class RichTextEditor { em: EditorModel; settings: RichTextEditorOptions; @@ -119,12 +119,12 @@ export default class RichTextEditor { doc!: Document; enabled?: boolean; - constructor(em: EditorModel, el: HTMLElement & { _rte: RichTextEditor }, settings: RichTextEditorOptions = {}) { + constructor(em: EditorModel, el: HTMLElement & { _rte?: RichTextEditor }, settings: RichTextEditorOptions = {}) { this.em = em; this.settings = settings; if (el[RTE_KEY]) { - return el[RTE_KEY]; + return el[RTE_KEY]!; } el[RTE_KEY] = this;