jodit
Version:
Jodit is awesome and usefully wysiwyg editor with filebrowser
658 lines (570 loc) • 16.1 kB
text/typescript
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* License GNU General License version 2 or later;
* Copyright 2013-2019 Valeriy Chupurnov https://xdsoft.net
*/
/**
* The module editor's event manager
*/
import { CallbackFunction, EventHandlerBlock } from '../../types';
import { defaultNameSpace, EventHandlersStore } from './store';
import { IEventsNative } from '../../types/events';
export class EventsNative implements IEventsNative {
private __key: string = '__JoditEventsNativeNamespaces';
private doc: Document = document;
private __stopped: EventHandlerBlock[][] = [];
private eachEvent(
events: string,
callback: (event: string, namespace: string) => void
) {
const eventParts: string[] = events.split(/[\s,]+/);
eventParts.forEach((eventNameSpace: string) => {
const eventAndNameSpace: string[] = eventNameSpace.split('.');
const namespace: string = eventAndNameSpace[1] || defaultNameSpace;
callback.call(this, eventAndNameSpace[0], namespace);
});
}
private getStore(subject: any): EventHandlersStore {
if (subject[this.__key] === undefined) {
const store: EventHandlersStore = new EventHandlersStore();
Object.defineProperty(subject, this.__key, {
enumerable: false,
configurable: true,
value: store
});
}
return subject[this.__key];
}
private clearStore(subject: any) {
if (subject[this.__key] !== undefined) {
delete subject[this.__key];
}
}
private prepareEvent = (
event: TouchEvent | MouseEvent | ClipboardEvent
) => {
if (event.cancelBubble) {
return;
}
if (
event.type.match(/^touch/) &&
(event as TouchEvent).changedTouches &&
(event as TouchEvent).changedTouches.length
) {
['clientX', 'clientY', 'pageX', 'pageY'].forEach((key: string) => {
Object.defineProperty(event, key, {
value: ((event as TouchEvent).changedTouches[0] as any)[
key
],
configurable: true,
enumerable: true
});
});
}
if (!(event as any).originalEvent) {
(event as any).originalEvent = event;
}
if (
event.type === 'paste' &&
(event as ClipboardEvent).clipboardData === undefined &&
(this.doc.defaultView as any).clipboardData
) {
Object.defineProperty(event, 'clipboardData', {
get: () => {
return (this.doc.defaultView as any).clipboardData;
},
configurable: true,
enumerable: true
});
}
};
private triggerNativeEvent(
element: Document | Element | HTMLElement | Window,
event: string | Event | MouseEvent
) {
const evt: Event = this.doc.createEvent('HTMLEvents');
if (typeof event === 'string') {
evt.initEvent(event, true, true);
} else {
evt.initEvent(event.type, event.bubbles, event.cancelable);
[
'screenX',
'screenY',
'clientX',
'clientY',
'target',
'srcElement',
'currentTarget',
'timeStamp',
'which',
'keyCode'
].forEach(property => {
Object.defineProperty(evt, property, {
value: (event as any)[property],
enumerable: true
});
});
Object.defineProperty(evt, 'originalEvent', {
value: event,
enumerable: true
});
}
element.dispatchEvent(evt);
}
private removeStop(currentBlocks: EventHandlerBlock[]) {
if (currentBlocks) {
const index: number = this.__stopped.indexOf(currentBlocks);
index !== -1 && this.__stopped.splice(index, 1);
}
}
private isStopped(currentBlocks: EventHandlerBlock[]): boolean {
return (
currentBlocks !== undefined &&
this.__stopped.indexOf(currentBlocks) !== -1
);
}
/**
* Get current event name
*
* @example
* ```javascript
* parent.events.on('openDialog closeDialog', function () {
* if (parent.events.current === 'closeDialog') {
* alert('Dialog was closed');
* } else {
* alert('Dialog was opened');
* }
* });
* ```
*/
current: string[] = [];
/**
* Sets the handler for the specified event ( Event List ) for a given element .
*
* @param {object|string} subjectOrEvents - The object for which toWYSIWYG set an event handler
* @param {string|Function} eventsOrCallback - List of events , separated by a space or comma
* @param {function} [handlerOrSelector] - The event handler
* @param {selector} [selector] - Selector for capturing
* @param {Boolean} [onTop=false] - Set handler in first
*
* @example
* ```javascript
* // set global handler
* parent.on('beforeCommand', function (command) {
* alert('command');
* });
* ```
* * @example
* ```javascript
* // set global handler
* parent.on(document.body, 'click', function (e) {
* alert(this.href);
* }, 'a');
* ```
*/
on(
subjectOrEvents: string,
eventsOrCallback: CallbackFunction,
handlerOrSelector?: void,
selector?: string,
onTop?: boolean
): EventsNative;
on(
subjectOrEvents: object,
eventsOrCallback: string,
handlerOrSelector: CallbackFunction,
selector?: string,
onTop?: boolean
): EventsNative;
on(
subjectOrEvents: object | string,
eventsOrCallback: string | CallbackFunction,
handlerOrSelector?: CallbackFunction | void,
selector?: string,
onTop: boolean = false
): EventsNative {
const subject: object =
typeof subjectOrEvents === 'string' ? this : subjectOrEvents;
const events: string =
typeof eventsOrCallback === 'string'
? eventsOrCallback
: (subjectOrEvents as string);
let callback = handlerOrSelector as CallbackFunction;
if (callback === undefined && typeof eventsOrCallback === 'function') {
callback = eventsOrCallback as CallbackFunction;
}
const store: EventHandlersStore = this.getStore(subject);
if (typeof events !== 'string' || events === '') {
throw new Error('Need events names');
}
if (typeof callback !== 'function') {
throw new Error('Need event handler');
}
if (Array.isArray(subject)) {
subject.forEach((subj: object) => {
this.on(subj, events, callback, selector);
});
return this;
}
const isDOMElement: boolean =
typeof (subject as any).addEventListener === 'function',
self: EventsNative = this;
let syntheticCallback = function(
this: any,
event: MouseEvent | TouchEvent
) {
return callback && callback.apply(this, arguments as any);
};
if (isDOMElement) {
syntheticCallback = function(
this: any,
event: MouseEvent | TouchEvent
): void | false {
self.prepareEvent(event as TouchEvent);
if (callback && callback.call(this, event) === false) {
event.preventDefault();
event.stopImmediatePropagation();
return false;
}
return;
};
if (selector) {
syntheticCallback = function(
this: any,
event: TouchEvent | MouseEvent
): false | void {
self.prepareEvent(event);
let node: Element | null = event.target as any;
while (node && node !== this) {
if (node.matches(selector as string)) {
Object.defineProperty(event, 'target', {
value: node,
configurable: true,
enumerable: true
});
if (
callback &&
callback.call(node, event) === false
) {
event.preventDefault();
return false;
}
return;
}
node = node.parentNode as Element | null;
}
};
}
}
this.eachEvent(
events,
(event: string, namespace: string): void => {
if (event === '') {
throw new Error('Need event name');
}
if (store.indexOf(event, namespace, callback) === false) {
const block: EventHandlerBlock = {
event,
originalCallback: callback,
syntheticCallback
};
store.set(event, namespace, block, onTop);
if (isDOMElement) {
(subject as HTMLElement).addEventListener(
event,
syntheticCallback as EventListener,
false
);
}
}
}
);
return this;
}
/**
* Disable all handlers specified event ( Event List ) for a given element. Either a specific event handler.
*
* @param {object} subjectOrEvents - The object which is disabled handlers
* @param {string|Function} [eventsOrCallback] - List of events, separated by a space or comma , which is necessary
* toWYSIWYG disable the handlers for a given object
* @param {function} [handler] - Specific event handler toWYSIWYG be removed
*
* @example
* ```javascript
* var a = {name: "Anton"};
* parent.events.on(a, 'open', function () {
* alert(this.name);
* });
*
* parent.events.fire(a, 'open');
* parent.events.off(a, 'open');
* var b = {name: "Ivan"}, hndlr = function () {
* alert(this.name);
* };
* parent.events.on(b, 'open close', hndlr);
* parent.events.fire(a, 'open');
* parent.events.off(a, 'open', hndlr);
* parent.events.fire(a, 'close');
* parent.events.on('someGlobalEvents', function () {
* console.log(this); // parent
* });
* parent.events.fire('someGlobalEvents');
* parent.events.off('someGlobalEvents');
* ```
*/
off(subjectOrEvents: string, eventsOrCallback?: () => void): EventsNative;
off(
subjectOrEvents: object,
eventsOrCallback?: string,
handler?: () => void
): EventsNative;
off(
subjectOrEvents: object | string,
eventsOrCallback?: string | (() => void),
handler?: () => void
): EventsNative {
const subject: object =
typeof subjectOrEvents === 'string' ? this : subjectOrEvents;
const events: string =
typeof eventsOrCallback === 'string'
? eventsOrCallback
: (subjectOrEvents as string);
const store: EventHandlersStore = this.getStore(subject);
let callback: () => void = handler as () => void;
if (typeof events !== 'string' || !events) {
store.namespaces().forEach((namespace: string) => {
this.off(subject, '.' + namespace);
});
this.clearStore(subject);
return this;
}
if (callback === undefined && typeof eventsOrCallback === 'function') {
callback = eventsOrCallback as () => void;
}
const isDOMElement: boolean =
typeof (subject as any).removeEventListener === 'function',
removeEventListener = (block: EventHandlerBlock) => {
if (isDOMElement) {
(subject as HTMLElement).removeEventListener(
block.event,
block.syntheticCallback as EventListener,
false
);
}
},
removeCallbackFromNameSpace = (
event: string,
namespace: string
) => {
if (event !== '') {
const blocks: EventHandlerBlock[] | void = store.get(
event,
namespace
);
if (blocks && blocks.length) {
if (typeof callback !== 'function') {
blocks.forEach(removeEventListener);
blocks.length = 0;
} else {
const index: number | false = store.indexOf(
event,
namespace,
callback
);
if (index !== false) {
removeEventListener(blocks[index]);
blocks.splice(index, 1);
}
}
}
} else {
store.events(namespace).forEach((eventName: string) => {
if (eventName !== '') {
removeCallbackFromNameSpace(eventName, namespace);
}
});
}
};
this.eachEvent(
events,
(event: string, namespace: string): void => {
if (namespace === defaultNameSpace) {
store.namespaces().forEach((name: string) => {
removeCallbackFromNameSpace(event, name);
});
} else {
removeCallbackFromNameSpace(event, namespace);
}
}
);
return this;
}
/**
* Stop execute all another listeners for this event
*
* @param subjectOrEvents
* @param eventsList
*/
stopPropagation(subjectOrEvents: string): void;
stopPropagation(subjectOrEvents: object, eventsList: string): void;
stopPropagation(subjectOrEvents: object | string, eventsList?: string) {
const subject: object =
typeof subjectOrEvents === 'string' ? this : subjectOrEvents;
const events: string =
typeof subjectOrEvents === 'string'
? subjectOrEvents
: (eventsList as string);
if (typeof events !== 'string') {
throw new Error('Need event names');
}
const store: EventHandlersStore = this.getStore(subject);
this.eachEvent(
events,
(event: string, namespace: string): void => {
const blocks: EventHandlerBlock[] | void = store.get(
event,
namespace
);
if (blocks) {
this.__stopped.push(blocks);
}
if (namespace === defaultNameSpace) {
store
.namespaces(true)
.forEach(ns =>
this.stopPropagation(subject, event + '.' + ns)
);
}
}
);
}
/**
* Sets the handler for the specified event (Event List) for a given element .
*
* @param {object|string} subjectOrEvents - The object which is caused by certain events
* @param {string|Array} eventsList - List of events , separated by a space or comma
* @param {Array} [args] - Options for the event handler
* @return {boolean} `false` if one of the handlers return `false`
* @example
* ```javascript
* var dialog = new Jodit.modules.Dialog();
* parent.events.on('afterClose', function () {
* dialog.destruct(); // will be removed from DOM
* });
* dialog.open('Hello world!!!');
* ```
* or you can trigger native browser listener
* ```javascript
* var events = new Jodit.modules.EventsNative();
* events.on(document.body, 'click',function (event) {
* alert('click on ' + event.target.id );
* });
* events.fire(document.body.querySelector('div'), 'click');
* ```
*
*/
fire(subjectOrEvents: string, eventsList?: any, ...args: any[]): any;
fire(
subjectOrEvents: object,
eventsList: string | Event,
...args: any[]
): any;
fire(
subjectOrEvents: object | string,
eventsList?: string | any | Event,
...args: any[]
): any {
let
result: any = undefined,
result_value: any;
const subject: object =
typeof subjectOrEvents === 'string' ? this : subjectOrEvents;
const events: string =
typeof subjectOrEvents === 'string'
? subjectOrEvents
: (eventsList as string);
const argumentsList: any[] =
typeof subjectOrEvents === 'string' ? [eventsList, ...args] : args;
const isDOMElement: boolean =
typeof (subject as any).dispatchEvent === 'function';
if (!isDOMElement && typeof events !== 'string') {
throw new Error('Need events names');
}
const store: EventHandlersStore = this.getStore(subject);
if (typeof events !== 'string' && isDOMElement) {
this.triggerNativeEvent(subject as HTMLElement, eventsList);
} else {
this.eachEvent(
events,
(event: string, namespace: string): void => {
if (isDOMElement) {
this.triggerNativeEvent(subject as HTMLElement, event);
} else {
const blocks: EventHandlerBlock[] | void = store.get(
event,
namespace
);
if (blocks) {
try {
blocks.every(
(block: EventHandlerBlock): boolean => {
if (this.isStopped(blocks)) {
return false;
}
this.current.push(event);
result_value = block.syntheticCallback.apply(
subject,
argumentsList
);
this.current.pop();
if (result_value !== undefined) {
result = result_value;
}
return true;
}
);
} finally {
this.removeStop(blocks);
}
}
if (namespace === defaultNameSpace && !isDOMElement) {
store
.namespaces()
.filter(ns => ns !== namespace)
.forEach((ns: string) => {
const result_second: any = this.fire.apply(
this,
[
subject,
event + '.' + ns,
...argumentsList
]
);
if (result_second !== undefined) {
result = result_second;
}
});
}
}
}
);
}
return result;
}
private isDestructed: boolean = false;
constructor(doc?: Document) {
if (doc) {
this.doc = doc;
}
this.__key += new Date().getTime();
}
destruct() {
if (!this.isDestructed) {
return;
}
this.isDestructed = true;
this.off(this);
this.getStore(this).clear();
delete (<any>this)[this.__key];
}
}