editor.js/src/components/utils.ts
Peter Savchenko 63eeec0f3b
Release / 2.18 (#1181)
Co-authored-by: George Berezhnoy <gohabereg@users.noreply.github.com>
Co-authored-by: Georgy Berezhnoy <gohabereg@gmail.com>
Co-authored-by: tasuku-s <tasuku@freemind.co.jp>
Co-authored-by: Athul Anil Kumar <athul7744@outlook.com>
Co-authored-by: Taly <vitalik7tv@yandex.ru>
Co-authored-by: flaming-cl <51183663+flaming-cl@users.noreply.github.com>
Co-authored-by: Nguyen Ngoc Son <sonnn.se@gmail.com>
Co-authored-by: Sisir Das K <37764463+sis-dk@users.noreply.github.com>
Co-authored-by: Sisir <sisir@hellosivi.com>
2020-06-03 11:17:29 +03:00

559 lines
13 KiB
TypeScript

/**
* Class Util
*/
import Dom from './dom';
/**
* Possible log levels
*/
export enum LogLevels {
VERBOSE = 'VERBOSE',
INFO = 'INFO',
WARN = 'WARN',
ERROR = 'ERROR',
}
/**
* Allow to use global VERSION, that will be overwritten by Webpack
*/
declare const VERSION: string;
/**
* @typedef {object} ChainData
* @property {object} data - data that will be passed to the success or fallback
* @property {Function} function - function's that must be called asynchronously
*
* @interface ChainData
*/
export interface ChainData {
data?: object;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function: (...args: any[]) => any;
}
/**
* Editor.js utils
*/
/**
* Returns basic keycodes as constants
*
* @returns {{}}
*/
export const keyCodes = {
BACKSPACE: 8,
TAB: 9,
ENTER: 13,
SHIFT: 16,
CTRL: 17,
ALT: 18,
ESC: 27,
SPACE: 32,
LEFT: 37,
UP: 38,
DOWN: 40,
RIGHT: 39,
DELETE: 46,
META: 91,
};
/**
* Return mouse buttons codes
*/
export const mouseButtons = {
LEFT: 0,
WHEEL: 1,
RIGHT: 2,
BACKWARD: 3,
FORWARD: 4,
};
/**
* Custom logger
*
* @param {boolean} labeled — if true, Editor.js label is shown
* @param {string} msg - message
* @param {string} type - logging type 'log'|'warn'|'error'|'info'
* @param {*} [args] - argument to log with a message
* @param {string} style - additional styling to message
*/
function _log(
labeled: boolean,
msg: string,
type = 'log',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
args?: any,
style = 'color: inherit'
): void {
if (!('console' in window) || !window.console[type]) {
return;
}
const isSimpleType = ['info', 'log', 'warn', 'error'].includes(type);
const argsToPass = [];
switch (_log.logLevel) {
case LogLevels.ERROR:
if (type !== 'error') {
return;
}
break;
case LogLevels.WARN:
if (!['error', 'warn'].includes(type)) {
return;
}
break;
case LogLevels.INFO:
if (!isSimpleType || labeled) {
return;
}
break;
}
if (args) {
argsToPass.push(args);
}
const editorLabelText = `Editor.js ${VERSION}`;
const editorLabelStyle = `line-height: 1em;
color: #006FEA;
display: inline-block;
font-size: 11px;
line-height: 1em;
background-color: #fff;
padding: 4px 9px;
border-radius: 30px;
border: 1px solid rgba(56, 138, 229, 0.16);
margin: 4px 5px 4px 0;`;
if (labeled) {
if (isSimpleType) {
argsToPass.unshift(editorLabelStyle, style);
msg = `%c${editorLabelText}%c ${msg}`;
} else {
msg = `( ${editorLabelText} )${msg}`;
}
}
try {
if (!isSimpleType) {
console[type](msg);
} else if (args) {
console[type](`${msg} %o`, ...argsToPass);
} else {
console[type](msg, ...argsToPass);
}
} catch (ignored) {}
}
/**
* Current log level
*/
_log.logLevel = LogLevels.VERBOSE;
/**
* Set current log level
*
* @param {LogLevels} logLevel - log level to set
*/
export function setLogLevel(logLevel: LogLevels): void {
_log.logLevel = logLevel;
}
/**
* _log method proxy without Editor.js label
*/
export const log = _log.bind(window, false);
/**
* _log method proxy with Editor.js label
*/
export const logLabeled = _log.bind(window, true);
/**
* Returns true if passed key code is printable (a-Z, 0-9, etc) character.
*
* @param {number} keyCode - key code
*
* @returns {boolean}
*/
export function isPrintableKey(keyCode: number): boolean {
return (keyCode > 47 && keyCode < 58) || // number keys
keyCode === 32 || keyCode === 13 || // Spacebar & return key(s)
(keyCode > 64 && keyCode < 91) || // letter keys
(keyCode > 95 && keyCode < 112) || // Numpad keys
(keyCode > 185 && keyCode < 193) || // ;=,-./` (in order)
(keyCode > 218 && keyCode < 223); // [\]' (in order)
}
/**
* Fires a promise sequence asynchronously
*
* @param {ChainData[]} chains - list or ChainData's
* @param {Function} success - success callback
* @param {Function} fallback - callback that fires in case of errors
*
* @returns {Promise}
*/
export async function sequence(
chains: ChainData[],
// eslint-disable-next-line @typescript-eslint/no-empty-function
success: (data: object) => void = (): void => {},
// eslint-disable-next-line @typescript-eslint/no-empty-function
fallback: (data: object) => void = (): void => {}
): Promise<void> {
/**
* Decorator
*
* @param {ChainData} chainData - Chain data
*
* @param {Function} successCallback - success callback
* @param {Function} fallbackCallback - fail callback
*
* @returns {Promise}
*/
async function waitNextBlock(
chainData: ChainData,
successCallback: (data: object) => void,
fallbackCallback: (data: object) => void
): Promise<void> {
try {
await chainData.function(chainData.data);
await successCallback(typeof chainData.data !== 'undefined' ? chainData.data : {});
} catch (e) {
fallbackCallback(typeof chainData.data !== 'undefined' ? chainData.data : {});
}
}
/**
* pluck each element from queue
* First, send resolved Promise as previous value
* Each plugins "prepare" method returns a Promise, that's why
* reduce current element will not be able to continue while can't get
* a resolved Promise
*/
return chains.reduce(async (previousValue, currentValue) => {
await previousValue;
return waitNextBlock(currentValue, success, fallback);
}, Promise.resolve());
}
/**
* Make array from array-like collection
*
* @param {ArrayLike} collection - collection to convert to array
*
* @returns {Array}
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function array(collection: ArrayLike<any>): any[] {
return Array.prototype.slice.call(collection);
}
/**
* Check if passed variable is a function
*
* @param {*} fn - function to check
*
* @returns {boolean}
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function isFunction(fn: any): fn is Function {
return typeof fn === 'function';
}
/**
* Check if passed function is a class
*
* @param {Function} fn - function to check
*
* @returns {boolean}
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function isClass(fn: any): boolean {
return typeof fn === 'function' && /^\s*class\s+/.test(fn.toString());
}
/**
* Checks if object is empty
*
* @param {object} object - object to check
*
* @returns {boolean}
*/
export function isEmpty(object: object): boolean {
if (!object) {
return true;
}
return Object.keys(object).length === 0 && object.constructor === Object;
}
/**
* Check if passed object is a Promise
*
* @param {*} object - object to check
* @returns {boolean}
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function isPromise(object: any): object is Promise<any> {
return Promise.resolve(object) === object;
}
/**
* Delays method execution
*
* @param {Function} method - method to execute
* @param {number} timeout - timeout in ms
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function delay(method: (...args: any[]) => any, timeout: number) {
return function (): void {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const context = this,
// eslint-disable-next-line prefer-rest-params
args = arguments;
window.setTimeout(() => method.apply(context, args), timeout);
};
}
/**
* Get file extension
*
* @param {File} file - file
*
* @returns {string}
*/
export function getFileExtension(file: File): string {
return file.name.split('.').pop();
}
/**
* Check if string is MIME type
*
* @param {string} type - string to check
*
* @returns {boolean}
*/
export function isValidMimeType(type: string): boolean {
return /^[-\w]+\/([-+\w]+|\*)$/.test(type);
}
/**
* Debouncing method
* Call method after passed time
*
* Note that this method returns Function and declared variable need to be called
*
* @param {Function} func - function that we're throttling
* @param {number} wait - time in milliseconds
* @param {boolean} immediate - call now
* @returns {Function}
*/
export function debounce(func: () => void, wait?: number, immediate?: boolean): () => void {
let timeout;
return (): void => {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const context = this,
// eslint-disable-next-line prefer-rest-params
args = arguments;
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const later = () => {
timeout = null;
if (!immediate) {
func.apply(context, args);
}
};
const callNow = immediate && !timeout;
window.clearTimeout(timeout);
timeout = window.setTimeout(later, wait);
if (callNow) {
func.apply(context, args);
}
};
}
/**
* Copies passed text to the clipboard
*
* @param text - text to copy
*/
export function copyTextToClipboard(text): void {
const el = Dom.make('div', 'codex-editor-clipboard', {
innerHTML: text,
});
document.body.appendChild(el);
const selection = window.getSelection();
const range = document.createRange();
range.selectNode(el);
window.getSelection().removeAllRanges();
selection.addRange(range);
document.execCommand('copy');
document.body.removeChild(el);
}
/**
* Returns object with os name as key and boolean as value. Shows current user OS
*/
export function getUserOS(): {[key: string]: boolean} {
const OS = {
win: false,
mac: false,
x11: false,
linux: false,
};
const userOS = Object.keys(OS).find((os: string) => navigator.appVersion.toLowerCase().indexOf(os) !== -1);
if (userOS) {
OS[userOS] = true;
return OS;
}
return OS;
}
/**
* Capitalizes first letter of the string
*
* @param {string} text - text to capitalize
*
* @returns {string}
*/
export function capitalize(text: string): string {
return text[0].toUpperCase() + text.slice(1);
}
/**
* Return string representation of the object type
*
* @param {*} object - object to get type
*
* @returns {string}
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function typeOf(object: any): string {
return Object.prototype.toString.call(object).match(/\s([a-zA-Z]+)/)[1].toLowerCase();
}
/**
* Merge to objects recursively
*
* @param {object} target - merge target
* @param {object[]} sources - merge sources
* @returns {object}
*/
export function deepMerge<T extends object>(target, ...sources): T {
const isObject = (item): item is object => item && typeOf(item) === 'object';
if (!sources.length) {
return target;
}
const source = sources.shift();
if (isObject(target) && isObject(source)) {
for (const key in source) {
if (isObject(source[key])) {
if (!target[key]) {
Object.assign(target, { [key]: {} });
}
deepMerge(target[key], source[key]);
} else {
Object.assign(target, { [key]: source[key] });
}
}
}
return deepMerge(target, ...sources);
}
/**
* Return true if current device supports touch events
*
* Note! This is a simple solution, it can give false-positive results.
* To detect touch devices more carefully, use 'touchstart' event listener
*
* @see http://www.stucox.com/blog/you-cant-detect-a-touchscreen/
*
* @returns {boolean}
*/
export const isTouchSupported: boolean = 'ontouchstart' in document.documentElement;
/**
* Make shortcut command more human-readable
*
* @param {string} shortcut — string like 'CMD+B'
*/
export function beautifyShortcut(shortcut: string): string {
const OS = getUserOS();
shortcut = shortcut
.replace(/shift/gi, '⇧')
.replace(/backspace/gi, '⌫')
.replace(/enter/gi, '⏎')
.replace(/up/gi, '↑')
.replace(/left/gi, '→')
.replace(/down/gi, '↓')
.replace(/right/gi, '←')
.replace(/escape/gi, '⎋')
.replace(/insert/gi, 'Ins')
.replace(/delete/gi, '␡')
.replace(/\+/gi, ' + ');
if (OS.mac) {
shortcut = shortcut.replace(/ctrl|cmd/gi, '⌘').replace(/alt/gi, '⌥');
} else {
shortcut = shortcut.replace(/cmd/gi, 'Ctrl').replace(/windows/gi, 'WIN');
}
return shortcut;
}
/**
* Returns valid URL. If it is going outside and valid, it returns itself
* If url has `one slash`, then it concatenates with window location origin
* or when url has `two lack` it appends only protocol
*
* @param {string} url - url to prettify
*/
export function getValidUrl(url: string): string {
try {
const urlObject = new URL(url);
return urlObject.href;
} catch (e) {
// do nothing but handle below
}
if (url.substring(0, 2) === '//') {
return window.location.protocol + url;
} else {
return window.location.origin + url;
}
}
/**
* Opens new Tab with passed URL
*
* @param {string} url - URL address to redirect
*/
export function openTab(url: string): void {
window.open(url, '_blank');
}