Skip to content

Commit feab37a

Browse files
committed
base component classes
1 parent f4dbcc8 commit feab37a

File tree

4 files changed

+267
-0
lines changed

4 files changed

+267
-0
lines changed

src/Component.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* Copyright © 2024 Cloudnode OÜ
3+
*
4+
* This file is part of @cldn/components.
5+
*
6+
* \@cldn/components is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser
7+
* General Public License as published by the Free Software Foundation, either version 3 of the License,
8+
* or (at your option) any later version.
9+
*
10+
* \@cldn/components is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
11+
* implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
12+
* for more details.
13+
*
14+
* You should have received a copy of the GNU Lesser General Public License along with @cldn/components.
15+
* If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
import {ElementComponent} from "./index.js";
18+
19+
type ElementToTagName<T extends HTMLElement> = {
20+
[K in keyof HTMLElementTagNameMap]: HTMLElementTagNameMap[K] extends T ? K : never
21+
}[keyof HTMLElementTagNameMap];
22+
23+
export class Component<T extends HTMLElement = HTMLElement> extends ElementComponent<T> {
24+
/**
25+
* @param element Instance or tag name
26+
* @protected
27+
*/
28+
public constructor(element: T | ElementToTagName<T>) {
29+
if (typeof element === "string") {
30+
const e = document.createElement(element) as any as T;
31+
super(e);
32+
}
33+
else super(element);
34+
}
35+
36+
/**
37+
* Create component from HTML code.
38+
*
39+
* Note: only the first child of the HTML code will be used.
40+
*/
41+
public static from<T extends HTMLElement = HTMLElement>(html: `<${ElementToTagName<T>}${">" | " "}${string}`) {
42+
return new Component<T>(document.createRange().createContextualFragment(html).children[0] as T);
43+
}
44+
45+
public override on<K extends keyof HTMLElementEventMap>(type: K, listener: (ev: HTMLElementEventMap[K], component: this) => any, options?: boolean | AddEventListenerOptions): this {
46+
return super.on(type as any, listener, options);
47+
}
48+
}

