jodit
Version:
Jodit is awesome and usefully wysiwyg editor with filebrowser
819 lines (681 loc) • 19.2 kB
text/typescript
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2020 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import './dialog.less';
import autobind from 'autobind-decorator';
import { Config, OptionsDefault } from '../../config';
import {
IControlType,
IDialogOptions,
IDictionary,
IToolbarCollection,
IContainer,
IDialog,
ContentItem,
Content,
IViewOptions
} from '../../types/';
import { KEY_ESC } from '../../core/constants';
import {
$$,
asArray,
attr,
css,
extend,
hasContainer,
isArray,
isBoolean,
isFunction,
isString,
splitArray
} from '../../core/helpers/';
import { ViewWithToolbar } from '../../core/view/view-with-toolbar';
import { Dom } from '../../core/dom';
import { STATUSES } from '../../core/component';
import { eventEmitter, pluginSystem } from '../../core/global';
/**
* @property {object} dialog module settings {@link Dialog|Dialog}
* @property {int} dialog.zIndex=1000 Default Z-index for dialog window. {@link Dialog|Dialog}'s settings
* @property {boolean} dialog.resizable=true This dialog can resize by trigger
* @property {boolean} dialog.draggable=true This dialog can move by header
* @property {boolean} dialog.fullsize=false A dialog window will open in full screen by default
* @property {Buttons} dialog.buttons=['close.dialog', 'fullsize.dialog']
*/
declare module '../../config' {
interface Config {
dialog: IDialogOptions;
}
}
Config.prototype.dialog = {
extraButtons: [],
resizable: true,
draggable: true,
buttons: ['dialog.close'],
removeButtons: []
};
Config.prototype.controls.dialog = {
close: {
icon: 'cancel',
exec: dialog => {
(dialog as Dialog).close();
}
}
} as IDictionary<IControlType>;
/**
* Module to generate dialog windows
*
* @param {Object} parent Jodit main object
* @param {Object} [opt] Extend Options
*/
export class Dialog extends ViewWithToolbar implements IDialog {
/**
* @property {HTMLDivElement} resizer
*/
private resizer!: HTMLDivElement;
toolbar!: IToolbarCollection;
private offsetX?: number;
private offsetY?: number;
private destination: HTMLElement = document.body;
private destroyAfterClose: boolean = false;
private moved: boolean = false;
private iSetMaximization: boolean = false;
private resizable: boolean = false;
private draggable: boolean = false;
private startX: number = 0;
private startY: number = 0;
private startPoint = { x: 0, y: 0, w: 0, h: 0 };
private lockSelect = () => {
this.container.classList.add('jodit-dialog__box-moved');
};
private unlockSelect = () => {
this.container.classList.remove('jodit-dialog__box-moved');
};
private setElements(
root: HTMLDivElement | HTMLHeadingElement,
elements: Content
) {
const elements_list: HTMLElement[] = [];
asArray<ContentItem | ContentItem[] | IContainer>(elements).forEach(
(elm: ContentItem | ContentItem[] | IContainer): any => {
if (isArray(elm)) {
const div = this.c.div('jodit-dialog__column');
elements_list.push(div);
root.appendChild(div);
return this.setElements(div, elm);
}
let element: HTMLElement;
if (isString(elm)) {
element = this.c.fromHTML(elm);
} else {
element = hasContainer(elm) ? elm.container : elm;
}
elements_list.push(element);
if (element.parentNode !== root) {
root.appendChild(element);
}
}
);
Array.from(root.childNodes).forEach((elm: ChildNode) => {
if (elements_list.indexOf(elm as HTMLElement) === -1) {
root.removeChild(elm);
}
});
}
private onMouseUp(): void {
if (this.draggable || this.resizable) {
this.e.off(this.ow, 'mousemove', this.onMouseMove);
this.draggable = false;
this.resizable = false;
this.unlockSelect();
if (this.e) {
this.removeGlobalListeners();
/**
* Fired when dialog box is finished to resizing
* @event endResize
*/
this.e.fire(this, 'endResize endMove');
}
}
}
/**
*
* @param {MouseEvent} e
*/
private onHeaderMouseDown(e: MouseEvent): void {
const target: HTMLElement = e.target as HTMLElement;
if (
!this.o.draggable ||
(target && target.nodeName.match(/^(INPUT|SELECT)$/))
) {
return;
}
this.draggable = true;
this.startX = e.clientX;
this.startY = e.clientY;
this.startPoint.x = css(this.dialog, 'left') as number;
this.startPoint.y = css(this.dialog, 'top') as number;
this.setMaxZIndex();
e.preventDefault();
this.lockSelect();
this.addGlobalListeners();
if (this.e) {
/**
* Fired when dialog box is started moving
* @event startMove
*/
this.e.fire(this, 'startMove');
}
}
private onMouseMove(e: MouseEvent) {
if (this.draggable && this.o.draggable) {
this.setPosition(
this.startPoint.x + e.clientX - this.startX,
this.startPoint.y + e.clientY - this.startY
);
if (this.e) {
/**
* Fired when dialog box is moved
* @event move
* @param {int} dx Delta X
* @param {int} dy Delta Y
*/
this.e.fire(
this,
'move',
e.clientX - this.startX,
e.clientY - this.startY
);
}
e.stopImmediatePropagation();
e.preventDefault();
}
if (this.resizable && this.o.resizable) {
this.setSize(
this.startPoint.w + e.clientX - this.startX,
this.startPoint.h + e.clientY - this.startY
);
if (this.e) {
/**
* Fired when dialog box is resized
* @event resizeDialog
* @param {int} dx Delta X
* @param {int} dy Delta Y
*/
this.e.fire(
this,
'resizeDialog',
e.clientX - this.startX,
e.clientY - this.startY
);
}
e.stopImmediatePropagation();
e.preventDefault();
}
}
private onEsc(e: KeyboardEvent): void {
if (this.isOpened && e.key === KEY_ESC) {
const me = this.getMaxZIndexDialog();
if (me) {
me.close();
} else {
this.close();
}
e.stopImmediatePropagation();
}
}
private onResize = () => {
if (
this.options &&
this.o.resizable &&
!this.moved &&
this.isOpened &&
!this.offsetX &&
!this.offsetY
) {
this.setPosition();
}
};
private onResizerMouseDown(e: MouseEvent) {
this.resizable = true;
this.startX = e.clientX;
this.startY = e.clientY;
this.startPoint.w = this.dialog.offsetWidth;
this.startPoint.h = this.dialog.offsetHeight;
this.lockSelect();
this.addGlobalListeners();
if (this.e) {
/**
* Fired when dialog box is started resizing
* @event startResize
*/
this.e.fire(this, 'startResize');
}
}
private addGlobalListeners(): void {
const self = this;
self.e
.on(self.ow, 'mousemove', self.onMouseMove)
.on(self.container, 'close_dialog', self.close)
.on(self.ow, 'mouseup', self.onMouseUp);
}
private removeGlobalListeners(): void {
const self = this;
self.e
.off(self.ow, 'mousemove', self.onMouseMove)
.off(self.container, 'close_dialog', self.close)
.off(self.ow, 'mouseup', self.onMouseUp);
}
options!: IDialogOptions;
/**
* @property {HTMLDivElement} dialog
*/
dialog!: HTMLDivElement;
workplace!: HTMLDivElement;
private dialogbox_header!: HTMLHeadingElement;
private dialogbox_content!: HTMLDivElement;
private dialogbox_footer!: HTMLDivElement;
private dialogbox_toolbar!: HTMLDivElement;
/**
* Specifies the size of the window
*
* @param {number} [w] - The width of the window
* @param {number} [h] - The height of the window
*/
setSize(w?: number | string, h?: number | string): this {
if (w) {
css(this.dialog, 'width', w);
}
if (h) {
css(this.dialog, 'height', h);
}
return this;
}
/**
* Specifies the position of the upper left corner of the window . If x and y are specified,
* the window is centered on the center of the screen
*
* @param {Number} [x] - Position px Horizontal
* @param {Number} [y] - Position px Vertical
*/
setPosition(x?: number, y?: number): this {
const w: number = this.ow.innerWidth,
h: number = this.ow.innerHeight;
let left: number = w / 2 - this.dialog.offsetWidth / 2,
top: number = h / 2 - this.dialog.offsetHeight / 2;
if (left < 0) {
left = 0;
}
if (top < 0) {
top = 0;
}
if (x !== undefined && y !== undefined) {
this.offsetX = x;
this.offsetY = y;
this.moved = Math.abs(x - left) > 100 || Math.abs(y - top) > 100;
}
this.dialog.style.left = (x || left) + 'px';
this.dialog.style.top = (y || top) + 'px';
return this;
}
/**
* Specifies the dialog box title . It can take a string and an array of objects
*
* @param {string|string[]|Element|Element[]} content - A string or an HTML element ,
* or an array of strings and elements
* @example
* ```javascript
* var dialog = new Jodi.modules.Dialog(parent);
* dialog.setHeader('Hello world');
* dialog.setHeader(['Hello world', '<button>OK</button>', $('<div>some</div>')]);
* dialog.open();
* ```
*/
setHeader(content: Content): this {
this.setElements(this.dialogbox_header, content);
return this;
}
/**
* It specifies the contents of the dialog box. It can take a string and an array of objects
*
* @param {string|string[]|Element|Element[]} content A string or an HTML element ,
* or an array of strings and elements
* @example
* ```javascript
* var dialog = new Jodi.modules.Dialog(parent);
* dialog.setHeader('Hello world');
* dialog.setContent('<form onsubmit="alert(1);"><input type="text" /></form>');
* dialog.open();
* ```
*/
setContent(content: Content): this {
this.setElements(this.dialogbox_content, content);
return this;
}
/**
* Sets the bottom of the dialog. It can take a string and an array of objects
*
* @param {string|string[]|Element|Element[]} content - A string or an HTML element ,
* or an array of strings and elements
* @example
* ```javascript
* var dialog = new Jodi.modules.Dialog(parent);
* dialog.setHeader('Hello world');
* dialog.setContent('<form><input id="someText" type="text" /></form>');
* dialog.setFooter([
* $('<a class="jodit-button">OK</a>').click(function () {
* alert($('someText').val())
* dialog.close();
* })
* ]);
* dialog.open();
* ```
*/
setFooter(content: Content): this {
this.setElements(this.dialogbox_footer, content);
this.dialog.classList.toggle('jodit-dialog_footer_true', !!content);
return this;
}
/**
* Get dialog instance with maximum z-index displaying it on top of all the dialog boxes
*
* @return {Dialog}
*/
getMaxZIndexDialog(): IDialog {
let maxZi: number = 0,
dlg: IDialog,
zIndex: number,
res: IDialog = this;
$$('.jodit-dialog__box', this.destination).forEach(
(dialog: HTMLElement) => {
dlg = (dialog as any).component as Dialog;
zIndex = parseInt(css(dialog, 'zIndex') as string, 10);
if (dlg.isOpened && !isNaN(zIndex) && zIndex > maxZi) {
res = dlg;
maxZi = zIndex;
}
}
);
return res;
}
/**
* Sets the maximum z-index dialog box, displaying it on top of all the dialog boxes
*/
setMaxZIndex(): void {
let maxzi: number = 0,
zIndex: number = 0;
$$('.jodit-dialog__box', this.destination).forEach(dialog => {
zIndex = parseInt(css(dialog, 'zIndex') as string, 10);
maxzi = Math.max(isNaN(zIndex) ? 0 : zIndex, maxzi);
});
this.container.style.zIndex = (maxzi + 1).toString();
}
/**
* Expands the dialog on full browser window
*
* @param {boolean} condition true - fullsize
* @return {boolean} true - fullsize
*/
maximization(condition?: boolean): boolean {
if (typeof condition !== 'boolean') {
condition = !this.container.classList.contains(
'jodit-dialog__box_fullsize'
);
}
this.container.classList.toggle(
'jodit-dialog__box_fullsize',
condition
);
[this.destination, this.destination.parentNode].forEach(
(box: Node | null) => {
box &&
(box as HTMLElement).classList &&
(box as HTMLElement).classList.toggle(
'jodit-fullsize_box',
condition
);
}
);
this.iSetMaximization = condition;
return condition;
}
open(destroyAfterClose: boolean): this;
open(destroyAfterClose: boolean, modal: boolean): this;
open(
content?: Content,
title?: Content,
destroyAfterClose?: boolean,
modal?: boolean
): this;
/**
* It opens a dialog box to center it, and causes the two event.
*
* @param {string|string[]|Element|Element[]} [content] specifies the contents of the dialog box.
* Can be false или undefined. see {@link Dialog~setContent|setContent}
* @param {string|string[]|Element|Element[]} [title] specifies the title of the dialog box, @see setHeader
* @param {boolean} [destroyAfterClose] true - After closing the window , the destructor will be called.
* see {@link Dialog~destruct|destruct}
* @param {boolean} [modal] - true window will be opened in modal mode
* @fires {@link event:beforeOpen} id returns 'false' then the window will not open
* @fires {@link event:afterOpen}
*/
open(
contentOrClose?: Content | boolean,
titleOrModal?: Content | boolean,
destroyAfterClose?: boolean,
modal?: boolean
): this {
eventEmitter.fire('closeAllPopups hideHelpers');
/**
* Called before the opening of the dialog box
*
* @event beforeOpen
*/
if (this.e.fire(this, 'beforeOpen') === false) {
return this;
}
if (isBoolean(contentOrClose)) {
destroyAfterClose = contentOrClose;
}
if (isBoolean(titleOrModal)) {
modal = titleOrModal;
}
this.destroyAfterClose = destroyAfterClose === true;
const content = isBoolean(contentOrClose) ? undefined : contentOrClose;
const title = isBoolean(titleOrModal) ? undefined : titleOrModal;
if (title !== undefined) {
this.setHeader(title);
}
if (content) {
this.setContent(content);
}
this.container.classList.add('jodit-dialog_active');
this.isOpened = true;
this.setModal(modal);
this.destination.appendChild(this.container);
this.setPosition(this.offsetX, this.offsetY);
this.setMaxZIndex();
if (this.o.fullsize) {
this.maximization(true);
}
/**
* Called after the opening of the dialog box
* @event afterOpen
*/
this.e.fire('afterOpen', this);
return this;
}
/**
* Set modal mode
* @param modal
*/
setModal(modal: undefined | boolean): this {
this.container.classList.toggle('jodit-modal', Boolean(modal));
return this;
}
/**
* True, if dialog was opened
*/
isOpened: boolean = false;
/**
* Closes the dialog box , if you want to call the method {@link Dialog~destruct|destruct}
*
* @see destroy
* @method close
* @fires beforeClose
* @fires afterClose
* @example
* ```javascript
* //You can close dialog two ways
* var dialog = new Jodit.modules.Dialog();
* dialog.open('Hello world!', 'Title');
* var $close = Jodit.modules.helper.dom('<a href="javascript:void(0)" style="float:left;" class="jodit-button">
* <i class="icon icon-check"></i> ' + Jodit.prototype.i18n('Ok') + '</a>');
* $close.addEventListener('click', function () {
* dialog.close();
* });
* dialog.setFooter($close);
* // and second way, you can close dialog from content
* dialog.open('<a onclick="var event = doc.createEvent('HTMLEvents'); event.initEvent('close_dialog', true, true);
* this.dispatchEvent(event)">Close</a>', 'Title');
* ```
*/
close(e?: MouseEvent): this {
if (this.isDestructed || !this.isOpened) {
return this;
}
if (e) {
e.stopImmediatePropagation();
e.preventDefault();
}
/**
* Called up to close the window
*
* @event beforeClose
* @this {Dialog} current dialog
*/
if (this.e) {
this.e.fire('beforeClose', this);
}
Dom.safeRemove(this.container);
this?.container?.classList.remove('jodit-dialog_active');
this.isOpened = false;
if (this.iSetMaximization) {
this.maximization(false);
}
this.removeGlobalListeners();
if (this.destroyAfterClose) {
this.destruct();
}
/**
* It called after the window is closed
*
* @event afterClose
* @this {Dialog} current dialog
*/
this.e?.fire(this, 'afterClose');
this.e?.fire(this.ow, 'joditCloseDialog');
return this;
}
constructor(options?: IViewOptions) {
super(options);
const self: Dialog = this;
self.options = new OptionsDefault(
extend(
true,
{
toolbarButtonSize: 'middle'
},
Config.prototype.dialog,
options
)
) as IDialogOptions;
Dom.safeRemove(self.container);
self.container = this.c.fromHTML(
'<div style="z-index:' +
self.o.zIndex +
'" class="jodit jodit-dialog__box">' +
'<div class="jodit-dialog__overlay"></div>' +
'<div class="jodit-dialog">' +
'<div class="jodit-dialog__header non-selected">' +
'<div class="jodit-dialog__header-title"></div>' +
'<div class="jodit-dialog__header-toolbar"></div>' +
'</div>' +
'<div class="jodit-dialog__content"></div>' +
'<div class="jodit-dialog__footer"></div>' +
(self.o.resizable
? '<div class="jodit-dialog__resizer"></div>'
: '') +
'</div>' +
'</div>'
) as HTMLDivElement;
attr(self.container, 'role', 'dialog');
Object.defineProperty(self.container, 'component', {
value: this
});
self.container.classList.add(
`jodit_theme_${this.o.theme || 'default'}`
);
self.dialog = self.container.querySelector(
'.jodit-dialog'
) as HTMLDivElement;
self.resizer = self.container.querySelector(
'.jodit-dialog__resizer'
) as HTMLDivElement;
self.dialogbox_header = self.container.querySelector(
'.jodit-dialog__header>.jodit-dialog__header-title'
) as HTMLHeadingElement;
self.dialogbox_content = self.container.querySelector(
'.jodit-dialog__content'
) as HTMLDivElement;
self.dialogbox_footer = self.container.querySelector(
'.jodit-dialog__footer'
) as HTMLDivElement;
self.dialogbox_toolbar = self.container.querySelector(
'.jodit-dialog__header>.jodit-dialog__header-toolbar'
) as HTMLDivElement;
self.o.buttons &&
self.toolbar
.build(splitArray(self.o.buttons))
.appendTo(self.dialogbox_toolbar);
const headerBox: HTMLDivElement | null = self.container.querySelector(
'.jodit-dialog__header'
);
headerBox && self.e.on(headerBox, 'mousedown', self.onHeaderMouseDown);
if (self.o.resizable) {
self.e.on(self.resizer, 'mousedown', self.onResizerMouseDown);
}
const fullSize = pluginSystem.get('fullsize') as Function;
isFunction(fullSize) && fullSize(self);
self.setStatus(STATUSES.ready);
this.e
.on(this.ow, 'keydown', this.onEsc)
.on(this.ow, 'resize', this.onResize);
}
/**
* It destroys all objects created for the windows and also includes all the handlers for the window object
*/
destruct(): void {
if (this.isInDestruct) {
return;
}
this.setStatus(STATUSES.beforeDestruct);
if (this.isOpened) {
this.close();
}
if (this.events) {
this.removeGlobalListeners();
this.events
.on(this.ow, 'keydown', this.onEsc)
.on(this.ow, 'resize', this.onResize);
}
super.destruct();
}
}