UNPKG

@print-one/grapesjs

Version:

Free and Open Source Web Builder Framework

837 lines (776 loc) 23.9 kB
/** * Editor contains the top level API which you'll probably use to customize the editor or extend it with plugins. * You get the Editor instance on init method and you can pass options via its [Configuration Object](https://github.com/GrapesJS/grapesjs/blob/master/src/editor/config/config.ts) * * ```js * const editor = grapesjs.init({ * // options * }); * ``` * * ## Available Events * * You can make use of available events in this way * ```js * editor.on('EVENT-NAME', (some, argument) => { * // do something * }) * ``` * * * `update` - The structure of the template is updated (its HTML/CSS) * * `undo` - Undo executed * * `redo` - Redo executed * * `load` - Editor is loaded * * ### Components * Check the [Components](/api/components.html) module. * ### Keymaps * Check the [Keymaps](/api/keymaps.html) module. * ### Style Manager * Check the [Style Manager](/api/style_manager.html) module. * ### Storage * Check the [Storage](/api/storage_manager.html) module. * ### Canvas * Check the [Canvas](/api/canvas.html) module. * ### RTE * Check the [Rich Text Editor](/api/rich_text_editor.html) module. * ### Commands * Check the [Commands](/api/commands.html) module. * ### Selectors * Check the [Selectors](/api/selector_manager.html) module. * ### Blocks * Check the [Blocks](/api/block_manager.html) module. * ### Assets * Check the [Assets](/api/assets.html) module. * ### Modal * Check the [Modal](/api/modal_dialog.html) module. * ### Devices * Check the [Devices](/api/device_manager.html) module. * ### Parser * Check the [Parser](/api/parser.html) module. * ### Pages * Check the [Pages](/api/pages.html) module. * * ## Methods * @module docsjs.Editor */ import { IBaseModule } from '../abstract/Module'; import AssetManager, { AssetEvent } from '../asset_manager'; import BlockManager, { BlockEvent } from '../block_manager'; import CanvasModule, { CanvasEvent } from '../canvas'; import CodeManagerModule from '../code_manager'; import CommandsModule, { CommandEvent } from '../commands'; import { AddOptions, EventHandler, LiteralUnion } from '../common'; import CssComposer from '../css_composer'; import CssRule from '../css_composer/model/CssRule'; import CssRules from '../css_composer/model/CssRules'; import DeviceManager from '../device_manager'; import ComponentManager, { ComponentEvent } from '../dom_components'; import Component from '../dom_components/model/Component'; import Components from '../dom_components/model/Components'; import ComponentWrapper from '../dom_components/model/ComponentWrapper'; import { ComponentAdd, DragMode } from '../dom_components/model/types'; import I18nModule from '../i18n'; import KeymapsModule, { KeymapEvent } from '../keymaps'; import ModalModule, { ModalEvent } from '../modal_dialog'; import LayerManager from '../navigator'; import PageManager from '../pages'; import PanelManager from '../panels'; import ParserModule from '../parser'; import { CustomParserCss } from '../parser/config/config'; import RichTextEditorModule, { RichTextEditorEvent } from '../rich_text_editor'; import { CustomRTE } from '../rich_text_editor/config/config'; import SelectorManager, { SelectorEvent } from '../selector_manager'; import StorageManager, { StorageEvent } from '../storage_manager'; import { ProjectData } from '../storage_manager/model/IStorage'; import StyleManager, { StyleManagerEvent } from '../style_manager'; import TraitManager from '../trait_manager'; import UndoManagerModule from '../undo_manager'; import UtilsModule from '../utils'; import html from '../utils/html'; import defaults, { EditorConfig, EditorConfigKeys } from './config/config'; import EditorModel from './model/Editor'; import EditorView from './view/EditorView'; export type ParsedRule = { selectors: string; style: Record<string, string>; atRule?: string; params?: string; }; type GeneralEvent = 'canvasScroll' | 'undo' | 'redo' | 'load' | 'update'; type EditorBuiltInEvents = | ComponentEvent | BlockEvent | AssetEvent | KeymapEvent | StyleManagerEvent | StorageEvent | CanvasEvent | SelectorEvent | RichTextEditorEvent | ModalEvent | CommandEvent | GeneralEvent; type EditorEvent = LiteralUnion<EditorBuiltInEvents, string>; type EditorConfigType = EditorConfig & { pStylePrefix?: string }; type EditorModelParam<T extends keyof EditorModel, N extends number> = Parameters<EditorModel[T]>[N]; export type EditorParam<T extends keyof Editor, N extends number> = Parameters<Editor[T]>[N]; export default class Editor implements IBaseModule<EditorConfig> { editorView?: EditorView; editor: EditorModel; $: any; em: EditorModel; config: EditorConfigType; constructor(config: EditorConfig = {}, opts: any = {}) { this.config = { ...defaults, ...config, pStylePrefix: config.stylePrefix ?? defaults.stylePrefix, }; this.em = new EditorModel(this.config); this.$ = opts.$; this.em.init(this); this.editor = this.em; } get Config() { return this.em.config; } get I18n(): I18nModule { return this.em.I18n; } get Utils(): UtilsModule { return this.em.Utils; } get Commands(): CommandsModule { return this.em.Commands; } get Keymaps(): KeymapsModule { return this.em.Keymaps; } get Modal(): ModalModule { return this.em.Modal; } get Panels(): PanelManager { return this.em.Panels; } get Canvas(): CanvasModule { return this.em.Canvas; } get Parser(): ParserModule { return this.em.Parser; } get CodeManager(): CodeManagerModule { return this.em.CodeManager; } get UndoManager(): UndoManagerModule { return this.em.UndoManager; } get RichTextEditor(): RichTextEditorModule { return this.em.RichTextEditor; } get Pages(): PageManager { return this.em.Pages; } get Components(): ComponentManager { return this.em.Components; } get DomComponents(): ComponentManager { return this.em.Components; } get Layers(): LayerManager { return this.em.Layers; } get LayerManager(): LayerManager { return this.em.Layers; } get Css(): CssComposer { return this.em.Css; } get CssComposer(): CssComposer { return this.em.Css; } get Storage(): StorageManager { return this.em.Storage; } get StorageManager(): StorageManager { return this.em.Storage; } get Assets(): AssetManager { return this.em.Assets; } get AssetManager(): AssetManager { return this.em.Assets; } get Blocks(): BlockManager { return this.em.Blocks; } get BlockManager(): BlockManager { return this.em.Blocks; } get Traits(): TraitManager { return this.em.Traits; } get TraitManager(): TraitManager { return this.em.Traits; } get Selectors(): SelectorManager { return this.em.Selectors; } get SelectorManager(): SelectorManager { return this.em.Selectors; } get Styles(): StyleManager { return this.em.Styles; } get StyleManager(): StyleManager { return this.em.Styles; } get Devices(): DeviceManager { return this.em.Devices; } get DeviceManager(): DeviceManager { return this.em.Devices; } get EditorModel() { return this.em; } /** * Returns configuration object * @returns {any} Returns the configuration object or the value of the specified property */ getConfig< P extends EditorConfigKeys | undefined = undefined, R = P extends EditorConfigKeys ? EditorConfig[P] : EditorConfig >(prop?: P): R { return this.em.getConfig(prop); } /** * Returns HTML built inside canvas * @param {Object} [opts={}] Options * @param {Component} [opts.component] Return the HTML of a specific Component * @param {Boolean} [opts.cleanId=false] Remove unnecessary IDs (eg. those created automatically) * @returns {string} HTML string */ getHtml(opts?: EditorModelParam<'getHtml', 0>) { return this.em.getHtml(opts); } /** * Returns CSS built inside canvas * @param {Object} [opts={}] Options * @param {Component} [opts.component] Return the CSS of a specific Component * @param {Boolean} [opts.json=false] Return an array of CssRules instead of the CSS string * @param {Boolean} [opts.avoidProtected=false] Don't include protected CSS * @param {Boolean} [opts.onlyMatched=false] Return only rules matched by the passed component. * @param {Boolean} [opts.keepUnusedStyles=false] Force keep all defined rules. Toggle on in case output looks different inside/outside of the editor. * @returns {String|Array<CssRule>} CSS string or array of CssRules */ getCss(opts?: EditorModelParam<'getCss', 0>) { return this.em.getCss(opts); } /** * Returns JS of all components * @param {Object} [opts={}] Options * @param {Component} [opts.component] Get the JS of a specific component * @returns {String} JS string */ getJs(opts?: EditorModelParam<'getJs', 0>) { return this.em.getJs(opts); } /** * Return the complete tree of components. Use `getWrapper` to include also the wrapper * @return {Components} */ getComponents(): Components { return this.Components.getComponents(); } /** * Return the wrapper and its all components * @return {Component} */ getWrapper(): ComponentWrapper | undefined { return this.Components.getWrapper(); } /** * Set components inside editor's canvas. This method overrides actual components * @param {Array<Object>|Object|string} components HTML string or components model * @param {Object} opt the options object to be used by the [setComponents]{@link em#setComponents} method * @return {this} * @example * editor.setComponents('<div class="cls">New component</div>'); * // or * editor.setComponents({ * type: 'text', * classes:['cls'], * content: 'New component' * }); */ setComponents(components: ComponentAdd, opt: AddOptions = {}) { this.em.setComponents(components, opt); return this; } /** * Add components * @param {Array<Object>|Object|string} components HTML string or components model * @param {Object} opts Options * @param {Boolean} [opts.avoidUpdateStyle=false] If the HTML string contains styles, * by default, they will be created and, if already exist, updated. When this option * is true, styles already created will not be updated. * @return {Array<Component>} * @example * editor.addComponents('<div class="cls">New component</div>'); * // or * editor.addComponents({ * type: 'text', * classes:['cls'], * content: 'New component' * }); */ addComponents(components: ComponentAdd, opts?: AddOptions): Component[] { return this.getWrapper()!.append(components, opts); } /** * Returns style in JSON format object * @return {Object} */ getStyle(): CssRules { return this.em.Css.getAll(); } /** * Set style inside editor's canvas. This method overrides actual style * @param {Array<Object>|Object|string} style CSS string or style model * @return {this} * @example * editor.setStyle('.cls{color: red}'); * //or * editor.setStyle({ * selectors: ['cls'], * style: { color: 'red' } * }); */ setStyle(style: any, opt: any = {}) { this.em.setStyle(style, opt); return this; } /** * Add styles to the editor * @param {Array<Object>|Object|string} style CSS string or style model * @returns {Array<CssRule>} Array of created CssRule instances * @example * editor.addStyle('.cls{color: red}'); */ addStyle(style: any, opts = {}): CssRule[] { return this.em.addStyle(style, opts); } /** * Returns the last selected component, if there is one * @return {Model} */ getSelected() { return this.em.getSelected(); } /** * Returns an array of all selected components * @return {Array} */ getSelectedAll() { return this.em.getSelectedAll(); } /** * Get a stylable entity from the selected component. * If you select a component without classes the entity is the Component * itself and all changes will go inside its 'style' attribute. Otherwise, * if the selected component has one or more classes, the function will * return the corresponding CSS Rule * @return {Model} */ getSelectedToStyle() { let selected = this.em.getSelected(); if (selected) { return this.StyleManager.getModelToStyle(selected); } } /** * Select a component * @param {Component|HTMLElement} el Component to select * @param {Object} [opts] Options * @param {Boolean} [opts.scroll] Scroll canvas to the selected element * @return {this} * @example * // Select dropped block * editor.on('block:drag:stop', function(model) { * editor.select(model); * }); */ select(el?: EditorModelParam<'setSelected', 0>, opts?: { scroll?: boolean }) { this.em.setSelected(el, opts); return this; } /** * Add component to selection * @param {Component|HTMLElement|Array} el Component to select * @return {this} * @example * editor.selectAdd(model); */ // selectAdd(el: Parameters<EditorModel['addSelected']>[0]) { selectAdd(el: EditorModelParam<'addSelected', 0>) { this.em.addSelected(el); return this; } /** * Remove component from selection * @param {Component|HTMLElement|Array} el Component to select * @return {this} * @example * editor.selectRemove(model); */ selectRemove(el: EditorModelParam<'removeSelected', 0>) { this.em.removeSelected(el); return this; } /** * Toggle component selection * @param {Component|HTMLElement|Array} el Component to select * @return {this} * @example * editor.selectToggle(model); */ selectToggle(el: EditorModelParam<'toggleSelected', 0>) { this.em.toggleSelected(el); return this; } /** * Returns, if active, the Component enabled in rich text editing mode. * @returns {Component|null} * @example * const textComp = editor.getEditing(); * if (textComp) { * console.log('HTML: ', textComp.toHTML()); * } */ getEditing() { return this.em.getEditing(); } /** * Set device to the editor. If the device exists it will * change the canvas to the proper width * @param {string} name Name of the device * @return {this} * @example * editor.setDevice('Tablet'); */ setDevice(name: string) { this.em.set('device', name); return this; } /** * Return the actual active device * @return {string} Device name * @example * var device = editor.getDevice(); * console.log(device); * // 'Tablet' */ getDevice(): string { return this.em.get('device'); } /** * Execute command * @param {string} id Command ID * @param {Object} options Custom options * @return {*} The return is defined by the command * @example * editor.runCommand('myCommand', {someValue: 1}); */ runCommand(id: string, options: Record<string, unknown> = {}) { return this.Commands.run(id, options); } /** * Stop the command if stop method was provided * @param {string} id Command ID * @param {Object} options Custom options * @return {*} The return is defined by the command * @example * editor.stopCommand('myCommand', {someValue: 1}); */ stopCommand(id: string, options: Record<string, unknown> = {}) { return this.Commands.stop(id, options); } /** * Store data to the current storage. * This will reset the counter of changes (`editor.getDirtyCount()`). * @param {Object} [options] Storage options. * @returns {Object} Stored data. * @example * const storedData = await editor.store(); */ async store(options: any) { return await this.em.store(options); } /** * Load data from the current storage. * @param {Object} [options] Storage options. * @returns {Object} Loaded data. * @example * const data = await editor.load(); */ async load(options: any) { return await this.em.load(options); } /** * Get the JSON project data, which could be stored and loaded back with `editor.loadProjectData(json)` * @returns {Object} * @example * console.log(editor.getProjectData()); * // { pages: [...], styles: [...], ... } */ getProjectData() { return this.em.storeData(); } /** * Load data from the JSON project * @param {Object} data Project to load * @example * editor.loadProjectData({ pages: [...], styles: [...], ... }) */ loadProjectData(data: ProjectData) { return this.em.loadData(data); } storeData() { return this.em.storeData(); } loadData(data: any) { return this.em.loadData(data); } /** * Returns container element. The one which was indicated as 'container' * on init method * @return {HTMLElement} */ getContainer() { return this.config.el; } /** * Return the count of changes made to the content and not yet stored. * This count resets at any `store()` * @return {number} */ getDirtyCount() { return this.em.getDirtyCount(); } /** * Reset the counter of changes. */ clearDirtyCount() { return this.em.clearDirtyCount(); } /** * Update editor dimension offsets * * This method could be useful when you update, for example, some position * of the editor element (eg. canvas, panels, etc.) with CSS, where without * refresh you'll get misleading position of tools * @param {Object} [options] Options * @param {Boolean} [options.tools=false] Update the position of tools (eg. rich text editor, component highlighter, etc.) */ refresh(opts?: { tools?: boolean }) { this.em.refreshCanvas(opts); } /** * Replace the built-in Rich Text Editor with a custom one. * @param {Object} obj Custom RTE Interface * @example * editor.setCustomRte({ * // Function for enabling custom RTE * // el is the HTMLElement of the double clicked Text Component * // rte is the same instance you have returned the first time you call * // enable(). This is useful if need to check if the RTE is already enabled so * // ion this case you'll need to return the RTE and the end of the function * enable: function(el, rte) { * rte = new MyCustomRte(el, {}); // this depends on the Custom RTE API * ... * return rte; // return the RTE instance * } * * // Disable the editor, called for example when you unfocus the Text Component * disable: function(el, rte) { * rte.blur(); // this depends on the Custom RTE API * } * * // Called when the Text Component is focused again. If you returned the RTE instance * // from the enable function, the enable won't be called again instead will call focus, * // in this case to avoid double binding of the editor * focus: function (el, rte) { * rte.focus(); // this depends on the Custom RTE API * } * }); */ setCustomRte<T>(obj: CustomRTE & ThisType<T & CustomRTE>) { this.RichTextEditor.customRte = obj; } /** * Replace the default CSS parser with a custom one. * The parser function receives a CSS string as a parameter and expects * an array of CSSRule objects as a result. If you need to remove the * custom parser, pass `null` as the argument * @param {Function|null} parser Parser function * @return {this} * @example * editor.setCustomParserCss(css => { * const result = []; * // ... parse the CSS string * result.push({ * selectors: '.someclass, div .otherclass', * style: { color: 'red' } * }) * // ... * return result; * }); */ setCustomParserCss(parser: CustomParserCss) { this.Parser.getConfig().parserCss = parser; return this; } /** * Change the global drag mode of components. * To get more about this feature read: https://github.com/GrapesJS/grapesjs/issues/1936 * @param {String} value Drag mode, options: 'absolute' | 'translate' * @returns {this} */ setDragMode(value: DragMode) { this.em.setDragMode(value); return this; } /** * Trigger event log message * @param {*} msg Message to log * @param {Object} [opts={}] Custom options * @param {String} [opts.ns=''] Namespace of the log (eg. to use in plugins) * @param {String} [opts.level='debug'] Level of the log, `debug`, `info`, `warning`, `error` * @return {this} * @example * editor.log('Something done!', { ns: 'from-plugin-x', level: 'info' }); * // This will trigger following events * // `log`, `log:info`, `log-from-plugin-x`, `log-from-plugin-x:info` * // Callbacks of those events will always receive the message and * // options, as arguments, eg: * // editor.on('log:info', (msg, opts) => console.info(msg, opts)) */ log(msg: string, opts: { ns?: string; level?: string } = {}) { this.em.log(msg, opts); return this; } /** * Translate label * @param {String} key Label to translate * @param {Object} [opts] Options for the translation * @param {Object} [opts.params] Params for the translation * @param {Boolean} [opts.noWarn] Avoid warnings in case of missing resources * @returns {String} * @example * editor.t('msg'); * // use params * editor.t('msg2', { params: { test: 'hello' } }); * // custom local * editor.t('msg2', { params: { test: 'hello' } l: 'it' }); */ t(...args: any[]) { return this.em.t(...args); } /** * Attach event * @param {string} event Event name * @param {Function} callback Callback function * @return {this} */ on(event: EditorEvent, callback: EventHandler) { this.em.on(event, callback); return this; } /** * Attach event and detach it after the first run * @param {string} event Event name * @param {Function} callback Callback function * @return {this} */ once(event: EditorEvent, callback: EventHandler) { this.em.once(event, callback); return this; } /** * Detach event * @param {string} event Event name * @param {Function} callback Callback function * @return {this} */ off(event: EditorEvent, callback: EventHandler) { this.em.off(event, callback); return this; } /** * Trigger event * @param {string} event Event to trigger * @return {this} */ trigger(event: EditorEvent, ...args: any[]) { this.em.trigger.apply(this.em, [event, ...args]); return this; } /** * Destroy the editor */ destroy() { if (!this.em) return; this.em.destroyAll(); this.editorView = undefined; } /** * Returns editor element * @return {HTMLElement} * @private */ getEl() { return this.editorView?.el; } /** * Returns editor model * @return {Model} * @private */ getModel() { return this.em; } /** * Render editor * @return {HTMLElement} */ render() { this.editorView?.remove(); this.editorView = new EditorView(this.em); return this.editorView.render().el; } /** * Trigger a callback once the editor is loaded and rendered. * The callback will be executed immediately if the method is called on the already rendered editor. * @param {Function} clb Callback to trigger * @example * editor.onReady(() => { * // perform actions * }); */ onReady(clb: EventHandler) { this.em.get('ready') ? clb(this) : this.em.on('load', clb); } /** * Print safe HTML by using ES6 tagged template strings. * @param {Array<String>} literals * @param {Array<String>} substs * @returns {String} * @example * const unsafeStr = '<script>....</script>'; * const safeStr = '<b>Hello</b>'; * // Use `$${var}` to avoid escaping * const strHtml = editor.html`Escaped ${unsafeStr} unescaped $${safeStr}`; */ html = html; }