diff --git a/src/rich_text_editor/model/RichTextEditor.js b/src/rich_text_editor/model/RichTextEditor.ts similarity index 78% rename from src/rich_text_editor/model/RichTextEditor.js rename to src/rich_text_editor/model/RichTextEditor.ts index 47d660bab..150c17638 100644 --- a/src/rich_text_editor/model/RichTextEditor.js +++ b/src/rich_text_editor/model/RichTextEditor.ts @@ -2,8 +2,20 @@ // and adapted to the GrapesJS's need import { isString } from 'underscore'; +import EditorModel from '../../editor/model/Editor'; import { on, off, getPointerEvent, getModel } from '../../utils/mixins'; +interface RichTextEditorAction { + name: string; + icon: string; + event?: string; + attributes?: Record; + result: (rte: RichTextEditor, action: RichTextEditorAction) => void; + update?: (rte: RichTextEditor, action: RichTextEditorAction) => number; + state?: (rte: RichTextEditor, doc: Document) => number; + btn?: HTMLElement; +} + const RTE_KEY = '_rte'; const btnState = { @@ -11,8 +23,8 @@ const btnState = { INACTIVE: 0, DISABLED: -1, }; -const isValidTag = (rte, tagName = 'A') => { - const { anchorNode, focusNode } = rte.selection(); +const isValidTag = (rte: RichTextEditor, tagName = 'A') => { + const { anchorNode, focusNode } = rte.selection() || {}; const parentAnchor = anchorNode?.parentNode; const parentFocus = focusNode?.parentNode; return parentAnchor?.nodeName == tagName || parentFocus?.nodeName == tagName; @@ -20,7 +32,7 @@ const isValidTag = (rte, tagName = 'A') => { const customElAttr = 'data-selectme'; -const defActions = { +const defActions: Record = { bold: { name: 'bold', icon: 'B', @@ -85,10 +97,31 @@ const defActions = { }, }; +export interface RichTextEditorOptions { + actions?: (RichTextEditorAction | string)[]; + classes?: Record; + actionbar?: HTMLElement; + actionbarContainer?: HTMLElement; + styleWithCSS?: boolean; +} + +type EffectOptions = { + event?: Event; +}; + export default class RichTextEditor { - constructor(settings = {}) { - const { el, em } = settings; + em: EditorModel; + settings: RichTextEditorOptions; + classes!: Record; + actionbar!: HTMLElement; + actions!: RichTextEditorAction[]; + el!: HTMLElement; + doc!: Document; + enabled?: boolean; + + constructor(em: EditorModel, el: HTMLElement & { _rte: RichTextEditor }, settings: RichTextEditorOptions = {}) { this.em = em; + this.settings = settings; if (el[RTE_KEY]) { return el[RTE_KEY]; @@ -102,30 +135,27 @@ export default class RichTextEditor { const acts = (settings.actions || []).map(action => { let result = action; - if (typeof action === 'string') { + if (isString(action)) { result = { ...defActions[action] }; } else if (defActions[action.name]) { result = { ...defActions[action.name], ...action }; } - return result; + return result as RichTextEditorAction; }); const actions = acts.length ? acts : Object.keys(defActions).map(a => defActions[a]); settings.classes = { - ...{ - actionbar: 'actionbar', - button: 'action', - active: 'active', - disabled: 'disabled', - inactive: 'inactive', - }, + actionbar: 'actionbar', + button: 'action', + active: 'active', + disabled: 'disabled', + inactive: 'inactive', ...settings.classes, }; const classes = settings.classes; let actionbar = settings.actionbar; - this.actionbar = actionbar; - this.settings = settings; + this.actionbar = actionbar!; this.classes = classes; this.actions = actions; @@ -133,7 +163,7 @@ export default class RichTextEditor { const actionbarCont = settings.actionbarContainer; actionbar = document.createElement('div'); actionbar.className = classes.actionbar; - actionbarCont.appendChild(actionbar); + actionbarCont?.appendChild(actionbar); this.actionbar = actionbar; actions.forEach(action => this.addAction(action)); } @@ -143,24 +173,24 @@ export default class RichTextEditor { } destroy() { - this.el = 0; - this.doc = 0; - this.actionbar = 0; - this.settings = {}; - this.classes = {}; - this.actions = []; + // this.el = 0; + // this.doc = 0; + // this.actionbar = 0; + // this.settings = {}; + // this.classes = {}; + // this.actions = []; } - setEl(el) { + setEl(el: HTMLElement) { this.el = el; this.doc = el.ownerDocument; } updateActiveActions() { this.getActions().forEach(action => { - const btn = action.btn; - const update = action.update; - const { active, inactive, disabled } = { ...this.classes }; + const { update } = action; + const btn = action.btn!; + const { active, inactive, disabled } = this.classes; const state = action.state; const name = action.name; const doc = this.doc; @@ -188,11 +218,12 @@ export default class RichTextEditor { btn.className += ` ${active}`; } } - update && update(this, action); + + update?.(this, action); }); } - enable(opts) { + enable(opts: EffectOptions) { if (this.enabled) return this; return this.__toggleEffects(true, opts); } @@ -201,11 +232,11 @@ export default class RichTextEditor { return this.__toggleEffects(false); } - __toggleEffects(enable = false, opts = {}) { + __toggleEffects(enable = false, opts: EffectOptions = {}) { const method = enable ? on : off; const { el, doc } = this; this.actionbarEl().style.display = enable ? '' : 'none'; - el.contentEditable = !!enable; + el.contentEditable = `${!!enable}`; method(el, 'mouseup keyup', this.updateActiveActions); method(doc, 'keydown', this.__onKeydown); method(doc, 'paste', this.__onPaste); @@ -222,14 +253,16 @@ export default class RichTextEditor { if (doc.caretRangeFromPoint) { const poiner = getPointerEvent(event); range = doc.caretRangeFromPoint(poiner.clientX, poiner.clientY); + // @ts-ignore } else if (event.rangeParent) { range = doc.createRange(); + // @ts-ignore range.setStart(event.rangeParent, event.rangeOffset); } const sel = doc.getSelection(); - sel.removeAllRanges(); - range && sel.addRange(range); + sel?.removeAllRanges(); + range && sel?.addRange(range); } el.focus(); @@ -238,17 +271,19 @@ export default class RichTextEditor { return this; } - __onKeydown(event) { + __onKeydown(event: Event) { + const ev = event as KeyboardEvent; const { doc } = this; const cmdList = ['insertOrderedList', 'insertUnorderedList']; - if (event.key === 'Enter' && !cmdList.some(cmd => doc.queryCommandState(cmd))) { + if (ev.key === 'Enter' && !cmdList.some(cmd => doc.queryCommandState(cmd))) { doc.execCommand('insertLineBreak'); - event.preventDefault(); + ev.preventDefault(); } } - __onPaste(ev) { + __onPaste(ev: Event) { + // @ts-ignore const clipboardData = ev.clipboardData || window.clipboardData; const text = clipboardData.getData('text'); const textHtml = clipboardData.getData('text/html'); @@ -268,7 +303,8 @@ export default class RichTextEditor { if (this.actionbar) { if (!action.state || (action.state && action.state(this, this.doc) >= 0)) { const event = action.event || 'click'; - action.btn[`on${event}`] = e => { + // @ts-ignore + action.btn[`on${event}`] = () => { action.result(this, action); this.updateActiveActions(); }; @@ -282,8 +318,8 @@ export default class RichTextEditor { * @param {Object} action * @param {Object} [opts={}] */ - addAction(action, opts = {}) { - const sync = opts.sync; + addAction(action: RichTextEditorAction, opts: { sync?: boolean } = {}) { + const { sync } = opts; const btn = document.createElement('span'); const icon = action.icon; const attr = action.attributes || {}; @@ -329,7 +365,7 @@ export default class RichTextEditor { * @param {string} command Command name * @param {any} [value=null Command's arguments */ - exec(command, value = null) { + exec(command: string, value?: string) { this.doc.execCommand(command, false, value); } @@ -346,7 +382,7 @@ export default class RichTextEditor { * doesn't work in the same way on all browsers * @param {string} value HTML string */ - insertHTML(value, { select } = {}) { + insertHTML(value: string | HTMLElement, { select }: { select?: boolean } = {}) { const { em, doc, el } = this; const sel = doc.getSelection();