TS: Converted app file and animations service
Some checks are pending
lint-js / build (push) Waiting to run
test-js / build (push) Waiting to run

Extracted functions out of app file during changes to clean up.
Altered animation function to use normal css prop names instead of JS
CSS prop names.
This commit is contained in:
Dan Brown 2024-10-11 15:19:19 +01:00
parent 2e8d6ce7d9
commit f41c02cbd7
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
11 changed files with 108 additions and 84 deletions

View File

@ -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();

23
resources/js/app.ts Normal file
View File

@ -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();

View File

@ -1,4 +1,4 @@
import {slideUp, slideDown} from '../services/animations'; import {slideUp, slideDown} from '../services/animations.ts';
import {Component} from './component'; import {Component} from './component';
export class ChapterContents extends Component { export class ChapterContents extends Component {

View File

@ -1,4 +1,4 @@
import {slideDown, slideUp} from '../services/animations'; import {slideDown, slideUp} from '../services/animations.ts';
import {Component} from './component'; import {Component} from './component';
/** /**

View File

@ -1,5 +1,5 @@
import {debounce} from '../services/util.ts'; import {debounce} from '../services/util.ts';
import {transitionHeight} from '../services/animations'; import {transitionHeight} from '../services/animations.ts';
import {Component} from './component'; import {Component} from './component';
export class DropdownSearch extends Component { export class DropdownSearch extends Component {

View File

@ -1,4 +1,4 @@
import {slideUp, slideDown} from '../services/animations'; import {slideUp, slideDown} from '../services/animations.ts';
import {Component} from './component'; import {Component} from './component';
export class ExpandToggle extends Component { export class ExpandToggle extends Component {

View File

@ -1,4 +1,4 @@
import {fadeIn, fadeOut} from '../services/animations'; import {fadeIn, fadeOut} from '../services/animations.ts';
import {onSelect} from '../services/dom'; import {onSelect} from '../services/dom';
import {Component} from './component'; import {Component} from './component';

View File

@ -7,10 +7,12 @@ declare global {
const __DEV__: boolean; const __DEV__: boolean;
interface Window { interface Window {
__DEV__: boolean;
$components: ComponentStore; $components: ComponentStore;
$events: EventManager; $events: EventManager;
$trans: Translator; $trans: Translator;
$http: HttpManager; $http: HttpManager;
baseUrl: (path: string) => string; baseUrl: (path: string) => string;
importVersioned: (module: string) => Promise<object>;
} }
} }

View File

@ -1,30 +1,30 @@
/** /**
* Used in the function below to store references of clean-up functions. * Used in the function below to store references of clean-up functions.
* Used to ensure only one transitionend function exists at any time. * 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. * 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] * 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); const styleNames = Object.keys(styles);
for (const style of styleNames) { for (const style of styleNames) {
element.style[style] = styles[style][0]; element.style.setProperty(style, styles[style][0]);
} }
const cleanup = () => { const cleanup = () => {
for (const style of styleNames) { 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); element.removeEventListener('transitionend', cleanup);
animateStylesCleanupMap.delete(element); animateStylesCleanupMap.delete(element);
if (onComplete) onComplete(); if (onComplete) onComplete();
@ -33,7 +33,7 @@ function animateStyles(element, styles, animTime = 400, onComplete = null) {
setTimeout(() => { setTimeout(() => {
element.style.transition = `all ease-in-out ${animTime}ms`; element.style.transition = `all ease-in-out ${animTime}ms`;
for (const style of styleNames) { for (const style of styleNames) {
element.style[style] = styles[style][1]; element.style.setProperty(style, styles[style][1]);
} }
element.addEventListener('transitionend', cleanup); 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. * Run the active cleanup action for the given element.
* @param {Element} element
*/ */
function cleanupExistingElementAnimation(element) { function cleanupExistingElementAnimation(element: Element) {
if (animateStylesCleanupMap.has(element)) { if (animateStylesCleanupMap.has(element)) {
const oldCleanup = animateStylesCleanupMap.get(element); const oldCleanup = animateStylesCleanupMap.get(element);
oldCleanup(); oldCleanup();
@ -54,15 +53,12 @@ function cleanupExistingElementAnimation(element) {
/** /**
* Fade in the given 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); cleanupExistingElementAnimation(element);
element.style.display = 'block'; element.style.display = 'block';
animateStyles(element, { animateStyles(element, {
opacity: ['0', '1'], 'opacity': ['0', '1'],
}, animTime, () => { }, animTime, () => {
if (onComplete) onComplete(); if (onComplete) onComplete();
}); });
@ -70,14 +66,11 @@ export function fadeIn(element, animTime = 400, onComplete = null) {
/** /**
* Fade out the given element. * 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); cleanupExistingElementAnimation(element);
animateStyles(element, { animateStyles(element, {
opacity: ['1', '0'], 'opacity': ['1', '0'],
}, animTime, () => { }, animTime, () => {
element.style.display = 'none'; element.style.display = 'none';
if (onComplete) onComplete(); if (onComplete) onComplete();
@ -86,20 +79,18 @@ export function fadeOut(element, animTime = 400, onComplete = null) {
/** /**
* Hide the element by sliding the contents upwards. * 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); cleanupExistingElementAnimation(element);
const currentHeight = element.getBoundingClientRect().height; const currentHeight = element.getBoundingClientRect().height;
const computedStyles = getComputedStyle(element); const computedStyles = getComputedStyle(element);
const currentPaddingTop = computedStyles.getPropertyValue('padding-top'); const currentPaddingTop = computedStyles.getPropertyValue('padding-top');
const currentPaddingBottom = computedStyles.getPropertyValue('padding-bottom'); const currentPaddingBottom = computedStyles.getPropertyValue('padding-bottom');
const animStyles = { const animStyles = {
maxHeight: [`${currentHeight}px`, '0px'], 'max-height': [`${currentHeight}px`, '0px'],
overflow: ['hidden', 'hidden'], 'overflow': ['hidden', 'hidden'],
paddingTop: [currentPaddingTop, '0px'], 'padding-top': [currentPaddingTop, '0px'],
paddingBottom: [currentPaddingBottom, '0px'], 'padding-bottom': [currentPaddingBottom, '0px'],
}; };
animateStyles(element, animStyles, animTime, () => { animateStyles(element, animStyles, animTime, () => {
@ -109,10 +100,8 @@ export function slideUp(element, animTime = 400) {
/** /**
* Show the given element by expanding the contents. * 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); cleanupExistingElementAnimation(element);
element.style.display = 'block'; element.style.display = 'block';
const targetHeight = element.getBoundingClientRect().height; const targetHeight = element.getBoundingClientRect().height;
@ -120,10 +109,10 @@ export function slideDown(element, animTime = 400) {
const targetPaddingTop = computedStyles.getPropertyValue('padding-top'); const targetPaddingTop = computedStyles.getPropertyValue('padding-top');
const targetPaddingBottom = computedStyles.getPropertyValue('padding-bottom'); const targetPaddingBottom = computedStyles.getPropertyValue('padding-bottom');
const animStyles = { const animStyles = {
maxHeight: ['0px', `${targetHeight}px`], 'max-height': ['0px', `${targetHeight}px`],
overflow: ['hidden', 'hidden'], 'overflow': ['hidden', 'hidden'],
paddingTop: ['0px', targetPaddingTop], 'padding-top': ['0px', targetPaddingTop],
paddingBottom: ['0px', targetPaddingBottom], 'padding-bottom': ['0px', targetPaddingBottom],
}; };
animateStyles(element, animStyles, animTime); 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 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. * 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. * 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 startHeight = element.getBoundingClientRect().height;
const initialComputedStyles = getComputedStyle(element); const initialComputedStyles = getComputedStyle(element);
const startPaddingTop = initialComputedStyles.getPropertyValue('padding-top'); const startPaddingTop = initialComputedStyles.getPropertyValue('padding-top');
@ -151,10 +137,10 @@ export function transitionHeight(element, animTime = 400) {
const targetPaddingTop = computedStyles.getPropertyValue('padding-top'); const targetPaddingTop = computedStyles.getPropertyValue('padding-top');
const targetPaddingBottom = computedStyles.getPropertyValue('padding-bottom'); const targetPaddingBottom = computedStyles.getPropertyValue('padding-bottom');
const animStyles = { const animStyles = {
height: [`${startHeight}px`, `${targetHeight}px`], 'height': [`${startHeight}px`, `${targetHeight}px`],
overflow: ['hidden', 'hidden'], 'overflow': ['hidden', 'hidden'],
paddingTop: [startPaddingTop, targetPaddingTop], 'padding-top': [startPaddingTop, targetPaddingTop],
paddingBottom: [startPaddingBottom, targetPaddingBottom], 'padding-bottom': [startPaddingBottom, targetPaddingBottom],
}; };
animateStyles(element, animStyles, animTime); animateStyles(element, animStyles, animTime);

View File

@ -99,3 +99,49 @@ export function wait(timeMs: number): Promise<any> {
setTimeout(res, timeMs); 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);
}