]> BookStack Code Mirror - bookstack/commitdiff
Merge pull request #5259 from BookStackApp/typescript-conversions
authorDan Brown <redacted>
Sun, 1 Dec 2024 13:04:59 +0000 (13:04 +0000)
committerGitHub <redacted>
Sun, 1 Dec 2024 13:04:59 +0000 (13:04 +0000)
Conversion of Services to TypeScript

41 files changed:
dev/build/esbuild.js
resources/js/app.js [deleted file]
resources/js/app.ts [new file with mode: 0644]
resources/js/components/add-remove-rows.js
resources/js/components/ajax-delete-row.js
resources/js/components/ajax-form.js
resources/js/components/attachments.js
resources/js/components/auto-suggest.js
resources/js/components/book-sort.js
resources/js/components/chapter-contents.js
resources/js/components/code-editor.js
resources/js/components/collapsible.js
resources/js/components/confirm-dialog.js
resources/js/components/dropdown-search.js
resources/js/components/dropdown.js
resources/js/components/dropzone.js
resources/js/components/entity-permissions.js
resources/js/components/entity-search.js
resources/js/components/entity-selector.js
resources/js/components/event-emit-select.js
resources/js/components/expand-toggle.js
resources/js/components/global-search.js
resources/js/components/image-manager.js
resources/js/components/index.ts [moved from resources/js/components/index.js with 100% similarity]
resources/js/components/optional-input.js
resources/js/components/page-comment.js
resources/js/components/page-comments.js
resources/js/components/page-display.js
resources/js/components/page-editor.js
resources/js/components/pointer.js
resources/js/components/popup.js
resources/js/components/template-manager.js
resources/js/components/user-select.js
resources/js/global.d.ts
resources/js/markdown/codemirror.js
resources/js/services/animations.ts [moved from resources/js/services/animations.js with 63% similarity]
resources/js/services/dom.ts [moved from resources/js/services/dom.js with 63% similarity]
resources/js/services/keyboard-navigation.ts [moved from resources/js/services/keyboard-navigation.js with 66% similarity]
resources/js/services/util.js [deleted file]
resources/js/services/util.ts [new file with mode: 0644]
resources/js/wysiwyg-tinymce/plugin-drawio.js

