mirror of
				https://github.com/BookStackApp/BookStack.git
				synced 2025-11-04 13:31:45 +03:00 
			
		
		
		
	
		
			
				
	
	
		
			166 lines
		
	
	
		
			4.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			166 lines
		
	
	
		
			4.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
import {kebabToCamel, camelToKebab} from './text';
 | 
						|
 | 
						|
/**
 | 
						|
 * 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.
 | 
						|
 * @type {Object<String, Component[]>}
 | 
						|
 */
 | 
						|
const components = {};
 | 
						|
 | 
						|
/**
 | 
						|
 * A mapping of component class models, keyed by name.
 | 
						|
 * @type {Object<String, Constructor<Component>>}
 | 
						|
 */
 | 
						|
const componentModelMap = {};
 | 
						|
 | 
						|
/**
 | 
						|
 * A mapping of active component maps, keyed by the element components are assigned to.
 | 
						|
 * @type {WeakMap<Element, Object<String, Component>>}
 | 
						|
 */
 | 
						|
const elementComponentMap = new WeakMap();
 | 
						|
 | 
						|
/**
 | 
						|
 * Parse out the element references within the given element
 | 
						|
 * for the given component name.
 | 
						|
 * @param {String} name
 | 
						|
 * @param {Element} element
 | 
						|
 */
 | 
						|
function parseRefs(name, element) {
 | 
						|
    const refs = {};
 | 
						|
    const manyRefs = {};
 | 
						|
 | 
						|
    const prefix = `${name}@`;
 | 
						|
    const selector = `[refs*="${prefix}"]`;
 | 
						|
    const refElems = [...element.querySelectorAll(selector)];
 | 
						|
    if (element.matches(selector)) {
 | 
						|
        refElems.push(element);
 | 
						|
    }
 | 
						|
 | 
						|
    for (const el of refElems) {
 | 
						|
        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.
 | 
						|
 * @param {String} componentName
 | 
						|
 * @param {Element} element
 | 
						|
 * @return {Object<String, String>}
 | 
						|
 */
 | 
						|
function parseOpts(componentName, element) {
 | 
						|
    const opts = {};
 | 
						|
    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;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Initialize a component instance on the given dom element.
 | 
						|
 * @param {String} name
 | 
						|
 * @param {Element} element
 | 
						|
 */
 | 
						|
function initComponent(name, element) {
 | 
						|
    /** @type {Function<Component>|undefined} * */
 | 
						|
    const ComponentModel = componentModelMap[name];
 | 
						|
    if (ComponentModel === undefined) return;
 | 
						|
 | 
						|
    // Create our component instance
 | 
						|
    /** @type {Component} * */
 | 
						|
    let instance;
 | 
						|
    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);
 | 
						|
    }
 | 
						|
 | 
						|
    // Add to global listing
 | 
						|
    if (typeof components[name] === 'undefined') {
 | 
						|
        components[name] = [];
 | 
						|
    }
 | 
						|
    components[name].push(instance);
 | 
						|
 | 
						|
    // Add to element mapping
 | 
						|
    const elComponents = elementComponentMap.get(element) || {};
 | 
						|
    elComponents[name] = instance;
 | 
						|
    elementComponentMap.set(element, elComponents);
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Initialize all components found within the given element.
 | 
						|
 * @param {Element|Document} parentElement
 | 
						|
 */
 | 
						|
export function init(parentElement = 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) {
 | 
						|
            initComponent(name, el);
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Register the given component mapping into the component system.
 | 
						|
 * @param {Object<String, ObjectConstructor<Component>>} mapping
 | 
						|
 */
 | 
						|
export function register(mapping) {
 | 
						|
    const keys = Object.keys(mapping);
 | 
						|
    for (const key of keys) {
 | 
						|
        componentModelMap[camelToKebab(key)] = mapping[key];
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Get the first component of the given name.
 | 
						|
 * @param {String} name
 | 
						|
 * @returns {Component|null}
 | 
						|
 */
 | 
						|
export function first(name) {
 | 
						|
    return (components[name] || [null])[0];
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Get all the components of the given name.
 | 
						|
 * @param {String} name
 | 
						|
 * @returns {Component[]}
 | 
						|
 */
 | 
						|
export function get(name) {
 | 
						|
    return components[name] || [];
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Get the first component, of the given name, that's assigned to the given element.
 | 
						|
 * @param {Element} element
 | 
						|
 * @param {String} name
 | 
						|
 * @returns {Component|null}
 | 
						|
 */
 | 
						|
export function firstOnElement(element, name) {
 | 
						|
    const elComponents = elementComponentMap.get(element) || {};
 | 
						|
    return elComponents[name] || null;
 | 
						|
}
 |