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/",
|
"livereload": "livereload ./public/dist/",
|
||||||
"permissions": "chown -R $USER:$USER bootstrap/cache storage public/uploads",
|
"permissions": "chown -R $USER:$USER bootstrap/cache storage public/uploads",
|
||||||
"lint": "eslint \"resources/**/*.js\" \"resources/**/*.mjs\"",
|
"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": {
|
"devDependencies": {
|
||||||
"@lezer/generator": "^1.5.1",
|
"@lezer/generator": "^1.5.1",
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import * as events from './services/events';
|
import * as events from './services/events';
|
||||||
import * as httpInstance from './services/http';
|
import * as httpInstance from './services/http';
|
||||||
import Translations from './services/translations';
|
import Translations from './services/translations';
|
||||||
|
|
||||||
import * as components from './services/components';
|
|
||||||
import * as componentMap from './components';
|
import * as componentMap from './components';
|
||||||
|
import {ComponentStore} from './services/components.ts';
|
||||||
|
|
||||||
// Url retrieval function
|
// Url retrieval function
|
||||||
window.baseUrl = function baseUrl(path) {
|
window.baseUrl = function baseUrl(path) {
|
||||||
@ -32,6 +31,6 @@ window.trans_choice = translator.getPlural.bind(translator);
|
|||||||
window.trans_plural = translator.parsePlural.bind(translator);
|
window.trans_plural = translator.parsePlural.bind(translator);
|
||||||
|
|
||||||
// Load & initialise components
|
// Load & initialise components
|
||||||
components.register(componentMap);
|
window.$components = new ComponentStore();
|
||||||
window.$components = components;
|
window.$components.register(componentMap);
|
||||||
components.init();
|
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' {
|
import {ComponentStore} from "./services/components";
|
||||||
const content: string;
|
|
||||||
export default content;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
$components: {
|
$components: ComponentStore,
|
||||||
first: (string) => Object,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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
|
* Convert a kebab-case string to camelCase
|
||||||
* @param {String} kebab
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
*/
|
||||||
export function kebabToCamel(kebab) {
|
export function kebabToCamel(kebab: string): string {
|
||||||
const ucFirst = word => word.slice(0, 1).toUpperCase() + word.slice(1);
|
const ucFirst = (word: string) => word.slice(0, 1).toUpperCase() + word.slice(1);
|
||||||
const words = kebab.split('-');
|
const words = kebab.split('-');
|
||||||
return words[0] + words.slice(1).map(ucFirst).join('');
|
return words[0] + words.slice(1).map(ucFirst).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a camelCase string to a kebab-case string.
|
* 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());
|
return camelStr.replace(/[A-Z]/g, (str, offset) => (offset > 0 ? '-' : '') + str.toLowerCase());
|
||||||
}
|
}
|
@ -10,6 +10,7 @@ import {
|
|||||||
import type {EditorConfig} from "lexical/LexicalEditor";
|
import type {EditorConfig} from "lexical/LexicalEditor";
|
||||||
import {el} from "../helpers";
|
import {el} from "../helpers";
|
||||||
import {EditorDecoratorAdapter} from "../ui/framework/decorator";
|
import {EditorDecoratorAdapter} from "../ui/framework/decorator";
|
||||||
|
import {CodeEditor} from "../../components";
|
||||||
|
|
||||||
export type SerializedCodeBlockNode = Spread<{
|
export type SerializedCodeBlockNode = Spread<{
|
||||||
language: string;
|
language: string;
|
||||||
@ -170,7 +171,7 @@ export function $openCodeEditorForNode(editor: LexicalEditor, node: CodeBlockNod
|
|||||||
const language = node.getLanguage();
|
const language = node.getLanguage();
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const codeEditor = window.$components.first('code-editor');
|
const codeEditor = window.$components.first('code-editor') as CodeEditor;
|
||||||
// TODO - Handle direction
|
// TODO - Handle direction
|
||||||
codeEditor.open(code, language, 'ltr', (newCode: string, newLang: string) => {
|
codeEditor.open(code, language, 'ltr', (newCode: string, newLang: string) => {
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"include": ["resources/js/**/*"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||||
|
|
||||||
@ -26,7 +27,7 @@
|
|||||||
|
|
||||||
/* Modules */
|
/* Modules */
|
||||||
"module": "commonjs", /* Specify what module code is generated. */
|
"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. */
|
// "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. */
|
// "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. */
|
"paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||||
|
Loading…
Reference in New Issue
Block a user