bonemarrow 1.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 +21 -0
- package/dist/bonemarrow.js +1068 -0
- package/dist/bonemarrow.min.js +1 -0
- package/package.json +17 -0
- package/readme.md +225 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Karthick Raj
|
|
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.
|
|
@@ -0,0 +1,1068 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var bone = (() => {
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
22
|
+
var index_exports = {};
|
|
23
|
+
__export(index_exports, {
|
|
24
|
+
Collection: () => Collection,
|
|
25
|
+
Elements: () => Elements,
|
|
26
|
+
Model: () => Model,
|
|
27
|
+
View: () => View,
|
|
28
|
+
createScope: () => createScope,
|
|
29
|
+
el: () => el,
|
|
30
|
+
fetchJson: () => fetchJson
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// src/core/scope.ts
|
|
34
|
+
function createScopeInternal(parent) {
|
|
35
|
+
const controller = new AbortController();
|
|
36
|
+
const cleanups = [];
|
|
37
|
+
const children = /* @__PURE__ */ new Set();
|
|
38
|
+
let disposed = false;
|
|
39
|
+
const scope = {
|
|
40
|
+
signal: controller.signal,
|
|
41
|
+
onDispose(fn) {
|
|
42
|
+
if (disposed) {
|
|
43
|
+
try {
|
|
44
|
+
fn();
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error("[Scope] Error in dispose callback:", error);
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
cleanups.push(fn);
|
|
51
|
+
},
|
|
52
|
+
createChild() {
|
|
53
|
+
if (disposed) {
|
|
54
|
+
console.warn(
|
|
55
|
+
"[Scope] Attempted to create child from disposed scope"
|
|
56
|
+
);
|
|
57
|
+
const deadScope = createScopeInternal(null);
|
|
58
|
+
deadScope.dispose();
|
|
59
|
+
return deadScope;
|
|
60
|
+
}
|
|
61
|
+
const child = createScopeInternal(scope);
|
|
62
|
+
children.add(child);
|
|
63
|
+
child.onDispose(() => children.delete(child));
|
|
64
|
+
return child;
|
|
65
|
+
},
|
|
66
|
+
dispose() {
|
|
67
|
+
if (disposed) return;
|
|
68
|
+
disposed = true;
|
|
69
|
+
for (const child of children) {
|
|
70
|
+
child.dispose();
|
|
71
|
+
}
|
|
72
|
+
children.clear();
|
|
73
|
+
controller.abort();
|
|
74
|
+
for (let i = cleanups.length - 1; i >= 0; i--) {
|
|
75
|
+
try {
|
|
76
|
+
cleanups[i]();
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error(
|
|
79
|
+
"[Scope] Error in cleanup function:",
|
|
80
|
+
error
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
cleanups.length = 0;
|
|
85
|
+
if (scope._inFlight) {
|
|
86
|
+
scope._inFlight.clear();
|
|
87
|
+
delete scope._inFlight;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
parent == null ? void 0 : parent.onDispose(() => scope.dispose());
|
|
92
|
+
return scope;
|
|
93
|
+
}
|
|
94
|
+
var windowScope = null;
|
|
95
|
+
function getWindowScope() {
|
|
96
|
+
if (windowScope) return windowScope;
|
|
97
|
+
windowScope = createScopeInternal(null);
|
|
98
|
+
const disposeAll = () => {
|
|
99
|
+
if (!windowScope) return;
|
|
100
|
+
windowScope.dispose();
|
|
101
|
+
windowScope = null;
|
|
102
|
+
};
|
|
103
|
+
window.addEventListener("pagehide", disposeAll);
|
|
104
|
+
window.addEventListener("beforeunload", disposeAll);
|
|
105
|
+
return windowScope;
|
|
106
|
+
}
|
|
107
|
+
function createScope() {
|
|
108
|
+
return getWindowScope().createChild();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/core/fetch.ts
|
|
112
|
+
function requestKey(url, init) {
|
|
113
|
+
var _a;
|
|
114
|
+
return `${(_a = init == null ? void 0 : init.method) != null ? _a : "GET"}:${url}`;
|
|
115
|
+
}
|
|
116
|
+
async function fetchJson(url, options = {}) {
|
|
117
|
+
var _a;
|
|
118
|
+
const {
|
|
119
|
+
scope,
|
|
120
|
+
abort = false,
|
|
121
|
+
timeout,
|
|
122
|
+
retryOnFailure = 0,
|
|
123
|
+
retryDelay = 0,
|
|
124
|
+
dedupe = false,
|
|
125
|
+
init,
|
|
126
|
+
parse
|
|
127
|
+
} = options;
|
|
128
|
+
const internalScope = scope;
|
|
129
|
+
let inFlight;
|
|
130
|
+
let key;
|
|
131
|
+
if (dedupe && internalScope) {
|
|
132
|
+
inFlight = (_a = internalScope._inFlight) != null ? _a : internalScope._inFlight = /* @__PURE__ */ new Map();
|
|
133
|
+
key = requestKey(url, init);
|
|
134
|
+
const existing = inFlight.get(key);
|
|
135
|
+
if (existing) {
|
|
136
|
+
return existing;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const promise = (async () => {
|
|
140
|
+
let attempt = 0;
|
|
141
|
+
while (true) {
|
|
142
|
+
const controller = new AbortController();
|
|
143
|
+
if (abort && scope) {
|
|
144
|
+
scope.signal.addEventListener(
|
|
145
|
+
"abort",
|
|
146
|
+
() => controller.abort(),
|
|
147
|
+
{ once: true }
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
let timeoutId;
|
|
151
|
+
if (typeof timeout === "number") {
|
|
152
|
+
timeoutId = setTimeout(
|
|
153
|
+
() => controller.abort(),
|
|
154
|
+
timeout
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const res = await fetch(url, {
|
|
159
|
+
...init,
|
|
160
|
+
signal: controller.signal
|
|
161
|
+
});
|
|
162
|
+
if (!res.ok) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`HTTP ${res.status}: ${res.statusText}`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
const json = await res.json();
|
|
168
|
+
return parse ? parse(json) : json;
|
|
169
|
+
} catch (err) {
|
|
170
|
+
attempt++;
|
|
171
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
172
|
+
throw err;
|
|
173
|
+
}
|
|
174
|
+
if (attempt > retryOnFailure) {
|
|
175
|
+
throw err;
|
|
176
|
+
}
|
|
177
|
+
if (retryDelay > 0) {
|
|
178
|
+
await new Promise(
|
|
179
|
+
(resolve) => setTimeout(resolve, retryDelay)
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
} finally {
|
|
183
|
+
if (timeoutId) {
|
|
184
|
+
clearTimeout(timeoutId);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
})();
|
|
189
|
+
if (inFlight && key && internalScope) {
|
|
190
|
+
inFlight.set(key, promise);
|
|
191
|
+
const cleanup = () => {
|
|
192
|
+
inFlight.delete(key);
|
|
193
|
+
};
|
|
194
|
+
promise.then(cleanup, cleanup);
|
|
195
|
+
internalScope.onDispose(cleanup);
|
|
196
|
+
}
|
|
197
|
+
return promise;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/core/emitter.ts
|
|
201
|
+
var Emitter = class {
|
|
202
|
+
constructor() {
|
|
203
|
+
this.events = /* @__PURE__ */ new Map();
|
|
204
|
+
}
|
|
205
|
+
on(event, fn, scope) {
|
|
206
|
+
let set = this.events.get(event);
|
|
207
|
+
if (!set) {
|
|
208
|
+
set = /* @__PURE__ */ new Set();
|
|
209
|
+
this.events.set(event, set);
|
|
210
|
+
}
|
|
211
|
+
set.add(fn);
|
|
212
|
+
let disposed = false;
|
|
213
|
+
const cleanup = () => {
|
|
214
|
+
if (disposed) return;
|
|
215
|
+
disposed = true;
|
|
216
|
+
set.delete(fn);
|
|
217
|
+
if (set.size === 0) {
|
|
218
|
+
this.events.delete(event);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
scope == null ? void 0 : scope.onDispose(cleanup);
|
|
222
|
+
return cleanup;
|
|
223
|
+
}
|
|
224
|
+
once(event, fn, scope) {
|
|
225
|
+
const wrapper = (...args) => {
|
|
226
|
+
cleanup();
|
|
227
|
+
fn(...args);
|
|
228
|
+
};
|
|
229
|
+
const cleanup = this.on(event, wrapper, scope);
|
|
230
|
+
return cleanup;
|
|
231
|
+
}
|
|
232
|
+
emit(event, ...args) {
|
|
233
|
+
const handlers = this.events.get(event);
|
|
234
|
+
if (!handlers || handlers.size === 0) return;
|
|
235
|
+
const handlersArray = Array.from(handlers);
|
|
236
|
+
for (const fn of handlersArray) {
|
|
237
|
+
try {
|
|
238
|
+
fn(...args);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
console.error(
|
|
241
|
+
`Error in event handler for "${event}":`,
|
|
242
|
+
error
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
off(event) {
|
|
248
|
+
this.events.delete(event);
|
|
249
|
+
}
|
|
250
|
+
clear() {
|
|
251
|
+
this.events.clear();
|
|
252
|
+
}
|
|
253
|
+
hasListeners(event) {
|
|
254
|
+
var _a, _b;
|
|
255
|
+
return ((_b = (_a = this.events.get(event)) == null ? void 0 : _a.size) != null ? _b : 0) > 0;
|
|
256
|
+
}
|
|
257
|
+
listenerCount(event) {
|
|
258
|
+
var _a, _b;
|
|
259
|
+
return (_b = (_a = this.events.get(event)) == null ? void 0 : _a.size) != null ? _b : 0;
|
|
260
|
+
}
|
|
261
|
+
eventNames() {
|
|
262
|
+
return Array.from(this.events.keys());
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// src/refresh/sequentialRefresh.ts
|
|
267
|
+
function startSequentialRefresh(fn, opts) {
|
|
268
|
+
const {
|
|
269
|
+
interval,
|
|
270
|
+
scope,
|
|
271
|
+
immediate = true,
|
|
272
|
+
onError,
|
|
273
|
+
maxRetries = 0,
|
|
274
|
+
backoff = false
|
|
275
|
+
} = opts;
|
|
276
|
+
let stopped = false;
|
|
277
|
+
let timeoutId;
|
|
278
|
+
let consecutiveErrors = 0;
|
|
279
|
+
let isRunning = false;
|
|
280
|
+
const calculateDelay = () => {
|
|
281
|
+
if (!backoff || consecutiveErrors === 0) {
|
|
282
|
+
return interval;
|
|
283
|
+
}
|
|
284
|
+
const multiplier = Math.min(2 ** consecutiveErrors, 10);
|
|
285
|
+
return interval * multiplier;
|
|
286
|
+
};
|
|
287
|
+
const stop = () => {
|
|
288
|
+
if (stopped) return;
|
|
289
|
+
stopped = true;
|
|
290
|
+
if (timeoutId !== void 0) {
|
|
291
|
+
clearTimeout(timeoutId);
|
|
292
|
+
timeoutId = void 0;
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
const loop = async () => {
|
|
296
|
+
if (stopped) return;
|
|
297
|
+
if (isRunning) {
|
|
298
|
+
console.warn(
|
|
299
|
+
"[SequentialRefresh] Previous execution still running"
|
|
300
|
+
);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
isRunning = true;
|
|
304
|
+
try {
|
|
305
|
+
await fn();
|
|
306
|
+
consecutiveErrors = 0;
|
|
307
|
+
} catch (error) {
|
|
308
|
+
consecutiveErrors++;
|
|
309
|
+
if (onError) {
|
|
310
|
+
try {
|
|
311
|
+
onError(error);
|
|
312
|
+
} catch (handlerError) {
|
|
313
|
+
console.error(
|
|
314
|
+
"[SequentialRefresh] Error in error handler:",
|
|
315
|
+
handlerError
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
} else {
|
|
319
|
+
console.error(
|
|
320
|
+
"[SequentialRefresh] Error in refresh function:",
|
|
321
|
+
error
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
if (maxRetries > 0 && consecutiveErrors >= maxRetries) {
|
|
325
|
+
console.error(
|
|
326
|
+
`[SequentialRefresh] Max retries (${maxRetries}) exceeded. Stopping refresh.`
|
|
327
|
+
);
|
|
328
|
+
stop();
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
} finally {
|
|
332
|
+
isRunning = false;
|
|
333
|
+
}
|
|
334
|
+
if (!stopped) {
|
|
335
|
+
const delay = calculateDelay();
|
|
336
|
+
timeoutId = setTimeout(loop, delay);
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
if (immediate) {
|
|
340
|
+
loop();
|
|
341
|
+
} else {
|
|
342
|
+
timeoutId = setTimeout(loop, interval);
|
|
343
|
+
}
|
|
344
|
+
scope.onDispose(stop);
|
|
345
|
+
return stop;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/data/model.ts
|
|
349
|
+
var Model = class {
|
|
350
|
+
constructor(initial) {
|
|
351
|
+
this.emitter = new Emitter();
|
|
352
|
+
this.destroyed = false;
|
|
353
|
+
this.initial = { ...initial };
|
|
354
|
+
this.data = { ...initial };
|
|
355
|
+
}
|
|
356
|
+
get(key) {
|
|
357
|
+
this.checkDestroyed();
|
|
358
|
+
return this.data[key];
|
|
359
|
+
}
|
|
360
|
+
getAll() {
|
|
361
|
+
this.checkDestroyed();
|
|
362
|
+
return { ...this.data };
|
|
363
|
+
}
|
|
364
|
+
set(patch) {
|
|
365
|
+
this.checkDestroyed();
|
|
366
|
+
const changes = {};
|
|
367
|
+
let hasChanges = false;
|
|
368
|
+
for (const key of Object.keys(patch)) {
|
|
369
|
+
if (patch[key] !== this.data[key]) {
|
|
370
|
+
changes[key] = patch[key];
|
|
371
|
+
hasChanges = true;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (!hasChanges) {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
Object.assign(this.data, changes);
|
|
378
|
+
this.emitter.emit("change", changes);
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
reset() {
|
|
382
|
+
this.checkDestroyed();
|
|
383
|
+
const resetData = { ...this.initial };
|
|
384
|
+
const hasChanges = Object.keys(resetData).some(
|
|
385
|
+
(key) => resetData[key] !== this.data[key]
|
|
386
|
+
);
|
|
387
|
+
if (hasChanges) {
|
|
388
|
+
this.data = resetData;
|
|
389
|
+
this.emitter.emit("change", resetData);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
has(key, value) {
|
|
393
|
+
this.checkDestroyed();
|
|
394
|
+
return this.data[key] === value;
|
|
395
|
+
}
|
|
396
|
+
onChange(fn, scope) {
|
|
397
|
+
this.checkDestroyed();
|
|
398
|
+
return this.emitter.on("change", fn, scope);
|
|
399
|
+
}
|
|
400
|
+
async fetch(url, options) {
|
|
401
|
+
this.checkDestroyed();
|
|
402
|
+
const patch = await fetchJson(url, options);
|
|
403
|
+
this.set(patch);
|
|
404
|
+
return this.getAll();
|
|
405
|
+
}
|
|
406
|
+
autoRefresh(url, options) {
|
|
407
|
+
this.checkDestroyed();
|
|
408
|
+
return startSequentialRefresh(
|
|
409
|
+
() => this.fetch(url, {
|
|
410
|
+
...options.fetch,
|
|
411
|
+
scope: options.scope
|
|
412
|
+
}),
|
|
413
|
+
options
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
destroy() {
|
|
417
|
+
if (this.destroyed) return;
|
|
418
|
+
this.destroyed = true;
|
|
419
|
+
this.emitter.clear();
|
|
420
|
+
}
|
|
421
|
+
isDestroyed() {
|
|
422
|
+
return this.destroyed;
|
|
423
|
+
}
|
|
424
|
+
checkDestroyed() {
|
|
425
|
+
if (this.destroyed) {
|
|
426
|
+
throw new Error("[Model] Cannot use destroyed model");
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
// src/data/collection.ts
|
|
432
|
+
var Collection = class {
|
|
433
|
+
constructor() {
|
|
434
|
+
this.items = [];
|
|
435
|
+
this.emitter = new Emitter();
|
|
436
|
+
this.destroyed = false;
|
|
437
|
+
}
|
|
438
|
+
getAll() {
|
|
439
|
+
this.checkDestroyed();
|
|
440
|
+
return [...this.items];
|
|
441
|
+
}
|
|
442
|
+
get length() {
|
|
443
|
+
this.checkDestroyed();
|
|
444
|
+
return this.items.length;
|
|
445
|
+
}
|
|
446
|
+
at(index) {
|
|
447
|
+
this.checkDestroyed();
|
|
448
|
+
return this.items[index];
|
|
449
|
+
}
|
|
450
|
+
add(...items) {
|
|
451
|
+
this.checkDestroyed();
|
|
452
|
+
this.items.push(...items);
|
|
453
|
+
this.emitter.emit("add", items);
|
|
454
|
+
}
|
|
455
|
+
remove(predicate) {
|
|
456
|
+
this.checkDestroyed();
|
|
457
|
+
const removed = [];
|
|
458
|
+
for (let i = this.items.length - 1; i >= 0; i--) {
|
|
459
|
+
if (predicate(this.items[i], i)) {
|
|
460
|
+
removed.unshift(this.items[i]);
|
|
461
|
+
this.items.splice(i, 1);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (removed.length > 0) {
|
|
465
|
+
this.emitter.emit("remove", removed);
|
|
466
|
+
}
|
|
467
|
+
return removed;
|
|
468
|
+
}
|
|
469
|
+
removeAt(index) {
|
|
470
|
+
this.checkDestroyed();
|
|
471
|
+
if (index < 0 || index >= this.items.length) {
|
|
472
|
+
return void 0;
|
|
473
|
+
}
|
|
474
|
+
const [item] = this.items.splice(index, 1);
|
|
475
|
+
this.emitter.emit("remove", [item]);
|
|
476
|
+
return item;
|
|
477
|
+
}
|
|
478
|
+
find(predicate) {
|
|
479
|
+
this.checkDestroyed();
|
|
480
|
+
return this.items.find(predicate);
|
|
481
|
+
}
|
|
482
|
+
findIndex(predicate) {
|
|
483
|
+
this.checkDestroyed();
|
|
484
|
+
return this.items.findIndex(predicate);
|
|
485
|
+
}
|
|
486
|
+
filter(predicate) {
|
|
487
|
+
this.checkDestroyed();
|
|
488
|
+
return this.items.filter(predicate);
|
|
489
|
+
}
|
|
490
|
+
map(fn) {
|
|
491
|
+
this.checkDestroyed();
|
|
492
|
+
return this.items.map(fn);
|
|
493
|
+
}
|
|
494
|
+
forEach(fn) {
|
|
495
|
+
this.checkDestroyed();
|
|
496
|
+
this.items.forEach(fn);
|
|
497
|
+
}
|
|
498
|
+
some(predicate) {
|
|
499
|
+
this.checkDestroyed();
|
|
500
|
+
return this.items.some(predicate);
|
|
501
|
+
}
|
|
502
|
+
every(predicate) {
|
|
503
|
+
this.checkDestroyed();
|
|
504
|
+
return this.items.every(predicate);
|
|
505
|
+
}
|
|
506
|
+
sort(compareFn) {
|
|
507
|
+
this.checkDestroyed();
|
|
508
|
+
this.items.sort(compareFn);
|
|
509
|
+
this.emitter.emit("sort");
|
|
510
|
+
}
|
|
511
|
+
reset(items) {
|
|
512
|
+
this.checkDestroyed();
|
|
513
|
+
this.items = [...items];
|
|
514
|
+
this.emitter.emit("reset", this.items);
|
|
515
|
+
}
|
|
516
|
+
clear() {
|
|
517
|
+
this.checkDestroyed();
|
|
518
|
+
if (this.items.length > 0) {
|
|
519
|
+
this.items = [];
|
|
520
|
+
this.emitter.emit("reset", []);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
async fetch(url, options) {
|
|
524
|
+
this.checkDestroyed();
|
|
525
|
+
const items = await fetchJson(url, options);
|
|
526
|
+
this.reset(items);
|
|
527
|
+
return this.getAll();
|
|
528
|
+
}
|
|
529
|
+
onAdd(fn, scope) {
|
|
530
|
+
this.checkDestroyed();
|
|
531
|
+
return this.emitter.on("add", fn, scope);
|
|
532
|
+
}
|
|
533
|
+
onRemove(fn, scope) {
|
|
534
|
+
this.checkDestroyed();
|
|
535
|
+
return this.emitter.on("remove", fn, scope);
|
|
536
|
+
}
|
|
537
|
+
onReset(fn, scope) {
|
|
538
|
+
this.checkDestroyed();
|
|
539
|
+
return this.emitter.on("reset", fn, scope);
|
|
540
|
+
}
|
|
541
|
+
onSort(fn, scope) {
|
|
542
|
+
this.checkDestroyed();
|
|
543
|
+
return this.emitter.on("sort", fn, scope);
|
|
544
|
+
}
|
|
545
|
+
onChange(fn, scope) {
|
|
546
|
+
this.checkDestroyed();
|
|
547
|
+
const c1 = this.onAdd(fn, scope);
|
|
548
|
+
const c2 = this.onRemove(fn, scope);
|
|
549
|
+
const c3 = this.onReset(fn, scope);
|
|
550
|
+
const c4 = this.onSort(fn, scope);
|
|
551
|
+
return () => {
|
|
552
|
+
c1();
|
|
553
|
+
c2();
|
|
554
|
+
c3();
|
|
555
|
+
c4();
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
autoRefresh(url, opts) {
|
|
559
|
+
this.checkDestroyed();
|
|
560
|
+
return startSequentialRefresh(
|
|
561
|
+
() => this.fetch(url, {
|
|
562
|
+
...opts.fetch,
|
|
563
|
+
scope: opts.scope
|
|
564
|
+
}),
|
|
565
|
+
opts
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
destroy() {
|
|
569
|
+
if (this.destroyed) return;
|
|
570
|
+
this.destroyed = true;
|
|
571
|
+
this.items = [];
|
|
572
|
+
this.emitter.clear();
|
|
573
|
+
}
|
|
574
|
+
isDestroyed() {
|
|
575
|
+
return this.destroyed;
|
|
576
|
+
}
|
|
577
|
+
checkDestroyed() {
|
|
578
|
+
if (this.destroyed) {
|
|
579
|
+
throw new Error(
|
|
580
|
+
"[Collection] Cannot use destroyed collection"
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
// src/dom/elements.ts
|
|
587
|
+
var Elements = class _Elements {
|
|
588
|
+
constructor(nodes) {
|
|
589
|
+
this.nodes = nodes;
|
|
590
|
+
}
|
|
591
|
+
get length() {
|
|
592
|
+
return this.nodes.length;
|
|
593
|
+
}
|
|
594
|
+
each(fn) {
|
|
595
|
+
this.nodes.forEach(fn);
|
|
596
|
+
return this;
|
|
597
|
+
}
|
|
598
|
+
get(index = 0) {
|
|
599
|
+
var _a;
|
|
600
|
+
return (_a = this.nodes[index]) != null ? _a : null;
|
|
601
|
+
}
|
|
602
|
+
first() {
|
|
603
|
+
var _a;
|
|
604
|
+
return (_a = this.nodes[0]) != null ? _a : null;
|
|
605
|
+
}
|
|
606
|
+
last() {
|
|
607
|
+
var _a;
|
|
608
|
+
return (_a = this.nodes[this.nodes.length - 1]) != null ? _a : null;
|
|
609
|
+
}
|
|
610
|
+
find(selector) {
|
|
611
|
+
const out = [];
|
|
612
|
+
this.each((el2) => {
|
|
613
|
+
out.push(...Array.from(el2.querySelectorAll(selector)));
|
|
614
|
+
});
|
|
615
|
+
return new _Elements(out);
|
|
616
|
+
}
|
|
617
|
+
filter(predicate) {
|
|
618
|
+
if (typeof predicate === "string") {
|
|
619
|
+
return new _Elements(
|
|
620
|
+
this.nodes.filter((el2) => el2.matches(predicate))
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
return new _Elements(this.nodes.filter(predicate));
|
|
624
|
+
}
|
|
625
|
+
parent() {
|
|
626
|
+
const parents = [];
|
|
627
|
+
this.each((el2) => {
|
|
628
|
+
if (el2.parentElement) {
|
|
629
|
+
parents.push(el2.parentElement);
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
return new _Elements(parents);
|
|
633
|
+
}
|
|
634
|
+
closest(selector) {
|
|
635
|
+
const matches = [];
|
|
636
|
+
this.each((el2) => {
|
|
637
|
+
const match = el2.closest(selector);
|
|
638
|
+
if (match) {
|
|
639
|
+
matches.push(match);
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
return new _Elements(matches);
|
|
643
|
+
}
|
|
644
|
+
children() {
|
|
645
|
+
const children = [];
|
|
646
|
+
this.each((el2) => {
|
|
647
|
+
children.push(...Array.from(el2.children));
|
|
648
|
+
});
|
|
649
|
+
return new _Elements(children);
|
|
650
|
+
}
|
|
651
|
+
text(value) {
|
|
652
|
+
var _a, _b;
|
|
653
|
+
if (value === void 0) {
|
|
654
|
+
return (_b = (_a = this.get()) == null ? void 0 : _a.textContent) != null ? _b : "";
|
|
655
|
+
}
|
|
656
|
+
return this.each((el2) => {
|
|
657
|
+
el2.textContent = value;
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
html(value) {
|
|
661
|
+
var _a, _b;
|
|
662
|
+
if (value === void 0) {
|
|
663
|
+
return (_b = (_a = this.get()) == null ? void 0 : _a.innerHTML) != null ? _b : "";
|
|
664
|
+
}
|
|
665
|
+
return this.each((el2) => {
|
|
666
|
+
el2.innerHTML = value;
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
attr(name, value) {
|
|
670
|
+
var _a, _b;
|
|
671
|
+
if (value === void 0) {
|
|
672
|
+
return (_b = (_a = this.get()) == null ? void 0 : _a.getAttribute(name)) != null ? _b : "";
|
|
673
|
+
}
|
|
674
|
+
return this.each((el2) => {
|
|
675
|
+
el2.setAttribute(name, value);
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
removeAttr(name) {
|
|
679
|
+
return this.each((el2) => {
|
|
680
|
+
el2.removeAttribute(name);
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
data(key, value) {
|
|
684
|
+
var _a;
|
|
685
|
+
if (value === void 0) {
|
|
686
|
+
const el2 = this.get();
|
|
687
|
+
return el2 instanceof HTMLElement ? (_a = el2.dataset[key]) != null ? _a : "" : "";
|
|
688
|
+
}
|
|
689
|
+
return this.each((el2) => {
|
|
690
|
+
if (el2 instanceof HTMLElement) {
|
|
691
|
+
el2.dataset[key] = value;
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
val(value) {
|
|
696
|
+
if (value === void 0) {
|
|
697
|
+
const el2 = this.get();
|
|
698
|
+
if (el2 instanceof HTMLInputElement || el2 instanceof HTMLTextAreaElement || el2 instanceof HTMLSelectElement) {
|
|
699
|
+
return el2.value;
|
|
700
|
+
}
|
|
701
|
+
return "";
|
|
702
|
+
}
|
|
703
|
+
return this.each((el2) => {
|
|
704
|
+
if (el2 instanceof HTMLInputElement || el2 instanceof HTMLTextAreaElement || el2 instanceof HTMLSelectElement) {
|
|
705
|
+
el2.value = value;
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
on(event, handler, scope) {
|
|
710
|
+
return this.each((el2) => {
|
|
711
|
+
el2.addEventListener(event, handler);
|
|
712
|
+
scope == null ? void 0 : scope.onDispose(() => {
|
|
713
|
+
el2.removeEventListener(event, handler);
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
once(event, handler) {
|
|
718
|
+
return this.each((el2) => {
|
|
719
|
+
el2.addEventListener(event, handler, { once: true });
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
off(event, handler) {
|
|
723
|
+
return this.each((el2) => {
|
|
724
|
+
el2.removeEventListener(event, handler);
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
trigger(event, detail) {
|
|
728
|
+
return this.each((el2) => {
|
|
729
|
+
el2.dispatchEvent(
|
|
730
|
+
new CustomEvent(event, { detail })
|
|
731
|
+
);
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
addClass(classes) {
|
|
735
|
+
const classList = classes.split(" ").filter(Boolean);
|
|
736
|
+
return this.each((el2) => {
|
|
737
|
+
el2.classList.add(...classList);
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
removeClass(classes) {
|
|
741
|
+
const classList = classes.split(" ").filter(Boolean);
|
|
742
|
+
return this.each((el2) => {
|
|
743
|
+
el2.classList.remove(...classList);
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
toggleClass(classes, force) {
|
|
747
|
+
const classList = classes.split(" ").filter(Boolean);
|
|
748
|
+
return this.each((el2) => {
|
|
749
|
+
classList.forEach((cls) => {
|
|
750
|
+
el2.classList.toggle(cls, force);
|
|
751
|
+
});
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
hasClass(className) {
|
|
755
|
+
return this.nodes.some(
|
|
756
|
+
(el2) => el2.classList.contains(className)
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
css(property, value) {
|
|
760
|
+
if (typeof property === "string" && value === void 0) {
|
|
761
|
+
const el2 = this.get();
|
|
762
|
+
return el2 instanceof HTMLElement ? getComputedStyle(el2).getPropertyValue(property) : "";
|
|
763
|
+
}
|
|
764
|
+
if (typeof property === "string") {
|
|
765
|
+
return this.each((el2) => {
|
|
766
|
+
if (el2 instanceof HTMLElement) {
|
|
767
|
+
el2.style.setProperty(property, value);
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
return this.each((el2) => {
|
|
772
|
+
if (el2 instanceof HTMLElement) {
|
|
773
|
+
Object.entries(property).forEach(
|
|
774
|
+
([key, val]) => {
|
|
775
|
+
el2.style.setProperty(key, val);
|
|
776
|
+
}
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
show() {
|
|
782
|
+
return this.each((el2) => {
|
|
783
|
+
if (el2 instanceof HTMLElement) {
|
|
784
|
+
el2.style.display = "";
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
hide() {
|
|
789
|
+
return this.each((el2) => {
|
|
790
|
+
if (el2 instanceof HTMLElement) {
|
|
791
|
+
el2.style.display = "none";
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
toggle(show) {
|
|
796
|
+
return this.each((el2) => {
|
|
797
|
+
if (el2 instanceof HTMLElement) {
|
|
798
|
+
const shouldShow = show != null ? show : el2.style.display === "none";
|
|
799
|
+
el2.style.display = shouldShow ? "" : "none";
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
isVisible() {
|
|
804
|
+
return this.nodes.some((el2) => {
|
|
805
|
+
if (el2 instanceof HTMLElement) {
|
|
806
|
+
return el2.style.display !== "none" && el2.offsetParent !== null;
|
|
807
|
+
}
|
|
808
|
+
return false;
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
append(content) {
|
|
812
|
+
return this.each((el2) => {
|
|
813
|
+
if (typeof content === "string") {
|
|
814
|
+
el2.insertAdjacentHTML("beforeend", content);
|
|
815
|
+
} else if (content instanceof _Elements) {
|
|
816
|
+
content.each(
|
|
817
|
+
(child) => el2.appendChild(child.cloneNode(true))
|
|
818
|
+
);
|
|
819
|
+
} else {
|
|
820
|
+
el2.appendChild(content);
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
prepend(content) {
|
|
825
|
+
return this.each((el2) => {
|
|
826
|
+
if (typeof content === "string") {
|
|
827
|
+
el2.insertAdjacentHTML("afterbegin", content);
|
|
828
|
+
} else if (content instanceof _Elements) {
|
|
829
|
+
const first = el2.firstChild;
|
|
830
|
+
content.each((child) => {
|
|
831
|
+
el2.insertBefore(
|
|
832
|
+
child.cloneNode(true),
|
|
833
|
+
first
|
|
834
|
+
);
|
|
835
|
+
});
|
|
836
|
+
} else {
|
|
837
|
+
el2.insertBefore(content, el2.firstChild);
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
remove() {
|
|
842
|
+
return this.each((el2) => {
|
|
843
|
+
el2.remove();
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
empty() {
|
|
847
|
+
return this.each((el2) => {
|
|
848
|
+
el2.innerHTML = "";
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
clone(deep = true) {
|
|
852
|
+
return new _Elements(
|
|
853
|
+
this.nodes.map(
|
|
854
|
+
(el2) => el2.cloneNode(deep)
|
|
855
|
+
)
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
focus() {
|
|
859
|
+
const el2 = this.get();
|
|
860
|
+
if (el2 instanceof HTMLElement) {
|
|
861
|
+
el2.focus();
|
|
862
|
+
}
|
|
863
|
+
return this;
|
|
864
|
+
}
|
|
865
|
+
blur() {
|
|
866
|
+
const el2 = this.get();
|
|
867
|
+
if (el2 instanceof HTMLElement) {
|
|
868
|
+
el2.blur();
|
|
869
|
+
}
|
|
870
|
+
return this;
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
// src/dom/el.ts
|
|
875
|
+
function el(input, root) {
|
|
876
|
+
if (typeof input === "string") {
|
|
877
|
+
const selector = input.trim();
|
|
878
|
+
if (!selector) {
|
|
879
|
+
return new Elements([]);
|
|
880
|
+
}
|
|
881
|
+
try {
|
|
882
|
+
return new Elements(
|
|
883
|
+
Array.from(
|
|
884
|
+
(root != null ? root : document).querySelectorAll(selector)
|
|
885
|
+
)
|
|
886
|
+
);
|
|
887
|
+
} catch (error) {
|
|
888
|
+
console.error(
|
|
889
|
+
`[el] Invalid selector: "${selector}"`,
|
|
890
|
+
error
|
|
891
|
+
);
|
|
892
|
+
return new Elements([]);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
if (input instanceof Element) {
|
|
896
|
+
return new Elements([input]);
|
|
897
|
+
}
|
|
898
|
+
if (input instanceof NodeList) {
|
|
899
|
+
return new Elements(
|
|
900
|
+
Array.from(input)
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
if (input instanceof HTMLCollection) {
|
|
904
|
+
return new Elements(Array.from(input));
|
|
905
|
+
}
|
|
906
|
+
if (Array.isArray(input)) {
|
|
907
|
+
const elements = input.filter(
|
|
908
|
+
(item) => item instanceof Element
|
|
909
|
+
);
|
|
910
|
+
if (elements.length !== input.length) {
|
|
911
|
+
console.warn(
|
|
912
|
+
"[el] Some items in array were not Elements and were filtered out"
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
return new Elements(elements);
|
|
916
|
+
}
|
|
917
|
+
console.warn(
|
|
918
|
+
"[el] Unrecognized input type:",
|
|
919
|
+
typeof input
|
|
920
|
+
);
|
|
921
|
+
return new Elements([]);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// src/view/view.ts
|
|
925
|
+
var View = class {
|
|
926
|
+
constructor(root, model, options = {}) {
|
|
927
|
+
this.root = root;
|
|
928
|
+
this.model = model;
|
|
929
|
+
this.destroyed = false;
|
|
930
|
+
this.children = /* @__PURE__ */ new Set();
|
|
931
|
+
this.options = {
|
|
932
|
+
autoDestroy: true,
|
|
933
|
+
...options
|
|
934
|
+
};
|
|
935
|
+
this.scope = options.parentScope ? options.parentScope.createChild() : createScope();
|
|
936
|
+
if (this.options.autoDestroy) {
|
|
937
|
+
this.setupAutoDestroy();
|
|
938
|
+
}
|
|
939
|
+
this.init();
|
|
940
|
+
}
|
|
941
|
+
init() {
|
|
942
|
+
}
|
|
943
|
+
$(selector) {
|
|
944
|
+
return el(selector, this.root);
|
|
945
|
+
}
|
|
946
|
+
$root() {
|
|
947
|
+
return el(this.root);
|
|
948
|
+
}
|
|
949
|
+
createChild(ViewClass, root, model) {
|
|
950
|
+
this.checkDestroyed();
|
|
951
|
+
const child = new ViewClass(root, model, {
|
|
952
|
+
parentScope: this.scope,
|
|
953
|
+
autoDestroy: false
|
|
954
|
+
});
|
|
955
|
+
this.children.add(child);
|
|
956
|
+
child.scope.onDispose(() => {
|
|
957
|
+
this.children.delete(child);
|
|
958
|
+
});
|
|
959
|
+
return child;
|
|
960
|
+
}
|
|
961
|
+
createChildren(ViewClass, selector, modelFn) {
|
|
962
|
+
this.checkDestroyed();
|
|
963
|
+
const views = [];
|
|
964
|
+
this.$(selector).each((element, index) => {
|
|
965
|
+
const model = modelFn ? modelFn(element, index) : void 0;
|
|
966
|
+
views.push(this.createChild(ViewClass, element, model));
|
|
967
|
+
});
|
|
968
|
+
return views;
|
|
969
|
+
}
|
|
970
|
+
emit(event, detail) {
|
|
971
|
+
this.checkDestroyed();
|
|
972
|
+
this.$root().trigger(event, detail);
|
|
973
|
+
}
|
|
974
|
+
on(event, handler) {
|
|
975
|
+
this.checkDestroyed();
|
|
976
|
+
this.$root().on(event, handler, this.scope);
|
|
977
|
+
}
|
|
978
|
+
show() {
|
|
979
|
+
this.checkDestroyed();
|
|
980
|
+
this.$root().show();
|
|
981
|
+
}
|
|
982
|
+
hide() {
|
|
983
|
+
this.checkDestroyed();
|
|
984
|
+
this.$root().hide();
|
|
985
|
+
}
|
|
986
|
+
toggle(force) {
|
|
987
|
+
this.checkDestroyed();
|
|
988
|
+
this.$root().toggle(force);
|
|
989
|
+
}
|
|
990
|
+
isVisible() {
|
|
991
|
+
return this.$root().isVisible();
|
|
992
|
+
}
|
|
993
|
+
isDestroyed() {
|
|
994
|
+
return this.destroyed;
|
|
995
|
+
}
|
|
996
|
+
getRoot() {
|
|
997
|
+
return this.root;
|
|
998
|
+
}
|
|
999
|
+
getModel() {
|
|
1000
|
+
return this.model;
|
|
1001
|
+
}
|
|
1002
|
+
destroy() {
|
|
1003
|
+
if (this.destroyed) return;
|
|
1004
|
+
this.destroyed = true;
|
|
1005
|
+
for (const child of this.children) {
|
|
1006
|
+
try {
|
|
1007
|
+
child.destroy();
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
console.error(
|
|
1010
|
+
"[View] Error destroying child view:",
|
|
1011
|
+
error
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
this.children.clear();
|
|
1016
|
+
try {
|
|
1017
|
+
this.scope.dispose();
|
|
1018
|
+
} catch (error) {
|
|
1019
|
+
console.error(
|
|
1020
|
+
"[View] Error disposing scope:",
|
|
1021
|
+
error
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
if (this.model && typeof this.model.destroy === "function") {
|
|
1025
|
+
try {
|
|
1026
|
+
this.model.destroy();
|
|
1027
|
+
} catch (error) {
|
|
1028
|
+
console.error(
|
|
1029
|
+
"[View] Error destroying model:",
|
|
1030
|
+
error
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
setupAutoDestroy() {
|
|
1036
|
+
const observer = new MutationObserver((mutations) => {
|
|
1037
|
+
for (const mutation of mutations) {
|
|
1038
|
+
for (const removed of Array.from(
|
|
1039
|
+
mutation.removedNodes
|
|
1040
|
+
)) {
|
|
1041
|
+
if (removed === this.root || removed instanceof Element && removed.contains(this.root)) {
|
|
1042
|
+
this.destroy();
|
|
1043
|
+
observer.disconnect();
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
});
|
|
1049
|
+
if (this.root.parentElement) {
|
|
1050
|
+
observer.observe(this.root.parentElement, {
|
|
1051
|
+
childList: true,
|
|
1052
|
+
subtree: true
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
this.scope.onDispose(() => {
|
|
1056
|
+
observer.disconnect();
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
checkDestroyed() {
|
|
1060
|
+
if (this.destroyed) {
|
|
1061
|
+
throw new Error(
|
|
1062
|
+
"[View] Cannot use destroyed view"
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
return __toCommonJS(index_exports);
|
|
1068
|
+
})();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var bone=(()=>{var F=Object.defineProperty;var V=Object.getOwnPropertyDescriptor;var R=Object.getOwnPropertyNames;var O=Object.prototype.hasOwnProperty;var H=(n,e)=>{for(var t in e)F(n,t,{get:e[t],enumerable:!0})},P=(n,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of R(e))!O.call(n,s)&&s!==t&&F(n,s,{get:()=>e[s],enumerable:!(r=V(e,s))||r.enumerable});return n};var $=n=>P(F({},"__esModule",{value:!0}),n);var q={};H(q,{Collection:()=>L,Elements:()=>a,Model:()=>x,View:()=>M,createScope:()=>S,el:()=>w,fetchJson:()=>E});function C(n){let e=new AbortController,t=[],r=new Set,s=!1,o={signal:e.signal,onDispose(i){if(s){try{i()}catch(l){console.error("[Scope] Error in dispose callback:",l)}return}t.push(i)},createChild(){if(s){console.warn("[Scope] Attempted to create child from disposed scope");let l=C(null);return l.dispose(),l}let i=C(o);return r.add(i),i.onDispose(()=>r.delete(i)),i},dispose(){if(!s){s=!0;for(let i of r)i.dispose();r.clear(),e.abort();for(let i=t.length-1;i>=0;i--)try{t[i]()}catch(l){console.error("[Scope] Error in cleanup function:",l)}t.length=0,o._inFlight&&(o._inFlight.clear(),delete o._inFlight)}}};return n==null||n.onDispose(()=>o.dispose()),o}var y=null;function K(){if(y)return y;y=C(null);let n=()=>{y&&(y.dispose(),y=null)};return window.addEventListener("pagehide",n),window.addEventListener("beforeunload",n),y}function S(){return K().createChild()}function I(n,e){var t;return`${(t=e==null?void 0:e.method)!=null?t:"GET"}:${n}`}async function E(n,e={}){var v;let{scope:t,abort:r=!1,timeout:s,retryOnFailure:o=0,retryDelay:i=0,dedupe:l=!1,init:m,parse:f}=e,h=t,d,p;if(l&&h){d=(v=h._inFlight)!=null?v:h._inFlight=new Map,p=I(n,m);let c=d.get(p);if(c)return c}let g=(async()=>{let c=0;for(;;){let b=new AbortController;r&&t&&t.signal.addEventListener("abort",()=>b.abort(),{once:!0});let A;typeof s=="number"&&(A=setTimeout(()=>b.abort(),s));try{let u=await fetch(n,{...m,signal:b.signal});if(!u.ok)throw new Error(`HTTP ${u.status}: ${u.statusText}`);let D=await u.json();return f?f(D):D}catch(u){if(c++,u instanceof DOMException&&u.name==="AbortError"||c>o)throw u;i>0&&await new Promise(D=>setTimeout(D,i))}finally{A&&clearTimeout(A)}}})();if(d&&p&&h){d.set(p,g);let c=()=>{d.delete(p)};g.then(c,c),h.onDispose(c)}return g}var T=class{constructor(){this.events=new Map}on(e,t,r){let s=this.events.get(e);s||(s=new Set,this.events.set(e,s)),s.add(t);let o=!1,i=()=>{o||(o=!0,s.delete(t),s.size===0&&this.events.delete(e))};return r==null||r.onDispose(i),i}once(e,t,r){let s=(...i)=>{o(),t(...i)},o=this.on(e,s,r);return o}emit(e,...t){let r=this.events.get(e);if(!r||r.size===0)return;let s=Array.from(r);for(let o of s)try{o(...t)}catch(i){console.error(`Error in event handler for "${e}":`,i)}}off(e){this.events.delete(e)}clear(){this.events.clear()}hasListeners(e){var t,r;return((r=(t=this.events.get(e))==null?void 0:t.size)!=null?r:0)>0}listenerCount(e){var t,r;return(r=(t=this.events.get(e))==null?void 0:t.size)!=null?r:0}eventNames(){return Array.from(this.events.keys())}};function k(n,e){let{interval:t,scope:r,immediate:s=!0,onError:o,maxRetries:i=0,backoff:l=!1}=e,m=!1,f,h=0,d=!1,p=()=>{if(!l||h===0)return t;let c=Math.min(2**h,10);return t*c},g=()=>{m||(m=!0,f!==void 0&&(clearTimeout(f),f=void 0))},v=async()=>{if(!m){if(d){console.warn("[SequentialRefresh] Previous execution still running");return}d=!0;try{await n(),h=0}catch(c){if(h++,o)try{o(c)}catch(b){console.error("[SequentialRefresh] Error in error handler:",b)}else console.error("[SequentialRefresh] Error in refresh function:",c);if(i>0&&h>=i){console.error(`[SequentialRefresh] Max retries (${i}) exceeded. Stopping refresh.`),g();return}}finally{d=!1}if(!m){let c=p();f=setTimeout(v,c)}}};return s?v():f=setTimeout(v,t),r.onDispose(g),g}var x=class{constructor(e){this.emitter=new T;this.destroyed=!1;this.initial={...e},this.data={...e}}get(e){return this.checkDestroyed(),this.data[e]}getAll(){return this.checkDestroyed(),{...this.data}}set(e){this.checkDestroyed();let t={},r=!1;for(let s of Object.keys(e))e[s]!==this.data[s]&&(t[s]=e[s],r=!0);return r?(Object.assign(this.data,t),this.emitter.emit("change",t),!0):!1}reset(){this.checkDestroyed();let e={...this.initial};Object.keys(e).some(r=>e[r]!==this.data[r])&&(this.data=e,this.emitter.emit("change",e))}has(e,t){return this.checkDestroyed(),this.data[e]===t}onChange(e,t){return this.checkDestroyed(),this.emitter.on("change",e,t)}async fetch(e,t){this.checkDestroyed();let r=await E(e,t);return this.set(r),this.getAll()}autoRefresh(e,t){return this.checkDestroyed(),k(()=>this.fetch(e,{...t.fetch,scope:t.scope}),t)}destroy(){this.destroyed||(this.destroyed=!0,this.emitter.clear())}isDestroyed(){return this.destroyed}checkDestroyed(){if(this.destroyed)throw new Error("[Model] Cannot use destroyed model")}};var L=class{constructor(){this.items=[];this.emitter=new T;this.destroyed=!1}getAll(){return this.checkDestroyed(),[...this.items]}get length(){return this.checkDestroyed(),this.items.length}at(e){return this.checkDestroyed(),this.items[e]}add(...e){this.checkDestroyed(),this.items.push(...e),this.emitter.emit("add",e)}remove(e){this.checkDestroyed();let t=[];for(let r=this.items.length-1;r>=0;r--)e(this.items[r],r)&&(t.unshift(this.items[r]),this.items.splice(r,1));return t.length>0&&this.emitter.emit("remove",t),t}removeAt(e){if(this.checkDestroyed(),e<0||e>=this.items.length)return;let[t]=this.items.splice(e,1);return this.emitter.emit("remove",[t]),t}find(e){return this.checkDestroyed(),this.items.find(e)}findIndex(e){return this.checkDestroyed(),this.items.findIndex(e)}filter(e){return this.checkDestroyed(),this.items.filter(e)}map(e){return this.checkDestroyed(),this.items.map(e)}forEach(e){this.checkDestroyed(),this.items.forEach(e)}some(e){return this.checkDestroyed(),this.items.some(e)}every(e){return this.checkDestroyed(),this.items.every(e)}sort(e){this.checkDestroyed(),this.items.sort(e),this.emitter.emit("sort")}reset(e){this.checkDestroyed(),this.items=[...e],this.emitter.emit("reset",this.items)}clear(){this.checkDestroyed(),this.items.length>0&&(this.items=[],this.emitter.emit("reset",[]))}async fetch(e,t){this.checkDestroyed();let r=await E(e,t);return this.reset(r),this.getAll()}onAdd(e,t){return this.checkDestroyed(),this.emitter.on("add",e,t)}onRemove(e,t){return this.checkDestroyed(),this.emitter.on("remove",e,t)}onReset(e,t){return this.checkDestroyed(),this.emitter.on("reset",e,t)}onSort(e,t){return this.checkDestroyed(),this.emitter.on("sort",e,t)}onChange(e,t){this.checkDestroyed();let r=this.onAdd(e,t),s=this.onRemove(e,t),o=this.onReset(e,t),i=this.onSort(e,t);return()=>{r(),s(),o(),i()}}autoRefresh(e,t){return this.checkDestroyed(),k(()=>this.fetch(e,{...t.fetch,scope:t.scope}),t)}destroy(){this.destroyed||(this.destroyed=!0,this.items=[],this.emitter.clear())}isDestroyed(){return this.destroyed}checkDestroyed(){if(this.destroyed)throw new Error("[Collection] Cannot use destroyed collection")}};var a=class n{constructor(e){this.nodes=e}get length(){return this.nodes.length}each(e){return this.nodes.forEach(e),this}get(e=0){var t;return(t=this.nodes[e])!=null?t:null}first(){var e;return(e=this.nodes[0])!=null?e:null}last(){var e;return(e=this.nodes[this.nodes.length-1])!=null?e:null}find(e){let t=[];return this.each(r=>{t.push(...Array.from(r.querySelectorAll(e)))}),new n(t)}filter(e){return typeof e=="string"?new n(this.nodes.filter(t=>t.matches(e))):new n(this.nodes.filter(e))}parent(){let e=[];return this.each(t=>{t.parentElement&&e.push(t.parentElement)}),new n(e)}closest(e){let t=[];return this.each(r=>{let s=r.closest(e);s&&t.push(s)}),new n(t)}children(){let e=[];return this.each(t=>{e.push(...Array.from(t.children))}),new n(e)}text(e){var t,r;return e===void 0?(r=(t=this.get())==null?void 0:t.textContent)!=null?r:"":this.each(s=>{s.textContent=e})}html(e){var t,r;return e===void 0?(r=(t=this.get())==null?void 0:t.innerHTML)!=null?r:"":this.each(s=>{s.innerHTML=e})}attr(e,t){var r,s;return t===void 0?(s=(r=this.get())==null?void 0:r.getAttribute(e))!=null?s:"":this.each(o=>{o.setAttribute(e,t)})}removeAttr(e){return this.each(t=>{t.removeAttribute(e)})}data(e,t){var r;if(t===void 0){let s=this.get();return s instanceof HTMLElement&&(r=s.dataset[e])!=null?r:""}return this.each(s=>{s instanceof HTMLElement&&(s.dataset[e]=t)})}val(e){if(e===void 0){let t=this.get();return t instanceof HTMLInputElement||t instanceof HTMLTextAreaElement||t instanceof HTMLSelectElement?t.value:""}return this.each(t=>{(t instanceof HTMLInputElement||t instanceof HTMLTextAreaElement||t instanceof HTMLSelectElement)&&(t.value=e)})}on(e,t,r){return this.each(s=>{s.addEventListener(e,t),r==null||r.onDispose(()=>{s.removeEventListener(e,t)})})}once(e,t){return this.each(r=>{r.addEventListener(e,t,{once:!0})})}off(e,t){return this.each(r=>{r.removeEventListener(e,t)})}trigger(e,t){return this.each(r=>{r.dispatchEvent(new CustomEvent(e,{detail:t}))})}addClass(e){let t=e.split(" ").filter(Boolean);return this.each(r=>{r.classList.add(...t)})}removeClass(e){let t=e.split(" ").filter(Boolean);return this.each(r=>{r.classList.remove(...t)})}toggleClass(e,t){let r=e.split(" ").filter(Boolean);return this.each(s=>{r.forEach(o=>{s.classList.toggle(o,t)})})}hasClass(e){return this.nodes.some(t=>t.classList.contains(e))}css(e,t){if(typeof e=="string"&&t===void 0){let r=this.get();return r instanceof HTMLElement?getComputedStyle(r).getPropertyValue(e):""}return typeof e=="string"?this.each(r=>{r instanceof HTMLElement&&r.style.setProperty(e,t)}):this.each(r=>{r instanceof HTMLElement&&Object.entries(e).forEach(([s,o])=>{r.style.setProperty(s,o)})})}show(){return this.each(e=>{e instanceof HTMLElement&&(e.style.display="")})}hide(){return this.each(e=>{e instanceof HTMLElement&&(e.style.display="none")})}toggle(e){return this.each(t=>{if(t instanceof HTMLElement){let r=e!=null?e:t.style.display==="none";t.style.display=r?"":"none"}})}isVisible(){return this.nodes.some(e=>e instanceof HTMLElement?e.style.display!=="none"&&e.offsetParent!==null:!1)}append(e){return this.each(t=>{typeof e=="string"?t.insertAdjacentHTML("beforeend",e):e instanceof n?e.each(r=>t.appendChild(r.cloneNode(!0))):t.appendChild(e)})}prepend(e){return this.each(t=>{if(typeof e=="string")t.insertAdjacentHTML("afterbegin",e);else if(e instanceof n){let r=t.firstChild;e.each(s=>{t.insertBefore(s.cloneNode(!0),r)})}else t.insertBefore(e,t.firstChild)})}remove(){return this.each(e=>{e.remove()})}empty(){return this.each(e=>{e.innerHTML=""})}clone(e=!0){return new n(this.nodes.map(t=>t.cloneNode(e)))}focus(){let e=this.get();return e instanceof HTMLElement&&e.focus(),this}blur(){let e=this.get();return e instanceof HTMLElement&&e.blur(),this}};function w(n,e){if(typeof n=="string"){let t=n.trim();if(!t)return new a([]);try{return new a(Array.from((e!=null?e:document).querySelectorAll(t)))}catch(r){return console.error(`[el] Invalid selector: "${t}"`,r),new a([])}}if(n instanceof Element)return new a([n]);if(n instanceof NodeList)return new a(Array.from(n));if(n instanceof HTMLCollection)return new a(Array.from(n));if(Array.isArray(n)){let t=n.filter(r=>r instanceof Element);return t.length!==n.length&&console.warn("[el] Some items in array were not Elements and were filtered out"),new a(t)}return console.warn("[el] Unrecognized input type:",typeof n),new a([])}var M=class{constructor(e,t,r={}){this.root=e;this.model=t;this.destroyed=!1;this.children=new Set;this.options={autoDestroy:!0,...r},this.scope=r.parentScope?r.parentScope.createChild():S(),this.options.autoDestroy&&this.setupAutoDestroy(),this.init()}init(){}$(e){return w(e,this.root)}$root(){return w(this.root)}createChild(e,t,r){this.checkDestroyed();let s=new e(t,r,{parentScope:this.scope,autoDestroy:!1});return this.children.add(s),s.scope.onDispose(()=>{this.children.delete(s)}),s}createChildren(e,t,r){this.checkDestroyed();let s=[];return this.$(t).each((o,i)=>{let l=r?r(o,i):void 0;s.push(this.createChild(e,o,l))}),s}emit(e,t){this.checkDestroyed(),this.$root().trigger(e,t)}on(e,t){this.checkDestroyed(),this.$root().on(e,t,this.scope)}show(){this.checkDestroyed(),this.$root().show()}hide(){this.checkDestroyed(),this.$root().hide()}toggle(e){this.checkDestroyed(),this.$root().toggle(e)}isVisible(){return this.$root().isVisible()}isDestroyed(){return this.destroyed}getRoot(){return this.root}getModel(){return this.model}destroy(){if(!this.destroyed){this.destroyed=!0;for(let e of this.children)try{e.destroy()}catch(t){console.error("[View] Error destroying child view:",t)}this.children.clear();try{this.scope.dispose()}catch(e){console.error("[View] Error disposing scope:",e)}if(this.model&&typeof this.model.destroy=="function")try{this.model.destroy()}catch(e){console.error("[View] Error destroying model:",e)}}}setupAutoDestroy(){let e=new MutationObserver(t=>{for(let r of t)for(let s of Array.from(r.removedNodes))if(s===this.root||s instanceof Element&&s.contains(this.root)){this.destroy(),e.disconnect();return}});this.root.parentElement&&e.observe(this.root.parentElement,{childList:!0,subtree:!0}),this.scope.onDispose(()=>{e.disconnect()})}checkDestroyed(){if(this.destroyed)throw new Error("[View] Cannot use destroyed view")}};return $(q);})();
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bonemarrow",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "A modern jQuery + Backbone replacement",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
|
|
7
|
+
"main": "dist/bonemarrow.js",
|
|
8
|
+
"unpkg": "dist/bonemarrow.js",
|
|
9
|
+
|
|
10
|
+
"files": ["dist"],
|
|
11
|
+
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "npm run build:iife && npm run build:min",
|
|
14
|
+
"build:iife": "esbuild src/index.ts --bundle --format=iife --global-name=bone --target=es2019 --outfile=dist/bonemarrow.js",
|
|
15
|
+
"build:min": "esbuild src/index.ts --bundle --format=iife --global-name=bone --target=es2019 --minify --outfile=dist/bonemarrow.min.js"
|
|
16
|
+
}
|
|
17
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# 🦴 BoneMarrow
|
|
2
|
+
|
|
3
|
+
**BoneMarrow** is a lightweight, lifecycle-aware JavaScript/TypeScript library for building structured UI logic on top of server-rendered HTML — without jQuery, virtual DOMs, or heavyweight frameworks.
|
|
4
|
+
|
|
5
|
+
It provides a small set of **explicit primitives** for managing UI behavior, async operations, and state in a predictable way.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Why BoneMarrow?
|
|
10
|
+
|
|
11
|
+
Modern web apps often fall into two extremes:
|
|
12
|
+
|
|
13
|
+
* **jQuery-style code** that becomes unmaintainable at scale
|
|
14
|
+
* **SPA frameworks** that are too heavy for many server-rendered apps
|
|
15
|
+
|
|
16
|
+
BoneMarrow sits in between.
|
|
17
|
+
|
|
18
|
+
It helps you:
|
|
19
|
+
|
|
20
|
+
* Modernize legacy applications incrementally
|
|
21
|
+
* Replace jQuery patterns safely
|
|
22
|
+
* Keep server-rendered HTML
|
|
23
|
+
* Avoid framework lock-in
|
|
24
|
+
* Write code that is easy to reason about and debug
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Core Ideas
|
|
29
|
+
|
|
30
|
+
* **Explicit lifecycles** using scopes
|
|
31
|
+
* **Abort-safe async** by default
|
|
32
|
+
* **No hidden background work**
|
|
33
|
+
* **No global mutable state**
|
|
34
|
+
* **No `$`**
|
|
35
|
+
* **No magic**
|
|
36
|
+
|
|
37
|
+
If something happens, you should know *where*, *when*, and *why*.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## What BoneMarrow Provides
|
|
42
|
+
|
|
43
|
+
### Scope
|
|
44
|
+
|
|
45
|
+
A lifecycle container that:
|
|
46
|
+
|
|
47
|
+
* Owns async work
|
|
48
|
+
* Aborts fetches on disposal
|
|
49
|
+
* Cleans up event listeners
|
|
50
|
+
* Supports parent / child scopes
|
|
51
|
+
|
|
52
|
+
### Fetch Pipeline
|
|
53
|
+
|
|
54
|
+
A unified fetch helper with optional:
|
|
55
|
+
|
|
56
|
+
* abort
|
|
57
|
+
* timeout
|
|
58
|
+
* retry on failure
|
|
59
|
+
* scope-local request de-duplication
|
|
60
|
+
|
|
61
|
+
### Model
|
|
62
|
+
|
|
63
|
+
* Simple observable state
|
|
64
|
+
* Patch-based updates
|
|
65
|
+
* Fetch and auto-refresh support
|
|
66
|
+
|
|
67
|
+
### Collection
|
|
68
|
+
|
|
69
|
+
* List-based state
|
|
70
|
+
* Reset semantics
|
|
71
|
+
* Fetch and auto-refresh support
|
|
72
|
+
|
|
73
|
+
### Auto Refresh
|
|
74
|
+
|
|
75
|
+
* Sequential polling (no overlapping requests)
|
|
76
|
+
* Next refresh starts only after the previous fetch completes
|
|
77
|
+
|
|
78
|
+
### DOM Utilities
|
|
79
|
+
|
|
80
|
+
* `el()` selector helper
|
|
81
|
+
* `Elements` wrapper
|
|
82
|
+
* Scoped event handling
|
|
83
|
+
|
|
84
|
+
### View
|
|
85
|
+
|
|
86
|
+
* UI composition primitive
|
|
87
|
+
* Owns a DOM root and scope
|
|
88
|
+
* Automatic cleanup on destroy
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Quick Example
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
const user = new bone.Model({ name: "John" });
|
|
96
|
+
|
|
97
|
+
class UserView extends bone.View<typeof user> {
|
|
98
|
+
protected init() {
|
|
99
|
+
this.$(".name").text(this.model.get("name"));
|
|
100
|
+
|
|
101
|
+
this.model.onChange(patch => {
|
|
102
|
+
if (patch.name) {
|
|
103
|
+
this.$(".name").text(patch.name);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
this.$(".btn").on(
|
|
108
|
+
"click",
|
|
109
|
+
() => this.model.set({ name: "Smith" }),
|
|
110
|
+
this.scope
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
new UserView(document.getElementById("app")!, user);
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Everything created inside the view:
|
|
119
|
+
|
|
120
|
+
* is scoped
|
|
121
|
+
* is cleaned up automatically
|
|
122
|
+
* stops when the view is destroyed
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Sequential Auto Refresh (Polling)
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
user.autoRefresh("/api/user/42", {
|
|
130
|
+
scope: this.scope,
|
|
131
|
+
interval: 5000,
|
|
132
|
+
fetch: { abort: true }
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Actual behavior:
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
fetch → complete → wait 5s → fetch → complete → wait 5s
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
No overlapping requests. No runaway timers.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## jQuery-like UI, Without jQuery
|
|
147
|
+
|
|
148
|
+
BoneMarrow is well suited for building:
|
|
149
|
+
|
|
150
|
+
* dialogs
|
|
151
|
+
* dropdowns
|
|
152
|
+
* tabs
|
|
153
|
+
* dashboards
|
|
154
|
+
* admin panels
|
|
155
|
+
|
|
156
|
+
All with:
|
|
157
|
+
|
|
158
|
+
* explicit ownership
|
|
159
|
+
* predictable cleanup
|
|
160
|
+
* no global state
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Progressive Enhancement Friendly
|
|
165
|
+
|
|
166
|
+
BoneMarrow works best when:
|
|
167
|
+
|
|
168
|
+
* HTML is rendered by the server
|
|
169
|
+
* JavaScript enhances behavior
|
|
170
|
+
* Pages still work without JS
|
|
171
|
+
|
|
172
|
+
Perfect for:
|
|
173
|
+
|
|
174
|
+
* ASP.NET MVC / Razor
|
|
175
|
+
* Rails
|
|
176
|
+
* PHP
|
|
177
|
+
* Django
|
|
178
|
+
* Large legacy apps
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Documentation
|
|
183
|
+
|
|
184
|
+
* Full documentation is available in **`DOCS.md`**
|
|
185
|
+
* Includes detailed API reference
|
|
186
|
+
* Covers scopes, fetch, models, collections, views, and patterns
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Size
|
|
191
|
+
|
|
192
|
+
Approximate sizes (production build):
|
|
193
|
+
|
|
194
|
+
* ~9 KB raw
|
|
195
|
+
* ~4–5 KB minified
|
|
196
|
+
* ~2–3 KB gzip
|
|
197
|
+
|
|
198
|
+
Smaller than most “micro” libraries, without sacrificing structure.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## What BoneMarrow Is Not
|
|
203
|
+
|
|
204
|
+
* ❌ Not a framework
|
|
205
|
+
* ❌ Not a virtual DOM
|
|
206
|
+
* ❌ Not reactive magic
|
|
207
|
+
* ❌ Not an SPA replacement
|
|
208
|
+
|
|
209
|
+
BoneMarrow provides **primitives**, not rules.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## License
|
|
214
|
+
|
|
215
|
+
MIT © Karthick Raj
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Final Thought
|
|
220
|
+
|
|
221
|
+
> **BoneMarrow is intentionally boring.
|
|
222
|
+
> Boring code scales.**
|
|
223
|
+
|
|
224
|
+
If you want clarity, explicit lifecycles, and predictable async behavior without the weight of a framework, BoneMarrow is built for that.
|
|
225
|
+
|