diff --git a/resources/js/app.js b/resources/js/app.js deleted file mode 100644 index 5f4902f86..000000000 --- a/resources/js/app.js +++ /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 index 000000000..141a50ef5 --- /dev/null +++ b/resources/js/app.ts @@ -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(); diff --git a/resources/js/components/chapter-contents.js b/resources/js/components/chapter-contents.js index 7c6480a1a..6b0707bdd 100644 --- a/resources/js/components/chapter-contents.js +++ b/resources/js/components/chapter-contents.js @@ -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 { diff --git a/resources/js/components/collapsible.js b/resources/js/components/collapsible.js index 6f740ed71..7b6fa79fb 100644 --- a/resources/js/components/collapsible.js +++ b/resources/js/components/collapsible.js @@ -1,4 +1,4 @@ -import {slideDown, slideUp} from '../services/animations'; +import {slideDown, slideUp} from '../services/animations.ts'; import {Component} from './component'; /** diff --git a/resources/js/components/dropdown-search.js b/resources/js/components/dropdown-search.js index 787e11c87..fcbabc022 100644 --- a/resources/js/components/dropdown-search.js +++ b/resources/js/components/dropdown-search.js @@ -1,5 +1,5 @@ import {debounce} from '../services/util.ts'; -import {transitionHeight} from '../services/animations'; +import {transitionHeight} from '../services/animations.ts'; import {Component} from './component'; export class DropdownSearch extends Component { diff --git a/resources/js/components/expand-toggle.js b/resources/js/components/expand-toggle.js index 0d2018b9d..29173a058 100644 --- a/resources/js/components/expand-toggle.js +++ b/resources/js/components/expand-toggle.js @@ -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 { diff --git a/resources/js/components/index.js b/resources/js/components/index.ts similarity index 100% rename from resources/js/components/index.js rename to resources/js/components/index.ts diff --git a/resources/js/components/popup.js b/resources/js/components/popup.js index 662736548..edd428037 100644 --- a/resources/js/components/popup.js +++ b/resources/js/components/popup.js @@ -1,4 +1,4 @@ -import {fadeIn, fadeOut} from '../services/animations'; +import {fadeIn, fadeOut} from '../services/animations.ts'; import {onSelect} from '../services/dom'; import {Component} from './component'; diff --git a/resources/js/global.d.ts b/resources/js/global.d.ts index e505c96e0..b637c97c1 100644 --- a/resources/js/global.d.ts +++ b/resources/js/global.d.ts @@ -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; } } \ No newline at end of file diff --git a/resources/js/services/animations.js b/resources/js/services/animations.ts similarity index 63% rename from resources/js/services/animations.js rename to resources/js/services/animations.ts index bc983c807..adf4cb3c9 100644 --- a/resources/js/services/animations.js +++ b/resources/js/services/animations.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} */ -const animateStylesCleanupMap = new WeakMap(); +const animateStylesCleanupMap: WeakMap = 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, + 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); diff --git a/resources/js/services/util.ts b/resources/js/services/util.ts index 7f684dd42..c5a5d2db8 100644 --- a/resources/js/services/util.ts +++ b/resources/js/services/util.ts @@ -99,3 +99,49 @@ export function wait(timeMs: number): Promise { 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 { + const importPath = window.baseUrl(`dist/${moduleName}.js?version=${getVersion()}`); + return import(importPath); +} \ No newline at end of file