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
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drag & Drop Plugin for Surf
|
|
3
|
+
*
|
|
4
|
+
* Enables declarative drag and drop functionality.
|
|
5
|
+
* Usage:
|
|
6
|
+
* <div d-drag-zone>
|
|
7
|
+
* <div d-draggable="true" d-drag-data='{"id": 1}'>Item</div>
|
|
8
|
+
* </div>
|
|
9
|
+
*/
|
|
10
|
+
const DragAndDrop = {
|
|
11
|
+
name: 'drag-and-drop',
|
|
12
|
+
|
|
13
|
+
install(Surf, options = {}) {
|
|
14
|
+
let draggedItem = null;
|
|
15
|
+
|
|
16
|
+
// Helper to initialize draggable elements
|
|
17
|
+
const initDraggables = (root = document) => {
|
|
18
|
+
root.querySelectorAll('[d-draggable="true"]').forEach(el => {
|
|
19
|
+
el.setAttribute('draggable', 'true');
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Initialize existing elements
|
|
24
|
+
initDraggables();
|
|
25
|
+
|
|
26
|
+
// Observe for new elements
|
|
27
|
+
const observer = new MutationObserver((mutations) => {
|
|
28
|
+
mutations.forEach((mutation) => {
|
|
29
|
+
mutation.addedNodes.forEach((node) => {
|
|
30
|
+
if (node.nodeType === 1) { // ELEMENT_NODE
|
|
31
|
+
if (node.hasAttribute('d-draggable')) {
|
|
32
|
+
node.setAttribute('draggable', 'true');
|
|
33
|
+
}
|
|
34
|
+
// Check children
|
|
35
|
+
initDraggables(node);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
42
|
+
|
|
43
|
+
// Global drag start handler
|
|
44
|
+
document.addEventListener('dragstart', (e) => {
|
|
45
|
+
// Allow preventing drag on specific children (like buttons, inputs, etc)
|
|
46
|
+
if (e.target.closest('[d-no-drag]')) {
|
|
47
|
+
e.preventDefault();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!e.target.closest('[d-draggable="true"]')) return;
|
|
52
|
+
|
|
53
|
+
const target = e.target.closest('[d-draggable="true"]');
|
|
54
|
+
draggedItem = target;
|
|
55
|
+
|
|
56
|
+
// Get data
|
|
57
|
+
const data = target.getAttribute('d-drag-data');
|
|
58
|
+
if (data) {
|
|
59
|
+
e.dataTransfer.setData('text/plain', data);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
63
|
+
target.classList.add('dragging');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Global drag end handler
|
|
67
|
+
document.addEventListener('dragend', (e) => {
|
|
68
|
+
if (draggedItem) {
|
|
69
|
+
draggedItem.classList.remove('dragging');
|
|
70
|
+
draggedItem = null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Cleanup drag-over classes
|
|
74
|
+
document.querySelectorAll('.drag-over').forEach(el => {
|
|
75
|
+
el.classList.remove('drag-over');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Global drag over handler
|
|
80
|
+
document.addEventListener('dragover', (e) => {
|
|
81
|
+
const dropZone = e.target.closest('[d-drop-zone]');
|
|
82
|
+
if (!dropZone) return;
|
|
83
|
+
|
|
84
|
+
e.preventDefault(); // Allow drop
|
|
85
|
+
e.dataTransfer.dropEffect = 'move';
|
|
86
|
+
|
|
87
|
+
dropZone.classList.add('drag-over');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Global drag leave handler
|
|
91
|
+
document.addEventListener('dragleave', (e) => {
|
|
92
|
+
const dropZone = e.target.closest('[d-drop-zone]');
|
|
93
|
+
if (!dropZone) return;
|
|
94
|
+
|
|
95
|
+
// Only remove if we really left the element (not just entered a child)
|
|
96
|
+
const rect = dropZone.getBoundingClientRect();
|
|
97
|
+
const x = e.clientX;
|
|
98
|
+
const y = e.clientY;
|
|
99
|
+
|
|
100
|
+
if (x < rect.left || x >= rect.right || y < rect.top || y >= rect.bottom) {
|
|
101
|
+
dropZone.classList.remove('drag-over');
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Global drop handler
|
|
106
|
+
document.addEventListener('drop', async (e) => {
|
|
107
|
+
const dropZone = e.target.closest('[d-drop-zone]');
|
|
108
|
+
if (!dropZone) return;
|
|
109
|
+
|
|
110
|
+
e.preventDefault();
|
|
111
|
+
dropZone.classList.remove('drag-over');
|
|
112
|
+
|
|
113
|
+
const url = dropZone.getAttribute('d-drop-url');
|
|
114
|
+
if (!url) return;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const rawData = e.dataTransfer.getData('text/plain');
|
|
118
|
+
if (!rawData) return;
|
|
119
|
+
|
|
120
|
+
const data = JSON.parse(rawData);
|
|
121
|
+
|
|
122
|
+
// Add drop zone data if exists
|
|
123
|
+
const zoneDataRaw = dropZone.getAttribute('d-drop-data');
|
|
124
|
+
if (zoneDataRaw) {
|
|
125
|
+
const zoneData = JSON.parse(zoneDataRaw);
|
|
126
|
+
Object.assign(data, zoneData);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const formData = new URLSearchParams();
|
|
130
|
+
for (const [key, value] of Object.entries(data)) {
|
|
131
|
+
formData.append(key, value);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const response = await fetch(url, {
|
|
135
|
+
method: 'POST',
|
|
136
|
+
headers: {
|
|
137
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
138
|
+
},
|
|
139
|
+
body: formData
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (response.ok) {
|
|
143
|
+
const patchHtml = await response.text();
|
|
144
|
+
if (Surf && Surf.applyPatch) {
|
|
145
|
+
Surf.applyPatch(patchHtml);
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
console.error('[Surf DnD] Drop request failed', response.status);
|
|
149
|
+
}
|
|
150
|
+
} catch (err) {
|
|
151
|
+
console.error('[Surf DnD] Drop error', err);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
console.log('[Surf] Drag & Drop plugin installed');
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export default DragAndDrop;
|
package/src/pulse.js
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse Module
|
|
3
|
+
*
|
|
4
|
+
* A Pulse represents user intent that triggers a server interaction.
|
|
5
|
+
* Examples: navigation, form submission, refresh.
|
|
6
|
+
*
|
|
7
|
+
* Defined with: d-pulse, d-target
|
|
8
|
+
*
|
|
9
|
+
* Pulse types:
|
|
10
|
+
* - navigate: GET request, replace surface
|
|
11
|
+
* - commit: POST form data, apply patch
|
|
12
|
+
* - refresh: GET current URL, refresh surface
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as Surface from './surface.js';
|
|
16
|
+
import * as Patch from './patch.js';
|
|
17
|
+
import * as Echo from './echo.js';
|
|
18
|
+
|
|
19
|
+
const PULSE_ATTR = 'd-pulse';
|
|
20
|
+
const TARGET_ATTR = 'd-target';
|
|
21
|
+
const ACTION_ATTR = 'd-action';
|
|
22
|
+
|
|
23
|
+
// Event emitter for pulse lifecycle
|
|
24
|
+
const listeners = {
|
|
25
|
+
'before:pulse': [],
|
|
26
|
+
'after:patch': [],
|
|
27
|
+
'error:network': []
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Emit an event to listeners
|
|
32
|
+
* @param {string} event
|
|
33
|
+
* @param {Object} detail
|
|
34
|
+
*/
|
|
35
|
+
function emit(event, detail) {
|
|
36
|
+
if (listeners[event]) {
|
|
37
|
+
listeners[event].forEach(cb => {
|
|
38
|
+
try {
|
|
39
|
+
cb(detail);
|
|
40
|
+
} catch (e) {
|
|
41
|
+
console.error(`[Surf] Error in ${event} listener:`, e);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Register an event listener
|
|
49
|
+
* @param {string} event
|
|
50
|
+
* @param {function} callback
|
|
51
|
+
*/
|
|
52
|
+
export function on(event, callback) {
|
|
53
|
+
if (listeners[event]) {
|
|
54
|
+
listeners[event].push(callback);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Remove an event listener
|
|
60
|
+
* @param {string} event
|
|
61
|
+
* @param {function} callback
|
|
62
|
+
*/
|
|
63
|
+
export function off(event, callback) {
|
|
64
|
+
if (listeners[event]) {
|
|
65
|
+
const index = listeners[event].indexOf(callback);
|
|
66
|
+
if (index > -1) {
|
|
67
|
+
listeners[event].splice(index, 1);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Apply patches to surfaces with Echo preservation
|
|
74
|
+
* @param {Array<{target: string, content: string}>} patches
|
|
75
|
+
*/
|
|
76
|
+
function applyPatches(patches) {
|
|
77
|
+
patches.forEach(({ target, content }) => {
|
|
78
|
+
const surface = Surface.getBySelector(target) || document.querySelector(target);
|
|
79
|
+
|
|
80
|
+
if (!surface) {
|
|
81
|
+
console.warn(`[Surf] Target not found for patch: ${target}`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
Echo.withPreservation(surface, content, () => {
|
|
86
|
+
Surface.replace(target, content);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Send a pulse request to the server
|
|
93
|
+
* @param {string} url
|
|
94
|
+
* @param {Object} options
|
|
95
|
+
* @param {string} targetSelector
|
|
96
|
+
*/
|
|
97
|
+
async function sendPulse(url, options, targetSelector) {
|
|
98
|
+
emit('before:pulse', { url, options, target: targetSelector });
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const response = await fetch(url, {
|
|
102
|
+
...options,
|
|
103
|
+
headers: {
|
|
104
|
+
'Accept': 'text/html',
|
|
105
|
+
'X-Surf-Request': 'true',
|
|
106
|
+
...options.headers
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const html = await response.text();
|
|
115
|
+
|
|
116
|
+
// Check if response is a patch
|
|
117
|
+
if (Patch.isPatch(html)) {
|
|
118
|
+
const patches = Patch.parse(html);
|
|
119
|
+
applyPatches(patches);
|
|
120
|
+
} else {
|
|
121
|
+
// Treat as single surface replacement
|
|
122
|
+
if (targetSelector) {
|
|
123
|
+
const surface = Surface.getBySelector(targetSelector) || document.querySelector(targetSelector);
|
|
124
|
+
if (surface) {
|
|
125
|
+
// Check for swap mode (inner, append, prepend) from options or element
|
|
126
|
+
const swapMode = options.swap || surface.getAttribute('d-swap') || 'inner';
|
|
127
|
+
|
|
128
|
+
Echo.withPreservation(surface, html, () => {
|
|
129
|
+
if (swapMode === 'append') {
|
|
130
|
+
Surface.append(surface, html);
|
|
131
|
+
} else if (swapMode === 'prepend') {
|
|
132
|
+
Surface.prepend(surface, html);
|
|
133
|
+
} else {
|
|
134
|
+
Surface.replace(surface, html);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
emit('after:patch', { url, target: targetSelector });
|
|
142
|
+
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error('[Surf] Pulse error:', error);
|
|
145
|
+
emit('error:network', { url, error });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Handle navigate pulse (GET request)
|
|
151
|
+
* @param {string} url
|
|
152
|
+
* @param {string} targetSelector
|
|
153
|
+
* @param {Object} options
|
|
154
|
+
*/
|
|
155
|
+
export async function navigate(url, targetSelector, options = {}) {
|
|
156
|
+
const target = targetSelector || 'body';
|
|
157
|
+
await sendPulse(url, { method: 'GET', ...options }, target);
|
|
158
|
+
|
|
159
|
+
// Update browser history
|
|
160
|
+
if (target) {
|
|
161
|
+
history.pushState({ surf: true, url, target }, '', url);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Handle commit pulse (POST form data)
|
|
167
|
+
* @param {HTMLFormElement} form
|
|
168
|
+
* @param {string} targetSelector
|
|
169
|
+
*/
|
|
170
|
+
export async function commit(form, targetSelector) {
|
|
171
|
+
const method = form.method?.toUpperCase() || 'POST';
|
|
172
|
+
let url = form.action || window.location.href;
|
|
173
|
+
const formData = new FormData(form);
|
|
174
|
+
const swap = form.getAttribute('d-swap');
|
|
175
|
+
const options = swap ? { swap } : {};
|
|
176
|
+
const target = targetSelector || 'body';
|
|
177
|
+
|
|
178
|
+
// Convert FormData to URLSearchParams for standard form encoding
|
|
179
|
+
const params = new URLSearchParams();
|
|
180
|
+
formData.forEach((value, key) => params.append(key, value));
|
|
181
|
+
|
|
182
|
+
// GET requests cannot have a body - append as URL params instead
|
|
183
|
+
if (method === 'GET') {
|
|
184
|
+
const separator = url.includes('?') ? '&' : '?';
|
|
185
|
+
url = url + separator + params.toString();
|
|
186
|
+
|
|
187
|
+
await sendPulse(url, { method: 'GET', ...options }, target);
|
|
188
|
+
} else {
|
|
189
|
+
await sendPulse(url, {
|
|
190
|
+
method,
|
|
191
|
+
headers: {
|
|
192
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
193
|
+
},
|
|
194
|
+
body: params.toString(),
|
|
195
|
+
...options
|
|
196
|
+
}, target);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Handle refresh pulse (GET current content)
|
|
202
|
+
* @param {string} targetSelector
|
|
203
|
+
*/
|
|
204
|
+
export async function refresh(targetSelector) {
|
|
205
|
+
const url = window.location.href;
|
|
206
|
+
const target = targetSelector || 'body';
|
|
207
|
+
const surface = Surface.getBySelector(target) || document.querySelector(target);
|
|
208
|
+
const swap = surface?.getAttribute('d-swap'); // Refresh usually respects surface preference
|
|
209
|
+
await sendPulse(url, { method: 'GET', swap }, target);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Handle action pulse (POST data to server)
|
|
214
|
+
* @param {string} url - Action endpoint URL
|
|
215
|
+
* @param {Object} data - Data to send
|
|
216
|
+
* @param {string} targetSelector - Surface to update with response
|
|
217
|
+
* @param {Object} options
|
|
218
|
+
*/
|
|
219
|
+
export async function action(url, data = {}, targetSelector, options = {}) {
|
|
220
|
+
const target = targetSelector || 'body';
|
|
221
|
+
await sendPulse(url, {
|
|
222
|
+
method: 'POST',
|
|
223
|
+
headers: {
|
|
224
|
+
'Content-Type': 'application/json'
|
|
225
|
+
},
|
|
226
|
+
body: JSON.stringify(data),
|
|
227
|
+
...options
|
|
228
|
+
}, target);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Handle click events on pulse elements
|
|
233
|
+
* @param {Event} event
|
|
234
|
+
*/
|
|
235
|
+
async function handleClick(event) {
|
|
236
|
+
const element = event.target.closest(`[${PULSE_ATTR}]`);
|
|
237
|
+
if (!element) return;
|
|
238
|
+
|
|
239
|
+
const pulseType = element.getAttribute(PULSE_ATTR);
|
|
240
|
+
const targetSelector = element.getAttribute(TARGET_ATTR);
|
|
241
|
+
const actionUrl = element.getAttribute(ACTION_ATTR);
|
|
242
|
+
const swap = element.getAttribute('d-swap');
|
|
243
|
+
const options = swap ? { swap } : {};
|
|
244
|
+
|
|
245
|
+
// Handle anchor navigation
|
|
246
|
+
if (element.tagName === 'A' && pulseType === 'navigate') {
|
|
247
|
+
event.preventDefault();
|
|
248
|
+
const url = element.href;
|
|
249
|
+
navigate(url, targetSelector, options);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Handle refresh
|
|
254
|
+
if (pulseType === 'refresh') {
|
|
255
|
+
event.preventDefault();
|
|
256
|
+
refresh(targetSelector);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Handle action - send POST to d-action URL
|
|
261
|
+
if (pulseType === 'action' && actionUrl) {
|
|
262
|
+
event.preventDefault();
|
|
263
|
+
|
|
264
|
+
// Collect data from data-* attributes on the element
|
|
265
|
+
const data = {};
|
|
266
|
+
for (const attr of element.attributes) {
|
|
267
|
+
if (attr.name.startsWith('data-') && attr.name !== 'data-surf-ready') {
|
|
268
|
+
const key = attr.name.slice(5); // Remove 'data-' prefix
|
|
269
|
+
data[key] = attr.value;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Also include Cell state if element is inside a d-cell
|
|
274
|
+
const parentCell = element.closest('[d-cell]');
|
|
275
|
+
if (parentCell) {
|
|
276
|
+
const Cell = await import('./cell.js');
|
|
277
|
+
const cellState = Cell.default ? Cell.default.getState(parentCell) : Cell.getState(parentCell);
|
|
278
|
+
if (cellState) {
|
|
279
|
+
Object.assign(data, cellState);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
action(actionUrl, data, targetSelector, options);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Handle form submission on pulse elements
|
|
290
|
+
* @param {Event} event
|
|
291
|
+
*/
|
|
292
|
+
function handleSubmit(event) {
|
|
293
|
+
const form = event.target;
|
|
294
|
+
if (!form.hasAttribute(PULSE_ATTR)) return;
|
|
295
|
+
|
|
296
|
+
const pulseType = form.getAttribute(PULSE_ATTR);
|
|
297
|
+
const targetSelector = form.getAttribute(TARGET_ATTR);
|
|
298
|
+
|
|
299
|
+
if (pulseType === 'commit') {
|
|
300
|
+
event.preventDefault();
|
|
301
|
+
commit(form, targetSelector);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Handle browser back/forward navigation
|
|
307
|
+
* @param {PopStateEvent} event
|
|
308
|
+
*/
|
|
309
|
+
function handlePopState(event) {
|
|
310
|
+
if (event.state?.surf) {
|
|
311
|
+
sendPulse(event.state.url, { method: 'GET' }, event.state.target);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Initialize pulse handling
|
|
317
|
+
*/
|
|
318
|
+
export function init() {
|
|
319
|
+
// Delegate click events
|
|
320
|
+
document.addEventListener('click', handleClick);
|
|
321
|
+
|
|
322
|
+
// Delegate form submissions
|
|
323
|
+
document.addEventListener('submit', handleSubmit);
|
|
324
|
+
|
|
325
|
+
// Handle browser navigation
|
|
326
|
+
window.addEventListener('popstate', handlePopState);
|
|
327
|
+
|
|
328
|
+
// Handle browser navigation
|
|
329
|
+
window.addEventListener('popstate', handlePopState);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Programmatic navigation (Surf.go)
|
|
334
|
+
* @param {string} url
|
|
335
|
+
* @param {Object} options
|
|
336
|
+
*/
|
|
337
|
+
export async function go(url, options = {}) {
|
|
338
|
+
const target = options.target || 'body';
|
|
339
|
+
await navigate(url, target, options);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Programmatic form submission (Surf.submit)
|
|
344
|
+
* @param {Element} element - Element within a form
|
|
345
|
+
*/
|
|
346
|
+
export function submit(element) {
|
|
347
|
+
const form = element.tagName === 'FORM' ? element : element.closest('form');
|
|
348
|
+
if (form) {
|
|
349
|
+
if (form.requestSubmit) {
|
|
350
|
+
form.requestSubmit();
|
|
351
|
+
} else {
|
|
352
|
+
form.submit();
|
|
353
|
+
}
|
|
354
|
+
} else {
|
|
355
|
+
console.warn('[Surf] No form found to submit for element:', element);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export default {
|
|
360
|
+
on,
|
|
361
|
+
off,
|
|
362
|
+
navigate,
|
|
363
|
+
commit,
|
|
364
|
+
refresh,
|
|
365
|
+
action,
|
|
366
|
+
go,
|
|
367
|
+
submit,
|
|
368
|
+
init
|
|
369
|
+
};
|