mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
JS: Converted come common services to typescript
This commit is contained in:
parent
feca1f0502
commit
a8f1160743
@ -14,7 +14,8 @@
|
||||
"livereload": "livereload ./public/dist/",
|
||||
"permissions": "chown -R $USER:$USER bootstrap/cache storage public/uploads",
|
||||
"lint": "eslint \"resources/**/*.js\" \"resources/**/*.mjs\"",
|
||||
"fix": "eslint --fix \"resources/**/*.js\" \"resources/**/*.mjs\""
|
||||
"fix": "eslint --fix \"resources/**/*.js\" \"resources/**/*.mjs\"",
|
||||
"ts:lint": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lezer/generator": "^1.5.1",
|
||||
|
@ -1,9 +1,8 @@
|
||||
import * as events from './services/events';
|
||||
import * as httpInstance from './services/http';
|
||||
import Translations from './services/translations';
|
||||
|
||||
import * as components from './services/components';
|
||||
import * as componentMap from './components';
|
||||
import {ComponentStore} from './services/components.ts';
|
||||
|
||||
// Url retrieval function
|
||||
window.baseUrl = function baseUrl(path) {
|
||||
@ -32,6 +31,6 @@ window.trans_choice = translator.getPlural.bind(translator);
|
||||
window.trans_plural = translator.parsePlural.bind(translator);
|
||||
|
||||
// Load & initialise components
|
||||
components.register(componentMap);
|
||||
window.$components = components;
|
||||
components.init();
|
||||
window.$components = new ComponentStore();
|
||||
window.$components.register(componentMap);
|
||||
window.$components.init();
|
||||
|
4
resources/js/custom.d.ts
vendored
Normal file
4
resources/js/custom.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
declare module '*.svg' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
9
resources/js/global.d.ts
vendored
9
resources/js/global.d.ts
vendored
@ -1,12 +1,7 @@
|
||||
declare module '*.svg' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
import {ComponentStore} from "./services/components";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
$components: {
|
||||
first: (string) => Object,
|
||||
}
|
||||
$components: ComponentStore,
|
||||
}
|
||||
}
|
@ -1,165 +0,0 @@
|
||||
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;
|
||||
}
|
153
resources/js/services/components.ts
Normal file
153
resources/js/services/components.ts
Normal file
@ -0,0 +1,153 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -1,19 +1,15 @@
|
||||
/**
|
||||
* Convert a kebab-case string to camelCase
|
||||
* @param {String} kebab
|
||||
* @returns {string}
|
||||
*/
|
||||
export function kebabToCamel(kebab) {
|
||||
const ucFirst = word => word.slice(0, 1).toUpperCase() + word.slice(1);
|
||||
export function kebabToCamel(kebab: string): string {
|
||||
const ucFirst = (word: string) => word.slice(0, 1).toUpperCase() + word.slice(1);
|
||||
const words = kebab.split('-');
|
||||
return words[0] + words.slice(1).map(ucFirst).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a camelCase string to a kebab-case string.
|
||||
* @param {String} camelStr
|
||||
* @returns {String}
|
||||
*/
|
||||
export function camelToKebab(camelStr) {
|
||||
export function camelToKebab(camelStr: string): string {
|
||||
return camelStr.replace(/[A-Z]/g, (str, offset) => (offset > 0 ? '-' : '') + str.toLowerCase());
|
||||
}
|
@ -10,6 +10,7 @@ import {
|
||||
import type {EditorConfig} from "lexical/LexicalEditor";
|
||||
import {el} from "../helpers";
|
||||
import {EditorDecoratorAdapter} from "../ui/framework/decorator";
|
||||
import {CodeEditor} from "../../components";
|
||||
|
||||
export type SerializedCodeBlockNode = Spread<{
|
||||
language: string;
|
||||
@ -170,7 +171,7 @@ export function $openCodeEditorForNode(editor: LexicalEditor, node: CodeBlockNod
|
||||
const language = node.getLanguage();
|
||||
|
||||
// @ts-ignore
|
||||
const codeEditor = window.$components.first('code-editor');
|
||||
const codeEditor = window.$components.first('code-editor') as CodeEditor;
|
||||
// TODO - Handle direction
|
||||
codeEditor.open(code, language, 'ltr', (newCode: string, newLang: string) => {
|
||||
editor.update(() => {
|
||||
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"include": ["resources/js/**/*"],
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
@ -26,7 +27,7 @@
|
||||
|
||||
/* Modules */
|
||||
"module": "commonjs", /* Specify what module code is generated. */
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
"rootDir": "./resources/js/", /* Specify the root folder within your source files. */
|
||||
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
"paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
|
Loading…
Reference in New Issue
Block a user