surf-core 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 +21 -0
- package/README.md +205 -0
- package/dist/plugins/debounce.js +2 -0
- package/dist/plugins/drag-and-drop.js +2 -0
- package/dist/surf.js +911 -0
- package/dist/surf.min.js +5 -0
- package/package.json +46 -0
- package/src/cell.js +197 -0
- package/src/echo.js +63 -0
- package/src/patch.js +81 -0
- package/src/plugins/auto-refresh.js +70 -0
- package/src/plugins/debounce.js +49 -0
- package/src/plugins/drag-and-drop.js +159 -0
- package/src/pulse.js +369 -0
- package/src/signal.js +507 -0
- package/src/surf.d.ts +77 -0
- package/src/surf.js +179 -0
- package/src/surface.js +160 -0
package/dist/surf.js
ADDED
|
@@ -0,0 +1,911 @@
|
|
|
1
|
+
var SurfModule = (() => {
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __esm = (fn, res) => function __init() {
|
|
7
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
8
|
+
};
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
22
|
+
|
|
23
|
+
// src/cell.js
|
|
24
|
+
var cell_exports = {};
|
|
25
|
+
__export(cell_exports, {
|
|
26
|
+
clearPreserved: () => clearPreserved,
|
|
27
|
+
default: () => cell_default,
|
|
28
|
+
findAll: () => findAll2,
|
|
29
|
+
getCellId: () => getCellId,
|
|
30
|
+
getState: () => getState,
|
|
31
|
+
init: () => init2,
|
|
32
|
+
initAll: () => initAll,
|
|
33
|
+
restore: () => restore,
|
|
34
|
+
setProperty: () => setProperty,
|
|
35
|
+
setState: () => setState,
|
|
36
|
+
snapshot: () => snapshot
|
|
37
|
+
});
|
|
38
|
+
function parseSeed(seedExpr) {
|
|
39
|
+
if (!seedExpr)
|
|
40
|
+
return {};
|
|
41
|
+
const trimmed = seedExpr.trim();
|
|
42
|
+
try {
|
|
43
|
+
const expression = trimmed.startsWith("{") ? `return ${trimmed}` : `return { ${trimmed} }`;
|
|
44
|
+
return new Function(expression)();
|
|
45
|
+
} catch (e) {
|
|
46
|
+
console.warn(`[Surf] Invalid seed expression: "${trimmed}"`, e);
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function getCellId(element) {
|
|
51
|
+
return element.getAttribute(CELL_ID_ATTR) || element.id || null;
|
|
52
|
+
}
|
|
53
|
+
function findAll2(container = document) {
|
|
54
|
+
return container.querySelectorAll(`[${CELL_ATTR}]`);
|
|
55
|
+
}
|
|
56
|
+
function init2(element) {
|
|
57
|
+
if (!element.hasAttribute(CELL_ATTR)) {
|
|
58
|
+
console.warn("[Surf] Element is not a cell:", element);
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
const cellId = getCellId(element);
|
|
62
|
+
if (cellId && cellIdStates.has(cellId)) {
|
|
63
|
+
const preservedState = cellIdStates.get(cellId);
|
|
64
|
+
cellStates.set(element, preservedState);
|
|
65
|
+
return preservedState;
|
|
66
|
+
}
|
|
67
|
+
const cellExpr = element.getAttribute(CELL_ATTR);
|
|
68
|
+
const state = parseSeed(cellExpr || "{}");
|
|
69
|
+
cellStates.set(element, state);
|
|
70
|
+
if (cellId) {
|
|
71
|
+
cellIdStates.set(cellId, state);
|
|
72
|
+
}
|
|
73
|
+
return state;
|
|
74
|
+
}
|
|
75
|
+
function getState(element) {
|
|
76
|
+
if (!cellStates.has(element)) {
|
|
77
|
+
return init2(element);
|
|
78
|
+
}
|
|
79
|
+
return cellStates.get(element);
|
|
80
|
+
}
|
|
81
|
+
function setState(element, newState) {
|
|
82
|
+
const currentState = getState(element);
|
|
83
|
+
const updatedState = { ...currentState, ...newState };
|
|
84
|
+
cellStates.set(element, updatedState);
|
|
85
|
+
const cellId = getCellId(element);
|
|
86
|
+
if (cellId) {
|
|
87
|
+
cellIdStates.set(cellId, updatedState);
|
|
88
|
+
}
|
|
89
|
+
return updatedState;
|
|
90
|
+
}
|
|
91
|
+
function setProperty(element, key, value) {
|
|
92
|
+
return setState(element, { [key]: value });
|
|
93
|
+
}
|
|
94
|
+
function snapshot(container = document) {
|
|
95
|
+
const cells = findAll2(container);
|
|
96
|
+
const snap = /* @__PURE__ */ new Map();
|
|
97
|
+
cells.forEach((cell) => {
|
|
98
|
+
const cellId = getCellId(cell);
|
|
99
|
+
if (cellId) {
|
|
100
|
+
snap.set(cellId, { ...getState(cell) });
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
return snap;
|
|
104
|
+
}
|
|
105
|
+
function restore(snap) {
|
|
106
|
+
snap.forEach((state, cellId) => {
|
|
107
|
+
cellIdStates.set(cellId, state);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
function initAll(container = document) {
|
|
111
|
+
const cells = findAll2(container);
|
|
112
|
+
cells.forEach((cell) => init2(cell));
|
|
113
|
+
}
|
|
114
|
+
function clearPreserved(cellId) {
|
|
115
|
+
cellIdStates.delete(cellId);
|
|
116
|
+
}
|
|
117
|
+
var CELL_ATTR, CELL_ID_ATTR, cellStates, cellIdStates, cell_default;
|
|
118
|
+
var init_cell = __esm({
|
|
119
|
+
"src/cell.js"() {
|
|
120
|
+
CELL_ATTR = "d-cell";
|
|
121
|
+
CELL_ID_ATTR = "d-cell-id";
|
|
122
|
+
cellStates = /* @__PURE__ */ new WeakMap();
|
|
123
|
+
cellIdStates = /* @__PURE__ */ new Map();
|
|
124
|
+
cell_default = {
|
|
125
|
+
findAll: findAll2,
|
|
126
|
+
getCellId,
|
|
127
|
+
init: init2,
|
|
128
|
+
getState,
|
|
129
|
+
setState,
|
|
130
|
+
setProperty,
|
|
131
|
+
snapshot,
|
|
132
|
+
restore,
|
|
133
|
+
initAll,
|
|
134
|
+
clearPreserved
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// src/surf.js
|
|
140
|
+
var surf_exports = {};
|
|
141
|
+
__export(surf_exports, {
|
|
142
|
+
Cell: () => cell_exports,
|
|
143
|
+
Echo: () => echo_exports,
|
|
144
|
+
Patch: () => patch_exports,
|
|
145
|
+
Pulse: () => pulse_exports,
|
|
146
|
+
Signal: () => signal_exports,
|
|
147
|
+
Surface: () => surface_exports,
|
|
148
|
+
default: () => surf_default
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// src/surface.js
|
|
152
|
+
var surface_exports = {};
|
|
153
|
+
__export(surface_exports, {
|
|
154
|
+
append: () => append,
|
|
155
|
+
default: () => surface_default,
|
|
156
|
+
findAll: () => findAll,
|
|
157
|
+
getById: () => getById,
|
|
158
|
+
getBySelector: () => getBySelector,
|
|
159
|
+
init: () => init,
|
|
160
|
+
prepend: () => prepend,
|
|
161
|
+
replace: () => replace
|
|
162
|
+
});
|
|
163
|
+
var SURFACE_ATTR = "d-surface";
|
|
164
|
+
function findAll() {
|
|
165
|
+
return document.querySelectorAll(`[${SURFACE_ATTR}]`);
|
|
166
|
+
}
|
|
167
|
+
function getById(id) {
|
|
168
|
+
const cleanId = id.startsWith("#") ? id.slice(1) : id;
|
|
169
|
+
const element = document.getElementById(cleanId);
|
|
170
|
+
if (element && element.hasAttribute(SURFACE_ATTR)) {
|
|
171
|
+
return element;
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
function getBySelector(selector) {
|
|
176
|
+
const element = document.querySelector(selector);
|
|
177
|
+
if (element && element.hasAttribute(SURFACE_ATTR)) {
|
|
178
|
+
return element;
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
function replace(selectorOrElement, html) {
|
|
183
|
+
let surface;
|
|
184
|
+
if (typeof selectorOrElement === "string") {
|
|
185
|
+
surface = getBySelector(selectorOrElement) || document.querySelector(selectorOrElement);
|
|
186
|
+
} else {
|
|
187
|
+
surface = selectorOrElement;
|
|
188
|
+
}
|
|
189
|
+
if (!surface) {
|
|
190
|
+
console.warn(`[Surf] Surface not found: ${selectorOrElement}`);
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
const template = document.createElement("template");
|
|
194
|
+
template.innerHTML = html.trim();
|
|
195
|
+
surface.innerHTML = "";
|
|
196
|
+
while (template.content.firstChild) {
|
|
197
|
+
surface.appendChild(template.content.firstChild);
|
|
198
|
+
}
|
|
199
|
+
return surface;
|
|
200
|
+
}
|
|
201
|
+
function append(selectorOrElement, html) {
|
|
202
|
+
return inject(selectorOrElement, html, "beforeend");
|
|
203
|
+
}
|
|
204
|
+
function prepend(selectorOrElement, html) {
|
|
205
|
+
return inject(selectorOrElement, html, "afterbegin");
|
|
206
|
+
}
|
|
207
|
+
function inject(selectorOrElement, html, position) {
|
|
208
|
+
let surface;
|
|
209
|
+
if (typeof selectorOrElement === "string") {
|
|
210
|
+
surface = getBySelector(selectorOrElement) || document.querySelector(selectorOrElement);
|
|
211
|
+
} else {
|
|
212
|
+
surface = selectorOrElement;
|
|
213
|
+
}
|
|
214
|
+
if (!surface) {
|
|
215
|
+
console.warn(`[Surf] Surface not found: ${selectorOrElement}`);
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
const template = document.createElement("template");
|
|
219
|
+
template.innerHTML = html.trim();
|
|
220
|
+
const fragment = document.createDocumentFragment();
|
|
221
|
+
while (template.content.firstChild) {
|
|
222
|
+
fragment.appendChild(template.content.firstChild);
|
|
223
|
+
}
|
|
224
|
+
if (position === "beforeend") {
|
|
225
|
+
surface.appendChild(fragment);
|
|
226
|
+
} else {
|
|
227
|
+
surface.insertBefore(fragment, surface.firstChild);
|
|
228
|
+
}
|
|
229
|
+
return surface;
|
|
230
|
+
}
|
|
231
|
+
function init() {
|
|
232
|
+
const surfaces = findAll();
|
|
233
|
+
surfaces.forEach((surface) => {
|
|
234
|
+
surface.setAttribute("data-surf-ready", "true");
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
var surface_default = {
|
|
238
|
+
findAll,
|
|
239
|
+
getById,
|
|
240
|
+
getBySelector,
|
|
241
|
+
replace,
|
|
242
|
+
append,
|
|
243
|
+
prepend,
|
|
244
|
+
init
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// src/surf.js
|
|
248
|
+
init_cell();
|
|
249
|
+
|
|
250
|
+
// src/signal.js
|
|
251
|
+
var signal_exports = {};
|
|
252
|
+
__export(signal_exports, {
|
|
253
|
+
cleanup: () => cleanup,
|
|
254
|
+
default: () => signal_default,
|
|
255
|
+
initAll: () => initAll2,
|
|
256
|
+
register: () => register,
|
|
257
|
+
updateBindings: () => updateBindings
|
|
258
|
+
});
|
|
259
|
+
init_cell();
|
|
260
|
+
var SIGNAL_ATTR = "d-signal";
|
|
261
|
+
var TEXT_ATTR = "d-text";
|
|
262
|
+
var SHOW_ATTR = "d-show";
|
|
263
|
+
var ATTR_ATTR = "d-attr";
|
|
264
|
+
var modules = {};
|
|
265
|
+
function register(name, module) {
|
|
266
|
+
modules[name] = module;
|
|
267
|
+
}
|
|
268
|
+
var boundListeners = /* @__PURE__ */ new WeakMap();
|
|
269
|
+
function findParentCell(element) {
|
|
270
|
+
return element.closest("[d-cell]");
|
|
271
|
+
}
|
|
272
|
+
function evaluate(expr, state) {
|
|
273
|
+
if (!expr || !state)
|
|
274
|
+
return void 0;
|
|
275
|
+
const trimmed = expr.trim();
|
|
276
|
+
if (trimmed in state) {
|
|
277
|
+
return state[trimmed];
|
|
278
|
+
}
|
|
279
|
+
if (trimmed.startsWith("!")) {
|
|
280
|
+
const prop = trimmed.slice(1).trim();
|
|
281
|
+
return !state[prop];
|
|
282
|
+
}
|
|
283
|
+
const eqMatch = trimmed.match(/^(\w+)\s*={2,3}\s*(.+)$/);
|
|
284
|
+
if (eqMatch) {
|
|
285
|
+
const [, prop, value] = eqMatch;
|
|
286
|
+
const propVal = state[prop];
|
|
287
|
+
const compareVal = value === "true" ? true : value === "false" ? false : value.startsWith('"') || value.startsWith("'") ? value.slice(1, -1) : Number(value);
|
|
288
|
+
return propVal === compareVal;
|
|
289
|
+
}
|
|
290
|
+
const neqMatch = trimmed.match(/^(\w+)\s*!=\s*(.+)$/);
|
|
291
|
+
if (neqMatch) {
|
|
292
|
+
const [, prop, value] = neqMatch;
|
|
293
|
+
const propVal = state[prop];
|
|
294
|
+
const compareVal = value === "true" ? true : value === "false" ? false : Number(value);
|
|
295
|
+
return propVal !== compareVal;
|
|
296
|
+
}
|
|
297
|
+
const gtMatch = trimmed.match(/^(\w+)\s*>\s*(\d+)$/);
|
|
298
|
+
if (gtMatch) {
|
|
299
|
+
return state[gtMatch[1]] > Number(gtMatch[2]);
|
|
300
|
+
}
|
|
301
|
+
const ltMatch = trimmed.match(/^(\w+)\s*<\s*(\d+)$/);
|
|
302
|
+
if (ltMatch) {
|
|
303
|
+
return state[ltMatch[1]] < Number(ltMatch[2]);
|
|
304
|
+
}
|
|
305
|
+
return void 0;
|
|
306
|
+
}
|
|
307
|
+
function executeAssignment(expr, state, event, element) {
|
|
308
|
+
const trimmed = expr.trim();
|
|
309
|
+
const methodMatch = trimmed.match(/^(\w+)\.(\w+)\((.*)\)$/);
|
|
310
|
+
if (methodMatch) {
|
|
311
|
+
const [, moduleName, methodName, argsStr] = methodMatch;
|
|
312
|
+
const module = modules[moduleName];
|
|
313
|
+
if (!module || typeof module[methodName] !== "function") {
|
|
314
|
+
console.warn(`[Surf] Unknown module method: ${moduleName}.${methodName}`);
|
|
315
|
+
return {};
|
|
316
|
+
}
|
|
317
|
+
const args = parseArguments(argsStr, state, event, element);
|
|
318
|
+
const result = module[methodName](...args);
|
|
319
|
+
return typeof result === "object" ? result : {};
|
|
320
|
+
}
|
|
321
|
+
const match = trimmed.match(/^(\w+)\s*=\s*(.+)$/);
|
|
322
|
+
if (!match)
|
|
323
|
+
return {};
|
|
324
|
+
const [, prop, valueExpr] = match;
|
|
325
|
+
const valueExprTrimmed = valueExpr.trim();
|
|
326
|
+
if (valueExprTrimmed === `!${prop}`) {
|
|
327
|
+
return { [prop]: !state[prop] };
|
|
328
|
+
}
|
|
329
|
+
if (valueExprTrimmed === "true") {
|
|
330
|
+
return { [prop]: true };
|
|
331
|
+
}
|
|
332
|
+
if (valueExprTrimmed === "false") {
|
|
333
|
+
return { [prop]: false };
|
|
334
|
+
}
|
|
335
|
+
const addMatch = valueExprTrimmed.match(/^(\w+)\s*\+\s*(\d+)$/);
|
|
336
|
+
if (addMatch) {
|
|
337
|
+
const [, srcProp, num] = addMatch;
|
|
338
|
+
return { [prop]: (state[srcProp] || 0) + Number(num) };
|
|
339
|
+
}
|
|
340
|
+
const subMatch = valueExprTrimmed.match(/^(\w+)\s*-\s*(\d+)$/);
|
|
341
|
+
if (subMatch) {
|
|
342
|
+
const [, srcProp, num] = subMatch;
|
|
343
|
+
return { [prop]: (state[srcProp] || 0) - Number(num) };
|
|
344
|
+
}
|
|
345
|
+
const maxMatch = valueExprTrimmed.match(/^Math\.max\((\w+)\s*-\s*(\d+),\s*(\d+)\)$/);
|
|
346
|
+
if (maxMatch) {
|
|
347
|
+
const [, srcProp, delta, min] = maxMatch;
|
|
348
|
+
const newVal = (state[srcProp] || 0) - Number(delta);
|
|
349
|
+
return { [prop]: Math.max(newVal, Number(min)) };
|
|
350
|
+
}
|
|
351
|
+
const minMatch = valueExprTrimmed.match(/^Math\.min\((\w+)\s*\+\s*(\d+),\s*(\d+)\)$/);
|
|
352
|
+
if (minMatch) {
|
|
353
|
+
const [, srcProp, delta, max] = minMatch;
|
|
354
|
+
const newVal = (state[srcProp] || 0) + Number(delta);
|
|
355
|
+
return { [prop]: Math.min(newVal, Number(max)) };
|
|
356
|
+
}
|
|
357
|
+
if (valueExprTrimmed.startsWith('"') || valueExprTrimmed.startsWith("'")) {
|
|
358
|
+
return { [prop]: valueExprTrimmed.slice(1, -1) };
|
|
359
|
+
}
|
|
360
|
+
if (/^\d+$/.test(valueExprTrimmed)) {
|
|
361
|
+
return { [prop]: Number(valueExprTrimmed) };
|
|
362
|
+
}
|
|
363
|
+
if (valueExprTrimmed in state) {
|
|
364
|
+
return { [prop]: state[valueExprTrimmed] };
|
|
365
|
+
}
|
|
366
|
+
return {};
|
|
367
|
+
}
|
|
368
|
+
function parseArguments(argsStr, state, event, element) {
|
|
369
|
+
if (argsStr.trim() === "")
|
|
370
|
+
return [];
|
|
371
|
+
return argsStr.split(",").map((arg) => {
|
|
372
|
+
const a = arg.trim();
|
|
373
|
+
if (a === "")
|
|
374
|
+
return void 0;
|
|
375
|
+
if (a === "true")
|
|
376
|
+
return true;
|
|
377
|
+
if (a === "false")
|
|
378
|
+
return false;
|
|
379
|
+
if (a === "event")
|
|
380
|
+
return event;
|
|
381
|
+
if (a === "this")
|
|
382
|
+
return element;
|
|
383
|
+
if (a.startsWith("'") || a.startsWith('"'))
|
|
384
|
+
return a.slice(1, -1);
|
|
385
|
+
if (!isNaN(a) && a !== "")
|
|
386
|
+
return Number(a);
|
|
387
|
+
if (a.startsWith("this.")) {
|
|
388
|
+
return resolvePath(element, a.slice(5));
|
|
389
|
+
}
|
|
390
|
+
return resolvePath(state, a);
|
|
391
|
+
}).filter((a) => a !== void 0 && a !== "");
|
|
392
|
+
}
|
|
393
|
+
function resolvePath(obj, path) {
|
|
394
|
+
const parts = path.split(".");
|
|
395
|
+
let current = obj;
|
|
396
|
+
for (const part of parts) {
|
|
397
|
+
if (current === void 0 || current === null)
|
|
398
|
+
return void 0;
|
|
399
|
+
current = current[part];
|
|
400
|
+
}
|
|
401
|
+
return current;
|
|
402
|
+
}
|
|
403
|
+
function updateBindings(cellElement) {
|
|
404
|
+
const state = getState(cellElement);
|
|
405
|
+
const textElements = cellElement.querySelectorAll(`[${TEXT_ATTR}]`);
|
|
406
|
+
textElements.forEach((el) => {
|
|
407
|
+
const prop = el.getAttribute(TEXT_ATTR);
|
|
408
|
+
const value = evaluate(prop, state);
|
|
409
|
+
if (value !== void 0) {
|
|
410
|
+
el.textContent = String(value);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
const showElements = cellElement.querySelectorAll(`[${SHOW_ATTR}]`);
|
|
414
|
+
showElements.forEach((el) => {
|
|
415
|
+
const expr = el.getAttribute(SHOW_ATTR);
|
|
416
|
+
const visible = evaluate(expr, state);
|
|
417
|
+
el.style.display = visible ? "" : "none";
|
|
418
|
+
});
|
|
419
|
+
const attrElements = cellElement.querySelectorAll(`[${ATTR_ATTR}]`);
|
|
420
|
+
attrElements.forEach((el) => {
|
|
421
|
+
const attrExpr = el.getAttribute(ATTR_ATTR);
|
|
422
|
+
const classMatch = attrExpr.match(/^class\.([\\w-]+):\\s*(.+)$/);
|
|
423
|
+
if (classMatch) {
|
|
424
|
+
const [, className, expr2] = classMatch;
|
|
425
|
+
const shouldAdd = evaluate(expr2.trim(), state);
|
|
426
|
+
if (shouldAdd) {
|
|
427
|
+
el.classList.add(className);
|
|
428
|
+
} else {
|
|
429
|
+
el.classList.remove(className);
|
|
430
|
+
}
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const match = attrExpr.match(/^(\\w+):(.+)$/);
|
|
434
|
+
if (!match)
|
|
435
|
+
return;
|
|
436
|
+
const [, attrName, expr] = match;
|
|
437
|
+
const value = evaluate(expr.trim(), state);
|
|
438
|
+
if (value === void 0)
|
|
439
|
+
return;
|
|
440
|
+
if (typeof value !== "boolean") {
|
|
441
|
+
el.setAttribute(attrName, String(value));
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (value) {
|
|
445
|
+
el.setAttribute(attrName, "");
|
|
446
|
+
} else {
|
|
447
|
+
el.removeAttribute(attrName);
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
function bindSignalElement(element) {
|
|
452
|
+
const signalExpr = element.getAttribute(SIGNAL_ATTR);
|
|
453
|
+
if (!signalExpr)
|
|
454
|
+
return;
|
|
455
|
+
const match = signalExpr.match(/^(\w+):\s*(.+)$/);
|
|
456
|
+
if (!match) {
|
|
457
|
+
console.warn(`[Surf] Invalid signal expression: ${signalExpr}`);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const [, eventName, actionExpr] = match;
|
|
461
|
+
const cellElement = findParentCell(element);
|
|
462
|
+
if (!cellElement) {
|
|
463
|
+
console.warn("[Surf] Signal element has no parent cell:", element);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const existing = boundListeners.get(element);
|
|
467
|
+
if (existing) {
|
|
468
|
+
element.removeEventListener(existing.event, existing.handler);
|
|
469
|
+
}
|
|
470
|
+
const handler = (e) => {
|
|
471
|
+
const state = getState(cellElement);
|
|
472
|
+
const changes = executeAssignment(actionExpr, state, e, element);
|
|
473
|
+
if (Object.keys(changes).length > 0) {
|
|
474
|
+
setState(cellElement, changes);
|
|
475
|
+
updateBindings(cellElement);
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
element.addEventListener(eventName, handler);
|
|
479
|
+
boundListeners.set(element, { event: eventName, handler });
|
|
480
|
+
}
|
|
481
|
+
function initAll2(container = document) {
|
|
482
|
+
const signalElements = container.querySelectorAll(`[${SIGNAL_ATTR}]`);
|
|
483
|
+
signalElements.forEach((el) => bindSignalElement(el));
|
|
484
|
+
const cells = findAll2(container);
|
|
485
|
+
cells.forEach((cell) => updateBindings(cell));
|
|
486
|
+
}
|
|
487
|
+
function cleanup(container) {
|
|
488
|
+
const signalElements = container.querySelectorAll(`[${SIGNAL_ATTR}]`);
|
|
489
|
+
signalElements.forEach((el) => {
|
|
490
|
+
const existing = boundListeners.get(el);
|
|
491
|
+
if (existing) {
|
|
492
|
+
el.removeEventListener(existing.event, existing.handler);
|
|
493
|
+
boundListeners.delete(el);
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
var signal_default = {
|
|
498
|
+
updateBindings,
|
|
499
|
+
initAll: initAll2,
|
|
500
|
+
cleanup,
|
|
501
|
+
evaluate,
|
|
502
|
+
executeAssignment,
|
|
503
|
+
register
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
// src/pulse.js
|
|
507
|
+
var pulse_exports = {};
|
|
508
|
+
__export(pulse_exports, {
|
|
509
|
+
action: () => action,
|
|
510
|
+
commit: () => commit,
|
|
511
|
+
default: () => pulse_default,
|
|
512
|
+
go: () => go,
|
|
513
|
+
init: () => init3,
|
|
514
|
+
navigate: () => navigate,
|
|
515
|
+
off: () => off,
|
|
516
|
+
on: () => on,
|
|
517
|
+
refresh: () => refresh
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// src/patch.js
|
|
521
|
+
var patch_exports = {};
|
|
522
|
+
__export(patch_exports, {
|
|
523
|
+
create: () => create,
|
|
524
|
+
default: () => patch_default,
|
|
525
|
+
isPatch: () => isPatch,
|
|
526
|
+
parse: () => parse
|
|
527
|
+
});
|
|
528
|
+
function parse(html) {
|
|
529
|
+
const patches = [];
|
|
530
|
+
const parser = new DOMParser();
|
|
531
|
+
const doc = parser.parseFromString(html, "text/html");
|
|
532
|
+
const patchElement = doc.querySelector("d-patch");
|
|
533
|
+
if (!patchElement) {
|
|
534
|
+
console.warn("[Surf] No <d-patch> element found in response");
|
|
535
|
+
return patches;
|
|
536
|
+
}
|
|
537
|
+
const surfaceElements = patchElement.querySelectorAll("surface");
|
|
538
|
+
surfaceElements.forEach((surface) => {
|
|
539
|
+
const target = surface.getAttribute("target");
|
|
540
|
+
if (!target) {
|
|
541
|
+
console.warn("[Surf] Surface element missing target attribute");
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
patches.push({
|
|
545
|
+
target,
|
|
546
|
+
content: surface.innerHTML
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
return patches;
|
|
550
|
+
}
|
|
551
|
+
function isPatch(html) {
|
|
552
|
+
return html.includes("<d-patch>") || html.includes("<d-patch ");
|
|
553
|
+
}
|
|
554
|
+
function create(surfaces) {
|
|
555
|
+
const surfaceHtml = surfaces.map((s) => ` <surface target="${s.target}">${s.content}</surface>`).join("\n");
|
|
556
|
+
return `<d-patch>
|
|
557
|
+
${surfaceHtml}
|
|
558
|
+
</d-patch>`;
|
|
559
|
+
}
|
|
560
|
+
var patch_default = {
|
|
561
|
+
parse,
|
|
562
|
+
isPatch,
|
|
563
|
+
create
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
// src/echo.js
|
|
567
|
+
var echo_exports = {};
|
|
568
|
+
__export(echo_exports, {
|
|
569
|
+
after: () => after,
|
|
570
|
+
before: () => before,
|
|
571
|
+
default: () => echo_default,
|
|
572
|
+
withPreservation: () => withPreservation
|
|
573
|
+
});
|
|
574
|
+
init_cell();
|
|
575
|
+
function before(surface) {
|
|
576
|
+
cleanup(surface);
|
|
577
|
+
return snapshot(surface);
|
|
578
|
+
}
|
|
579
|
+
function after(surface, snapshot2) {
|
|
580
|
+
restore(snapshot2);
|
|
581
|
+
initAll(surface);
|
|
582
|
+
initAll2(surface);
|
|
583
|
+
}
|
|
584
|
+
function withPreservation(surface, newContent, replaceFn) {
|
|
585
|
+
const snapshot2 = before(surface);
|
|
586
|
+
replaceFn();
|
|
587
|
+
after(surface, snapshot2);
|
|
588
|
+
}
|
|
589
|
+
var echo_default = {
|
|
590
|
+
before,
|
|
591
|
+
after,
|
|
592
|
+
withPreservation
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
// src/pulse.js
|
|
596
|
+
var PULSE_ATTR = "d-pulse";
|
|
597
|
+
var TARGET_ATTR = "d-target";
|
|
598
|
+
var ACTION_ATTR = "d-action";
|
|
599
|
+
var listeners = {
|
|
600
|
+
"before:pulse": [],
|
|
601
|
+
"after:patch": [],
|
|
602
|
+
"error:network": []
|
|
603
|
+
};
|
|
604
|
+
function emit(event, detail) {
|
|
605
|
+
if (listeners[event]) {
|
|
606
|
+
listeners[event].forEach((cb) => {
|
|
607
|
+
try {
|
|
608
|
+
cb(detail);
|
|
609
|
+
} catch (e) {
|
|
610
|
+
console.error(`[Surf] Error in ${event} listener:`, e);
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
function on(event, callback) {
|
|
616
|
+
if (listeners[event]) {
|
|
617
|
+
listeners[event].push(callback);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
function off(event, callback) {
|
|
621
|
+
if (listeners[event]) {
|
|
622
|
+
const index = listeners[event].indexOf(callback);
|
|
623
|
+
if (index > -1) {
|
|
624
|
+
listeners[event].splice(index, 1);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
function applyPatches(patches) {
|
|
629
|
+
patches.forEach(({ target, content }) => {
|
|
630
|
+
const surface = getBySelector(target) || document.querySelector(target);
|
|
631
|
+
if (!surface) {
|
|
632
|
+
console.warn(`[Surf] Target not found for patch: ${target}`);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
withPreservation(surface, content, () => {
|
|
636
|
+
replace(target, content);
|
|
637
|
+
});
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
async function sendPulse(url, options, targetSelector) {
|
|
641
|
+
emit("before:pulse", { url, options, target: targetSelector });
|
|
642
|
+
try {
|
|
643
|
+
const response = await fetch(url, {
|
|
644
|
+
...options,
|
|
645
|
+
headers: {
|
|
646
|
+
"Accept": "text/html",
|
|
647
|
+
"X-Surf-Request": "true",
|
|
648
|
+
...options.headers
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
if (!response.ok) {
|
|
652
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
653
|
+
}
|
|
654
|
+
const html = await response.text();
|
|
655
|
+
if (isPatch(html)) {
|
|
656
|
+
const patches = parse(html);
|
|
657
|
+
applyPatches(patches);
|
|
658
|
+
} else {
|
|
659
|
+
if (targetSelector) {
|
|
660
|
+
const surface = getBySelector(targetSelector) || document.querySelector(targetSelector);
|
|
661
|
+
if (surface) {
|
|
662
|
+
const swapMode = options.swap || surface.getAttribute("d-swap") || "inner";
|
|
663
|
+
withPreservation(surface, html, () => {
|
|
664
|
+
if (swapMode === "append") {
|
|
665
|
+
append(surface, html);
|
|
666
|
+
} else if (swapMode === "prepend") {
|
|
667
|
+
prepend(surface, html);
|
|
668
|
+
} else {
|
|
669
|
+
replace(surface, html);
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
emit("after:patch", { url, target: targetSelector });
|
|
676
|
+
} catch (error) {
|
|
677
|
+
console.error("[Surf] Pulse error:", error);
|
|
678
|
+
emit("error:network", { url, error });
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
async function navigate(url, targetSelector, options = {}) {
|
|
682
|
+
await sendPulse(url, { method: "GET", ...options }, targetSelector);
|
|
683
|
+
if (targetSelector) {
|
|
684
|
+
history.pushState({ surf: true, url, target: targetSelector }, "", url);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
async function commit(form, targetSelector) {
|
|
688
|
+
const method = form.method?.toUpperCase() || "POST";
|
|
689
|
+
let url = form.action || window.location.href;
|
|
690
|
+
const formData = new FormData(form);
|
|
691
|
+
const swap = form.getAttribute("d-swap");
|
|
692
|
+
const options = swap ? { swap } : {};
|
|
693
|
+
const params = new URLSearchParams();
|
|
694
|
+
formData.forEach((value, key) => params.append(key, value));
|
|
695
|
+
if (method === "GET") {
|
|
696
|
+
const separator = url.includes("?") ? "&" : "?";
|
|
697
|
+
url = url + separator + params.toString();
|
|
698
|
+
await sendPulse(url, { method: "GET", ...options }, targetSelector);
|
|
699
|
+
} else {
|
|
700
|
+
await sendPulse(url, {
|
|
701
|
+
method,
|
|
702
|
+
headers: {
|
|
703
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
704
|
+
},
|
|
705
|
+
body: params.toString(),
|
|
706
|
+
...options
|
|
707
|
+
}, targetSelector);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
async function refresh(targetSelector) {
|
|
711
|
+
const url = window.location.href;
|
|
712
|
+
const surface = getBySelector(targetSelector) || document.querySelector(targetSelector);
|
|
713
|
+
const swap = surface?.getAttribute("d-swap");
|
|
714
|
+
await sendPulse(url, { method: "GET", swap }, targetSelector);
|
|
715
|
+
}
|
|
716
|
+
async function action(url, data = {}, targetSelector, options = {}) {
|
|
717
|
+
await sendPulse(url, {
|
|
718
|
+
method: "POST",
|
|
719
|
+
headers: {
|
|
720
|
+
"Content-Type": "application/json"
|
|
721
|
+
},
|
|
722
|
+
body: JSON.stringify(data),
|
|
723
|
+
...options
|
|
724
|
+
}, targetSelector);
|
|
725
|
+
}
|
|
726
|
+
async function handleClick(event) {
|
|
727
|
+
const element = event.target.closest(`[${PULSE_ATTR}]`);
|
|
728
|
+
if (!element)
|
|
729
|
+
return;
|
|
730
|
+
const pulseType = element.getAttribute(PULSE_ATTR);
|
|
731
|
+
const targetSelector = element.getAttribute(TARGET_ATTR);
|
|
732
|
+
const actionUrl = element.getAttribute(ACTION_ATTR);
|
|
733
|
+
const swap = element.getAttribute("d-swap");
|
|
734
|
+
const options = swap ? { swap } : {};
|
|
735
|
+
if (element.tagName === "A" && pulseType === "navigate") {
|
|
736
|
+
event.preventDefault();
|
|
737
|
+
const url = element.href;
|
|
738
|
+
navigate(url, targetSelector, options);
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
if (pulseType === "refresh") {
|
|
742
|
+
event.preventDefault();
|
|
743
|
+
refresh(targetSelector);
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
if (pulseType === "action" && actionUrl) {
|
|
747
|
+
event.preventDefault();
|
|
748
|
+
const data = {};
|
|
749
|
+
for (const attr of element.attributes) {
|
|
750
|
+
if (attr.name.startsWith("data-") && attr.name !== "data-surf-ready") {
|
|
751
|
+
const key = attr.name.slice(5);
|
|
752
|
+
data[key] = attr.value;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
const parentCell = element.closest("[d-cell]");
|
|
756
|
+
if (parentCell) {
|
|
757
|
+
const Cell = await Promise.resolve().then(() => (init_cell(), cell_exports));
|
|
758
|
+
const cellState = Cell.default ? Cell.default.getState(parentCell) : Cell.getState(parentCell);
|
|
759
|
+
if (cellState) {
|
|
760
|
+
Object.assign(data, cellState);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
action(actionUrl, data, targetSelector, options);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
function handleSubmit(event) {
|
|
768
|
+
const form = event.target;
|
|
769
|
+
if (!form.hasAttribute(PULSE_ATTR))
|
|
770
|
+
return;
|
|
771
|
+
const pulseType = form.getAttribute(PULSE_ATTR);
|
|
772
|
+
const targetSelector = form.getAttribute(TARGET_ATTR);
|
|
773
|
+
if (pulseType === "commit") {
|
|
774
|
+
event.preventDefault();
|
|
775
|
+
commit(form, targetSelector);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
function handlePopState(event) {
|
|
779
|
+
if (event.state?.surf) {
|
|
780
|
+
sendPulse(event.state.url, { method: "GET" }, event.state.target);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
function init3() {
|
|
784
|
+
document.addEventListener("click", handleClick);
|
|
785
|
+
document.addEventListener("submit", handleSubmit);
|
|
786
|
+
window.addEventListener("popstate", handlePopState);
|
|
787
|
+
window.addEventListener("popstate", handlePopState);
|
|
788
|
+
}
|
|
789
|
+
async function go(url, options = {}) {
|
|
790
|
+
const target = options.target || "body";
|
|
791
|
+
await navigate(url, target, options);
|
|
792
|
+
}
|
|
793
|
+
var pulse_default = {
|
|
794
|
+
on,
|
|
795
|
+
off,
|
|
796
|
+
navigate,
|
|
797
|
+
commit,
|
|
798
|
+
refresh,
|
|
799
|
+
action,
|
|
800
|
+
go,
|
|
801
|
+
init: init3
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
// src/surf.js
|
|
805
|
+
var Surf = {
|
|
806
|
+
/**
|
|
807
|
+
* Framework version
|
|
808
|
+
*/
|
|
809
|
+
version: "0.1.0",
|
|
810
|
+
/**
|
|
811
|
+
* Navigate to a URL
|
|
812
|
+
* @param {string} url - The URL to navigate to
|
|
813
|
+
* @param {Object} options - Optional settings (target, swap, etc)
|
|
814
|
+
*/
|
|
815
|
+
go(url, options = {}) {
|
|
816
|
+
return go(url, options);
|
|
817
|
+
},
|
|
818
|
+
/**
|
|
819
|
+
* Refresh a surface's content from the server
|
|
820
|
+
* @param {string} selector - The surface selector to refresh
|
|
821
|
+
*/
|
|
822
|
+
refresh(selector) {
|
|
823
|
+
return refresh(selector);
|
|
824
|
+
},
|
|
825
|
+
/**
|
|
826
|
+
* Subscribe to framework events
|
|
827
|
+
* @param {string} event - Event name: 'before:pulse', 'after:patch', 'error:network'
|
|
828
|
+
* @param {function} callback - Event handler
|
|
829
|
+
*/
|
|
830
|
+
on(event, callback) {
|
|
831
|
+
on(event, callback);
|
|
832
|
+
},
|
|
833
|
+
/**
|
|
834
|
+
* Unsubscribe from framework events
|
|
835
|
+
* @param {string} event - Event name
|
|
836
|
+
* @param {function} callback - Event handler to remove
|
|
837
|
+
*/
|
|
838
|
+
off(event, callback) {
|
|
839
|
+
off(event, callback);
|
|
840
|
+
},
|
|
841
|
+
/**
|
|
842
|
+
* Get cell state for an element
|
|
843
|
+
* @param {Element|string} cellOrSelector - Cell element or selector
|
|
844
|
+
* @returns {Object} The cell's current state
|
|
845
|
+
*/
|
|
846
|
+
getState(cellOrSelector) {
|
|
847
|
+
const cell = typeof cellOrSelector === "string" ? document.querySelector(cellOrSelector) : cellOrSelector;
|
|
848
|
+
return getState(cell);
|
|
849
|
+
},
|
|
850
|
+
/**
|
|
851
|
+
* Set cell state for an element
|
|
852
|
+
* @param {Element|string} cellOrSelector - Cell element or selector
|
|
853
|
+
* @param {Object} state - State to merge
|
|
854
|
+
*/
|
|
855
|
+
setState(cellOrSelector, state) {
|
|
856
|
+
const cell = typeof cellOrSelector === "string" ? document.querySelector(cellOrSelector) : cellOrSelector;
|
|
857
|
+
setState(cell, state);
|
|
858
|
+
updateBindings(cell);
|
|
859
|
+
},
|
|
860
|
+
/**
|
|
861
|
+
* Manually apply a patch response
|
|
862
|
+
* @param {string} patchHtml - The patch HTML string
|
|
863
|
+
*/
|
|
864
|
+
applyPatch(patchHtml) {
|
|
865
|
+
const patches = parse(patchHtml);
|
|
866
|
+
patches.forEach(({ target, content }) => {
|
|
867
|
+
const surface = getBySelector(target) || document.querySelector(target);
|
|
868
|
+
if (surface) {
|
|
869
|
+
withPreservation(surface, content, () => {
|
|
870
|
+
replace(target, content);
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
});
|
|
874
|
+
},
|
|
875
|
+
/**
|
|
876
|
+
* Register a module for signal expressions
|
|
877
|
+
* @param {string} name - Module namespace
|
|
878
|
+
* @param {Object} module - Object with methods
|
|
879
|
+
*/
|
|
880
|
+
register(name, module) {
|
|
881
|
+
register(name, module);
|
|
882
|
+
},
|
|
883
|
+
// Expose modules for advanced usage
|
|
884
|
+
_modules: {
|
|
885
|
+
Surface: surface_exports,
|
|
886
|
+
Cell: cell_exports,
|
|
887
|
+
Signal: signal_exports,
|
|
888
|
+
Pulse: pulse_exports,
|
|
889
|
+
Patch: patch_exports,
|
|
890
|
+
Echo: echo_exports
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
function init4() {
|
|
894
|
+
init();
|
|
895
|
+
initAll();
|
|
896
|
+
initAll2();
|
|
897
|
+
init3();
|
|
898
|
+
console.log(`[Surf] Initialized v${Surf.version}`);
|
|
899
|
+
}
|
|
900
|
+
if (document.readyState === "loading") {
|
|
901
|
+
document.addEventListener("DOMContentLoaded", init4);
|
|
902
|
+
} else {
|
|
903
|
+
init4();
|
|
904
|
+
}
|
|
905
|
+
var surf_default = Surf;
|
|
906
|
+
if (typeof window !== "undefined") {
|
|
907
|
+
window.Surf = Surf;
|
|
908
|
+
}
|
|
909
|
+
return __toCommonJS(surf_exports);
|
|
910
|
+
})();
|
|
911
|
+
window.Surf=SurfModule.default
|