jodit
Version:
Jodit is an awesome and useful wysiwyg editor with filebrowser
339 lines (338 loc) • 12.3 kB
JavaScript
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import { IS_ES_NEXT } from "../constants.js";
import { clearTimeout, setTimeout } from "../helpers/async/index.js";
import { isAbortError } from "../helpers/checker/is-abort-error.js";
import { isFunction } from "../helpers/checker/is-function.js";
import { isNumber } from "../helpers/checker/is-number.js";
import { isPlainObject } from "../helpers/checker/is-plain-object.js";
import { isPromise } from "../helpers/checker/is-promise.js";
import { isString } from "../helpers/checker/is-string.js";
import { isVoid } from "../helpers/checker/is-void.js";
import { assert } from "../helpers/utils/assert.js";
import { abort } from "../helpers/utils/error/errors/abort-error.js";
export class Async {
constructor() {
var _a, _b, _c, _d, _e;
this.timers = new Map();
this.__callbacks = new Map();
this.__queueMicrotaskNative = (_a = queueMicrotask === null || queueMicrotask === void 0 ? void 0 : queueMicrotask.bind(window)) !== null && _a !== void 0 ? _a : Promise.resolve().then.bind(Promise.resolve());
this.promisesRejections = new Set();
this.__requestsIdle = new Set();
this.__controllers = new Set();
this.__requestsRaf = new Set();
this.__requestIdleCallbackNative = (_c = (_b = window['requestIdleCallback']) === null || _b === void 0 ? void 0 : _b.bind(window)) !== null && _c !== void 0 ? _c : ((callback, options) => {
var _a;
const start = Date.now();
return this.setTimeout(() => {
callback({
didTimeout: false,
timeRemaining: () => Math.max(0, 50 - (Date.now() - start))
});
}, (_a = options === null || options === void 0 ? void 0 : options.timeout) !== null && _a !== void 0 ? _a : 1);
});
this.__cancelIdleCallbackNative = (_e = (_d = window['cancelIdleCallback']) === null || _d === void 0 ? void 0 : _d.bind(window)) !== null && _e !== void 0 ? _e : ((request) => {
this.clearTimeout(request);
});
this.isDestructed = false;
}
delay(timeout) {
return this.promise(resolve => this.setTimeout(resolve, timeout));
}
setTimeout(callback, timeout, ...args) {
if (this.isDestructed) {
return 0;
}
let options = {};
if (isVoid(timeout)) {
timeout = 0;
}
if (!isNumber(timeout)) {
options = timeout;
timeout = options.timeout || 0;
}
if (options.label) {
this.clearLabel(options.label);
}
const timer = setTimeout(callback, timeout, ...args);
const key = options.label || timer;
this.timers.set(key, timer);
this.__callbacks.set(key, callback);
return timer;
}
updateTimeout(label, timeout) {
assert(label && this.timers.has(label), 'Label does not exist');
if (!label || !this.timers.has(label)) {
return null;
}
const callback = this.__callbacks.get(label);
assert(isFunction(callback), 'Callback is not a function');
return this.setTimeout(callback, { label, timeout });
}
clearLabel(label) {
if (label && this.timers.has(label)) {
clearTimeout(this.timers.get(label));
this.timers.delete(label);
this.__callbacks.delete(label);
}
}
clearTimeout(timerOrLabel) {
if (isString(timerOrLabel)) {
return this.clearLabel(timerOrLabel);
}
clearTimeout(timerOrLabel);
this.timers.delete(timerOrLabel);
this.__callbacks.delete(timerOrLabel);
}
/**
* Debouncing enforces that a function not be called again until a certain amount of time has passed without
* it being called. As in "execute this function only if 100 milliseconds have passed without it being called."
*
* @example
* ```javascript
* var jodit = Jodit.make('.editor');
* jodit.e.on('mousemove', jodit.async.debounce(() => {
* // Do expensive things
* }, 100));
* ```
*/
debounce(fn, timeout, firstCallImmediately = false) {
let timer = 0, fired = false;
const promises = [];
const callFn = (...args) => {
if (!fired) {
timer = 0;
const res = fn(...args);
fired = true;
if (promises.length) {
const runPromises = () => {
promises.forEach(res => res());
promises.length = 0;
};
isPromise(res) ? res.finally(runPromises) : runPromises();
}
}
};
const onFire = (...args) => {
fired = false;
if (!timeout) {
callFn(...args);
}
else {
if (!timer && firstCallImmediately) {
callFn(...args);
}
clearTimeout(timer);
timer = this.setTimeout(() => callFn(...args), isFunction(timeout) ? timeout() : timeout);
this.timers.set(fn, timer);
}
};
return isPlainObject(timeout) && timeout.promisify
? (...args) => {
const promise = this.promise(res => {
promises.push(res);
}).catch((e) => {
if (isAbortError(e)) {
return null;
}
throw e;
});
onFire(...args);
return promise;
}
: onFire;
}
microDebounce(fn, firstCallImmediately = false) {
let scheduled = false;
let needCall = true;
let savedArgs;
return ((...args) => {
savedArgs = args;
if (scheduled) {
needCall = true;
return;
}
needCall = true;
if (firstCallImmediately) {
needCall = false;
fn(...savedArgs);
}
scheduled = true;
this.__queueMicrotaskNative(() => {
scheduled = false;
if (this.isDestructed) {
return;
}
needCall && fn(...savedArgs);
});
});
}
/**
* Throttling enforces a maximum number of times a function can be called over time.
* As in "execute this function at most once every 100 milliseconds."
*
* @example
* ```javascript
* var jodit = Jodit.make('.editor');
* jodit.e.on(document.body, 'scroll', jodit.async.throttle(function() {
* // Do expensive things
* }, 100));
* ```
*/
throttle(fn, timeout, ignore = false) {
let timer = null, needInvoke, callee, lastArgs;
return (...args) => {
needInvoke = true;
lastArgs = args;
if (!timeout) {
fn(...lastArgs);
return;
}
if (!timer) {
callee = () => {
if (needInvoke) {
fn(...lastArgs);
needInvoke = false;
timer = this.setTimeout(callee, isFunction(timeout) ? timeout() : timeout);
this.timers.set(callee, timer);
}
else {
timer = null;
}
};
callee();
}
};
}
promise(executor) {
let rejectCallback = () => { };
const promise = new Promise((resolve, reject) => {
rejectCallback = () => reject(abort('Abort async'));
this.promisesRejections.add(rejectCallback);
executor(resolve, reject);
});
if (!promise.finally && typeof process !== 'undefined' && !IS_ES_NEXT) {
promise.finally = (onfinally) => {
promise.then(onfinally).catch(onfinally);
return promise;
};
}
promise
.finally(() => {
this.promisesRejections.delete(rejectCallback);
})
.catch(() => null);
promise.rejectCallback = rejectCallback;
return promise;
}
/**
* Get Promise status
*/
promiseState(p) {
if (p.status) {
return p.status;
}
// Hi IE11
if (!Promise.race) {
return new Promise(resolve => {
p.then(v => {
resolve('fulfilled');
return v;
}, e => {
resolve('rejected');
throw e;
});
this.setTimeout(() => {
resolve('pending');
}, 100);
});
}
const t = {};
return Promise.race([p, t]).then(v => (v === t ? 'pending' : 'fulfilled'), () => 'rejected');
}
requestIdleCallback(callback, options = { timeout: 100 }) {
const request = this.__requestIdleCallbackNative(callback, options);
this.__requestsIdle.add(request);
return request;
}
requestIdlePromise(options) {
return this.promise(res => {
const request = this.requestIdleCallback(() => res(request), options);
});
}
/**
* Try to use scheduler.postTask if it is available https://wicg.github.io/scheduling-apis/
*/
schedulerPostTask(task, options = {
delay: 0,
priority: 'user-visible'
}) {
const controller = new AbortController();
if (options.signal) {
options.signal.addEventListener('abort', () => controller.abort());
}
this.__controllers.add(controller);
// @ts-ignore
if (typeof globalThis.scheduler !== 'undefined') {
const scheduler = globalThis.scheduler;
const promise = scheduler.postTask(task, {
...options,
signal: controller.signal
});
promise
.finally(() => {
this.__controllers.delete(controller);
})
.catch(() => null);
return promise;
}
return this.promise((resolve, reject) => {
const timeout = this.setTimeout(() => {
try {
resolve(task());
}
catch (e) {
reject(e);
}
this.__controllers.delete(controller);
}, options.delay || 1);
controller.signal.addEventListener('abort', () => {
this.clearTimeout(timeout);
this.__controllers.delete(controller);
reject(abort());
});
});
}
schedulerYield() {
return this.schedulerPostTask(() => { }, { priority: 'user-visible' });
}
cancelIdleCallback(request) {
this.__requestsIdle.delete(request);
return this.__cancelIdleCallbackNative(request);
}
requestAnimationFrame(callback) {
const request = requestAnimationFrame(callback);
this.__requestsRaf.add(request);
return request;
}
cancelAnimationFrame(request) {
this.__requestsRaf.delete(request);
cancelAnimationFrame(request);
}
clear() {
this.__requestsIdle.forEach(key => this.cancelIdleCallback(key));
this.__requestsRaf.forEach(key => this.cancelAnimationFrame(key));
this.__controllers.forEach(controller => controller.abort());
this.timers.forEach(key => clearTimeout(this.timers.get(key)));
this.timers.clear();
this.promisesRejections.forEach(reject => reject());
this.promisesRejections.clear();
}
destruct() {
this.clear();
this.isDestructed = true;
}
}