/* * This file is part of ORY Editor. * * ORY Editor is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * ORY Editor is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with ORY Editor. If not, see . * * @license LGPL-3.0 * @copyright 2016-2018 Aeneas Rekkas * @author Aeneas Rekkas * */ import semver from 'semver'; import { AnyAction } from 'redux'; import { Omit } from '../../types/omit'; import { NativeFactory, AbstractCell } from '../../types/editable'; export type Plugins = { layout?: LayoutPluginConfig[]; content?: ContentPluginConfig[]; native?: NativeFactory; }; export type PluginsInternal = { layout?: LayoutPlugin[]; content?: ContentPlugin[]; native?: NativeFactory; }; export type OmitInPluginConfig = | 'id' | 'focus' | 'blur' | 'editable' | 'readOnly' | 'state' | 'onChange' | 'focused'; // tslint:disable-next-line:no-any export type PluginConfig = Omit< PluginProps, OmitInPluginConfig >; // tslint:disable-next-line:no-any export type ContentPluginConfig = Omit< ContentPluginProps, | OmitInPluginConfig | 'isEditMode' | 'isResizeMode' | 'isLayoutMode' | 'isPreviewMode' | 'isInsertMode' >; // tslint:disable-next-line:no-any export type LayoutPluginConfig = Omit< LayoutPluginProps, OmitInPluginConfig >; // tslint:disable-next-line:no-any export type NativePluginConfig = Omit< NativePluginProps, OmitInPluginConfig >; // tslint:disable-next-line:no-any export type ContentPluginExtraProps = { /** * @member if the cell is currently in edit mode. */ isEditMode: boolean; /** * @member if the cell is currently in resize mode. */ isResizeMode: boolean; /** * @member if the cell is currently in insert mode. */ isInsertMode: boolean; /** * @member if the cell is currently in preview mode. */ isPreviewMode: boolean; /** * @member if the cell is currently in layout mode. */ isLayoutMode: boolean; allowInlineNeighbours?: boolean; isInlineable?: boolean; Component?: PluginComponentType>; }; export type ContentPluginProps< // tslint:disable-next-line:no-any T = any > = ContentPluginExtraProps & PluginProps>; // tslint:disable-next-line:no-any export type LayoutPluginExtraProps = { // tslint:disable-next-line:no-any createInitialChildren?: () => any; Component?: PluginComponentType>; }; export type LayoutPluginProps< // tslint:disable-next-line:no-any T = any > = LayoutPluginExtraProps & PluginProps>; // tslint:disable-next-line:no-any export type PluginComponentType = React.ComponentType; export type PluginProps< // tslint:disable-next-line:no-any StateT = any, ExtraPropsT = {} > = { /** * @member a unique identifier. */ id: string; /** * @member the plugin's name */ name: string; /** * @member if the cell is currently in readOnly mode. */ readOnly: boolean; /** * @member if true, the cell is currently focused. */ focused: boolean; /** * @member the plugin's state. */ state: StateT; /** * @member the plugin's version */ version: string; Component?: PluginComponentType< PluginProps & ExtraPropsT >; IconComponent?: React.ReactNode; text?: string; // tslint:disable-next-line:no-any serialize?: (state: StateT) => any; // tslint:disable-next-line:no-any unserialize?: (raw: any) => StateT; description?: string; handleRemoveHotKey?: (e: Event, props: AbstractCell) => Promise; handleFocusNextHotKey?: ( e: Event, props: AbstractCell ) => Promise; handleFocusPreviousHotKey?: ( e: Event, props: AbstractCell ) => Promise; handleFocus?: ( props: PluginProps & ExtraPropsT, focusSource: string, ref: HTMLElement ) => void; handleBlur?: (props: PluginProps & ExtraPropsT) => void; reducer?: (state: StateT, action: AnyAction) => StateT; migrations?: Migration[]; createInitialState?: () => StateT; focus?: (props: { source: string }) => void; blur?: (id: string) => void; editable?: string; /** * Should be called with the new state if the plugin's state changes. * * @param state */ onChange(state: Partial): void; }; export interface MigrationConfig { toVersion: string; fromVersionRange: string; // tslint:disable-next-line:no-any migrate: (state: any) => any; } /** * @class the class used to migrate plugin content between toVersion */ export class Migration { fromVersionRange: string; toVersion: string; constructor(config: MigrationConfig) { const { toVersion, migrate, fromVersionRange } = config; if ( !migrate || !toVersion || !fromVersionRange || semver.valid(toVersion) === null || semver.validRange(fromVersionRange) === null ) { throw new Error( `A migration toVersion, fromVersionRange and migrate function must be defined, got ${JSON.stringify( config )}` ); } this.toVersion = toVersion; this.migrate = migrate; this.fromVersionRange = fromVersionRange; } // tslint:disable-next-line:no-any migrate = (state: any): any => state; } /** * @class the abstract class for content and layout plugins. It will be instantiated once and used for every cell that is equipped with it. */ // tslint:disable-next-line:no-any export class Plugin { // tslint:disable-next-line:no-any config: PluginConfig; /** * @member a unique identifier of the plugin. */ name: string; /** * @member describes the plugin in a few words. */ description: string; /** * @member migrations used to migrate plugin state from older version to new one */ migrations: Migration[]; /** * @member the semantic version (www.semver.org) of this plugin. */ version: string; /** * @member the icon that will be shown in the toolbar. */ // tslint:disable-next-line:no-any IconComponent: any; // IconComponent: Element<*> | Component<*, *, *> /** * @member the plugin's react component. */ // tslint:disable-next-line:no-any Component: any; // Component: Element<*> | Component<*, *, *> | (props: any) => Element<*> /** * @member the text that will be shown alongside the icon in the toolbar. */ text: string; // tslint:disable-next-line:no-any constructor(config: PluginConfig) { const { name, version, Component, IconComponent, text, serialize, unserialize, description, handleRemoveHotKey, handleFocusNextHotKey, handleFocusPreviousHotKey, handleFocus, handleBlur, reducer, migrations, } = config; if (!name || !version || !Component) { throw new Error( `A plugin's version, name and Component must be defined, got ${JSON.stringify( config )}` ); } this.name = name; this.version = version; this.Component = Component; this.IconComponent = IconComponent; this.text = text; this.description = description; this.config = config; this.migrations = migrations ? migrations : []; this.serialize = serialize ? serialize.bind(this) : this.serialize; this.unserialize = unserialize ? unserialize.bind(this) : this.unserialize; this.handleRemoveHotKey = handleRemoveHotKey ? handleRemoveHotKey.bind(this) : this.handleRemoveHotKey; this.handleFocusNextHotKey = handleFocusNextHotKey ? handleFocusNextHotKey.bind(this) : this.handleFocusNextHotKey; this.handleFocusPreviousHotKey = handleFocusPreviousHotKey ? handleFocusPreviousHotKey.bind(this) : this.handleFocusPreviousHotKey; this.handleFocus = handleFocus ? handleFocus.bind(this) : this.handleFocus; this.handleBlur = handleBlur ? handleBlur.bind(this) : this.handleBlur; this.reducer = reducer ? reducer.bind(this) : this.reducer; } /** * Serialize a the plugin state * * @param raw the raw state. * @returns the serialized state. */ serialize = (raw: Object): Object => raw; /** * Unserialize the plugin state. * * @param state the plugin state. * @returns the unserialized state. */ unserialize = (state: Object): Object => state; /** * Will be called when the user presses the delete key. When returning a resolving promise, * the cell will be removed. If the promise is rejected, nothing happens. * * @param e * @param props * @returns a promise */ handleRemoveHotKey = (e: Event, props: ContentPluginProps): Promise => Promise.reject() /** * Will be called when the user presses the right or down key. When returning a resolving promise, * the next cell will be focused. If the promise is rejected, focus stays the same. * * @param e * @param props * @returns a promise */ handleFocusNextHotKey = ( e: Event, props: ContentPluginProps ): Promise => Promise.resolve() /** * Will be called when the user presses the left or up key. When returning a resolving promise, * the next cell will be focused. If the promise is rejected, focus stays the same. * * @param e * @param props * @returns a promise */ handleFocusPreviousHotKey = ( e: Event, props: ContentPluginProps ): Promise => Promise.resolve() /** * This function will be called when one of the plugin's cell is blurred. * * @param props */ handleFocus = ( props: ContentPluginProps, focusSource: string, ref: HTMLElement ): void => null /** * This function will be called when one of the plugin's cell is focused. * * @param props */ handleBlur = (props: ContentPluginProps): void => null; /** * Specify a custom reducer for the plugin's cell. * * @param state * @param action */ // tslint:disable-next-line:no-any reducer = (state: any, action: any) => state; } /** * @class this is the base class for content plugins. */ // tslint:disable-next-line:no-any export class ContentPlugin extends Plugin< StateT, ContentPluginExtraProps > { /** * @member if isInlineable is true, the plugin is allowed to be placed with floating to left or right. */ isInlineable: boolean; /** * @member if true allows that isInlineable elements may be placed "in" this plugin. */ allowInlineNeighbours: boolean; // tslint:disable-next-line:no-any constructor(config: ContentPluginConfig) { super(config); const { createInitialState, allowInlineNeighbours = false, isInlineable = false, } = config; this.isInlineable = isInlineable; this.allowInlineNeighbours = allowInlineNeighbours; this.createInitialState = createInitialState ? createInitialState.bind(this) : this.createInitialState; } /** * Create the plugin's initial state. * * @returns the initial state. */ createInitialState = (): Object => ({}); /** * Specify a custom reducer for the plugin's cell. * * @param state * @param action */ // tslint:disable-next-line:no-any reducer = (state: any, action: any) => state; } /** * @class this is the base class for layout plugins. */ // tslint:disable-next-line:no-any export class LayoutPlugin extends Plugin< StateT, LayoutPluginExtraProps > { constructor(config: LayoutPluginConfig) { super(config); const { createInitialState, createInitialChildren } = config; this.createInitialState = createInitialState ? createInitialState.bind(this) : this.createInitialState; this.createInitialChildren = createInitialChildren ? createInitialChildren.bind(this) : this.createInitialChildren; } /** * Create the plugin's initial state. * * @returns the initial state. */ createInitialState = (): StateT => ({} as StateT); /** * Create the plugin's initial children (rows/cells). * * @returns the initial state. */ // tslint:disable-next-line:no-any createInitialChildren = (): any => ({} as any); } // tslint:disable-next-line:no-any export type NativePluginProps = PluginProps & { type?: string; // tslint:disable-next-line:no-any createInitialChildren?: () => any; allowInlineNeighbours?: boolean; isInlineable?: boolean; }; export class NativePlugin extends Plugin { /** * @member can be 'content' or 'layout' depending on the type the native plugin should create */ type: string; /** * @member if isInlineable is true, the plugin is allowed to be placed with floating to left or right. */ isInlineable: boolean; /** * @member if true allows that isInlineable elements may be placed "in" this plugin. */ allowInlineNeighbours: boolean; // tslint:disable-next-line:no-any constructor(config: NativePluginConfig) { super(config); const { createInitialState, allowInlineNeighbours = false, isInlineable = false, createInitialChildren, type = 'content', } = config; this.isInlineable = isInlineable; this.allowInlineNeighbours = allowInlineNeighbours; this.createInitialState = createInitialState ? createInitialState.bind(this) : this.createInitialState; this.createInitialChildren = createInitialChildren ? createInitialChildren.bind(this) : this.createInitialChildren; this.type = type; } /** * Create the plugin's initial children (rows/cells). * * @returns the initial state. */ createInitialChildren = (): Object => ({}); /** * Create the plugin's initial state. * * @returns the initial state. */ createInitialState = (): Object => ({}); }