index fea8c01e353887d2bb169d330fec1bc51b997a0a..cd8bf279f28db0858cbd3c238ea029d4582d107d 100644 (file)
@@ -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'),
diff --git a/resources/js/app.js b/resources/js/app.js
deleted file mode 100644 (file)
index 5f4902f..0000000
+++ /dev/null
@@ -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();
diff --git a/resources/js/app.ts b/resources/js/app.ts
new file mode 100644 (file)
index 0000000..141a50e
--- /dev/null
@@ -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();
index 3213c4835aa45fd5bad209e34b4b5a9e6b44967c..e7de15ae5fa03bd63a720508638cb91663374712 100644 (file)
@@ -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';
 
 /**
index aa2801f19e666c52ee9f1d738b71478ec105552a..6ed3deedf4dd6ed9e16d7f77b9a5edaa54bded8a 100644 (file)
@@ -1,4 +1,4 @@
-import {onSelect} from '../services/dom';
+import {onSelect} from '../services/dom.ts';
 import {Component} from './component';
 
 export class AjaxDeleteRow extends Component {
index 583dde5724424defb44906504ab4973a2fb88af5..de1a6db43a7e83cad11b8d783fb3db9be9de01fd 100644 (file)
@@ -1,4 +1,4 @@
-import {onEnterPress, onSelect} from '../services/dom';
+import {onEnterPress, onSelect} from '../services/dom.ts';
 import {Component} from './component';
 
 /**
index f45b25e36d05f92d96d175e8b5207020bdcd4d14..2dc7313a880901014a08c2e2f9c7a163030ab2ea 100644 (file)
@@ -1,4 +1,4 @@
-import {showLoading} from '../services/dom';
+import {showLoading} from '../services/dom.ts';
 import {Component} from './component';
 
 export class Attachments extends Component {
index 2eede241c694dc323f53ff33070163de2a79cea4..0b828e71bd1ea35976f348d02fd1c31dca2c5de6 100644 (file)
@@ -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 = {};
 
index 2ba7d5d36b00dc912be8c3d1f25f1b5e464d2ffe..48557141f6f7d80e66c501bfe9655340694566f0 100644 (file)
@@ -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 = {
index 7c6480a1af0b8a6c176e1f6dd8beca9ada8a5033..6b0707bdd0570b4acbae3d32a7cf23f2864232ba 100644 (file)
@@ -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 {
index 091c3483f4d6bd1ecffabb7f52f3f0b17933abfa..12937d47293b31effb7e2766d68af08598834899 100644 (file)
@@ -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 {
index 6f740ed7163204020fcbd6569393b14729d787c4..7b6fa79fb3bcef4ab8c13b59090711754efac7c9 100644 (file)
@@ -1,4 +1,4 @@
-import {slideDown, slideUp} from '../services/animations';
+import {slideDown, slideUp} from '../services/animations.ts';
 import {Component} from './component';
 
 /**
index 184618fccfad9928cca02f81f3450b61fbe21ebe..00f3cfed201c34016da201a0fca14afb6c07e37c 100644 (file)
@@ -1,4 +1,4 @@
-import {onSelect} from '../services/dom';
+import {onSelect} from '../services/dom.ts';
 import {Component} from './component';
 
 /**
index 2344619f5e9b794de3177c6c4acf8a5c7550b351..fcbabc022a7c21395272c092e175fb968d773ab2 100644 (file)
@@ -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 {
index 4efd428acf72b408dc4867e94a203173ed06c049..5dd5dd93b013023ebf466ef021e9237dd1b57ce7 100644 (file)
@@ -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';
 
 /**
index 920fe875f22135de5a302dee0a8e44d7207aaf86..598e0d8d48b685cdb58a9782d5addc4eafa64732 100644 (file)
@@ -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 {
 
index 7ab99a2a70bb2f45445d485b6aff49e018b25d7b..b020c5d85ba8f785785df1e2f98e2607f0e9b385 100644 (file)
@@ -1,4 +1,4 @@
-import {htmlToDom} from '../services/dom';
+import {htmlToDom} from '../services/dom.ts';
 import {Component} from './component';
 
 export class EntityPermissions extends Component {
index 7a50444708dee6313ab5b2caee5a2dd21def7cdb..9d45133266d7e4095219970ecc22361d9368aa2d 100644 (file)
@@ -1,4 +1,4 @@
-import {onSelect} from '../services/dom';
+import {onSelect} from '../services/dom.ts';
 import {Component} from './component';
 
 export class EntitySearch extends Component {
index 561370d7a342004dacc592e81d5366a07e1d00a9..7491119a137ffbb24d0d83f0f4be1ec46add9ac7 100644 (file)
@@ -1,4 +1,4 @@
-import {onChildEvent} from '../services/dom';
+import {onChildEvent} from '../services/dom.ts';
 import {Component} from './component';
 
 /**
index 2097c0528868181cdc9e94c67cc630fdfca18652..f722a25e71b75faebb784b0ddbc044af3099fd76 100644 (file)
@@ -1,4 +1,4 @@
-import {onSelect} from '../services/dom';
+import {onSelect} from '../services/dom.ts';
 import {Component} from './component';
 
 /**
index 0d2018b9da23531b27d98b18db682335ab47615b..29173a058293d99f88ed25390892961ea39e92e1 100644 (file)
@@ -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 {
index 798bd7aacb0d5c00fb5d0ece92c6ae604678a235..2cdaf591ac458d99c13c5d3f8638d86a52ef3141 100644 (file)
@@ -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';
 
 /**
index 47231477b684fc00e1d453bbc5a83d88a3188500..c8108ab28c19f6b456e419a50833ae8cd98f9b34 100644 (file)
@@ -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 {
index 64cee12cd29b6a212a2fb882be6a0be193598a46..1b133047d00d1d23d2bec486ee5ca7a84beb0af9 100644 (file)
@@ -1,4 +1,4 @@
-import {onSelect} from '../services/dom';
+import {onSelect} from '../services/dom.ts';
 import {Component} from './component';
 
 export class OptionalInput extends Component {
index fd8ad1f2e47b0ac5a5c6494f7ffcc9c5b2df5d8e..8c0a8b33e5406a4659a8ff7cfe97407a1acc6df8 100644 (file)
@@ -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 {
index 1d6abfe2044ff3a0a9e006f29e04fb4fef94601c..3d7e1365f30946e5d577787b6f251e306496900e 100644 (file)
@@ -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 {
index 1e13ae38800ebf6cd86a588cbe7e07ccc0957200..d3ac78a4ad19347779583d1331cb470d6532959a 100644 (file)
@@ -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) {
index 2160675529bd5eef9a132141af2f7f9ee853dc01..7ffceb0f9048a281f53ae82dfe31732e52d17247 100644 (file)
@@ -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';
 
index 607576cb9f9595a6cb9382c44e6c8a7f6002cba4..292b923e5519f47742990918214c7f46b7e9140b 100644 (file)
@@ -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';
 
index 6627365483e69f6c90b37d5eac7528f1f0c71be3..6bd8f9c722b7044b9ae9ab5cd73867e8383a5434 100644 (file)
@@ -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';
 
 /**
index 56ec876d48bbb66a78e65038e4bfdd45a09759d5..cf81990ab3f934539bdc104a3226d2575e745d9e 100644 (file)
@@ -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 {
index e6adc3c23c82d4e163c12c4f4c42adfc1d80d249..f9ec03ed366a4c6ea4bd643dea0ab7e524ff5ae7 100644 (file)
@@ -1,4 +1,4 @@
-import {onChildEvent} from '../services/dom';
+import {onChildEvent} from '../services/dom.ts';
 import {Component} from './component';
 
 export class UserSelect extends Component {
index e505c96e0d4a24f1a5d83fcee75a9e1b70962273..b637c97c1b92a7cf55dd3556c427d4e0e1992e54 100644 (file)
@@ -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>;
     }
 }
\ No newline at end of file
index a6332cbb844fddc44d022e9708b0051fca5b41e3..664767605b8a91b33125512efc96476068da4c32 100644 (file)
@@ -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';
 
 /**
similarity index 63%
rename from resources/js/services/animations.js
rename to resources/js/services/animations.ts
index bc983c8072aa529d7693018716c7b0ed255db087..adf4cb3c932f66afb140fbcd66386dbdb6adeb40 100644 (file)
@@ -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);
similarity index 63%
rename from resources/js/services/dom.js
rename to resources/js/services/dom.ts
index bcfd0b565da2ab9b9144af2243d2cdb2fe13cc50..c88827bac40a1788b89295152799b62e9871425f 100644 (file)
@@ -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;
 }
similarity index 66%
rename from resources/js/services/keyboard-navigation.js
rename to resources/js/services/keyboard-navigation.ts
index 34111bb2d37886e3d98a7e150fd7ada800ba186b..13fbdfecc9d81561bec3ef7ce183e605f914fce7 100644 (file)
@@ -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;
     }
 
diff --git a/resources/js/services/util.js b/resources/js/services/util.js
deleted file mode 100644 (file)
index 1264d10..0000000
+++ /dev/null
@@ -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, '&amp;')
-        .replace(/</g, '&lt;')
-        .replace(/>/g, '&gt;')
-        .replace(/"/g, '&quot;')
-        .replace(/'/g, '&#039;');
-}
-
-/**
- * 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);
-    });
-}
diff --git a/resources/js/services/util.ts b/resources/js/services/util.ts
new file mode 100644 (file)
index 0000000..c5a5d2d
--- /dev/null
@@ -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, '&amp;')
+        .replace(/</g, '&lt;')
+        .replace(/>/g, '&gt;')
+        .replace(/"/g, '&quot;')
+        .replace(/'/g, '&#039;');
+}
+
+/**
+ * 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
index 342cac0af74c4df3677482e5efc8ba4a92e60f90..197c50b0e448aec7c7d62f3a25f943ecf4112a1e 100644 (file)
@@ -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;