Merge pull request #5259 from BookStackApp/typescript-conversions
Conversion of Services to TypeScript
This commit is contained in:
		
						commit
						0a07b0d162
					
				| 
						 | 
				
			
			@ -10,7 +10,7 @@ const isProd = process.argv[2] === 'production';
 | 
			
		|||
 | 
			
		||||
// Gather our input files
 | 
			
		||||
const entryPoints = {
 | 
			
		||||
    app: path.join(__dirname, '../../resources/js/app.js'),
 | 
			
		||||
    app: path.join(__dirname, '../../resources/js/app.ts'),
 | 
			
		||||
    code: path.join(__dirname, '../../resources/js/code/index.mjs'),
 | 
			
		||||
    'legacy-modes': path.join(__dirname, '../../resources/js/code/legacy-modes.mjs'),
 | 
			
		||||
    markdown: path.join(__dirname, '../../resources/js/markdown/index.mjs'),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,33 +0,0 @@
 | 
			
		|||
import {EventManager} from './services/events.ts';
 | 
			
		||||
import {HttpManager} from './services/http.ts';
 | 
			
		||||
import {Translator} from './services/translations.ts';
 | 
			
		||||
import * as componentMap from './components';
 | 
			
		||||
import {ComponentStore} from './services/components.ts';
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line no-underscore-dangle
 | 
			
		||||
window.__DEV__ = false;
 | 
			
		||||
 | 
			
		||||
// Url retrieval function
 | 
			
		||||
window.baseUrl = function baseUrl(path) {
 | 
			
		||||
    let targetPath = path;
 | 
			
		||||
    let basePath = document.querySelector('meta[name="base-url"]').getAttribute('content');
 | 
			
		||||
    if (basePath[basePath.length - 1] === '/') basePath = basePath.slice(0, basePath.length - 1);
 | 
			
		||||
    if (targetPath[0] === '/') targetPath = targetPath.slice(1);
 | 
			
		||||
    return `${basePath}/${targetPath}`;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
window.importVersioned = function importVersioned(moduleName) {
 | 
			
		||||
    const version = document.querySelector('link[href*="/dist/styles.css?version="]').href.split('?version=').pop();
 | 
			
		||||
    const importPath = window.baseUrl(`dist/${moduleName}.js?version=${version}`);
 | 
			
		||||
    return import(importPath);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Set events, http & translation services on window
 | 
			
		||||
window.$http = new HttpManager();
 | 
			
		||||
window.$events = new EventManager();
 | 
			
		||||
window.$trans = new Translator();
 | 
			
		||||
 | 
			
		||||
// Load & initialise components
 | 
			
		||||
window.$components = new ComponentStore();
 | 
			
		||||
window.$components.register(componentMap);
 | 
			
		||||
window.$components.init();
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,23 @@
 | 
			
		|||
import {EventManager} from './services/events';
 | 
			
		||||
import {HttpManager} from './services/http';
 | 
			
		||||
import {Translator} from './services/translations';
 | 
			
		||||
import * as componentMap from './components/index';
 | 
			
		||||
import {ComponentStore} from './services/components';
 | 
			
		||||
import {baseUrl, importVersioned} from "./services/util";
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line no-underscore-dangle
 | 
			
		||||
window.__DEV__ = false;
 | 
			
		||||
 | 
			
		||||
// Make common important util functions global
 | 
			
		||||
window.baseUrl = baseUrl;
 | 
			
		||||
window.importVersioned = importVersioned;
 | 
			
		||||
 | 
			
		||||
// Setup events, http & translation services
 | 
			
		||||
window.$http = new HttpManager();
 | 
			
		||||
window.$events = new EventManager();
 | 
			
		||||
window.$trans = new Translator();
 | 
			
		||||
 | 
			
		||||
// Load & initialise components
 | 
			
		||||
window.$components = new ComponentStore();
 | 
			
		||||
window.$components.register(componentMap);
 | 
			
		||||
window.$components.init();
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
import {onChildEvent} from '../services/dom';
 | 
			
		||||
import {uniqueId} from '../services/util';
 | 
			
		||||
import {onChildEvent} from '../services/dom.ts';
 | 
			
		||||
import {uniqueId} from '../services/util.ts';
 | 
			
		||||
import {Component} from './component';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import {onSelect} from '../services/dom';
 | 
			
		||||
import {onSelect} from '../services/dom.ts';
 | 
			
		||||
import {Component} from './component';
 | 
			
		||||
 | 
			
		||||
export class AjaxDeleteRow extends Component {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import {onEnterPress, onSelect} from '../services/dom';
 | 
			
		||||
import {onEnterPress, onSelect} from '../services/dom.ts';
 | 
			
		||||
import {Component} from './component';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import {showLoading} from '../services/dom';
 | 
			
		||||
import {showLoading} from '../services/dom.ts';
 | 
			
		||||
import {Component} from './component';
 | 
			
		||||
 | 
			
		||||
export class Attachments extends Component {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
import {escapeHtml} from '../services/util';
 | 
			
		||||
import {onChildEvent} from '../services/dom';
 | 
			
		||||
import {escapeHtml} from '../services/util.ts';
 | 
			
		||||
import {onChildEvent} from '../services/dom.ts';
 | 
			
		||||
import {Component} from './component';
 | 
			
		||||
import {KeyboardNavigationHandler} from '../services/keyboard-navigation';
 | 
			
		||||
import {KeyboardNavigationHandler} from '../services/keyboard-navigation.ts';
 | 
			
		||||
 | 
			
		||||
const ajaxCache = {};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
import Sortable, {MultiDrag} from 'sortablejs';
 | 
			
		||||
import {Component} from './component';
 | 
			
		||||
import {htmlToDom} from '../services/dom';
 | 
			
		||||
import {htmlToDom} from '../services/dom.ts';
 | 
			
		||||
 | 
			
		||||
// Auto sort control
 | 
			
		||||
const sortOperations = {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import {slideUp, slideDown} from '../services/animations';
 | 
			
		||||
import {slideUp, slideDown} from '../services/animations.ts';
 | 
			
		||||
import {Component} from './component';
 | 
			
		||||
 | 
			
		||||
export class ChapterContents extends Component {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import {onChildEvent, onEnterPress, onSelect} from '../services/dom';
 | 
			
		||||
import {onChildEvent, onEnterPress, onSelect} from '../services/dom.ts';
 | 
			
		||||
import {Component} from './component';
 | 
			
		||||
 | 
			
		||||
export class CodeEditor extends Component {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import {slideDown, slideUp} from '../services/animations';
 | 
			
		||||
import {slideDown, slideUp} from '../services/animations.ts';
 | 
			
		||||
import {Component} from './component';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import {onSelect} from '../services/dom';
 | 
			
		||||
import {onSelect} from '../services/dom.ts';
 | 
			
		||||
import {Component} from './component';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
import {debounce} from '../services/util';
 | 
			
		||||
import {transitionHeight} from '../services/animations';
 | 
			
		||||
import {debounce} from '../services/util.ts';
 | 
			
		||||
import {transitionHeight} from '../services/animations.ts';
 | 
			
		||||
import {Component} from './component';
 | 
			
		||||
 | 
			
		||||
export class DropdownSearch extends Component {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
import {onSelect} from '../services/dom';
 | 
			
		||||
import {KeyboardNavigationHandler} from '../services/keyboard-navigation';
 | 
			
		||||
import {onSelect} from '../services/dom.ts';
 | 
			
		||||
import {KeyboardNavigationHandler} from '../services/keyboard-navigation.ts';
 | 
			
		||||
import {Component} from './component';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@ import {Component} from './component';
 | 
			
		|||
import {Clipboard} from '../services/clipboard.ts';
 | 
			
		||||
import {
 | 
			
		||||
    elem, getLoading, onSelect, removeLoading,
 | 
			
		||||
} from '../services/dom';
 | 
			
		||||
} from '../services/dom.ts';
 | 
			
		||||
 | 
			
		||||
export class Dropzone extends Component {
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import {htmlToDom} from '../services/dom';
 | 
			
		||||
import {htmlToDom} from '../services/dom.ts';
 | 
			
		||||
import {Component} from './component';
 | 
			
		||||
 | 
			
		||||
export class EntityPermissions extends Component {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import {onSelect} from '../services/dom';
 | 
			
		||||
import {onSelect} from '../services/dom.ts';
 | 
			
		||||
import {Component} from './component';
 | 
			
		||||
 | 
			
		||||
export class EntitySearch extends Component {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import {onChildEvent} from '../services/dom';
 | 
			
		||||
import {onChildEvent} from '../services/dom.ts';
 | 
			
		||||
import {Component} from './component';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import {onSelect} from '../services/dom';
 | 
			
		||||
import {onSelect} from '../services/dom.ts';
 | 
			
		||||
import {Component} from './component';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import {slideUp, slideDown} from '../services/animations';
 | 
			
		||||
import {slideUp, slideDown} from '../services/animations.ts';
 | 
			
		||||
import {Component} from './component';
 | 
			
		||||
 | 
			
		||||
export class ExpandToggle extends Component {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
import {htmlToDom} from '../services/dom';
 | 
			
		||||
import {debounce} from '../services/util';
 | 
			
		||||
import {KeyboardNavigationHandler} from '../services/keyboard-navigation';
 | 
			
		||||
import {htmlToDom} from '../services/dom.ts';
 | 
			
		||||
import {debounce} from '../services/util.ts';
 | 
			
		||||
import {KeyboardNavigationHandler} from '../services/keyboard-navigation.ts';
 | 
			
		||||
import {Component} from './component';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
import {
 | 
			
		||||
    onChildEvent, onSelect, removeLoading, showLoading,
 | 
			
		||||
} from '../services/dom';
 | 
			
		||||
} from '../services/dom.ts';
 | 
			
		||||
import {Component} from './component';
 | 
			
		||||
 | 
			
		||||
export class ImageManager extends Component {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import {onSelect} from '../services/dom';
 | 
			
		||||
import {onSelect} from '../services/dom.ts';
 | 
			
		||||
import {Component} from './component';
 | 
			
		||||
 | 
			
		||||
export class OptionalInput extends Component {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
import {Component} from './component';
 | 
			
		||||
import {getLoading, htmlToDom} from '../services/dom';
 | 
			
		||||
import {getLoading, htmlToDom} from '../services/dom.ts';
 | 
			
		||||
import {buildForInput} from '../wysiwyg-tinymce/config';
 | 
			
		||||
 | 
			
		||||
export class PageComment extends Component {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
import {Component} from './component';
 | 
			
		||||
import {getLoading, htmlToDom} from '../services/dom';
 | 
			
		||||
import {getLoading, htmlToDom} from '../services/dom.ts';
 | 
			
		||||
import {buildForInput} from '../wysiwyg-tinymce/config';
 | 
			
		||||
 | 
			
		||||
export class PageComments extends Component {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
import * as DOM from '../services/dom';
 | 
			
		||||
import {scrollAndHighlightElement} from '../services/util';
 | 
			
		||||
import * as DOM from '../services/dom.ts';
 | 
			
		||||
import {scrollAndHighlightElement} from '../services/util.ts';
 | 
			
		||||
import {Component} from './component';
 | 
			
		||||
 | 
			
		||||
function toggleAnchorHighlighting(elementId, shouldHighlight) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
import {onSelect} from '../services/dom';
 | 
			
		||||
import {debounce} from '../services/util';
 | 
			
		||||
import {onSelect} from '../services/dom.ts';
 | 
			
		||||
import {debounce} from '../services/util.ts';
 | 
			
		||||
import {Component} from './component';
 | 
			
		||||
import {utcTimeStampToLocalTime} from '../services/dates.ts';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import * as DOM from '../services/dom';
 | 
			
		||||
import * as DOM from '../services/dom.ts';
 | 
			
		||||
import {Component} from './component';
 | 
			
		||||
import {copyTextToClipboard} from '../services/clipboard.ts';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
import {fadeIn, fadeOut} from '../services/animations';
 | 
			
		||||
import {onSelect} from '../services/dom';
 | 
			
		||||
import {fadeIn, fadeOut} from '../services/animations.ts';
 | 
			
		||||
import {onSelect} from '../services/dom.ts';
 | 
			
		||||
import {Component} from './component';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import * as DOM from '../services/dom';
 | 
			
		||||
import * as DOM from '../services/dom.ts';
 | 
			
		||||
import {Component} from './component';
 | 
			
		||||
 | 
			
		||||
export class TemplateManager extends Component {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import {onChildEvent} from '../services/dom';
 | 
			
		||||
import {onChildEvent} from '../services/dom.ts';
 | 
			
		||||
import {Component} from './component';
 | 
			
		||||
 | 
			
		||||
export class UserSelect extends Component {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,10 +7,12 @@ declare global {
 | 
			
		|||
    const __DEV__: boolean;
 | 
			
		||||
 | 
			
		||||
    interface Window {
 | 
			
		||||
        __DEV__: boolean;
 | 
			
		||||
        $components: ComponentStore;
 | 
			
		||||
        $events: EventManager;
 | 
			
		||||
        $trans: Translator;
 | 
			
		||||
        $http: HttpManager;
 | 
			
		||||
        baseUrl: (path: string) => string;
 | 
			
		||||
        importVersioned: (module: string) => Promise<object>;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
import {provideKeyBindings} from './shortcuts';
 | 
			
		||||
import {debounce} from '../services/util';
 | 
			
		||||
import {debounce} from '../services/util.ts';
 | 
			
		||||
import {Clipboard} from '../services/clipboard.ts';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,30 +1,30 @@
 | 
			
		|||
/**
 | 
			
		||||
 * Used in the function below to store references of clean-up functions.
 | 
			
		||||
 * Used to ensure only one transitionend function exists at any time.
 | 
			
		||||
 * @type {WeakMap<object, any>}
 | 
			
		||||
 */
 | 
			
		||||
const animateStylesCleanupMap = new WeakMap();
 | 
			
		||||
const animateStylesCleanupMap: WeakMap<object, any> = new WeakMap();
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Animate the css styles of an element using FLIP animation techniques.
 | 
			
		||||
 * Styles must be an object where the keys are style properties, camelcase, and the values
 | 
			
		||||
 * Styles must be an object where the keys are style rule names and the values
 | 
			
		||||
 * are an array of two items in the format [initialValue, finalValue]
 | 
			
		||||
 * @param {Element} element
 | 
			
		||||
 * @param {Object} styles
 | 
			
		||||
 * @param {Number} animTime
 | 
			
		||||
 * @param {Function} onComplete
 | 
			
		||||
 */
 | 
			
		||||
function animateStyles(element, styles, animTime = 400, onComplete = null) {
 | 
			
		||||
function animateStyles(
 | 
			
		||||
    element: HTMLElement,
 | 
			
		||||
    styles: Record<string, string[]>,
 | 
			
		||||
    animTime: number = 400,
 | 
			
		||||
    onComplete: Function | null = null
 | 
			
		||||
): void {
 | 
			
		||||
    const styleNames = Object.keys(styles);
 | 
			
		||||
    for (const style of styleNames) {
 | 
			
		||||
        element.style[style] = styles[style][0];
 | 
			
		||||
        element.style.setProperty(style, styles[style][0]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const cleanup = () => {
 | 
			
		||||
        for (const style of styleNames) {
 | 
			
		||||
            element.style[style] = null;
 | 
			
		||||
            element.style.removeProperty(style);
 | 
			
		||||
        }
 | 
			
		||||
        element.style.transition = null;
 | 
			
		||||
        element.style.removeProperty('transition');
 | 
			
		||||
        element.removeEventListener('transitionend', cleanup);
 | 
			
		||||
        animateStylesCleanupMap.delete(element);
 | 
			
		||||
        if (onComplete) onComplete();
 | 
			
		||||
| 
						 | 
				
			
			@ -33,7 +33,7 @@ function animateStyles(element, styles, animTime = 400, onComplete = null) {
 | 
			
		|||
    setTimeout(() => {
 | 
			
		||||
        element.style.transition = `all ease-in-out ${animTime}ms`;
 | 
			
		||||
        for (const style of styleNames) {
 | 
			
		||||
            element.style[style] = styles[style][1];
 | 
			
		||||
            element.style.setProperty(style, styles[style][1]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        element.addEventListener('transitionend', cleanup);
 | 
			
		||||
| 
						 | 
				
			
			@ -43,9 +43,8 @@ function animateStyles(element, styles, animTime = 400, onComplete = null) {
 | 
			
		|||
 | 
			
		||||
/**
 | 
			
		||||
 * Run the active cleanup action for the given element.
 | 
			
		||||
 * @param {Element} element
 | 
			
		||||
 */
 | 
			
		||||
function cleanupExistingElementAnimation(element) {
 | 
			
		||||
function cleanupExistingElementAnimation(element: Element) {
 | 
			
		||||
    if (animateStylesCleanupMap.has(element)) {
 | 
			
		||||
        const oldCleanup = animateStylesCleanupMap.get(element);
 | 
			
		||||
        oldCleanup();
 | 
			
		||||
| 
						 | 
				
			
			@ -54,15 +53,12 @@ function cleanupExistingElementAnimation(element) {
 | 
			
		|||
 | 
			
		||||
/**
 | 
			
		||||
 * Fade in the given element.
 | 
			
		||||
 * @param {Element} element
 | 
			
		||||
 * @param {Number} animTime
 | 
			
		||||
 * @param {Function|null} onComplete
 | 
			
		||||
 */
 | 
			
		||||
export function fadeIn(element, animTime = 400, onComplete = null) {
 | 
			
		||||
export function fadeIn(element: HTMLElement, animTime: number = 400, onComplete: Function | null = null): void {
 | 
			
		||||
    cleanupExistingElementAnimation(element);
 | 
			
		||||
    element.style.display = 'block';
 | 
			
		||||
    animateStyles(element, {
 | 
			
		||||
        opacity: ['0', '1'],
 | 
			
		||||
        'opacity': ['0', '1'],
 | 
			
		||||
    }, animTime, () => {
 | 
			
		||||
        if (onComplete) onComplete();
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			@ -70,14 +66,11 @@ export function fadeIn(element, animTime = 400, onComplete = null) {
 | 
			
		|||
 | 
			
		||||
/**
 | 
			
		||||
 * Fade out the given element.
 | 
			
		||||
 * @param {Element} element
 | 
			
		||||
 * @param {Number} animTime
 | 
			
		||||
 * @param {Function|null} onComplete
 | 
			
		||||
 */
 | 
			
		||||
export function fadeOut(element, animTime = 400, onComplete = null) {
 | 
			
		||||
export function fadeOut(element: HTMLElement, animTime: number = 400, onComplete: Function | null = null): void {
 | 
			
		||||
    cleanupExistingElementAnimation(element);
 | 
			
		||||
    animateStyles(element, {
 | 
			
		||||
        opacity: ['1', '0'],
 | 
			
		||||
        'opacity': ['1', '0'],
 | 
			
		||||
    }, animTime, () => {
 | 
			
		||||
        element.style.display = 'none';
 | 
			
		||||
        if (onComplete) onComplete();
 | 
			
		||||
| 
						 | 
				
			
			@ -86,20 +79,18 @@ export function fadeOut(element, animTime = 400, onComplete = null) {
 | 
			
		|||
 | 
			
		||||
/**
 | 
			
		||||
 * Hide the element by sliding the contents upwards.
 | 
			
		||||
 * @param {Element} element
 | 
			
		||||
 * @param {Number} animTime
 | 
			
		||||
 */
 | 
			
		||||
export function slideUp(element, animTime = 400) {
 | 
			
		||||
export function slideUp(element: HTMLElement, animTime: number = 400) {
 | 
			
		||||
    cleanupExistingElementAnimation(element);
 | 
			
		||||
    const currentHeight = element.getBoundingClientRect().height;
 | 
			
		||||
    const computedStyles = getComputedStyle(element);
 | 
			
		||||
    const currentPaddingTop = computedStyles.getPropertyValue('padding-top');
 | 
			
		||||
    const currentPaddingBottom = computedStyles.getPropertyValue('padding-bottom');
 | 
			
		||||
    const animStyles = {
 | 
			
		||||
        maxHeight: [`${currentHeight}px`, '0px'],
 | 
			
		||||
        overflow: ['hidden', 'hidden'],
 | 
			
		||||
        paddingTop: [currentPaddingTop, '0px'],
 | 
			
		||||
        paddingBottom: [currentPaddingBottom, '0px'],
 | 
			
		||||
        'max-height': [`${currentHeight}px`, '0px'],
 | 
			
		||||
        'overflow': ['hidden', 'hidden'],
 | 
			
		||||
        'padding-top': [currentPaddingTop, '0px'],
 | 
			
		||||
        'padding-bottom': [currentPaddingBottom, '0px'],
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    animateStyles(element, animStyles, animTime, () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -109,10 +100,8 @@ export function slideUp(element, animTime = 400) {
 | 
			
		|||
 | 
			
		||||
/**
 | 
			
		||||
 * Show the given element by expanding the contents.
 | 
			
		||||
 * @param {Element} element - Element to animate
 | 
			
		||||
 * @param {Number} animTime - Animation time in ms
 | 
			
		||||
 */
 | 
			
		||||
export function slideDown(element, animTime = 400) {
 | 
			
		||||
export function slideDown(element: HTMLElement, animTime: number = 400) {
 | 
			
		||||
    cleanupExistingElementAnimation(element);
 | 
			
		||||
    element.style.display = 'block';
 | 
			
		||||
    const targetHeight = element.getBoundingClientRect().height;
 | 
			
		||||
| 
						 | 
				
			
			@ -120,10 +109,10 @@ export function slideDown(element, animTime = 400) {
 | 
			
		|||
    const targetPaddingTop = computedStyles.getPropertyValue('padding-top');
 | 
			
		||||
    const targetPaddingBottom = computedStyles.getPropertyValue('padding-bottom');
 | 
			
		||||
    const animStyles = {
 | 
			
		||||
        maxHeight: ['0px', `${targetHeight}px`],
 | 
			
		||||
        overflow: ['hidden', 'hidden'],
 | 
			
		||||
        paddingTop: ['0px', targetPaddingTop],
 | 
			
		||||
        paddingBottom: ['0px', targetPaddingBottom],
 | 
			
		||||
        'max-height': ['0px', `${targetHeight}px`],
 | 
			
		||||
        'overflow': ['hidden', 'hidden'],
 | 
			
		||||
        'padding-top': ['0px', targetPaddingTop],
 | 
			
		||||
        'padding-bottom': ['0px', targetPaddingBottom],
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    animateStyles(element, animStyles, animTime);
 | 
			
		||||
| 
						 | 
				
			
			@ -134,11 +123,8 @@ export function slideDown(element, animTime = 400) {
 | 
			
		|||
 * Call with first state, and you'll receive a function in return.
 | 
			
		||||
 * Call the returned function in the second state to animate between those two states.
 | 
			
		||||
 * If animating to/from 0-height use the slide-up/slide down as easier alternatives.
 | 
			
		||||
 * @param {Element} element - Element to animate
 | 
			
		||||
 * @param {Number} animTime - Animation time in ms
 | 
			
		||||
 * @returns {function} - Function to run in second state to trigger animation.
 | 
			
		||||
 */
 | 
			
		||||
export function transitionHeight(element, animTime = 400) {
 | 
			
		||||
export function transitionHeight(element: HTMLElement, animTime: number = 400): () => void {
 | 
			
		||||
    const startHeight = element.getBoundingClientRect().height;
 | 
			
		||||
    const initialComputedStyles = getComputedStyle(element);
 | 
			
		||||
    const startPaddingTop = initialComputedStyles.getPropertyValue('padding-top');
 | 
			
		||||
| 
						 | 
				
			
			@ -151,10 +137,10 @@ export function transitionHeight(element, animTime = 400) {
 | 
			
		|||
        const targetPaddingTop = computedStyles.getPropertyValue('padding-top');
 | 
			
		||||
        const targetPaddingBottom = computedStyles.getPropertyValue('padding-bottom');
 | 
			
		||||
        const animStyles = {
 | 
			
		||||
            height: [`${startHeight}px`, `${targetHeight}px`],
 | 
			
		||||
            overflow: ['hidden', 'hidden'],
 | 
			
		||||
            paddingTop: [startPaddingTop, targetPaddingTop],
 | 
			
		||||
            paddingBottom: [startPaddingBottom, targetPaddingBottom],
 | 
			
		||||
            'height': [`${startHeight}px`, `${targetHeight}px`],
 | 
			
		||||
            'overflow': ['hidden', 'hidden'],
 | 
			
		||||
            'padding-top': [startPaddingTop, targetPaddingTop],
 | 
			
		||||
            'padding-bottom': [startPaddingBottom, targetPaddingBottom],
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        animateStyles(element, animStyles, animTime);
 | 
			
		||||
| 
						 | 
				
			
			@ -1,12 +1,15 @@
 | 
			
		|||
/**
 | 
			
		||||
 * Check if the given param is a HTMLElement
 | 
			
		||||
 */
 | 
			
		||||
export function isHTMLElement(el: any): el is HTMLElement {
 | 
			
		||||
    return el instanceof HTMLElement;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Create a new element with the given attrs and children.
 | 
			
		||||
 * Children can be a string for text nodes or other elements.
 | 
			
		||||
 * @param {String} tagName
 | 
			
		||||
 * @param {Object<String, String>} attrs
 | 
			
		||||
 * @param {Element[]|String[]}children
 | 
			
		||||
 * @return {*}
 | 
			
		||||
 */
 | 
			
		||||
export function elem(tagName, attrs = {}, children = []) {
 | 
			
		||||
export function elem(tagName: string, attrs: Record<string, string> = {}, children: Element[]|string[] = []): HTMLElement {
 | 
			
		||||
    const el = document.createElement(tagName);
 | 
			
		||||
 | 
			
		||||
    for (const [key, val] of Object.entries(attrs)) {
 | 
			
		||||
| 
						 | 
				
			
			@ -30,10 +33,8 @@ export function elem(tagName, attrs = {}, children = []) {
 | 
			
		|||
 | 
			
		||||
/**
 | 
			
		||||
 * Run the given callback against each element that matches the given selector.
 | 
			
		||||
 * @param {String} selector
 | 
			
		||||
 * @param {Function<Element>} callback
 | 
			
		||||
 */
 | 
			
		||||
export function forEach(selector, callback) {
 | 
			
		||||
export function forEach(selector: string, callback: (el: Element) => any) {
 | 
			
		||||
    const elements = document.querySelectorAll(selector);
 | 
			
		||||
    for (const element of elements) {
 | 
			
		||||
        callback(element);
 | 
			
		||||
| 
						 | 
				
			
			@ -42,11 +43,8 @@ export function forEach(selector, callback) {
 | 
			
		|||
 | 
			
		||||
/**
 | 
			
		||||
 * Helper to listen to multiple DOM events
 | 
			
		||||
 * @param {Element} listenerElement
 | 
			
		||||
 * @param {Array<String>} events
 | 
			
		||||
 * @param {Function<Event>} callback
 | 
			
		||||
 */
 | 
			
		||||
export function onEvents(listenerElement, events, callback) {
 | 
			
		||||
export function onEvents(listenerElement: Element, events: string[], callback: (e: Event) => any): void {
 | 
			
		||||
    for (const eventName of events) {
 | 
			
		||||
        listenerElement.addEventListener(eventName, callback);
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -55,10 +53,8 @@ export function onEvents(listenerElement, events, callback) {
 | 
			
		|||
/**
 | 
			
		||||
 * Helper to run an action when an element is selected.
 | 
			
		||||
 * A "select" is made to be accessible, So can be a click, space-press or enter-press.
 | 
			
		||||
 * @param {HTMLElement|Array} elements
 | 
			
		||||
 * @param {function} callback
 | 
			
		||||
 */
 | 
			
		||||
export function onSelect(elements, callback) {
 | 
			
		||||
export function onSelect(elements: HTMLElement|HTMLElement[], callback: (e: Event) => any): void {
 | 
			
		||||
    if (!Array.isArray(elements)) {
 | 
			
		||||
        elements = [elements];
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -76,16 +72,13 @@ export function onSelect(elements, callback) {
 | 
			
		|||
 | 
			
		||||
/**
 | 
			
		||||
 * Listen to key press on the given element(s).
 | 
			
		||||
 * @param {String} key
 | 
			
		||||
 * @param {HTMLElement|Array} elements
 | 
			
		||||
 * @param {function} callback
 | 
			
		||||
 */
 | 
			
		||||
function onKeyPress(key, elements, callback) {
 | 
			
		||||
function onKeyPress(key: string, elements: HTMLElement|HTMLElement[], callback: (e: KeyboardEvent) => any): void {
 | 
			
		||||
    if (!Array.isArray(elements)) {
 | 
			
		||||
        elements = [elements];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const listener = event => {
 | 
			
		||||
    const listener = (event: KeyboardEvent) => {
 | 
			
		||||
        if (event.key === key) {
 | 
			
		||||
            callback(event);
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -96,19 +89,15 @@ function onKeyPress(key, elements, callback) {
 | 
			
		|||
 | 
			
		||||
/**
 | 
			
		||||
 * Listen to enter press on the given element(s).
 | 
			
		||||
 * @param {HTMLElement|Array} elements
 | 
			
		||||
 * @param {function} callback
 | 
			
		||||
 */
 | 
			
		||||
export function onEnterPress(elements, callback) {
 | 
			
		||||
export function onEnterPress(elements: HTMLElement|HTMLElement[], callback: (e: KeyboardEvent) => any): void {
 | 
			
		||||
    onKeyPress('Enter', elements, callback);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Listen to escape press on the given element(s).
 | 
			
		||||
 * @param {HTMLElement|Array} elements
 | 
			
		||||
 * @param {function} callback
 | 
			
		||||
 */
 | 
			
		||||
export function onEscapePress(elements, callback) {
 | 
			
		||||
export function onEscapePress(elements: HTMLElement|HTMLElement[], callback: (e: KeyboardEvent) => any): void {
 | 
			
		||||
    onKeyPress('Escape', elements, callback);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -116,14 +105,15 @@ export function onEscapePress(elements, callback) {
 | 
			
		|||
 * Set a listener on an element for an event emitted by a child
 | 
			
		||||
 * matching the given childSelector param.
 | 
			
		||||
 * Used in a similar fashion to jQuery's $('listener').on('eventName', 'childSelector', callback)
 | 
			
		||||
 * @param {Element} listenerElement
 | 
			
		||||
 * @param {String} childSelector
 | 
			
		||||
 * @param {String} eventName
 | 
			
		||||
 * @param {Function} callback
 | 
			
		||||
 */
 | 
			
		||||
export function onChildEvent(listenerElement, childSelector, eventName, callback) {
 | 
			
		||||
    listenerElement.addEventListener(eventName, event => {
 | 
			
		||||
        const matchingChild = event.target.closest(childSelector);
 | 
			
		||||
export function onChildEvent(
 | 
			
		||||
    listenerElement: HTMLElement,
 | 
			
		||||
    childSelector: string,
 | 
			
		||||
    eventName: string,
 | 
			
		||||
    callback: (this: HTMLElement, e: Event, child: HTMLElement) => any
 | 
			
		||||
): void {
 | 
			
		||||
    listenerElement.addEventListener(eventName, (event: Event) => {
 | 
			
		||||
        const matchingChild = (event.target as HTMLElement|null)?.closest(childSelector) as HTMLElement;
 | 
			
		||||
        if (matchingChild) {
 | 
			
		||||
            callback.call(matchingChild, event, matchingChild);
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -132,16 +122,13 @@ export function onChildEvent(listenerElement, childSelector, eventName, callback
 | 
			
		|||
 | 
			
		||||
/**
 | 
			
		||||
 * Look for elements that match the given selector and contain the given text.
 | 
			
		||||
 * Is case insensitive and returns the first result or null if nothing is found.
 | 
			
		||||
 * @param {String} selector
 | 
			
		||||
 * @param {String} text
 | 
			
		||||
 * @returns {Element}
 | 
			
		||||
 * Is case-insensitive and returns the first result or null if nothing is found.
 | 
			
		||||
 */
 | 
			
		||||
export function findText(selector, text) {
 | 
			
		||||
export function findText(selector: string, text: string): Element|null {
 | 
			
		||||
    const elements = document.querySelectorAll(selector);
 | 
			
		||||
    text = text.toLowerCase();
 | 
			
		||||
    for (const element of elements) {
 | 
			
		||||
        if (element.textContent.toLowerCase().includes(text)) {
 | 
			
		||||
        if ((element.textContent || '').toLowerCase().includes(text) && isHTMLElement(element)) {
 | 
			
		||||
            return element;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -151,17 +138,15 @@ export function findText(selector, text) {
 | 
			
		|||
/**
 | 
			
		||||
 * Show a loading indicator in the given element.
 | 
			
		||||
 * This will effectively clear the element.
 | 
			
		||||
 * @param {Element} element
 | 
			
		||||
 */
 | 
			
		||||
export function showLoading(element) {
 | 
			
		||||
export function showLoading(element: HTMLElement): void {
 | 
			
		||||
    element.innerHTML = '<div class="loading-container"><div></div><div></div><div></div></div>';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Get a loading element indicator element.
 | 
			
		||||
 * @returns {Element}
 | 
			
		||||
 */
 | 
			
		||||
export function getLoading() {
 | 
			
		||||
export function getLoading(): HTMLElement {
 | 
			
		||||
    const wrap = document.createElement('div');
 | 
			
		||||
    wrap.classList.add('loading-container');
 | 
			
		||||
    wrap.innerHTML = '<div></div><div></div><div></div>';
 | 
			
		||||
| 
						 | 
				
			
			@ -170,9 +155,8 @@ export function getLoading() {
 | 
			
		|||
 | 
			
		||||
/**
 | 
			
		||||
 * Remove any loading indicators within the given element.
 | 
			
		||||
 * @param {Element} element
 | 
			
		||||
 */
 | 
			
		||||
export function removeLoading(element) {
 | 
			
		||||
export function removeLoading(element: HTMLElement): void {
 | 
			
		||||
    const loadingEls = element.querySelectorAll('.loading-container');
 | 
			
		||||
    for (const el of loadingEls) {
 | 
			
		||||
        el.remove();
 | 
			
		||||
| 
						 | 
				
			
			@ -182,12 +166,15 @@ export function removeLoading(element) {
 | 
			
		|||
/**
 | 
			
		||||
 * Convert the given html data into a live DOM element.
 | 
			
		||||
 * Initiates any components defined in the data.
 | 
			
		||||
 * @param {String} html
 | 
			
		||||
 * @returns {Element}
 | 
			
		||||
 */
 | 
			
		||||
export function htmlToDom(html) {
 | 
			
		||||
export function htmlToDom(html: string): HTMLElement {
 | 
			
		||||
    const wrap = document.createElement('div');
 | 
			
		||||
    wrap.innerHTML = html;
 | 
			
		||||
    window.$components.init(wrap);
 | 
			
		||||
    return wrap.children[0];
 | 
			
		||||
    const firstChild = wrap.children[0];
 | 
			
		||||
    if (!isHTMLElement(firstChild)) {
 | 
			
		||||
        throw new Error('Could not find child HTMLElement when creating DOM element from HTML');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return firstChild;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,14 +1,17 @@
 | 
			
		|||
import {isHTMLElement} from "./dom";
 | 
			
		||||
 | 
			
		||||
type OptionalKeyEventHandler = ((e: KeyboardEvent) => any)|null;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Handle common keyboard navigation events within a given container.
 | 
			
		||||
 */
 | 
			
		||||
export class KeyboardNavigationHandler {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param {Element} container
 | 
			
		||||
     * @param {Function|null} onEscape
 | 
			
		||||
     * @param {Function|null} onEnter
 | 
			
		||||
     */
 | 
			
		||||
    constructor(container, onEscape = null, onEnter = null) {
 | 
			
		||||
    protected containers: HTMLElement[];
 | 
			
		||||
    protected onEscape: OptionalKeyEventHandler;
 | 
			
		||||
    protected onEnter: OptionalKeyEventHandler;
 | 
			
		||||
 | 
			
		||||
    constructor(container: HTMLElement, onEscape: OptionalKeyEventHandler = null, onEnter: OptionalKeyEventHandler = null) {
 | 
			
		||||
        this.containers = [container];
 | 
			
		||||
        this.onEscape = onEscape;
 | 
			
		||||
        this.onEnter = onEnter;
 | 
			
		||||
| 
						 | 
				
			
			@ -18,9 +21,8 @@ export class KeyboardNavigationHandler {
 | 
			
		|||
    /**
 | 
			
		||||
     * Also share the keyboard event handling to the given element.
 | 
			
		||||
     * Only elements within the original container are considered focusable though.
 | 
			
		||||
     * @param {Element} element
 | 
			
		||||
     */
 | 
			
		||||
    shareHandlingToEl(element) {
 | 
			
		||||
    shareHandlingToEl(element: HTMLElement) {
 | 
			
		||||
        this.containers.push(element);
 | 
			
		||||
        element.addEventListener('keydown', this.#keydownHandler.bind(this));
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -30,7 +32,8 @@ export class KeyboardNavigationHandler {
 | 
			
		|||
     */
 | 
			
		||||
    focusNext() {
 | 
			
		||||
        const focusable = this.#getFocusable();
 | 
			
		||||
        const currentIndex = focusable.indexOf(document.activeElement);
 | 
			
		||||
        const activeEl = document.activeElement;
 | 
			
		||||
        const currentIndex = isHTMLElement(activeEl) ? focusable.indexOf(activeEl) : -1;
 | 
			
		||||
        let newIndex = currentIndex + 1;
 | 
			
		||||
        if (newIndex >= focusable.length) {
 | 
			
		||||
            newIndex = 0;
 | 
			
		||||
| 
						 | 
				
			
			@ -44,7 +47,8 @@ export class KeyboardNavigationHandler {
 | 
			
		|||
     */
 | 
			
		||||
    focusPrevious() {
 | 
			
		||||
        const focusable = this.#getFocusable();
 | 
			
		||||
        const currentIndex = focusable.indexOf(document.activeElement);
 | 
			
		||||
        const activeEl = document.activeElement;
 | 
			
		||||
        const currentIndex = isHTMLElement(activeEl) ? focusable.indexOf(activeEl) : -1;
 | 
			
		||||
        let newIndex = currentIndex - 1;
 | 
			
		||||
        if (newIndex < 0) {
 | 
			
		||||
            newIndex = focusable.length - 1;
 | 
			
		||||
| 
						 | 
				
			
			@ -53,12 +57,9 @@ export class KeyboardNavigationHandler {
 | 
			
		|||
        focusable[newIndex].focus();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param {KeyboardEvent} event
 | 
			
		||||
     */
 | 
			
		||||
    #keydownHandler(event) {
 | 
			
		||||
    #keydownHandler(event: KeyboardEvent) {
 | 
			
		||||
        // Ignore certain key events in inputs to allow text editing.
 | 
			
		||||
        if (event.target.matches('input') && (event.key === 'ArrowRight' || event.key === 'ArrowLeft')) {
 | 
			
		||||
        if (isHTMLElement(event.target) && event.target.matches('input') && (event.key === 'ArrowRight' || event.key === 'ArrowLeft')) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -71,7 +72,7 @@ export class KeyboardNavigationHandler {
 | 
			
		|||
        } else if (event.key === 'Escape') {
 | 
			
		||||
            if (this.onEscape) {
 | 
			
		||||
                this.onEscape(event);
 | 
			
		||||
            } else if (document.activeElement) {
 | 
			
		||||
            } else if (isHTMLElement(document.activeElement)) {
 | 
			
		||||
                document.activeElement.blur();
 | 
			
		||||
            }
 | 
			
		||||
        } else if (event.key === 'Enter' && this.onEnter) {
 | 
			
		||||
| 
						 | 
				
			
			@ -81,14 +82,15 @@ export class KeyboardNavigationHandler {
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an array of focusable elements within the current containers.
 | 
			
		||||
     * @returns {Element[]}
 | 
			
		||||
     */
 | 
			
		||||
    #getFocusable() {
 | 
			
		||||
        const focusable = [];
 | 
			
		||||
    #getFocusable(): HTMLElement[] {
 | 
			
		||||
        const focusable: HTMLElement[] = [];
 | 
			
		||||
        const selector = '[tabindex]:not([tabindex="-1"]),[href],button:not([tabindex="-1"],[disabled]),input:not([type=hidden])';
 | 
			
		||||
        for (const container of this.containers) {
 | 
			
		||||
            focusable.push(...container.querySelectorAll(selector));
 | 
			
		||||
            const toAdd = [...container.querySelectorAll(selector)].filter(e => isHTMLElement(e));
 | 
			
		||||
            focusable.push(...toAdd);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return focusable;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1,107 +0,0 @@
 | 
			
		|||
/**
 | 
			
		||||
 * Returns a function, that, as long as it continues to be invoked, will not
 | 
			
		||||
 * be triggered. The function will be called after it stops being called for
 | 
			
		||||
 * N milliseconds. If `immediate` is passed, trigger the function on the
 | 
			
		||||
 * leading edge, instead of the trailing.
 | 
			
		||||
 * @attribution https://davidwalsh.name/javascript-debounce-function
 | 
			
		||||
 * @param {Function} func
 | 
			
		||||
 * @param {Number} waitMs
 | 
			
		||||
 * @param {Boolean} immediate
 | 
			
		||||
 * @returns {Function}
 | 
			
		||||
 */
 | 
			
		||||
export function debounce(func, waitMs, immediate) {
 | 
			
		||||
    let timeout;
 | 
			
		||||
    return function debouncedWrapper(...args) {
 | 
			
		||||
        const context = this;
 | 
			
		||||
        const later = function debouncedTimeout() {
 | 
			
		||||
            timeout = null;
 | 
			
		||||
            if (!immediate) func.apply(context, args);
 | 
			
		||||
        };
 | 
			
		||||
        const callNow = immediate && !timeout;
 | 
			
		||||
        clearTimeout(timeout);
 | 
			
		||||
        timeout = setTimeout(later, waitMs);
 | 
			
		||||
        if (callNow) func.apply(context, args);
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Scroll and highlight an element.
 | 
			
		||||
 * @param {HTMLElement} element
 | 
			
		||||
 */
 | 
			
		||||
export function scrollAndHighlightElement(element) {
 | 
			
		||||
    if (!element) return;
 | 
			
		||||
 | 
			
		||||
    let parent = element;
 | 
			
		||||
    while (parent.parentElement) {
 | 
			
		||||
        parent = parent.parentElement;
 | 
			
		||||
        if (parent.nodeName === 'DETAILS' && !parent.open) {
 | 
			
		||||
            parent.open = true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    element.scrollIntoView({behavior: 'smooth'});
 | 
			
		||||
 | 
			
		||||
    const highlight = getComputedStyle(document.body).getPropertyValue('--color-link');
 | 
			
		||||
    element.style.outline = `2px dashed ${highlight}`;
 | 
			
		||||
    element.style.outlineOffset = '5px';
 | 
			
		||||
    element.style.transition = null;
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
        element.style.transition = 'outline linear 3s';
 | 
			
		||||
        element.style.outline = '2px dashed rgba(0, 0, 0, 0)';
 | 
			
		||||
        const listener = () => {
 | 
			
		||||
            element.removeEventListener('transitionend', listener);
 | 
			
		||||
            element.style.transition = null;
 | 
			
		||||
            element.style.outline = null;
 | 
			
		||||
            element.style.outlineOffset = null;
 | 
			
		||||
        };
 | 
			
		||||
        element.addEventListener('transitionend', listener);
 | 
			
		||||
    }, 1000);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Escape any HTML in the given 'unsafe' string.
 | 
			
		||||
 * Take from https://stackoverflow.com/a/6234804.
 | 
			
		||||
 * @param {String} unsafe
 | 
			
		||||
 * @returns {string}
 | 
			
		||||
 */
 | 
			
		||||
export function escapeHtml(unsafe) {
 | 
			
		||||
    return unsafe
 | 
			
		||||
        .replace(/&/g, '&')
 | 
			
		||||
        .replace(/</g, '<')
 | 
			
		||||
        .replace(/>/g, '>')
 | 
			
		||||
        .replace(/"/g, '"')
 | 
			
		||||
        .replace(/'/g, ''');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Generate a random unique ID.
 | 
			
		||||
 *
 | 
			
		||||
 * @returns {string}
 | 
			
		||||
 */
 | 
			
		||||
export function uniqueId() {
 | 
			
		||||
    // eslint-disable-next-line no-bitwise
 | 
			
		||||
    const S4 = () => (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
 | 
			
		||||
    return (`${S4() + S4()}-${S4()}-${S4()}-${S4()}-${S4()}${S4()}${S4()}`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Generate a random smaller unique ID.
 | 
			
		||||
 *
 | 
			
		||||
 * @returns {string}
 | 
			
		||||
 */
 | 
			
		||||
export function uniqueIdSmall() {
 | 
			
		||||
    // eslint-disable-next-line no-bitwise
 | 
			
		||||
    const S4 = () => (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
 | 
			
		||||
    return S4();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Create a promise that resolves after the given time.
 | 
			
		||||
 * @param {int} timeMs
 | 
			
		||||
 * @returns {Promise}
 | 
			
		||||
 */
 | 
			
		||||
export function wait(timeMs) {
 | 
			
		||||
    return new Promise(res => {
 | 
			
		||||
        setTimeout(res, timeMs);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,147 @@
 | 
			
		|||
/**
 | 
			
		||||
 * Returns a function, that, as long as it continues to be invoked, will not
 | 
			
		||||
 * be triggered. The function will be called after it stops being called for
 | 
			
		||||
 * N milliseconds. If `immediate` is passed, trigger the function on the
 | 
			
		||||
 * leading edge, instead of the trailing.
 | 
			
		||||
 * @attribution https://davidwalsh.name/javascript-debounce-function
 | 
			
		||||
 */
 | 
			
		||||
export function debounce(func: Function, waitMs: number, immediate: boolean): Function {
 | 
			
		||||
    let timeout: number|null = null;
 | 
			
		||||
    return function debouncedWrapper(this: any, ...args: any[]) {
 | 
			
		||||
        const context: any = this;
 | 
			
		||||
        const later = function debouncedTimeout() {
 | 
			
		||||
            timeout = null;
 | 
			
		||||
            if (!immediate) func.apply(context, args);
 | 
			
		||||
        };
 | 
			
		||||
        const callNow = immediate && !timeout;
 | 
			
		||||
        if (timeout) {
 | 
			
		||||
            clearTimeout(timeout);
 | 
			
		||||
        }
 | 
			
		||||
        timeout = window.setTimeout(later, waitMs);
 | 
			
		||||
        if (callNow) func.apply(context, args);
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isDetailsElement(element: HTMLElement): element is HTMLDetailsElement {
 | 
			
		||||
    return element.nodeName === 'DETAILS';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Scroll-to and highlight an element.
 | 
			
		||||
 */
 | 
			
		||||
export function scrollAndHighlightElement(element: HTMLElement): void {
 | 
			
		||||
    if (!element) return;
 | 
			
		||||
 | 
			
		||||
    // Open up parent <details> elements if within
 | 
			
		||||
    let parent = element;
 | 
			
		||||
    while (parent.parentElement) {
 | 
			
		||||
        parent = parent.parentElement;
 | 
			
		||||
        if (isDetailsElement(parent) && !parent.open) {
 | 
			
		||||
            parent.open = true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    element.scrollIntoView({behavior: 'smooth'});
 | 
			
		||||
 | 
			
		||||
    const highlight = getComputedStyle(document.body).getPropertyValue('--color-link');
 | 
			
		||||
    element.style.outline = `2px dashed ${highlight}`;
 | 
			
		||||
    element.style.outlineOffset = '5px';
 | 
			
		||||
    element.style.removeProperty('transition');
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
        element.style.transition = 'outline linear 3s';
 | 
			
		||||
        element.style.outline = '2px dashed rgba(0, 0, 0, 0)';
 | 
			
		||||
        const listener = () => {
 | 
			
		||||
            element.removeEventListener('transitionend', listener);
 | 
			
		||||
            element.style.removeProperty('transition');
 | 
			
		||||
            element.style.removeProperty('outline');
 | 
			
		||||
            element.style.removeProperty('outlineOffset');
 | 
			
		||||
        };
 | 
			
		||||
        element.addEventListener('transitionend', listener);
 | 
			
		||||
    }, 1000);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Escape any HTML in the given 'unsafe' string.
 | 
			
		||||
 * Take from https://stackoverflow.com/a/6234804.
 | 
			
		||||
 */
 | 
			
		||||
export function escapeHtml(unsafe: string): string {
 | 
			
		||||
    return unsafe
 | 
			
		||||
        .replace(/&/g, '&')
 | 
			
		||||
        .replace(/</g, '<')
 | 
			
		||||
        .replace(/>/g, '>')
 | 
			
		||||
        .replace(/"/g, '"')
 | 
			
		||||
        .replace(/'/g, ''');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Generate a random unique ID.
 | 
			
		||||
 */
 | 
			
		||||
export function uniqueId(): string {
 | 
			
		||||
    // eslint-disable-next-line no-bitwise
 | 
			
		||||
    const S4 = () => (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
 | 
			
		||||
    return (`${S4() + S4()}-${S4()}-${S4()}-${S4()}-${S4()}${S4()}${S4()}`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Generate a random smaller unique ID.
 | 
			
		||||
 */
 | 
			
		||||
export function uniqueIdSmall(): string {
 | 
			
		||||
    // eslint-disable-next-line no-bitwise
 | 
			
		||||
    const S4 = () => (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
 | 
			
		||||
    return S4();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Create a promise that resolves after the given time.
 | 
			
		||||
 */
 | 
			
		||||
export function wait(timeMs: number): Promise<any> {
 | 
			
		||||
    return new Promise(res => {
 | 
			
		||||
        setTimeout(res, timeMs);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Generate a full URL from the given relative URL, using a base
 | 
			
		||||
 * URL defined in the head of the page.
 | 
			
		||||
 */
 | 
			
		||||
export function baseUrl(path: string): string {
 | 
			
		||||
    let targetPath = path;
 | 
			
		||||
    const baseUrlMeta = document.querySelector('meta[name="base-url"]');
 | 
			
		||||
    if (!baseUrlMeta) {
 | 
			
		||||
        throw new Error('Could not find expected base-url meta tag in document');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let basePath = baseUrlMeta.getAttribute('content') || '';
 | 
			
		||||
    if (basePath[basePath.length - 1] === '/') {
 | 
			
		||||
        basePath = basePath.slice(0, basePath.length - 1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (targetPath[0] === '/') {
 | 
			
		||||
        targetPath = targetPath.slice(1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return `${basePath}/${targetPath}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Get the current version of BookStack in use.
 | 
			
		||||
 * Grabs this from the version query used on app assets.
 | 
			
		||||
 */
 | 
			
		||||
function getVersion(): string {
 | 
			
		||||
    const styleLink = document.querySelector('link[href*="/dist/styles.css?version="]');
 | 
			
		||||
    if (!styleLink) {
 | 
			
		||||
        throw new Error('Could not find expected style link in document for version use');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const href = (styleLink.getAttribute('href') || '');
 | 
			
		||||
    return href.split('?version=').pop() || '';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Perform a module import, Ensuring the import is fetched with the current
 | 
			
		||||
 * app version as a cache-breaker.
 | 
			
		||||
 */
 | 
			
		||||
export function importVersioned(moduleName: string): Promise<object> {
 | 
			
		||||
    const importPath = window.baseUrl(`dist/${moduleName}.js?version=${getVersion()}`);
 | 
			
		||||
    return import(importPath);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
import * as DrawIO from '../services/drawio.ts';
 | 
			
		||||
import {wait} from '../services/util';
 | 
			
		||||
import {wait} from '../services/util.ts';
 | 
			
		||||
 | 
			
		||||
let pageEditor = null;
 | 
			
		||||
let currentNode = null;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue