// 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'),
+++ /dev/null
-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();
--- /dev/null
+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();
-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';
/**
-import {onSelect} from '../services/dom';
+import {onSelect} from '../services/dom.ts';
import {Component} from './component';
export class AjaxDeleteRow extends Component {
-import {onEnterPress, onSelect} from '../services/dom';
+import {onEnterPress, onSelect} from '../services/dom.ts';
import {Component} from './component';
/**
-import {showLoading} from '../services/dom';
+import {showLoading} from '../services/dom.ts';
import {Component} from './component';
export class Attachments extends Component {
-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 = {};
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 = {
-import {slideUp, slideDown} from '../services/animations';
+import {slideUp, slideDown} from '../services/animations.ts';
import {Component} from './component';
export class ChapterContents extends Component {
-import {onChildEvent, onEnterPress, onSelect} from '../services/dom';
+import {onChildEvent, onEnterPress, onSelect} from '../services/dom.ts';
import {Component} from './component';
export class CodeEditor extends Component {
-import {slideDown, slideUp} from '../services/animations';
+import {slideDown, slideUp} from '../services/animations.ts';
import {Component} from './component';
/**
-import {onSelect} from '../services/dom';
+import {onSelect} from '../services/dom.ts';
import {Component} from './component';
/**
-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 {
-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';
/**
import {Clipboard} from '../services/clipboard.ts';
import {
elem, getLoading, onSelect, removeLoading,
-} from '../services/dom';
+} from '../services/dom.ts';
export class Dropzone extends Component {
-import {htmlToDom} from '../services/dom';
+import {htmlToDom} from '../services/dom.ts';
import {Component} from './component';
export class EntityPermissions extends Component {
-import {onSelect} from '../services/dom';
+import {onSelect} from '../services/dom.ts';
import {Component} from './component';
export class EntitySearch extends Component {
-import {onChildEvent} from '../services/dom';
+import {onChildEvent} from '../services/dom.ts';
import {Component} from './component';
/**
-import {onSelect} from '../services/dom';
+import {onSelect} from '../services/dom.ts';
import {Component} from './component';
/**
-import {slideUp, slideDown} from '../services/animations';
+import {slideUp, slideDown} from '../services/animations.ts';
import {Component} from './component';
export class ExpandToggle extends Component {
-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';
/**
import {
onChildEvent, onSelect, removeLoading, showLoading,
-} from '../services/dom';
+} from '../services/dom.ts';
import {Component} from './component';
export class ImageManager extends Component {
-import {onSelect} from '../services/dom';
+import {onSelect} from '../services/dom.ts';
import {Component} from './component';
export class OptionalInput extends Component {
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 {
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 {
-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) {
-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';
-import * as DOM from '../services/dom';
+import * as DOM from '../services/dom.ts';
import {Component} from './component';
import {copyTextToClipboard} from '../services/clipboard.ts';
-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';
/**
-import * as DOM from '../services/dom';
+import * as DOM from '../services/dom.ts';
import {Component} from './component';
export class TemplateManager extends Component {
-import {onChildEvent} from '../services/dom';
+import {onChildEvent} from '../services/dom.ts';
import {Component} from './component';
export class UserSelect extends Component {
const __DEV__: boolean;
interface Window {
+ __DEV__: boolean;
$components: ComponentStore;
$events: EventManager;
$trans: Translator;
$http: HttpManager;
baseUrl: (path: string) => string;
+ importVersioned: (module: string) => Promise<object>;
}
}
\ No newline at end of file
import {provideKeyBindings} from './shortcuts';
-import {debounce} from '../services/util';
+import {debounce} from '../services/util.ts';
import {Clipboard} from '../services/clipboard.ts';
/**
/**
* 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();
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);
/**
* 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();
/**
* 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();
});
/**
* 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();
/**
* 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, () => {
/**
* 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;
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);
* 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');
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);
+/**
+ * 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)) {
/**
* 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);
/**
* 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);
}
/**
* 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];
}
/**
* 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);
}
/**
* 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);
}
* 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);
}
/**
* 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;
}
}
/**
* 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>';
/**
* 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();
/**
* 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;
}
+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;
/**
* 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));
}
*/
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;
*/
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;
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;
}
} 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) {
/**
* 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;
}
+++ /dev/null
-/**
- * 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);
- });
-}
--- /dev/null
+/**
+ * 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);
+}
\ No newline at end of file
import * as DrawIO from '../services/drawio.ts';
-import {wait} from '../services/util';
+import {wait} from '../services/util.ts';
let pageEditor = null;
let currentNode = null;