jodit
Version:
Jodit is awesome and usefully wysiwyg editor with filebrowser
768 lines (674 loc) • 17.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 { 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
);
}
}