@lordfokas/yrframe 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 LordFokas
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,2 @@
1
+ # yrframe
2
+ A Typescript class-based WebComponents library using Material Design
@@ -0,0 +1,74 @@
1
+ import { ComponentEvents } from "./ComponentEvents.js";
2
+ import { ComponentFactory } from "./ComponentFactory.js";
3
+ const evt = Symbol('evt');
4
+ export class Component extends HTMLElement {
5
+ /** Attribute Event Qualifier chars. Attributes starting with this are special. */
6
+ static AEQ = 'yr:';
7
+ [evt];
8
+ events() {
9
+ return this[evt];
10
+ }
11
+ constructor(props, defaults) {
12
+ super();
13
+ this[evt] = new ComponentEvents(this);
14
+ const all = {};
15
+ // Assign all defaults to object and overwrite with existing values in provided props
16
+ if (defaults)
17
+ Object.assign(all, defaults);
18
+ if (props)
19
+ Object.assign(all, props);
20
+ const attrs = {};
21
+ for (const [k, v] of Object.entries(all)) {
22
+ if (!k.startsWith(Component.AEQ)) {
23
+ attrs[k] = v;
24
+ }
25
+ }
26
+ ComponentFactory.setAttributesAndEvents(this, attrs);
27
+ }
28
+ /** Remove all children nodes. */
29
+ clearChildren() {
30
+ this.innerHTML = "";
31
+ }
32
+ isDisabled() {
33
+ return this.hasAttribute("disabled");
34
+ }
35
+ /** HTML5 Custom Element lifecycle callback (remove from page) */
36
+ disconnectedCallback() {
37
+ this[evt].disconnect();
38
+ }
39
+ /** HTML5 Custom Element lifecycle callback (add to page) */
40
+ connectedCallback() {
41
+ this[evt].connect();
42
+ this.redraw();
43
+ }
44
+ /** Draw this component's internals. Should return only children nodes. */
45
+ render() { return null; }
46
+ /** Redraw this component. Works by deleting all children, calling render() and appending the results. */
47
+ redraw() {
48
+ const child = this.render();
49
+ this.clearChildren();
50
+ if (child) {
51
+ if (Array.isArray(child)) {
52
+ ComponentFactory.appendChildren(this, child);
53
+ }
54
+ else {
55
+ ComponentFactory.appendChildren(this, [child]);
56
+ }
57
+ }
58
+ this.inject();
59
+ }
60
+ /** Called after redraw() to do special manipulation of children nodes. */
61
+ inject() { }
62
+ /** Get this component's real width in pixels. */
63
+ width() {
64
+ return parseFloat(getComputedStyle(this, null).width.replace("px", ""));
65
+ }
66
+ /** Get this component's real height in pixels. */
67
+ height() {
68
+ return parseFloat(getComputedStyle(this, null).height.replace("px", ""));
69
+ }
70
+ /** Shortcut to register this component with a given tag name */
71
+ static register(tag) {
72
+ window.customElements.define(tag, this);
73
+ }
74
+ }
@@ -0,0 +1,165 @@
1
+ import { EventBus, EventListener } from '@lordfokas/event-bus';
2
+ class PersistentListener extends EventListener {
3
+ type;
4
+ constructor(type, callback, nice) {
5
+ super(callback, nice);
6
+ this.type = type;
7
+ }
8
+ }
9
+ /**
10
+ * Event manager for component special functions.
11
+ * A developer isn't meant to directly interface with this class,
12
+ * except by calling Component.events()
13
+ */
14
+ export class ComponentEvents {
15
+ listeners = [];
16
+ sources = {};
17
+ triggers = {};
18
+ component;
19
+ owner;
20
+ constructor(component) {
21
+ this.component = component;
22
+ this.owner = component.constructor.name;
23
+ }
24
+ /** Called by the component when entering DOM */
25
+ connect() {
26
+ this.listeners.forEach(l => EventBus.GLOBAL.subscribe(l.type, l));
27
+ }
28
+ /** Called by the component when leaving DOM */
29
+ disconnect() {
30
+ this.listeners.forEach(l => EventBus.GLOBAL.unsubscribe(l));
31
+ }
32
+ /** Set up and attach a listener, given a configuration */
33
+ createListener(target, name, handler) {
34
+ const [type, path, nice] = target;
35
+ this.listeners.push(new PersistentListener(type, event => {
36
+ handler.call(this.component, event.traverse(...path), event);
37
+ }, nice).named(name, this.owner));
38
+ }
39
+ /** Enforce mandatory targets and skip missing optional ones. */
40
+ skip(target, name, kind, optional) {
41
+ if (!target) {
42
+ if (optional)
43
+ return true;
44
+ else
45
+ throw new Error(`Missing config for mandatory ${kind} '${name}' at '${this.owner}'`);
46
+ }
47
+ return false;
48
+ }
49
+ /** Attach a generic listener for an event. */
50
+ attachListener(target, handler, name, kind, optional) {
51
+ if (this.skip(target, name, kind, optional))
52
+ return;
53
+ this.createListener(target, name, handler);
54
+ }
55
+ attachGeneric(target, handler, name, optional) {
56
+ this.attachListener(target, handler, "generic listener", optional);
57
+ }
58
+ attachConsumer(target, handler, name, optional) {
59
+ this.attachListener(target, handler, "consumer", optional);
60
+ }
61
+ /** Attach a listener that enriches the target event with data from this component. */
62
+ attachSupplier(target, source, name, optional) {
63
+ if (this.skip(target, name, "supplier", optional))
64
+ return;
65
+ const [type, path, nice] = target;
66
+ this.listeners.push(new PersistentListener(type, event => {
67
+ const path = target[1];
68
+ const data = source.call(this);
69
+ if (path.length == 0)
70
+ event.with(data);
71
+ else if (path.length == 1)
72
+ event.with({ [path[0]]: data });
73
+ else if (path.length > 1)
74
+ throw new Error("Unsupported path length > 1 for suppliers");
75
+ }, nice).named(name, this.owner));
76
+ }
77
+ /**
78
+ * Attach a listener that removes data from an event if a predicate passes.
79
+ * If the target configuration has no path, the whole event is stopped instead.
80
+ */
81
+ attachRemover(target, predicate, name, optional) {
82
+ if (this.skip(target, name, "remover", optional))
83
+ return;
84
+ const [type, path, nice] = target;
85
+ this.listeners.push(new PersistentListener(type, event => {
86
+ const path = target[1];
87
+ if (!predicate.call(this))
88
+ return;
89
+ if (path.length == 0)
90
+ event.stop(`Halted by '${name}' at '${this.owner}'`);
91
+ else if (path.length == 1)
92
+ delete event[path[0]];
93
+ else if (path.length > 1)
94
+ throw new Error("Unsupported path length > 1 for removers");
95
+ }, nice).named(name, this.owner));
96
+ }
97
+ /** Attach a listener that disables a component based on a boolean event field. */
98
+ attachDisabler(target, source, name, optional) {
99
+ if (this.skip(target, name, "disabler", optional))
100
+ return;
101
+ this.createListener(target, name, (value, _) => {
102
+ if (typeof value !== "boolean")
103
+ return;
104
+ const element = source.call(this);
105
+ if (value) {
106
+ element.setAttribute("disabled", "");
107
+ }
108
+ else {
109
+ element.removeAttribute("disabled");
110
+ }
111
+ });
112
+ }
113
+ /** Create a function that will fire an event and process data from it. */
114
+ attachSource(target, name, optional) {
115
+ if (this.skip(target, name, "source", optional))
116
+ return;
117
+ const [type, path] = target;
118
+ this.sources[name] = (callback) => {
119
+ new type().publish().then(event => {
120
+ callback.call(this.component, event.traverse(...path), event);
121
+ });
122
+ };
123
+ }
124
+ /** Create a fake source function that supplies a static or local value. */
125
+ attachStatic(value, source, name, optional) {
126
+ if (value === undefined) {
127
+ if (optional)
128
+ return;
129
+ else
130
+ throw new Error(`Missing config for mandatory static '${name}' at '${this.owner}'`);
131
+ }
132
+ this.sources[name] = (callback) => {
133
+ callback.call(this.component, source.call(this.component), undefined);
134
+ };
135
+ }
136
+ /** Fire a source function and process the returned data. */
137
+ seek(name, callback) {
138
+ if (!this.sources[name])
139
+ callback.call(this.component, undefined, undefined);
140
+ else
141
+ this.sources[name].call(callback);
142
+ }
143
+ /** Create a function that will fire an event to export data. */
144
+ attachTrigger(target, name, optional) {
145
+ if (this.skip(target, name, "trigger", optional))
146
+ return;
147
+ const [type, path] = target;
148
+ this.triggers[name] = (data, parent) => {
149
+ const event = new type();
150
+ if (parent)
151
+ event.parent(parent);
152
+ if (path.length == 0)
153
+ event.with(data);
154
+ if (path.length == 1)
155
+ event.with({ [path[0]]: data });
156
+ if (path.length > 1)
157
+ throw new Error("Unsupported path length > 1 for triggers");
158
+ event.publish();
159
+ };
160
+ }
161
+ /** Fire a trigger function with the given data. */
162
+ fire(name, data = {}, parent) {
163
+ this.triggers[name].call(this.component, data, parent);
164
+ }
165
+ }
@@ -0,0 +1,75 @@
1
+ import { Facade } from "./Facade.js";
2
+ /**
3
+ * JSX / TSX component factory.
4
+ * Should not be invoked manually, is used by TSC when building the project.
5
+ */
6
+ export class ComponentFactory {
7
+ static make(tag, props, ...children) {
8
+ if (tag === Array)
9
+ return children; // Allow returning multiple elements from render()
10
+ // Initialize Element
11
+ let element;
12
+ if (typeof tag === 'function') { // Construct class based components
13
+ if (tag.prototype instanceof Facade) {
14
+ element = new tag(props ?? {}).content();
15
+ }
16
+ else {
17
+ element = new tag(props ?? {});
18
+ }
19
+ }
20
+ else { // Construct vanilla HTML tag
21
+ element = document.createElement(tag);
22
+ this.setAttributesAndEvents(element, props);
23
+ }
24
+ // Append children if any
25
+ if (children && children.length > 0) {
26
+ this.appendChildren(element, children);
27
+ }
28
+ return element;
29
+ }
30
+ /** Apply vanilla HTML attributes and event callback functions. There is no component level logic here. */
31
+ static setAttributesAndEvents(element, props) {
32
+ if (props)
33
+ for (const key in props) {
34
+ const value = props[key];
35
+ if (value === undefined)
36
+ continue; // Skip empty prop
37
+ if (typeof value === "function") { // Set vanilla HTML event callback function
38
+ element[key] = value;
39
+ }
40
+ else {
41
+ element.setAttribute(key, value);
42
+ }
43
+ }
44
+ }
45
+ /** Logic for appending children to a parent element according to the possible returns of render() */
46
+ static appendChildren(element, children) {
47
+ for (const child of children) {
48
+ // Allow returning null from render() when there is nothing to do
49
+ if (child === null)
50
+ continue;
51
+ // Disallow returning undefined to prevent mistakes being ignored. No-ops must explicitely return null.
52
+ if (child === undefined)
53
+ throw new Error("An element's child cannot be undefined");
54
+ if (Array.isArray(child)) { // Allow returning multiple elements from render()
55
+ this.appendChildren(element, child);
56
+ }
57
+ else {
58
+ element.append(child);
59
+ }
60
+ }
61
+ }
62
+ }
63
+ /**
64
+ * ## Here be Shenanigans
65
+ * In the event your TSC refuses to find ComponentFactory.make
66
+ * and insists on using React.createElement, fear not:
67
+ *
68
+ * Just `import { FakeReact as React }` and call it a day.
69
+ *
70
+ * At least until you can be arsed to figure it out...
71
+ * `¯\_(ツ)_/¯`
72
+ */
73
+ export class FakeReact {
74
+ static createElement = ComponentFactory.make;
75
+ }
package/dist/Facade.js ADDED
@@ -0,0 +1,51 @@
1
+ import { ComponentEvents } from "./ComponentEvents.js";
2
+ import { ComponentFactory } from "./ComponentFactory.js";
3
+ const evt = Symbol('evt');
4
+ /**
5
+ * Fake component that constructs and returns another component instead.
6
+ * Used for aliasing and adapting components from other libraries.
7
+ */
8
+ export class Facade {
9
+ static AEQ = 'yr:'; // TODO: centralize these
10
+ node;
11
+ isCustom;
12
+ events() {
13
+ return this.node[evt];
14
+ }
15
+ constructor(tag, props, defaults) {
16
+ const node = this.node = document.createElement(tag);
17
+ const events = node[evt] = new ComponentEvents(node);
18
+ this.isCustom = tag.includes('-');
19
+ // Attach to existing lifecycle callbacks.
20
+ if (this.isCustom) {
21
+ const { connectedCallback, disconnectedCallback } = node;
22
+ node.connectedCallback = () => {
23
+ events.connect();
24
+ return connectedCallback?.call(node);
25
+ };
26
+ node.disconnectedCallback = () => {
27
+ events.disconnect();
28
+ return disconnectedCallback?.call(node);
29
+ };
30
+ }
31
+ else { // TODO: rethink this, there's clearly use cases for it.
32
+ throw new Error(`Facade for native element '${tag}' cannot hook to lifecycle calls.`);
33
+ }
34
+ const all = {};
35
+ // Assign all defaults to object and overwrite with existing values in provided props
36
+ if (defaults)
37
+ Object.assign(all, defaults);
38
+ if (props)
39
+ Object.assign(all, props);
40
+ const attrs = {};
41
+ for (const [k, v] of Object.entries(all)) {
42
+ if (!k.startsWith(Facade.AEQ)) {
43
+ attrs[k] = v;
44
+ }
45
+ }
46
+ ComponentFactory.setAttributesAndEvents(node, attrs);
47
+ }
48
+ content() {
49
+ return this.node;
50
+ }
51
+ }
package/dist/utils.js ADDED
@@ -0,0 +1,3 @@
1
+ export function has($) {
2
+ return $ !== undefined;
3
+ }
@@ -0,0 +1,3 @@
1
+ export { Component } from './Component.js';
2
+ export { ComponentEvents } from './ComponentEvents.js';
3
+ export { ComponentFactory, FakeReact } from './ComponentFactory.js';
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@lordfokas/yrframe",
3
+ "version": "0.1.0",
4
+ "description": "A Typescript class-based WebComponents library using Material Design.",
5
+ "main": "dist/yrframe.js",
6
+ "scripts": {
7
+ "prebuild": "rm -rf ./dist",
8
+ "build": "tsc -p ./tsconfig.json",
9
+ "prepublishOnly": "npm run build"
10
+ },
11
+ "keywords": [
12
+ "material",
13
+ "design",
14
+ "components",
15
+ "typescript"
16
+ ],
17
+ "author": "LordFokas",
18
+ "license": "MIT",
19
+ "devDependencies": {
20
+ "typescript": "^5.3.3"
21
+ },
22
+ "dependencies": {
23
+ "@lordfokas/event-bus": "^1.0.1"
24
+ }
25
+ }