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/src/surf.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SURF - HTML-first, server-driven UI framework
|
|
3
|
+
*
|
|
4
|
+
* Mental model: "Surface changes, Cell lives."
|
|
5
|
+
*
|
|
6
|
+
* The server is the source of truth.
|
|
7
|
+
* The client handles only temporary, local interactions.
|
|
8
|
+
* HTML is the primary data format.
|
|
9
|
+
* UI changes happen through HTML patches, not JSON APIs.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as Surface from './surface.js';
|
|
13
|
+
import * as Cell from './cell.js';
|
|
14
|
+
import * as Signal from './signal.js';
|
|
15
|
+
import * as Pulse from './pulse.js';
|
|
16
|
+
import * as Patch from './patch.js';
|
|
17
|
+
import * as Echo from './echo.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Global Surf object - the public API
|
|
21
|
+
*/
|
|
22
|
+
const Surf = {
|
|
23
|
+
/**
|
|
24
|
+
* Framework version
|
|
25
|
+
*/
|
|
26
|
+
version: '0.1.0',
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Navigate to a URL
|
|
30
|
+
* @param {string} url - The URL to navigate to
|
|
31
|
+
* @param {Object} options - Optional settings (target, swap, etc)
|
|
32
|
+
*/
|
|
33
|
+
go(url, options = {}) {
|
|
34
|
+
return Pulse.go(url, options);
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Refresh a surface's content from the server
|
|
39
|
+
* @param {string} selector - The surface selector to refresh
|
|
40
|
+
*/
|
|
41
|
+
refresh(selector) {
|
|
42
|
+
return Pulse.refresh(selector);
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Subscribe to framework events
|
|
47
|
+
* @param {string} event - Event name: 'before:pulse', 'after:patch', 'error:network'
|
|
48
|
+
* @param {function} callback - Event handler
|
|
49
|
+
*/
|
|
50
|
+
on(event, callback) {
|
|
51
|
+
Pulse.on(event, callback);
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Unsubscribe from framework events
|
|
56
|
+
* @param {string} event - Event name
|
|
57
|
+
* @param {function} callback - Event handler to remove
|
|
58
|
+
*/
|
|
59
|
+
off(event, callback) {
|
|
60
|
+
Pulse.off(event, callback);
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get cell state for an element
|
|
65
|
+
* @param {Element|string} cellOrSelector - Cell element or selector
|
|
66
|
+
* @returns {Object} The cell's current state
|
|
67
|
+
*/
|
|
68
|
+
getState(cellOrSelector) {
|
|
69
|
+
const cell = typeof cellOrSelector === 'string'
|
|
70
|
+
? document.querySelector(cellOrSelector)
|
|
71
|
+
: cellOrSelector;
|
|
72
|
+
return Cell.getState(cell);
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Set cell state for an element
|
|
77
|
+
* @param {Element|string} cellOrSelector - Cell element or selector
|
|
78
|
+
* @param {Object} state - State to merge
|
|
79
|
+
*/
|
|
80
|
+
setState(cellOrSelector, state) {
|
|
81
|
+
const cell = typeof cellOrSelector === 'string'
|
|
82
|
+
? document.querySelector(cellOrSelector)
|
|
83
|
+
: cellOrSelector;
|
|
84
|
+
Cell.setState(cell, state);
|
|
85
|
+
Signal.updateBindings(cell);
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Manually apply a patch response
|
|
90
|
+
* @param {string} patchHtml - The patch HTML string
|
|
91
|
+
*/
|
|
92
|
+
applyPatch(patchHtml) {
|
|
93
|
+
const patches = Patch.parse(patchHtml);
|
|
94
|
+
patches.forEach(({ target, content }) => {
|
|
95
|
+
const surface = Surface.getBySelector(target) || document.querySelector(target);
|
|
96
|
+
if (surface) {
|
|
97
|
+
Echo.withPreservation(surface, content, () => {
|
|
98
|
+
Surface.replace(target, content);
|
|
99
|
+
// Re-initialize signals and cells on the updated surface
|
|
100
|
+
Cell.initAll(surface);
|
|
101
|
+
Signal.initAll(surface);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Register a module for signal expressions
|
|
109
|
+
* @param {string} name - Module namespace
|
|
110
|
+
* @param {Object} module - Object with methods
|
|
111
|
+
*/
|
|
112
|
+
register(name, module) {
|
|
113
|
+
Signal.register(name, module);
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Install a plugin
|
|
118
|
+
* @param {Object} plugin - Plugin object with install method
|
|
119
|
+
* @param {Object} options - Plugin options
|
|
120
|
+
*/
|
|
121
|
+
use(plugin, options = {}) {
|
|
122
|
+
if (plugin && typeof plugin.install === 'function') {
|
|
123
|
+
plugin.install(this, options);
|
|
124
|
+
}
|
|
125
|
+
return this;
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
// Expose modules for advanced usage
|
|
133
|
+
_modules: {
|
|
134
|
+
Surface,
|
|
135
|
+
Cell,
|
|
136
|
+
Signal,
|
|
137
|
+
Pulse,
|
|
138
|
+
Patch,
|
|
139
|
+
Echo
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Initialize SURF when the DOM is ready
|
|
145
|
+
*/
|
|
146
|
+
function init() {
|
|
147
|
+
// Initialize surfaces
|
|
148
|
+
Surface.init();
|
|
149
|
+
|
|
150
|
+
// Initialize cells
|
|
151
|
+
Cell.initAll();
|
|
152
|
+
|
|
153
|
+
// Initialize signals (reactive bindings)
|
|
154
|
+
Signal.initAll();
|
|
155
|
+
|
|
156
|
+
// Initialize pulse (event interception)
|
|
157
|
+
Pulse.init();
|
|
158
|
+
|
|
159
|
+
// Register core modules for signals
|
|
160
|
+
Signal.register('Pulse', Pulse);
|
|
161
|
+
|
|
162
|
+
console.log(`[Surf] Initialized v${Surf.version}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Auto-initialize on DOMContentLoaded
|
|
166
|
+
if (document.readyState === 'loading') {
|
|
167
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
168
|
+
} else {
|
|
169
|
+
init();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Export for module usage
|
|
173
|
+
export { Surface, Cell, Signal, Pulse, Patch, Echo };
|
|
174
|
+
export default Surf;
|
|
175
|
+
|
|
176
|
+
// Attach to window for script tag usage
|
|
177
|
+
if (typeof window !== 'undefined') {
|
|
178
|
+
window.Surf = Surf;
|
|
179
|
+
}
|
package/src/surface.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surface Module
|
|
3
|
+
*
|
|
4
|
+
* A Surface is a DOM region that can be replaced by server responses.
|
|
5
|
+
* Defined with: d-surface
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const SURFACE_ATTR = 'd-surface';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Find all surface elements in the document
|
|
12
|
+
* @returns {NodeListOf<Element>}
|
|
13
|
+
*/
|
|
14
|
+
export function findAll() {
|
|
15
|
+
return document.querySelectorAll(`[${SURFACE_ATTR}]`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get a surface element by its ID
|
|
20
|
+
* @param {string} id - The surface ID (without #)
|
|
21
|
+
* @returns {Element|null}
|
|
22
|
+
*/
|
|
23
|
+
export function getById(id) {
|
|
24
|
+
const cleanId = id.startsWith('#') ? id.slice(1) : id;
|
|
25
|
+
const element = document.getElementById(cleanId);
|
|
26
|
+
|
|
27
|
+
if (element && element.hasAttribute(SURFACE_ATTR)) {
|
|
28
|
+
return element;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get a surface element by selector
|
|
36
|
+
* @param {string} selector - CSS selector
|
|
37
|
+
* @returns {Element|null}
|
|
38
|
+
*/
|
|
39
|
+
export function getBySelector(selector) {
|
|
40
|
+
const element = document.querySelector(selector);
|
|
41
|
+
|
|
42
|
+
if (element && element.hasAttribute(SURFACE_ATTR)) {
|
|
43
|
+
return element;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Replace surface content with new HTML
|
|
51
|
+
* @param {string|Element} selectorOrElement - Target surface selector or element
|
|
52
|
+
* @param {string} html - New HTML content
|
|
53
|
+
* @returns {Element|null} - The updated surface element
|
|
54
|
+
*/
|
|
55
|
+
export function replace(selectorOrElement, html) {
|
|
56
|
+
// Handle both selector strings and element references
|
|
57
|
+
let surface;
|
|
58
|
+
if (typeof selectorOrElement === 'string') {
|
|
59
|
+
surface = getBySelector(selectorOrElement) || document.querySelector(selectorOrElement);
|
|
60
|
+
} else {
|
|
61
|
+
surface = selectorOrElement;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!surface) {
|
|
65
|
+
console.warn(`[Surf] Surface not found: ${selectorOrElement}`);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Create a template to parse the HTML
|
|
70
|
+
const template = document.createElement('template');
|
|
71
|
+
template.innerHTML = html.trim();
|
|
72
|
+
|
|
73
|
+
// Replace inner content, preserving the surface element itself
|
|
74
|
+
surface.innerHTML = '';
|
|
75
|
+
|
|
76
|
+
// Append all children from the template
|
|
77
|
+
while (template.content.firstChild) {
|
|
78
|
+
surface.appendChild(template.content.firstChild);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return surface;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Append content to surface
|
|
86
|
+
* @param {string|Element} selectorOrElement
|
|
87
|
+
* @param {string} html
|
|
88
|
+
* @returns {Element|null}
|
|
89
|
+
*/
|
|
90
|
+
export function append(selectorOrElement, html) {
|
|
91
|
+
return inject(selectorOrElement, html, 'beforeend');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Prepend content to surface
|
|
96
|
+
* @param {string|Element} selectorOrElement
|
|
97
|
+
* @param {string} html
|
|
98
|
+
* @returns {Element|null}
|
|
99
|
+
*/
|
|
100
|
+
export function prepend(selectorOrElement, html) {
|
|
101
|
+
return inject(selectorOrElement, html, 'afterbegin');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Inject content into surface at position
|
|
106
|
+
* @param {string|Element} selectorOrElement
|
|
107
|
+
* @param {string} html
|
|
108
|
+
* @param {string} position - 'beforeend' or 'afterbegin'
|
|
109
|
+
* @returns {Element|null}
|
|
110
|
+
*/
|
|
111
|
+
function inject(selectorOrElement, html, position) {
|
|
112
|
+
let surface;
|
|
113
|
+
if (typeof selectorOrElement === 'string') {
|
|
114
|
+
surface = getBySelector(selectorOrElement) || document.querySelector(selectorOrElement);
|
|
115
|
+
} else {
|
|
116
|
+
surface = selectorOrElement;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!surface) {
|
|
120
|
+
console.warn(`[Surf] Surface not found: ${selectorOrElement}`);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const template = document.createElement('template');
|
|
125
|
+
template.innerHTML = html.trim();
|
|
126
|
+
|
|
127
|
+
// Use DocumentFragment for efficient insertion
|
|
128
|
+
const fragment = document.createDocumentFragment();
|
|
129
|
+
while (template.content.firstChild) {
|
|
130
|
+
fragment.appendChild(template.content.firstChild);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (position === 'beforeend') {
|
|
134
|
+
surface.appendChild(fragment);
|
|
135
|
+
} else {
|
|
136
|
+
surface.insertBefore(fragment, surface.firstChild);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return surface;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Initialize surfaces - mark them as ready
|
|
144
|
+
*/
|
|
145
|
+
export function init() {
|
|
146
|
+
const surfaces = findAll();
|
|
147
|
+
surfaces.forEach(surface => {
|
|
148
|
+
surface.setAttribute('data-surf-ready', 'true');
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export default {
|
|
153
|
+
findAll,
|
|
154
|
+
getById,
|
|
155
|
+
getBySelector,
|
|
156
|
+
replace,
|
|
157
|
+
append,
|
|
158
|
+
prepend,
|
|
159
|
+
init
|
|
160
|
+
};
|