jodit
Version:
Jodit is awesome and usefully wysiwyg editor with filebrowser
766 lines (663 loc) • 19.4 kB
text/typescript
/*!
* 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 { IDialogOptions } from '../../types/dialog';
import { KEY_ESC } from '../../constants';
import { IDictionary, IJodit } from '../../types';
import { IControlType } from '../../types/toolbar';
import { IViewBased } from '../../types/view';
import { $$, asArray, css } from '../helpers/';
import { View } from '../view/view';
import { Dom } from '../Dom';
import { isJoditObject } from '../helpers/checker/isJoditObject';
/**
* @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 = {
resizable: true,
draggable: true,
buttons: ['dialog.close'],
removeButtons: []
};
Config.prototype.controls.dialog = {
close: {
icon: 'cancel',
exec: dialog => {
(dialog as Dialog).close();
}
},
fullsize: {
icon: 'fullsize',
getLabel: (editor, btn: IControlType, button) => {
if (
Config.prototype.controls.fullsize &&
Config.prototype.controls.fullsize.getLabel &&
typeof Config.prototype.controls.fullsize.getLabel ===
'function'
) {
return Config.prototype.controls.fullsize.getLabel(
editor,
btn,
button
);
}
},
exec: dialog => {
dialog.toggleFullSize();
}
}
} as IDictionary<IControlType>;
type Content = string | HTMLElement | Array<string | HTMLElement>;
/**
* Module to generate dialog windows
*
* @param {Object} parent Jodit main object
* @param {Object} [opt] Extend Options
*/
export class Dialog extends View {
/**
* @property {HTMLDivElement} resizer
*/
private resizer: HTMLDivElement;
public toolbar: ToolbarCollection;
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(elements).forEach(elm => {
const element: HTMLElement =
typeof elm === 'string' ? this.create.fromHTML(elm) : 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 = () => {
if (this.draggable || this.resizable) {
this.draggable = false;
this.resizable = false;
this.unlockSelect();
if (this.jodit && this.jodit.events) {
/**
* Fired when dialog box is finished to resizing
* @event endResize
*/
this.jodit.events.fire(this, 'endResize endMove');
}
}
};
/**
*
* @param {MouseEvent} e
*/
private onHeaderMouseDown = (e: MouseEvent) => {
const target: HTMLElement = e.target as HTMLElement;
if (
!this.options.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();
if (this.jodit && this.jodit.events) {
/**
* Fired when dialog box is started moving
* @event startMove
*/
this.jodit.events.fire(this, 'startMove');
}
};
private onMouseMove = (e: MouseEvent) => {
if (this.draggable && this.options.draggable) {
this.setPosition(
this.startPoint.x + e.clientX - this.startX,
this.startPoint.y + e.clientY - this.startY
);
if (this.jodit && this.jodit.events) {
/**
* Fired when dialog box is moved
* @event move
* @param {int} dx Delta X
* @param {int} dy Delta Y
*/
this.jodit.events.fire(
this,
'move',
e.clientX - this.startX,
e.clientY - this.startY
);
}
e.stopImmediatePropagation();
e.preventDefault();
}
if (this.resizable && this.options.resizable) {
this.setSize(
this.startPoint.w + e.clientX - this.startX,
this.startPoint.h + e.clientY - this.startY
);
if (this.jodit && this.jodit.events) {
/**
* Fired when dialog box is resized
* @event resizeDialog
* @param {int} dx Delta X
* @param {int} dy Delta Y
*/
this.jodit.events.fire(
this,
'resizeDialog',
e.clientX - this.startX,
e.clientY - this.startY
);
}
e.stopImmediatePropagation();
e.preventDefault();
}
};
/**
*
* @param {MouseEvent} e
*/
private onKeyDown = (e: KeyboardEvent) => {
if (this.isOpened() && e.which === KEY_ESC) {
const me = this.getMaxZIndexDialog();
if (me) {
me.close();
} else {
this.close();
}
e.stopImmediatePropagation();
}
};
private onResize = () => {
if (
this.options &&
this.options.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();
if (this.jodit.events) {
/**
* Fired when dialog box is started resizing
* @event startResize
*/
this.jodit.events.fire(this, 'startResize');
}
}
public options: IDialogOptions;
/**
* @property {HTMLDivElement} dialog
*/
public dialog: HTMLDivElement;
public dialogbox_header: HTMLHeadingElement;
public dialogbox_content: HTMLDivElement;
public dialogbox_footer: HTMLDivElement;
public dialogbox_toolbar: HTMLDivElement;
public document: Document = document;
public window: Window = window;
/**
* Specifies the size of the window
*
* @param {number} [w] - The width of the window
* @param {number} [h] - The height of the window
*/
public setSize(w?: number | string, h?: number | string) {
if (w) {
css(this.dialog, 'width', w);
}
if (h) {
css(this.dialog, 'height', h);
}
}
/**
* 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
*/
public setPosition(x?: number, y?: number) {
const
w: number = this.window.innerWidth,
h: number = this.window.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';
}
/**
* 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.setTitle('Hello world');
* dialog.setTitle(['Hello world', '<button>OK</button>', $('<div>some</div>')]);
* dialog.open();
* ```
*/
public setTitle(content: Content) {
this.setElements(this.dialogbox_header, content);
}
/**
* 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.setTitle('Hello world');
* dialog.setContent('<form onsubmit="alert(1);"><input type="text" /></form>');
* dialog.open();
* ```
*/
public setContent(content: Content) {
this.setElements(this.dialogbox_content, content);
}
/**
* 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.setTitle('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();
* ```
*/
public setFooter(content: Content) {
this.setElements(this.dialogbox_footer, content);
this.dialog.classList.toggle('with_footer', !!content);
}
/**
* Return current Z-index
* @return {number}
*/
public getZIndex(): number {
return parseInt(this.container.style.zIndex || '0', 10);
}
/**
* Get dialog instance with maximum z-index displaying it on top of all the dialog boxes
*
* @return {Dialog}
*/
public getMaxZIndexDialog() {
let maxzi: number = 0,
dlg: Dialog,
zIndex: number,
res: Dialog = this;
$$('.jodit_dialog_box', this.destination).forEach(
(dialog: HTMLElement) => {
dlg = (dialog as any).__jodit_dialog 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
*/
public setMaxZIndex() {
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
*/
public 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;
}
/**
* 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 setTitle
* @param {boolean} [destroyAfter] 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}
*/
public open(
content?: Content,
title?: Content,
destroyAfter?: boolean,
modal?: boolean
) {
/**
* Called before the opening of the dialog box
*
* @event beforeOpen
*/
if (this.jodit && this.jodit.events) {
if (this.jodit.events.fire(this, 'beforeOpen') === false) {
return;
}
}
this.destroyAfterClose = destroyAfter === true;
if (title !== undefined) {
this.setTitle(title);
}
if (content) {
this.setContent(content);
}
this.container.classList.add('active');
if (modal) {
this.container.classList.add('jodit_modal');
}
this.setPosition(this.offsetX, this.offsetY);
this.setMaxZIndex();
if (this.options.fullsize) {
this.maximization(true);
}
/**
* Called after the opening of the dialog box
*
* @event afterOpen
*/
if (this.jodit && this.jodit.events) {
this.jodit.events.fire('afterOpen', this);
}
}
/**
* Open if the current window
*
* @return {boolean} - true window open
*/
public isOpened(): boolean {
return (
!this.isDestructed &&
this.container &&
this.container.classList.contains('active')
);
}
/**
* 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');
* ```
*/
public close = (e?: MouseEvent) => {
if (this.isDestructed) {
return;
}
if (e) {
e.stopImmediatePropagation();
e.preventDefault();
}
/**
* Called up to close the window
*
* @event beforeClose
* @this {Dialog} current dialog
*/
if (this.jodit && this.jodit.events) {
this.jodit.events.fire('beforeClose', this);
}
this.container &&
this.container.classList &&
this.container.classList.remove('active');
if (this.iSetMaximization) {
this.maximization(false);
}
if (this.destroyAfterClose) {
this.destruct();
}
/**
* It called after the window is closed
*
* @event afterClose
* @this {Dialog} current dialog
*/
if (this.jodit && this.jodit.events) {
this.jodit.events.fire(this, 'afterClose');
this.jodit.events.fire(this.ownerWindow, 'jodit_close_dialog');
}
};
constructor(jodit?: IViewBased, options: any = Config.prototype.dialog) {
super(jodit, options);
if (isJoditObject(jodit)) {
this.window = jodit.ownerWindow;
this.document = jodit.ownerDocument;
jodit.events.on('beforeDestruct', () => {
this.destruct();
});
}
const self: Dialog = this;
const opt =
jodit && (jodit as View).options
? (jodit as IJodit).options.dialog
: Config.prototype.dialog;
self.options = { ...opt, ...self.options } as IDialogOptions;
self.container = this.create.fromHTML(
'<div style="z-index:' +
self.options.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.options.resizable
? '<div class="jodit_dialog_resizer"></div>'
: '') +
'</div>' +
'</div>'
) as HTMLDivElement;
if (jodit && (<IViewBased>jodit).id) {
self.container.setAttribute(
'data-editor_id',
(<IViewBased>jodit).id
);
}
Object.defineProperty(self.container, '__jodit_dialog', {
value: self
});
self.dialog = self.container.querySelector(
'.jodit_dialog'
) as HTMLDivElement;
self.resizer = self.container.querySelector(
'.jodit_dialog_resizer'
) as HTMLDivElement;
if (self.jodit && self.jodit.options && self.jodit.options.textIcons) {
self.container.classList.add('jodit_text_icons');
}
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.destination.appendChild(self.container);
self.container.addEventListener('close_dialog', self.close as any);
self.toolbar = JoditToolbarCollection.makeCollection(self);
self.toolbar.build(self.options.buttons, self.dialogbox_toolbar);
self.events
.on(this.window, 'mousemove', self.onMouseMove)
.on(this.window, 'mouseup', self.onMouseUp)
.on(this.window, 'keydown', self.onKeyDown)
.on(this.window, 'resize', self.onResize);
const headerBox: HTMLDivElement | null = self.container.querySelector(
'.jodit_dialog_header'
);
headerBox &&
headerBox.addEventListener(
'mousedown',
self.onHeaderMouseDown.bind(self)
);
if (self.options.resizable) {
self.resizer.addEventListener(
'mousedown',
self.onResizerMouseDown.bind(self)
);
}
Jodit.plugins.fullsize(self);
}
/**
* It destroys all objects created for the windows and also includes all the handlers for the window object
*/
destruct() {
if (this.isDestructed) {
return;
}
if (this.toolbar) {
this.toolbar.destruct();
delete this.toolbar;
}
if (this.events) {
this.events
.off(this.window, 'mousemove', this.onMouseMove)
.off(this.window, 'mouseup', this.onMouseUp)
.off(this.window, 'keydown', this.onKeyDown)
.off(this.window, 'resize', this.onResize);
}
if (!this.jodit && this.events) {
this.events.destruct();
delete this.events;
}
if (this.container) {
Dom.safeRemove(this.container);
delete this.container;
}
super.destruct();
}
}
import { Jodit } from '../../Jodit';
import { JoditToolbarCollection, ToolbarCollection } from '..';