UNPKG

jodit

Version:

Jodit is awesome and usefully wysiwyg editor with filebrowser

768 lines (674 loc) 17.4 kB
/*! * Jodit Editor (https://xdsoft.net/jodit/) * Licensed under GNU General Public License version 2 or later or a commercial license or MIT; * For GPL see LICENSE-GPL.txt in the project root for license information. * For MIT see LICENSE-MIT.txt in the project root for license information. * For commercial licenses see https://xdsoft.net/jodit/commercial/ * Copyright (c) 2013-2019 Valeriy Chupurnov. All rights reserved. https://xdsoft.net */ import { Config } from '../Config'; import { Widget } from '../modules/Widget'; import ColorPickerWidget = Widget.ColorPickerWidget; import TabsWidget = Widget.TabsWidget; import { Dom } from '../modules/Dom'; import { css, debounce, offset, splitArray } from '../modules/helpers/'; import { Plugin } from '../modules/Plugin'; import { Table } from '../modules/Table'; import { Popup } from '../modules/popup/popup'; import { IDictionary, IJodit, IToolbarCollection } from '../types'; import { IControlType } from '../types/toolbar'; import { IBound } from '../types/types'; import { JoditToolbarCollection } from '../modules/toolbar/joditToolbarCollection'; import { ToolbarCollection } from '../modules'; declare module '../Config' { interface Config { popup: IDictionary<Array<IControlType | string>>; toolbarInline: boolean; toolbarInlineDisableFor: string | string[]; } } Config.prototype.toolbarInline = true; Config.prototype.toolbarInlineDisableFor = []; Config.prototype.popup = { a: [ { name: 'eye', tooltip: 'Open link', exec: (editor: IJodit, current: Node) => { const href: | string | null = (current as HTMLElement).getAttribute('href'); if (current && href) { editor.ownerWindow.open(href); } } } as IControlType, { name: 'link', tooltip: 'Edit link', icon: 'pencil' }, 'unlink', 'brush', 'file' ], jodit: [ { name: 'bin', tooltip: 'Delete', exec: (editor: IJodit, image: Node) => { if (image.parentNode) { Dom.safeRemove(image); editor.events.fire('hidePopup'); } } } ], 'jodit-media': [ { name: 'bin', tooltip: 'Delete', exec: (editor: IJodit, image: Node) => { if (image.parentNode) { Dom.safeRemove(image); editor.events.fire('hidePopup'); } } } ], img: [ { name: 'delete', icon: 'bin', tooltip: 'Delete', exec: (editor: IJodit, image: Node) => { if (image.parentNode) { Dom.safeRemove(image); editor.events.fire('hidePopup'); } } }, { name: 'pencil', exec(editor: IJodit, current: Node) { const tagName: string = (current as HTMLElement).tagName.toLowerCase(); if (tagName === 'img') { editor.events.fire('openImageProperties', current); } }, tooltip: 'Edit' }, { name: 'valign', list: ['Top', 'Middle', 'Bottom'], tooltip: 'Vertical align', exec: ( editor: IJodit, image: HTMLImageElement, control: IControlType ) => { const tagName: string = (image as HTMLElement).tagName.toLowerCase(); if (tagName !== 'img') { return; } const command: string = control.args && typeof control.args[1] === 'string' ? control.args[1].toLowerCase() : ''; css(image, 'vertical-align', command); editor.events.fire('recalcPositionPopup'); } }, { name: 'left', list: ['Left', 'Right', 'Center', 'Normal'], exec: ( editor: IJodit, image: HTMLImageElement, control: IControlType ) => { const tagName: string = (image as HTMLElement).tagName.toLowerCase(); if (tagName !== 'img') { return; } const clearCenterAlign = () => { if (css(image, 'display') === 'block') { css(image, 'display', ''); } if ( image.style.marginLeft === 'auto' && image.style.marginRight === 'auto' ) { image.style.marginLeft = ''; image.style.marginRight = ''; } }; const command: string = control.args && typeof control.args[1] === 'string' ? control.args[1].toLowerCase() : ''; if (command !== 'normal') { if (['right', 'left'].indexOf(command) !== -1) { css(image, 'float', command); clearCenterAlign(); } else { css(image, 'float', ''); css(image, { display: 'block', 'margin-left': 'auto', 'margin-right': 'auto' }); } } else { if ( css(image, 'float') && ['right', 'left'].indexOf( (css(image, 'float') as string).toLowerCase() ) !== -1 ) { css(image, 'float', ''); } clearCenterAlign(); } editor.events.fire('recalcPositionPopup'); }, tooltip: 'Horizontal align' } ], table: [ { name: 'brush', popup: (editor: IJodit, elm: HTMLTableElement) => { const selected: HTMLTableCellElement[] = Table.getAllSelectedCells( elm ); let $bg: HTMLElement, $cl: HTMLElement, $br: HTMLElement, $tab: HTMLElement, color: string, br_color: string, bg_color: string; if (!selected.length) { return false; } color = css(selected[0], 'color') as string; bg_color = css(selected[0], 'background-color') as string; br_color = css(selected[0], 'border-color') as string; $bg = ColorPickerWidget( editor, (value: string) => { selected.forEach((cell: HTMLTableCellElement) => { css(cell, 'background-color', value); }); editor.setEditorValue(); // close(); }, bg_color ); $cl = ColorPickerWidget( editor, (value: string) => { selected.forEach((cell: HTMLTableCellElement) => { css(cell, 'color', value); }); editor.setEditorValue(); // close(); }, color ); $br = ColorPickerWidget( editor, (value: string) => { selected.forEach((cell: HTMLTableCellElement) => { css(cell, 'border-color', value); }); editor.setEditorValue(); // close(); }, br_color ); $tab = TabsWidget(editor, { Background: $bg, Text: $cl, Border: $br } as IDictionary<HTMLElement>); return $tab; }, tooltip: 'Background' }, { name: 'valign', list: ['Top', 'Middle', 'Bottom'], exec: ( editor: IJodit, table: HTMLTableElement, control: IControlType ) => { const command: string = control.args && typeof control.args[1] === 'string' ? control.args[1].toLowerCase() : ''; Table.getAllSelectedCells(table).forEach( (cell: HTMLTableCellElement) => { css(cell, 'vertical-align', command); } ); }, tooltip: 'Vertical align' }, { name: 'splitv', list: { tablesplitv: 'Split vertical', tablesplitg: 'Split horizontal' }, tooltip: 'Split' }, { name: 'align', icon: 'left' }, '\n', { name: 'merge', command: 'tablemerge', tooltip: 'Merge' }, { name: 'addcolumn', list: { tableaddcolumnbefore: 'Insert column before', tableaddcolumnafter: 'Insert column after' }, exec: ( editor: IJodit, table: HTMLTableElement, control: IControlType ) => { const command: string = control.args && typeof control.args[0] === 'string' ? control.args[0].toLowerCase() : ''; editor.execCommand(command, false, table); }, tooltip: 'Add column' }, { name: 'addrow', list: { tableaddrowbefore: 'Insert row above', tableaddrowafter: 'Insert row below' }, exec: ( editor: IJodit, table: HTMLTableElement, control: IControlType ) => { const command: string = control.args && typeof control.args[0] === 'string' ? control.args[0].toLowerCase() : ''; editor.execCommand(command, false, table); }, tooltip: 'Add row' }, { name: 'delete', icon: 'bin', list: { tablebin: 'Delete table', tablebinrow: 'Delete row', tablebincolumn: 'Delete column', tableempty: 'Empty cell' }, exec: ( editor: IJodit, table: HTMLTableElement, control: IControlType ) => { const command: string = control.args && typeof control.args[0] === 'string' ? control.args[0].toLowerCase() : ''; editor.execCommand(command, false, table); editor.events.fire('hidePopup'); }, tooltip: 'Delete' } ] } as IDictionary<Array<IControlType | string>>; /** * Support inline toolbar * * @param {Jodit} editor */ export class inlinePopup extends Plugin { private toolbar: IToolbarCollection; private popup: Popup; private target: HTMLDivElement; private container: HTMLDivElement; private _hiddenClass = 'jodit_toolbar_popup-inline-target-hidden'; private __getRect: () => IBound; // was started selection private isSelectionStarted = false; private onSelectionEnd = debounce(() => { if (this.isDestructed || !this.jodit.isEditorMode()) { return; } if (this.isSelectionStarted) { if (!this.isTargetAction) { this.onChangeSelection(); } } this.isSelectionStarted = false; this.isTargetAction = false; }, this.jodit.defaultTimeout); private isTargetAction: boolean = false; /** * Popup was opened for some selection text (not for image or link) * @type {boolean} */ private isSelectionPopup: boolean = false; private calcWindSizes = (): IBound => { const win: Window = this.jodit.ownerWindow; const docElement: HTMLElement | null = this.jodit.ownerDocument .documentElement; if (!docElement) { return { left: 0, top: 0, width: 0, height: 0 }; } const body: HTMLElement = this.jodit.ownerDocument.body; const scrollTop: number = win.pageYOffset || docElement.scrollTop || body.scrollTop; const clientTop: number = docElement.clientTop || body.clientTop || 0; const scrollLeft: number = win.pageXOffset || docElement.scrollLeft || body.scrollLeft; const clientLeft: number = docElement.clientLeft || body.clientLeft || 0; const windWidth: number = docElement.clientWidth + scrollLeft - clientLeft; const windHeight: number = docElement.clientHeight + scrollTop - clientTop; return { left: clientLeft, top: clientTop, width: windWidth, height: windHeight }; }; private calcPosition = (rect: IBound, windowSize: IBound) => { if (this.isDestructed) { return; } this.popup.target.classList.remove(this._hiddenClass); const selectionCenterLeft: number = rect.left + rect.width / 2; const workplacePosition: IBound = offset( this.jodit.workplace, this.jodit, this.jodit.ownerDocument, true ); let targetTop: number = rect.top + rect.height + 10; const diff = 50; this.target.style.left = selectionCenterLeft + 'px'; this.target.style.top = targetTop + 'px'; if (this.jodit.isFullSize()) { this.target.style.zIndex = css( this.jodit.container, 'zIndex' ).toString(); } const halfWidthPopup: number = this.container.offsetWidth / 2; let marginLeft: number = -halfWidthPopup; this.popup.container.classList.remove('jodit_toolbar_popup-inline-top'); if (targetTop + this.container.offsetHeight > windowSize.height) { targetTop = rect.top - this.container.offsetHeight - 10; this.target.style.top = targetTop + 'px'; this.popup.container.classList.add( 'jodit_toolbar_popup-inline-top' ); } if (selectionCenterLeft - halfWidthPopup < 0) { marginLeft = -(rect.width / 2 + rect.left); } if (selectionCenterLeft + halfWidthPopup > windowSize.width) { marginLeft = -( this.container.offsetWidth - (windowSize.width - selectionCenterLeft) ); } this.container.style.marginLeft = marginLeft + 'px'; if ( workplacePosition.top - targetTop > diff || targetTop - (workplacePosition.top + workplacePosition.height) > diff ) { this.popup.target.classList.add(this._hiddenClass); } }; private isExcludedTarget(type: string): boolean { return ( splitArray(this.jodit.options.toolbarInlineDisableFor) .map(a => a.toLowerCase()) .indexOf(type.toLowerCase()) !== -1 ); } private reCalcPosition = () => { if (this.__getRect) { this.calcPosition(this.__getRect(), this.calcWindSizes()); } }; private showPopup = ( rect: () => IBound, type: string, elm?: HTMLElement ): boolean => { if ( !this.jodit.options.toolbarInline || !this.jodit.options.popup[type.toLowerCase()] ) { return false; } if (this.isExcludedTarget(type)) { return true; } this.isShown = true; this.isTargetAction = true; const windSize: IBound = this.calcWindSizes(); this.target.parentNode || this.jodit.ownerDocument.body.appendChild(this.target); this.toolbar.build( this.jodit.options.popup[type.toLowerCase()], this.container, elm ); this.popup.open(this.container, false, true); this.__getRect = rect; this.calcPosition(rect(), windSize); return true; }; private hidePopup = (root?: HTMLElement | Popup) => { if (this.isDestructed) { return; } if ( root && (Dom.isNode(root, this.jodit.editorWindow || window) || root instanceof Popup) && Dom.isOrContains( this.target, root instanceof Popup ? root.target : root ) ) { return; } this.isTargetAction = false; this.isShown = false; this.popup.close(); Dom.safeRemove(this.target); }; private onSelectionStart = (event: MouseEvent) => { if (this.isDestructed || !this.jodit.isEditorMode()) { return; } this.isTargetAction = false; this.isSelectionPopup = false; if (!this.isSelectionStarted) { const elements: string = Object.keys(this.jodit.options.popup).join( '|' ), target: HTMLElement | false = (event.target as Node).nodeName === 'IMG' ? (event.target as HTMLImageElement) : (Dom.closest( event.target as Node, elements, this.jodit.editor ) as HTMLTableElement); if ( !target || !this.showPopup( () => offset(target, this.jodit, this.jodit.editorDocument), target.nodeName, target ) ) { this.isSelectionStarted = true; } } }; private hideIfCollapsed(): boolean { if (this.jodit.selection.isCollapsed()) { this.hidePopup(); return true; } return false; } private checkIsTargetEvent = () => { if (!this.isTargetAction) { this.hidePopup(); } else { this.isTargetAction = false; } }; public isShown: boolean = false; public onChangeSelection = () => { if (!this.jodit.options.toolbarInline || !this.jodit.isEditorMode()) { return; } if (this.hideIfCollapsed()) { return; } if (this.jodit.options.popup.selection !== undefined) { const sel = this.jodit.selection.sel; if (sel && sel.rangeCount) { this.isSelectionPopup = true; const range: Range = sel.getRangeAt(0); this.showPopup( () => offset(range, this.jodit, this.jodit.editorDocument), 'selection' ); } } }; afterInit(editor: IJodit) { this.toolbar = JoditToolbarCollection.makeCollection(editor); this.target = editor.create.div('jodit_toolbar_popup-inline-target'); this.container = editor.create.div(); this.popup = new Popup( editor, this.target, undefined, 'jodit_toolbar_popup-inline' ); editor.events .on( this.target, 'mousedown keydown touchstart', (e: MouseEvent) => { e.stopPropagation(); } ) .on('beforeOpenPopup hidePopup afterSetMode', this.hidePopup) .on('recalcPositionPopup', this.reCalcPosition) .on( 'getDiffButtons.mobile', (_toolbar: ToolbarCollection): void | string[] => { if (this.toolbar === _toolbar) { return splitArray(editor.options.buttons) .filter(name => name !== '|' && name !== '\n') .filter((name: string) => { return ( this.toolbar .getButtonsList() .indexOf(name) < 0 ); }); } } ) .on('selectionchange', this.onChangeSelection) .on('afterCommand afterExec', () => { if (this.isShown && this.isSelectionPopup) { this.onChangeSelection(); } }) .on( 'showPopup', (elm: HTMLElement | string, rect: () => IBound) => { const elementName: string = (typeof elm === 'string' ? elm : elm.nodeName ).toLowerCase(); this.isSelectionPopup = false; this.showPopup( rect, elementName, typeof elm === 'string' ? undefined : elm ); } ) .on('mousedown keydown touchstart', this.onSelectionStart) .on( [editor.ownerWindow, editor.editor], 'scroll resize', this.reCalcPosition ) .on( [editor.ownerWindow], 'mouseup keyup touchend', this.onSelectionEnd ) .on( [editor.ownerWindow], 'mousedown keydown touchstart', this.checkIsTargetEvent ); } beforeDestruct(editor: IJodit) { this.popup && this.popup.destruct(); delete this.popup; this.toolbar && this.toolbar.destruct(); delete this.toolbar; Dom.safeRemove(this.target); Dom.safeRemove(this.container); editor.events && editor.events .off([editor.ownerWindow], 'scroll resize', this.reCalcPosition) .off( [editor.ownerWindow], 'mouseup keyup touchend', this.onSelectionEnd ) .off( [editor.ownerWindow], 'mousedown keydown touchstart', this.checkIsTargetEvent ); } }