@schukai/monster 4.80.0 → 4.81.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/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
 
4
4
 
5
+ ## [4.81.0] - 2026-01-07
6
+
7
+ ### Add Features
8
+
9
+ - Add new State Machine implementation and Control Flow documentation
10
+ ### Changes
11
+
12
+ - update doc; close isse [#365](https://gitlab.schukai.com/oss/libraries/javascript/monster/issues/365)
13
+
14
+
15
+
5
16
  ## [4.80.0] - 2026-01-07
6
17
 
7
18
  ### Add Features
package/package.json CHANGED
@@ -1 +1 @@
1
- {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.4","@popperjs/core":"^2.11.8"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.80.0"}
1
+ {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.4","@popperjs/core":"^2.11.8"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.81.0"}
@@ -30,7 +30,7 @@ import {
30
30
  ATTRIBUTE_ROLE,
31
31
  } from "../../dom/constants.mjs";
32
32
  import { getWindow } from "../../dom/util.mjs";
33
- import { fireEvent } from "../../dom/events.mjs";
33
+ import { fireCustomEvent, fireEvent } from "../../dom/events.mjs";
34
34
  import { addErrorAttribute } from "../../dom/error.mjs";
35
35
 
36
36
  export { ToggleSwitch };
@@ -224,6 +224,8 @@ class ToggleSwitch extends CustomControl {
224
224
  toggleOn() {
225
225
  this.setOption("value", this.getOption("values.on"));
226
226
  fireEvent(this, "change");
227
+ fireCustomEvent(this, "monster-change", { value: this.value });
228
+ fireCustomEvent(this, "monster-changed", { value: this.value });
227
229
  return this;
228
230
  }
229
231
 
@@ -240,6 +242,8 @@ class ToggleSwitch extends CustomControl {
240
242
  toggleOff() {
241
243
  this.setOption("value", this.getOption("values.off"));
242
244
  fireEvent(this, "change");
245
+ fireCustomEvent(this, "monster-change", { value: this.value });
246
+ fireCustomEvent(this, "monster-changed", { value: this.value });
243
247
  return this;
244
248
  }
245
249
 
@@ -0,0 +1,415 @@
1
+ /**
2
+ * Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved.
3
+ * Node module: @schukai/monster
4
+ *
5
+ * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
6
+ * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
7
+ *
8
+ * For those who do not wish to adhere to the AGPLv3, a commercial license is available.
9
+ * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
10
+ * For more information about purchasing a commercial license, please contact Volker Schukai.
11
+ *
12
+ * SPDX-License-Identifier: AGPL-3.0
13
+ */
14
+
15
+ import { Pathfinder } from "../data/pathfinder.mjs";
16
+ import { ProxyObserver } from "../types/proxyobserver.mjs";
17
+ import { Observer } from "../types/observer.mjs";
18
+ import { isArray, isFunction, isObject, isString } from "../types/is.mjs";
19
+ import { getDocument } from "./util.mjs";
20
+
21
+ export { ControlFlow };
22
+
23
+ /**
24
+ * Lightweight control flow for event-driven control dependencies.
25
+ * Config is defined in JS and supports callbacks per event.
26
+ */
27
+ class ControlFlow {
28
+ constructor({
29
+ stateObserver,
30
+ state = {},
31
+ controls = {},
32
+ datasources = {},
33
+ rules = [],
34
+ } = {}) {
35
+ this[internalSymbol] = {
36
+ stateObserver:
37
+ stateObserver instanceof ProxyObserver
38
+ ? stateObserver
39
+ : new ProxyObserver(state),
40
+ controls: resolveControls(controls),
41
+ datasources: resolveDatasources(datasources),
42
+ rules: isArray(rules) ? rules : [],
43
+ listeners: [],
44
+ };
45
+
46
+ this.state = this[internalSymbol].stateObserver.getSubject();
47
+
48
+ this[internalSymbol].stateObserver.attachObserver(
49
+ new Observer(() => {
50
+ this.emit({
51
+ source: "state",
52
+ id: "root",
53
+ event: "changed",
54
+ value: this.state,
55
+ });
56
+ }),
57
+ );
58
+
59
+ bindControlEvents.call(this);
60
+ bindDatasourceEvents.call(this);
61
+ }
62
+
63
+ /**
64
+ * Create an API helper for rule handlers.
65
+ * @return {object}
66
+ */
67
+ createApi() {
68
+ return createApi(this);
69
+ }
70
+
71
+ /**
72
+ * Add a rule at runtime.
73
+ * @param {object} rule
74
+ */
75
+ addRule(rule) {
76
+ if (isObject(rule)) {
77
+ this[internalSymbol].rules.push(rule);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Remove all listeners.
83
+ */
84
+ destroy() {
85
+ for (const entry of this[internalSymbol].listeners) {
86
+ entry.element.removeEventListener(entry.type, entry.handler);
87
+ }
88
+ this[internalSymbol].listeners = [];
89
+ }
90
+
91
+ /**
92
+ * Emit a synthetic event into the rule system.
93
+ * @param {object} ctx
94
+ */
95
+ emit(ctx) {
96
+ const context = normalizeContext(ctx, this);
97
+ for (const rule of this[internalSymbol].rules) {
98
+ if (!matchRule(rule, context)) {
99
+ continue;
100
+ }
101
+ const run = rule?.run;
102
+ if (isFunction(run)) {
103
+ run(context, createApi(this));
104
+ }
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Rule helper: control events.
110
+ * @param {string} id
111
+ * @param {string|string[]} events
112
+ * @param {function} run
113
+ * @return {object}
114
+ */
115
+ static onControl(id, events, run) {
116
+ return {
117
+ on: (ctx) =>
118
+ ctx.source === "control" &&
119
+ ctx.id === id &&
120
+ matchEvent(ctx.event, events),
121
+ run,
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Rule helper: datasource events.
127
+ * @param {string} id
128
+ * @param {string|string[]} events
129
+ * @param {function} run
130
+ * @return {object}
131
+ */
132
+ static onDatasource(id, events, run) {
133
+ return {
134
+ on: (ctx) =>
135
+ ctx.source === "datasource" &&
136
+ ctx.id === id &&
137
+ matchEvent(ctx.event, events),
138
+ run,
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Rule helper: state events.
144
+ * @param {string|string[]} events
145
+ * @param {function} run
146
+ * @return {object}
147
+ */
148
+ static onState(events, run) {
149
+ return {
150
+ on: (ctx) =>
151
+ ctx.source === "state" && matchEvent(ctx.event, events),
152
+ run,
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Rule helper: custom app events.
158
+ * @param {string|string[]} events
159
+ * @param {function} run
160
+ * @return {object}
161
+ */
162
+ static onApp(events, run) {
163
+ return {
164
+ on: (ctx) => ctx.source === "app" && matchEvent(ctx.event, events),
165
+ run,
166
+ };
167
+ }
168
+ }
169
+
170
+ /**
171
+ * @private
172
+ */
173
+ const internalSymbol = Symbol("controlFlowInternal");
174
+
175
+ /**
176
+ * @private
177
+ */
178
+ function resolveControls(controls) {
179
+ const resolved = {};
180
+ for (const [id, def] of Object.entries(controls || {})) {
181
+ const element = resolveElement(def?.element || def);
182
+ if (!element) continue;
183
+ resolved[id] = {
184
+ element,
185
+ events: def?.events || ["monster-changed", "change"],
186
+ };
187
+ }
188
+ return resolved;
189
+ }
190
+
191
+ /**
192
+ * @private
193
+ */
194
+ function resolveDatasources(datasources) {
195
+ const resolved = {};
196
+ for (const [id, def] of Object.entries(datasources || {})) {
197
+ const element = resolveElement(def?.element || def);
198
+ if (!element) continue;
199
+ resolved[id] = {
200
+ element,
201
+ events: def?.events || [
202
+ "monster-datasource-fetched",
203
+ "monster-datasource-error",
204
+ ],
205
+ };
206
+ }
207
+ return resolved;
208
+ }
209
+
210
+ /**
211
+ * @private
212
+ */
213
+ function resolveElement(elementOrSelector) {
214
+ if (elementOrSelector instanceof HTMLElement) {
215
+ return elementOrSelector;
216
+ }
217
+ if (isString(elementOrSelector)) {
218
+ return getDocument().querySelector(elementOrSelector);
219
+ }
220
+ return null;
221
+ }
222
+
223
+ /**
224
+ * @private
225
+ */
226
+ function bindControlEvents() {
227
+ for (const [id, control] of Object.entries(this[internalSymbol].controls)) {
228
+ for (const type of control.events) {
229
+ const handler = (event) => {
230
+ this.emit({
231
+ source: "control",
232
+ id,
233
+ event: type,
234
+ value: readControlValue(control.element),
235
+ rawEvent: event,
236
+ });
237
+ };
238
+ control.element.addEventListener(type, handler);
239
+ this[internalSymbol].listeners.push({
240
+ element: control.element,
241
+ type,
242
+ handler,
243
+ });
244
+ }
245
+ }
246
+ }
247
+
248
+ /**
249
+ * @private
250
+ */
251
+ function bindDatasourceEvents() {
252
+ for (const [id, datasource] of Object.entries(
253
+ this[internalSymbol].datasources,
254
+ )) {
255
+ for (const type of datasource.events) {
256
+ const handler = (event) => {
257
+ this.emit({
258
+ source: "datasource",
259
+ id,
260
+ event: type,
261
+ value: datasource.element?.data,
262
+ rawEvent: event,
263
+ });
264
+ };
265
+ datasource.element.addEventListener(type, handler);
266
+ this[internalSymbol].listeners.push({
267
+ element: datasource.element,
268
+ type,
269
+ handler,
270
+ });
271
+ }
272
+ }
273
+ }
274
+
275
+ /**
276
+ * @private
277
+ */
278
+ function readControlValue(element) {
279
+ if (element && "value" in element) {
280
+ return element.value;
281
+ }
282
+ if (isFunction(element?.getOption)) {
283
+ return element.getOption("value");
284
+ }
285
+ return undefined;
286
+ }
287
+
288
+ /**
289
+ * @private
290
+ */
291
+ function matchRule(rule, ctx) {
292
+ if (!rule) return false;
293
+ if (isFunction(rule.on)) {
294
+ return !!rule.on(ctx);
295
+ }
296
+ if (isString(rule.on)) {
297
+ return rule.on === ctx.key || rule.on === ctx.topic;
298
+ }
299
+ if (isArray(rule.on)) {
300
+ return rule.on.includes(ctx.key) || rule.on.includes(ctx.topic);
301
+ }
302
+ return false;
303
+ }
304
+
305
+ /**
306
+ * @private
307
+ */
308
+ function normalizeContext(ctx, flow) {
309
+ const context = Object.assign({}, ctx || {});
310
+ context.key = `${context.source}:${context.id}:${context.event}`;
311
+ context.topic = `${context.source}:${context.id}`;
312
+ context.state = flow.state;
313
+ context.controls = flow[internalSymbol].controls;
314
+ context.datasources = flow[internalSymbol].datasources;
315
+ return context;
316
+ }
317
+
318
+ /**
319
+ * @private
320
+ */
321
+ function matchEvent(event, events) {
322
+ if (!events) return true;
323
+ if (isString(events)) {
324
+ return event === events;
325
+ }
326
+ if (isArray(events)) {
327
+ return events.includes(event);
328
+ }
329
+ return false;
330
+ }
331
+
332
+ /**
333
+ * @private
334
+ */
335
+ function createApi(flow) {
336
+ return {
337
+ state: flow.state,
338
+ getState(path, fallback) {
339
+ try {
340
+ return new Pathfinder(flow.state).getVia(path);
341
+ } catch (e) {
342
+ return fallback;
343
+ }
344
+ },
345
+ setState(path, value) {
346
+ new Pathfinder(flow.state).setVia(path, value);
347
+ },
348
+ getControl(id) {
349
+ return flow[internalSymbol].controls?.[id]?.element;
350
+ },
351
+ setControlOption(id, path, value) {
352
+ const control = flow[internalSymbol].controls?.[id]?.element;
353
+ if (isFunction(control?.setOption)) {
354
+ control.setOption(path, value);
355
+ }
356
+ },
357
+ setControlOptions(id, options) {
358
+ const control = flow[internalSymbol].controls?.[id]?.element;
359
+ if (isFunction(control?.setOptions)) {
360
+ control.setOptions(options);
361
+ } else if (isFunction(control?.setOption)) {
362
+ control.setOption("options", options);
363
+ }
364
+ },
365
+ setControlValue(id, value) {
366
+ const control = flow[internalSymbol].controls?.[id]?.element;
367
+ if (control && "value" in control) {
368
+ control.value = value;
369
+ } else if (isFunction(control?.setOption)) {
370
+ control.setOption("value", value);
371
+ }
372
+ },
373
+ clearControl(id) {
374
+ const control = flow[internalSymbol].controls?.[id]?.element;
375
+ if (control && "value" in control) {
376
+ control.value = "";
377
+ } else if (isFunction(control?.setOption)) {
378
+ control.setOption("value", "");
379
+ }
380
+ },
381
+ disableControl(id, disabled) {
382
+ const control = flow[internalSymbol].controls?.[id]?.element;
383
+ if (isFunction(control?.setOption)) {
384
+ control.setOption("disabled", !!disabled);
385
+ }
386
+ },
387
+ fetchDatasource(id, { url, transform } = {}) {
388
+ const datasource = flow[internalSymbol].datasources?.[id]?.element;
389
+ if (!datasource) {
390
+ return Promise.reject(new Error("datasource not found"));
391
+ }
392
+ return new Promise((resolve, reject) => {
393
+ const onFetched = () => resolve(datasource.data);
394
+ const onError = (event) => {
395
+ reject(event?.detail?.error || new Error("Datasource error"));
396
+ };
397
+ datasource.addEventListener("monster-datasource-fetched", onFetched, {
398
+ once: true,
399
+ });
400
+ datasource.addEventListener("monster-datasource-error", onError, {
401
+ once: true,
402
+ });
403
+ if (isFunction(transform)) {
404
+ datasource.setOption("read.responseCallback", (payload) => {
405
+ datasource.data = transform(payload);
406
+ });
407
+ }
408
+ if (url) {
409
+ datasource.setOption("read.url", url);
410
+ }
411
+ datasource.read();
412
+ });
413
+ },
414
+ };
415
+ }
@@ -161,6 +161,7 @@ export * from "./constraints/andoperator.mjs";
161
161
  export * from "./constraints/isarray.mjs";
162
162
  export * from "./constraints/abstract.mjs";
163
163
  export * from "./constraints/valid.mjs";
164
+ export * from "./dom/controlflow.mjs";
164
165
  export * from "./dom/dimension.mjs";
165
166
  export * from "./dom/error.mjs";
166
167
  export * from "./dom/resource/link/stylesheet.mjs";