Browse Source

Move RTE module to TS

pull/4746/head
Artur Arseniev 4 years ago
parent
commit
ecd0b40d61
  1. 350
      src/rich_text_editor/index.js
  2. 345
      src/rich_text_editor/index.ts
  3. 30
      src/rich_text_editor/model/RichTextEditor.ts

350
src/rich_text_editor/index.js

@ -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>B</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(`<a href="#">${rte.selection()}</a>`)
* });
* // An example with fontSize
* rte.add('fontSize', {
* icon: `<select class="gjs-field">
* <option>1</option>
* <option>4</option>
* <option>7</option>
* </select>`,
* // 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: `<span style="transform:rotate(45deg)">&supdsub;</span>`,
* 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(`<a class="link" href="">${rte.selection()}</a>`);
* }
* }
* })
*/
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);
}
},
};
};

345
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<T = any> {
enable: (el: HTMLElement, rte: T) => T;
disable: (el: HTMLElement, rte: T) => T;
destroy?: () => void;
}
export default class RichTextEditorModule extends Module<RichTextEditorConfig & { pStylePrefix?: string }> {
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>B</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(`<a href="#">${rte.selection()}</a>`)
* });
* // An example with fontSize
* rte.add('fontSize', {
* icon: `<select class="gjs-field">
* <option>1</option>
* <option>4</option>
* <option>7</option>
* </select>`,
* // 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: `<span style="transform:rotate(45deg)">&supdsub;</span>`,
* 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(`<a class="link" href="">${rte.selection()}</a>`);
* }
* }
* })
*/
add(name: string, action: Partial<RichTextEditorAction> = {}) {
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);
}
}
}

30
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<string, string>;
actionbar?: HTMLElement;
actionbarContainer?: HTMLElement;
styleWithCSS?: boolean;
}
type EffectOptions = {
event?: Event;
};
const RTE_KEY = '_rte';
const btnState = {
@ -97,18 +109,6 @@ const defActions: Record<string, RichTextEditorAction> = {
},
};
export interface RichTextEditorOptions {
actions?: (RichTextEditorAction | string)[];
classes?: Record<string, string>;
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;

Loading…
Cancel
Save