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;