mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-12-15 15:44:03 +08:00
154 lines
5.1 KiB
TypeScript
154 lines
5.1 KiB
TypeScript
|
import {kebabToCamel, camelToKebab} from './text';
|
||
|
import {Component} from "../components/component";
|
||
|
|
||
|
/**
|
||
|
* Parse out the element references within the given element
|
||
|
* for the given component name.
|
||
|
*/
|
||
|
function parseRefs(name: string, element: HTMLElement):
|
||
|
{refs: Record<string, HTMLElement>, manyRefs: Record<string, HTMLElement[]>} {
|
||
|
const refs: Record<string, HTMLElement> = {};
|
||
|
const manyRefs: Record<string, HTMLElement[]> = {};
|
||
|
|
||
|
const prefix = `${name}@`;
|
||
|
const selector = `[refs*="${prefix}"]`;
|
||
|
const refElems = [...element.querySelectorAll(selector)];
|
||
|
if (element.matches(selector)) {
|
||
|
refElems.push(element);
|
||
|
}
|
||
|
|
||
|
for (const el of refElems as HTMLElement[]) {
|
||
|
const refNames = (el.getAttribute('refs') || '')
|
||
|
.split(' ')
|
||
|
.filter(str => str.startsWith(prefix))
|
||
|
.map(str => str.replace(prefix, ''))
|
||
|
.map(kebabToCamel);
|
||
|
for (const ref of refNames) {
|
||
|
refs[ref] = el;
|
||
|
if (typeof manyRefs[ref] === 'undefined') {
|
||
|
manyRefs[ref] = [];
|
||
|
}
|
||
|
manyRefs[ref].push(el);
|
||
|
}
|
||
|
}
|
||
|
return {refs, manyRefs};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Parse out the element component options.
|
||
|
*/
|
||
|
function parseOpts(componentName: string, element: HTMLElement): Record<string, string> {
|
||
|
const opts: Record<string, string> = {};
|
||
|
const prefix = `option:${componentName}:`;
|
||
|
for (const {name, value} of element.attributes) {
|
||
|
if (name.startsWith(prefix)) {
|
||
|
const optName = name.replace(prefix, '');
|
||
|
opts[kebabToCamel(optName)] = value || '';
|
||
|
}
|
||
|
}
|
||
|
return opts;
|
||
|
}
|
||
|
|
||
|
export class ComponentStore {
|
||
|
/**
|
||
|
* A mapping of active components keyed by name, with values being arrays of component
|
||
|
* instances since there can be multiple components of the same type.
|
||
|
*/
|
||
|
protected components: Record<string, Component[]> = {};
|
||
|
|
||
|
/**
|
||
|
* A mapping of component class models, keyed by name.
|
||
|
*/
|
||
|
protected componentModelMap: Record<string, typeof Component> = {};
|
||
|
|
||
|
/**
|
||
|
* A mapping of active component maps, keyed by the element components are assigned to.
|
||
|
*/
|
||
|
protected elementComponentMap: WeakMap<HTMLElement, Record<string, Component>> = new WeakMap();
|
||
|
|
||
|
/**
|
||
|
* Initialize a component instance on the given dom element.
|
||
|
*/
|
||
|
protected initComponent(name: string, element: HTMLElement): void {
|
||
|
const ComponentModel = this.componentModelMap[name];
|
||
|
if (ComponentModel === undefined) return;
|
||
|
|
||
|
// Create our component instance
|
||
|
let instance: Component|null = null;
|
||
|
try {
|
||
|
instance = new ComponentModel();
|
||
|
instance.$name = name;
|
||
|
instance.$el = element;
|
||
|
const allRefs = parseRefs(name, element);
|
||
|
instance.$refs = allRefs.refs;
|
||
|
instance.$manyRefs = allRefs.manyRefs;
|
||
|
instance.$opts = parseOpts(name, element);
|
||
|
instance.setup();
|
||
|
} catch (e) {
|
||
|
console.error('Failed to create component', e, name, element);
|
||
|
}
|
||
|
|
||
|
if (!instance) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Add to global listing
|
||
|
if (typeof this.components[name] === 'undefined') {
|
||
|
this.components[name] = [];
|
||
|
}
|
||
|
this.components[name].push(instance);
|
||
|
|
||
|
// Add to element mapping
|
||
|
const elComponents = this.elementComponentMap.get(element) || {};
|
||
|
elComponents[name] = instance;
|
||
|
this.elementComponentMap.set(element, elComponents);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Initialize all components found within the given element.
|
||
|
*/
|
||
|
public init(parentElement: Document|HTMLElement = document) {
|
||
|
const componentElems = parentElement.querySelectorAll('[component],[components]');
|
||
|
|
||
|
for (const el of componentElems) {
|
||
|
const componentNames = `${el.getAttribute('component') || ''} ${(el.getAttribute('components'))}`.toLowerCase().split(' ').filter(Boolean);
|
||
|
for (const name of componentNames) {
|
||
|
this.initComponent(name, el as HTMLElement);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Register the given component mapping into the component system.
|
||
|
* @param {Object<String, ObjectConstructor<Component>>} mapping
|
||
|
*/
|
||
|
public register(mapping: Record<string, typeof Component>) {
|
||
|
const keys = Object.keys(mapping);
|
||
|
for (const key of keys) {
|
||
|
this.componentModelMap[camelToKebab(key)] = mapping[key];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the first component of the given name.
|
||
|
*/
|
||
|
public first(name: string): Component|null {
|
||
|
return (this.components[name] || [null])[0];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get all the components of the given name.
|
||
|
*/
|
||
|
public get(name: string): Component[] {
|
||
|
return this.components[name] || [];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the first component, of the given name, that's assigned to the given element.
|
||
|
*/
|
||
|
public firstOnElement(element: HTMLElement, name: string): Component|null {
|
||
|
const elComponents = this.elementComponentMap.get(element) || {};
|
||
|
return elComponents[name] || null;
|
||
|
}
|
||
|
}
|