mirror of
https://github.com/codex-team/editor.js
synced 2024-06-04 23:12:34 +02:00
8f156a87ea
* the popover component, vertical toolbox * toolbox position improved * popover width improved * always show the plus button * search field added * search input in popover * trying to create mobile toolbox * feat(toolbox): popover adapted for mobile devices (#2004) * FIx mobile popover fixed positioning * Add mobile popover overlay * Hide mobile popover on scroll * Alter toolbox buttons hover * Fix closing popover on overlay click * Tests fix * Fix onchange test * restore focus after toolbox closing by ESC * don't move toolbar by block-hover on mobile Resolves #1972 * popover mobile styles improved * Cleanup * Remove scroll event listener * Lock scroll on mobile * don't show shortcuts in mobile popover * Change data attr name * Remove unused styles * Remove unused listeners * disable hover on mobile popover * Scroll fix * Lint * Revert "Scroll fix" This reverts commit82deae543e
. * Return back background color for active state of toolbox buttons Co-authored-by: Peter Savchenko <specc.dev@gmail.com> * Vertical toolbox fixes (#2017) * Replace visibility property with display for hiding popover * Disable arrow right and left keys for popover * Revert "Replace visibility property with display for hiding popover" This reverts commitaf521cf6f2
. * Hide popover via setting max-height to 0 to fix animation in safari * Remove redundant condition * Extend element interface to avoid ts errors * Do not subscribe to block hovered if mobile * Add unsubscribing from overlay click event * Rename isMobile to isMobileScreen * Cleanup * fix: popover opening direction (#2022) * Change popover opening direction based on available space below it * Update check * Use cacheable decorator * Update src/components/flipper.ts Co-authored-by: George Berezhnoy <gohabereg@users.noreply.github.com> * Fixes * Fix test * Clear search on popover hide * Fix popover width * Fix for tests * Update todos * Linter fixes * rm todo about beforeInsert because I have no idea what does it mean * i18n for search labels done * rm methods for hiding/showing of + * some code style update * Update CHANGELOG.md * make the list items a little bit compact * fix z-index issue caused by block-appearing animation also, improve popover padding for two reasons: - make the popover more consistent with the Table tool popover (in future, it can be done with the same api method) - make popover looks better Co-authored-by: Tanya Fomina <fomina.tatianaaa@yandex.ru> Co-authored-by: George Berezhnoy <gohabereg@users.noreply.github.com>
772 lines
18 KiB
TypeScript
772 lines
18 KiB
TypeScript
/**
|
|
* Class Util
|
|
*/
|
|
|
|
import { nanoid } from 'nanoid';
|
|
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);
|
|
|
|
/**
|
|
* 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();
|
|
}
|
|
|
|
/**
|
|
* 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 (...args: any[]) => any {
|
|
return typeOf(fn) === 'function' || typeOf(fn) === 'asyncfunction';
|
|
}
|
|
|
|
/**
|
|
* Checks if passed argument is an object
|
|
*
|
|
* @param {*} v - object to check
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
export function isObject(v: any): v is object {
|
|
return typeOf(v) === 'object';
|
|
}
|
|
|
|
/**
|
|
* Checks if passed argument is a string
|
|
*
|
|
* @param {*} v - variable to check
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
export function isString(v: any): v is string {
|
|
return typeOf(v) === 'string';
|
|
}
|
|
|
|
/**
|
|
* Checks if passed argument is boolean
|
|
*
|
|
* @param {*} v - variable to check
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
export function isBoolean(v: any): v is boolean {
|
|
return typeOf(v) === 'boolean';
|
|
}
|
|
|
|
/**
|
|
* Checks if passed argument is number
|
|
*
|
|
* @param {*} v - variable to check
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
export function isNumber(v: any): v is number {
|
|
return typeOf(v) === 'number';
|
|
}
|
|
|
|
/**
|
|
* Checks if passed argument is undefined
|
|
*
|
|
* @param {*} v - variable to check
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
export function isUndefined(v: any): v is undefined {
|
|
return typeOf(v) === 'undefined';
|
|
}
|
|
|
|
/**
|
|
* 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 isFunction(fn) && /^\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;
|
|
}
|
|
|
|
/**
|
|
* 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 === 229 || // processing key input for certain languages — Chinese, Japanese, etc.
|
|
(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(!isUndefined(chainData.data) ? chainData.data : {});
|
|
} catch (e) {
|
|
fallbackCallback(!isUndefined(chainData.data) ? 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);
|
|
}
|
|
|
|
/**
|
|
* 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: (...args: unknown[]) => void, wait?: number, immediate?: boolean): () => void {
|
|
let timeout;
|
|
|
|
return (...args: unknown[]): void => {
|
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
const context = this;
|
|
|
|
// 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);
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Returns a function, that, when invoked, will only be triggered at most once during a given window of time.
|
|
*
|
|
* @param func - function to throttle
|
|
* @param wait - function will be called only once for that period
|
|
* @param options - Normally, the throttled function will run as much as it can
|
|
* without ever going more than once per `wait` duration;
|
|
* but if you'd like to disable the execution on the leading edge, pass
|
|
* `{leading: false}`. To disable execution on the trailing edge, ditto.
|
|
*/
|
|
export function throttle(func, wait, options: {leading?: boolean; trailing?: boolean} = undefined): () => void {
|
|
let context, args, result;
|
|
let timeout = null;
|
|
let previous = 0;
|
|
|
|
if (!options) {
|
|
options = {};
|
|
}
|
|
|
|
const later = function (): void {
|
|
previous = options.leading === false ? 0 : Date.now();
|
|
timeout = null;
|
|
result = func.apply(context, args);
|
|
|
|
if (!timeout) {
|
|
context = args = null;
|
|
}
|
|
};
|
|
|
|
return function (): unknown {
|
|
const now = Date.now();
|
|
|
|
if (!previous && options.leading === false) {
|
|
previous = now;
|
|
}
|
|
|
|
const remaining = wait - (now - previous);
|
|
|
|
context = this;
|
|
|
|
// eslint-disable-next-line prefer-rest-params
|
|
args = arguments;
|
|
|
|
if (remaining <= 0 || remaining > wait) {
|
|
if (timeout) {
|
|
clearTimeout(timeout);
|
|
timeout = null;
|
|
}
|
|
previous = now;
|
|
result = func.apply(context, args);
|
|
|
|
if (!timeout) {
|
|
context = args = null;
|
|
}
|
|
} else if (!timeout && options.trailing !== false) {
|
|
timeout = setTimeout(later, remaining);
|
|
}
|
|
|
|
return result;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
/**
|
|
* 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 {
|
|
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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a block id
|
|
*
|
|
* @returns {string}
|
|
*/
|
|
export function generateBlockId(): string {
|
|
return nanoid(10);
|
|
}
|
|
|
|
/**
|
|
* Opens new Tab with passed URL
|
|
*
|
|
* @param {string} url - URL address to redirect
|
|
*/
|
|
export function openTab(url: string): void {
|
|
window.open(url, '_blank');
|
|
}
|
|
|
|
/**
|
|
* Returns random generated identifier
|
|
*
|
|
* @param {string} prefix - identifier prefix
|
|
*
|
|
* @returns {string}
|
|
*/
|
|
export function generateId(prefix = ''): string {
|
|
// tslint:disable-next-line:no-bitwise
|
|
return `${prefix}${(Math.floor(Math.random() * 1e8)).toString(16)}`;
|
|
}
|
|
|
|
/**
|
|
* Common method for printing a warning about the usage of deprecated property or method.
|
|
*
|
|
* @param condition - condition for deprecation.
|
|
* @param oldProperty - deprecated property.
|
|
* @param newProperty - the property that should be used instead.
|
|
*/
|
|
export function deprecationAssert(condition: boolean, oldProperty: string, newProperty: string): void {
|
|
const message = `«${oldProperty}» is deprecated and will be removed in the next major release. Please use the «${newProperty}» instead.`;
|
|
|
|
if (condition) {
|
|
logLabeled(message, 'warn');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decorator which provides ability to cache method or accessor result
|
|
*
|
|
* @param target - target instance or constructor function
|
|
* @param propertyKey - method or accessor name
|
|
* @param descriptor - property descriptor
|
|
*/
|
|
export function cacheable<Target, Value, Arguments extends unknown[] = unknown[]>(
|
|
target: Target,
|
|
propertyKey: string,
|
|
descriptor: PropertyDescriptor
|
|
): PropertyDescriptor {
|
|
const propertyToOverride = descriptor.value ? 'value' : 'get';
|
|
const originalMethod = descriptor[propertyToOverride];
|
|
const cacheKey = `#${propertyKey}Cache`;
|
|
|
|
/**
|
|
* Override get or value descriptor property to cache return value
|
|
*
|
|
* @param args - method args
|
|
*/
|
|
descriptor[propertyToOverride] = function (...args: Arguments): Value {
|
|
/**
|
|
* If there is no cache, create it
|
|
*/
|
|
if (this[cacheKey] === undefined) {
|
|
this[cacheKey] = originalMethod.apply(this, ...args);
|
|
}
|
|
|
|
return this[cacheKey];
|
|
};
|
|
|
|
/**
|
|
* If get accessor has been overridden, we need to override set accessor to clear cache
|
|
*
|
|
* @param value - value to set
|
|
*/
|
|
if (propertyToOverride === 'get' && descriptor.set) {
|
|
const originalSet = descriptor.set;
|
|
|
|
descriptor.set = function (value: unknown): void {
|
|
delete target[cacheKey];
|
|
|
|
originalSet.apply(this, value);
|
|
};
|
|
}
|
|
|
|
return descriptor;
|
|
};
|
|
|
|
/**
|
|
* True if screen has mobile size
|
|
*/
|
|
export function isMobileScreen(): boolean {
|
|
return window.matchMedia('(max-width: 650px)').matches;
|
|
}
|