src/ElementComponent.ts

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/**
2+
* Copyright © 2024 Cloudnode OÜ
3+
*
4+
* This file is part of @cldn/components.
5+
*
6+
* \@cldn/components is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser
7+
* General Public License as published by the Free Software Foundation, either version 3 of the License,
8+
* or (at your option) any later version.
9+
*
10+
* \@cldn/components is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
11+
* implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
12+
* for more details.
13+
*
14+
* You should have received a copy of the GNU Lesser General Public License along with @cldn/components.
15+
* If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
18+
/**
19+
* Non-readonly non-method keys
20+
*/
21+
type WritableKeys<T> = Extract<
22+
{
23+
[Prop in keyof T]: (
24+
(<G>() => G extends Pick<T, Prop> ? 1 : 2) extends
25+
(<G>() => G extends Record<Prop, T[Prop]> ? 1 : 2)
26+
? true
27+
: false
28+
) extends false
29+
? never
30+
: Prop;
31+
}[keyof T],
32+
{
33+
[K in keyof T]: T[K] extends Function ? never : K;
34+
}[keyof T]
35+
>;
36+
37+
38+
/**
39+
* An {@link Element} component
40+
*/
41+
export abstract class ElementComponent<T extends Element> {
42+
public readonly element: T;
43+
44+
protected constructor(element: T) {
45+
this.element = element;
46+
}
47+
48+
/**
49+
* Insert component after the last child
50+
*/
51+
public append(component: ElementComponent<any>) {
52+
this.element.appendChild(component.element);
53+
return this;
54+
}
55+
56+
/**
57+
* Insert component before the first child
58+
*/
59+
public prepend(component: ElementComponent<any>) {
60+
this.element.prepend(component.element);
61+
return this;
62+
}
63+
64+
/**
65+
* Add classes
66+
*/
67+
public class(...classes: string[]) {
68+
this.element.classList.add(...classes.flatMap(c => c.split(" ")));
69+
return this;
70+
}
71+
72+
/**
73+
* Remove classes
74+
*/
75+
public removeClass(...classes: string[]) {
76+
this.element.classList.remove(...classes.flatMap(c => c.split(" ")));
77+
return this;
78+
}
79+
80+
/**
81+
* Toggle classes
82+
*/
83+
public toggleClass(...classes: string[]) {
84+
for (const c of new Set(classes.flatMap(c => c.split(" "))))
85+
this.element.classList.toggle(c);
86+
return this;
87+
}
88+
89+
/**
90+
* Replace classes
91+
*
92+
* @param oldClasses If all of these classes are present, they will be removed.
93+
* @param newClasses The classes to add if all {@link oldClasses} are present
94+
*/
95+
public replaceClass(oldClasses: string | string[], newClasses: string | string[]) {
96+
const remove = (typeof oldClasses === "string" ? [oldClasses] : oldClasses).flatMap(c => c.split(" "));
97+
const add = (typeof newClasses === "string" ? [newClasses] : newClasses).flatMap(c => c.split(" "));
98+
if (this.hasClass(...remove)) {
99+
this.removeClass(...remove);
100+
this.class(...add);
101+
}
102+
return this;
103+
}
104+
105+
/**
106+
* Check if component has class
107+
* @returns true if component has all the specified classes
108+
*/
109+
public hasClass(...classes: string[]) {
110+
return classes.every(c => this.element.classList.contains(c));
111+
}
112+
113+
/**
114+
* Set attribute
115+
* @param name attribute name
116+
* @param [value] attribute value
117+
*/
118+
public attr(name: string, value?: string) {
119+
this.element.setAttribute(name, value ?? "");
120+
return this;
121+
}
122+
123+
/**
124+
* Remove attribute
125+
* @param name attribute name
126+
*/
127+
public removeAttr(name: string) {
128+
this.element.removeAttribute(name);
129+
return this;
130+
}
131+
132+
/**
133+
* Set element property
134+
* @param name property name
135+
* @param value property value
136+
*/
137+
public set<K extends WritableKeys<T>>(name: K, value: T[K]) {
138+
this.element[name] = value;
139+
return this;
140+
}
141+
142+
/**
143+
* Get element property
144+
* @param name property name
145+
*/
146+
public get<K extends WritableKeys<T>>(name: K): T[K] {
147+
return this.element[name];
148+
}
149+
150+
/**
151+
* Add event listener
152+
* @param type
153+
* @param listener
154+
* @param options
155+
*/
156+
public on<K extends keyof ElementEventMap>(type: K, listener: (ev: ElementEventMap[K], component: this) => any, options?: boolean | AddEventListenerOptions): this {
157+
this.element.addEventListener(type, e => listener(e, this), options);
158+
return this;
159+
}
160+
161+
/**
162+
* Get this component's outer HTML
163+
*/
164+
public toString() {
165+
return this.element.outerHTML;
166+
}
167+
}

src/SvgComponent.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* Copyright © 2024 Cloudnode OÜ
3+
*
4+
* This file is part of @cldn/components.
5+
*
6+
* \@cldn/components is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser
7+
* General Public License as published by the Free Software Foundation, either version 3 of the License,
8+
* or (at your option) any later version.
9+
*
10+
* \@cldn/components is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
11+
* implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
12+
* for more details.
13+
*
14+
* You should have received a copy of the GNU Lesser General Public License along with @cldn/components.
15+
* If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
import {ElementComponent} from "./index.js";
18+
19+
/**
20+
* An SVG component (`<svg>`)
21+
*/
22+
export class SvgComponent extends ElementComponent<SVGSVGElement> {
23+
public constructor(element?: SVGSVGElement) {
24+
super(element ?? document.createElementNS("http://www.w3.org/2000/svg", "svg"));
25+
}
26+
27+
/**
28+
* Create SVG component from `<svg>...</svg>` code
29+
*/
30+
public static from(svg: string) {
31+
return new SvgComponent(document.createRange().createContextualFragment(svg).children[0] as SVGSVGElement);
32+
}
33+
}

src/index.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Copyright © 2024 Cloudnode OÜ
3+
*
4+
* This file is part of @cldn/components.
5+
*
6+
* \@cldn/components is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser
7+
* General Public License as published by the Free Software Foundation, either version 3 of the License,
8+
* or (at your option) any later version.
9+
*
10+
* \@cldn/components is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
11+
* implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
12+
* for more details.
13+
*
14+
* You should have received a copy of the GNU Lesser General Public License along with @cldn/components.
15+
* If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
export {ElementComponent} from "./ElementComponent.js";
18+
export {Component} from "./Component.js";
19+
export {SvgComponent} from "./SvgComponent.js";

0 commit comments

Comments
 (0)