@vortexm/vjt 0.1.15 → 0.1.17
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/dist/index.js +386 -29
- package/dist/lib/network.d.ts +5 -2
- package/dist/lib/types.d.ts +5 -0
- package/package.json +1 -1
- package/vjt-styles.css +6 -0
package/dist/index.js
CHANGED
|
@@ -3865,7 +3865,7 @@ var NetworkRuntime = class {
|
|
|
3865
3865
|
this.rerenderRoot = options.rerenderRoot;
|
|
3866
3866
|
}
|
|
3867
3867
|
connectSseStreams() {
|
|
3868
|
-
if (typeof window === "undefined" ||
|
|
3868
|
+
if (typeof window === "undefined" || this.eventSources.length > 0) {
|
|
3869
3869
|
return;
|
|
3870
3870
|
}
|
|
3871
3871
|
for (const config of this.sseConfigs) {
|
|
@@ -3877,26 +3877,10 @@ var NetworkRuntime = class {
|
|
|
3877
3877
|
logRuntimeError("sseConfig", new Error("Blocked unsafe SSE url"), { url: config.url });
|
|
3878
3878
|
continue;
|
|
3879
3879
|
}
|
|
3880
|
-
const
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
if (!eventDefinition?.name) {
|
|
3884
|
-
continue;
|
|
3885
|
-
}
|
|
3886
|
-
source.addEventListener(eventDefinition.name, (event) => {
|
|
3887
|
-
void this.handleSseEvent(eventDefinition, event).catch((error) => {
|
|
3888
|
-
logRuntimeError("handleSseEvent", error, {
|
|
3889
|
-
eventName: eventDefinition.name,
|
|
3890
|
-
url: safeUrl
|
|
3891
|
-
});
|
|
3892
|
-
});
|
|
3893
|
-
});
|
|
3880
|
+
const connection = this.createSseConnection(config, safeUrl);
|
|
3881
|
+
if (connection) {
|
|
3882
|
+
this.eventSources.push(connection);
|
|
3894
3883
|
}
|
|
3895
|
-
source.onerror = (event) => {
|
|
3896
|
-
logRuntimeError("sseConnection", event, {
|
|
3897
|
-
url: config.url
|
|
3898
|
-
});
|
|
3899
|
-
};
|
|
3900
3884
|
}
|
|
3901
3885
|
}
|
|
3902
3886
|
async executeRequest(requestName, currentValue) {
|
|
@@ -4099,21 +4083,309 @@ var NetworkRuntime = class {
|
|
|
4099
4083
|
return this.stringifyPrimitive(value);
|
|
4100
4084
|
}
|
|
4101
4085
|
}
|
|
4102
|
-
|
|
4086
|
+
createSseConnection(config, safeUrl) {
|
|
4087
|
+
if (typeof fetch === "function" && typeof AbortController !== "undefined" && typeof TextDecoder !== "undefined" && typeof ReadableStream !== "undefined") {
|
|
4088
|
+
return this.createFetchSseConnection(config, safeUrl);
|
|
4089
|
+
}
|
|
4090
|
+
if (typeof EventSource !== "undefined") {
|
|
4091
|
+
return this.createNativeSseConnection(config, safeUrl);
|
|
4092
|
+
}
|
|
4093
|
+
logRuntimeError("sseConnection", new Error("SSE is not supported in this browser"), {
|
|
4094
|
+
url: safeUrl
|
|
4095
|
+
});
|
|
4096
|
+
return null;
|
|
4097
|
+
}
|
|
4098
|
+
createNativeSseConnection(config, safeUrl) {
|
|
4099
|
+
const source = new EventSource(safeUrl);
|
|
4100
|
+
source.onopen = () => {
|
|
4101
|
+
logRuntimeDebug(this.debugLogging, "sse-open", {
|
|
4102
|
+
url: safeUrl,
|
|
4103
|
+
transport: "event-source"
|
|
4104
|
+
});
|
|
4105
|
+
};
|
|
4106
|
+
source.onmessage = (event) => {
|
|
4107
|
+
const payload = typeof event.data === "string" ? event.data : "";
|
|
4108
|
+
logRuntimeDebug(this.debugLogging, "sse-message", {
|
|
4109
|
+
url: safeUrl,
|
|
4110
|
+
transport: "event-source",
|
|
4111
|
+
eventName: "message",
|
|
4112
|
+
payloadLength: payload.length
|
|
4113
|
+
});
|
|
4114
|
+
};
|
|
4115
|
+
for (const eventDefinition of config.events ?? []) {
|
|
4116
|
+
if (!eventDefinition?.name) {
|
|
4117
|
+
continue;
|
|
4118
|
+
}
|
|
4119
|
+
source.addEventListener(eventDefinition.name, (event) => {
|
|
4120
|
+
void this.handleSseEvent(eventDefinition, event.data ?? "", safeUrl).catch((error) => {
|
|
4121
|
+
logRuntimeError("handleSseEvent", error, {
|
|
4122
|
+
eventName: eventDefinition.name,
|
|
4123
|
+
url: safeUrl
|
|
4124
|
+
});
|
|
4125
|
+
});
|
|
4126
|
+
});
|
|
4127
|
+
}
|
|
4128
|
+
source.onerror = (event) => {
|
|
4129
|
+
logRuntimeError("sseConnection", event, {
|
|
4130
|
+
url: config.url,
|
|
4131
|
+
transport: "event-source"
|
|
4132
|
+
});
|
|
4133
|
+
};
|
|
4134
|
+
logRuntimeDebug(this.debugLogging, "sse-connect", {
|
|
4135
|
+
url: safeUrl,
|
|
4136
|
+
transport: "event-source"
|
|
4137
|
+
});
|
|
4138
|
+
return {
|
|
4139
|
+
close: () => {
|
|
4140
|
+
source.close();
|
|
4141
|
+
}
|
|
4142
|
+
};
|
|
4143
|
+
}
|
|
4144
|
+
createFetchSseConnection(config, safeUrl) {
|
|
4145
|
+
let closed = false;
|
|
4146
|
+
let reconnectDelayMs = 2e3;
|
|
4147
|
+
let reconnectTimeoutId = null;
|
|
4148
|
+
let activeAbortController = null;
|
|
4149
|
+
let lastEventId = "";
|
|
4150
|
+
let connectionAttempt = 0;
|
|
4151
|
+
const clearReconnectTimeout = () => {
|
|
4152
|
+
if (reconnectTimeoutId !== null && typeof window !== "undefined") {
|
|
4153
|
+
window.clearTimeout(reconnectTimeoutId);
|
|
4154
|
+
}
|
|
4155
|
+
reconnectTimeoutId = null;
|
|
4156
|
+
};
|
|
4157
|
+
const scheduleReconnect = (reason) => {
|
|
4158
|
+
if (closed || typeof window === "undefined") {
|
|
4159
|
+
return;
|
|
4160
|
+
}
|
|
4161
|
+
logRuntimeDebug(this.debugLogging, "sse-reconnect-scheduled", {
|
|
4162
|
+
url: safeUrl,
|
|
4163
|
+
transport: "fetch",
|
|
4164
|
+
delayMs: reconnectDelayMs,
|
|
4165
|
+
reason,
|
|
4166
|
+
lastEventId
|
|
4167
|
+
});
|
|
4168
|
+
clearReconnectTimeout();
|
|
4169
|
+
reconnectTimeoutId = window.setTimeout(() => {
|
|
4170
|
+
reconnectTimeoutId = null;
|
|
4171
|
+
void connectLoop().catch((error) => {
|
|
4172
|
+
logRuntimeError("sseConnection", error, {
|
|
4173
|
+
url: safeUrl,
|
|
4174
|
+
transport: "fetch"
|
|
4175
|
+
});
|
|
4176
|
+
});
|
|
4177
|
+
}, reconnectDelayMs);
|
|
4178
|
+
};
|
|
4179
|
+
const flushSseChunk = (eventName, dataLines) => {
|
|
4180
|
+
if (!dataLines.length) {
|
|
4181
|
+
return;
|
|
4182
|
+
}
|
|
4183
|
+
const payload = dataLines.join("\n");
|
|
4184
|
+
const matchingDefinitions = (config.events ?? []).filter((definition) => definition?.name === eventName);
|
|
4185
|
+
logRuntimeDebug(this.debugLogging, "sse-dispatch", {
|
|
4186
|
+
url: safeUrl,
|
|
4187
|
+
transport: "fetch",
|
|
4188
|
+
eventName,
|
|
4189
|
+
payloadLength: payload.length,
|
|
4190
|
+
matchedHandlers: matchingDefinitions.length,
|
|
4191
|
+
lastEventId
|
|
4192
|
+
});
|
|
4193
|
+
if (!matchingDefinitions.length) {
|
|
4194
|
+
return;
|
|
4195
|
+
}
|
|
4196
|
+
for (const eventDefinition of matchingDefinitions) {
|
|
4197
|
+
void this.handleSseEvent(eventDefinition, payload, safeUrl).catch((error) => {
|
|
4198
|
+
logRuntimeError("handleSseEvent", error, {
|
|
4199
|
+
eventName,
|
|
4200
|
+
url: safeUrl
|
|
4201
|
+
});
|
|
4202
|
+
});
|
|
4203
|
+
}
|
|
4204
|
+
};
|
|
4205
|
+
const processSseText = (chunk) => {
|
|
4206
|
+
let eventName = "message";
|
|
4207
|
+
let dataLines = [];
|
|
4208
|
+
const normalized = chunk.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
|
|
4209
|
+
const lines = normalized.split("\n");
|
|
4210
|
+
for (const line of lines) {
|
|
4211
|
+
if (!line) {
|
|
4212
|
+
flushSseChunk(eventName, dataLines);
|
|
4213
|
+
eventName = "message";
|
|
4214
|
+
dataLines = [];
|
|
4215
|
+
continue;
|
|
4216
|
+
}
|
|
4217
|
+
if (line.startsWith(":")) {
|
|
4218
|
+
continue;
|
|
4219
|
+
}
|
|
4220
|
+
const separatorIndex = line.indexOf(":");
|
|
4221
|
+
const field = separatorIndex >= 0 ? line.slice(0, separatorIndex) : line;
|
|
4222
|
+
let value = separatorIndex >= 0 ? line.slice(separatorIndex + 1) : "";
|
|
4223
|
+
if (value.startsWith(" ")) {
|
|
4224
|
+
value = value.slice(1);
|
|
4225
|
+
}
|
|
4226
|
+
switch (field) {
|
|
4227
|
+
case "event":
|
|
4228
|
+
eventName = value || "message";
|
|
4229
|
+
break;
|
|
4230
|
+
case "data":
|
|
4231
|
+
dataLines.push(value);
|
|
4232
|
+
break;
|
|
4233
|
+
case "id":
|
|
4234
|
+
if (!value.includes("\0")) {
|
|
4235
|
+
lastEventId = value;
|
|
4236
|
+
}
|
|
4237
|
+
break;
|
|
4238
|
+
case "retry": {
|
|
4239
|
+
const parsedDelay = Number.parseInt(value, 10);
|
|
4240
|
+
if (Number.isFinite(parsedDelay) && parsedDelay >= 0) {
|
|
4241
|
+
reconnectDelayMs = parsedDelay;
|
|
4242
|
+
}
|
|
4243
|
+
break;
|
|
4244
|
+
}
|
|
4245
|
+
default:
|
|
4246
|
+
break;
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4249
|
+
flushSseChunk(eventName, dataLines);
|
|
4250
|
+
};
|
|
4251
|
+
const findEventBoundary = (value) => value.search(/\r?\n\r?\n/);
|
|
4252
|
+
const connectLoop = async () => {
|
|
4253
|
+
if (closed) {
|
|
4254
|
+
return;
|
|
4255
|
+
}
|
|
4256
|
+
connectionAttempt += 1;
|
|
4257
|
+
activeAbortController = new AbortController();
|
|
4258
|
+
let reconnectReason = "stream-ended";
|
|
4259
|
+
try {
|
|
4260
|
+
logRuntimeDebug(this.debugLogging, "sse-connect-attempt", {
|
|
4261
|
+
url: safeUrl,
|
|
4262
|
+
transport: "fetch",
|
|
4263
|
+
attempt: connectionAttempt,
|
|
4264
|
+
lastEventId
|
|
4265
|
+
});
|
|
4266
|
+
const headers = {
|
|
4267
|
+
Accept: "text/event-stream"
|
|
4268
|
+
};
|
|
4269
|
+
if (lastEventId) {
|
|
4270
|
+
headers["Last-Event-ID"] = lastEventId;
|
|
4271
|
+
}
|
|
4272
|
+
const response = await fetch(safeUrl, {
|
|
4273
|
+
method: "GET",
|
|
4274
|
+
headers,
|
|
4275
|
+
cache: "no-store",
|
|
4276
|
+
credentials: "same-origin",
|
|
4277
|
+
signal: activeAbortController.signal
|
|
4278
|
+
});
|
|
4279
|
+
if (!response.ok) {
|
|
4280
|
+
throw new Error(`SSE connection failed: ${response.status} ${response.statusText}`);
|
|
4281
|
+
}
|
|
4282
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
4283
|
+
if (!contentType.toLowerCase().includes("text/event-stream")) {
|
|
4284
|
+
const previewText = await response.text().catch(() => "");
|
|
4285
|
+
const normalizedPreview = previewText.replaceAll(/\s+/g, " ").trim().slice(0, 240);
|
|
4286
|
+
const error = new Error(`SSE response has unexpected content-type: ${contentType || "<empty>"}`);
|
|
4287
|
+
error.sseDetails = {
|
|
4288
|
+
status: response.status,
|
|
4289
|
+
statusText: response.statusText,
|
|
4290
|
+
responseUrl: response.url,
|
|
4291
|
+
redirected: response.redirected,
|
|
4292
|
+
contentType,
|
|
4293
|
+
bodyPreview: normalizedPreview
|
|
4294
|
+
};
|
|
4295
|
+
throw error;
|
|
4296
|
+
}
|
|
4297
|
+
logRuntimeDebug(this.debugLogging, "sse-open", {
|
|
4298
|
+
url: safeUrl,
|
|
4299
|
+
transport: "fetch",
|
|
4300
|
+
status: response.status,
|
|
4301
|
+
contentType,
|
|
4302
|
+
attempt: connectionAttempt,
|
|
4303
|
+
lastEventId,
|
|
4304
|
+
requestUsesLastEventIdHeader: Boolean(lastEventId)
|
|
4305
|
+
});
|
|
4306
|
+
if (!response.body || typeof response.body.getReader !== "function") {
|
|
4307
|
+
throw new Error("SSE streaming is not available in fetch response body");
|
|
4308
|
+
}
|
|
4309
|
+
const reader = response.body.getReader();
|
|
4310
|
+
const decoder = new TextDecoder();
|
|
4311
|
+
let buffer = "";
|
|
4312
|
+
let receivedChunkCount = 0;
|
|
4313
|
+
let receivedByteCount = 0;
|
|
4314
|
+
while (!closed) {
|
|
4315
|
+
const { done, value } = await reader.read();
|
|
4316
|
+
if (done) {
|
|
4317
|
+
break;
|
|
4318
|
+
}
|
|
4319
|
+
receivedChunkCount += 1;
|
|
4320
|
+
receivedByteCount += value?.byteLength ?? 0;
|
|
4321
|
+
buffer += decoder.decode(value, { stream: true });
|
|
4322
|
+
logRuntimeDebug(this.debugLogging, "sse-chunk", {
|
|
4323
|
+
url: safeUrl,
|
|
4324
|
+
transport: "fetch",
|
|
4325
|
+
chunkIndex: receivedChunkCount,
|
|
4326
|
+
chunkBytes: value?.byteLength ?? 0,
|
|
4327
|
+
totalBytes: receivedByteCount,
|
|
4328
|
+
bufferedChars: buffer.length
|
|
4329
|
+
});
|
|
4330
|
+
let boundaryIndex = findEventBoundary(buffer);
|
|
4331
|
+
while (boundaryIndex >= 0) {
|
|
4332
|
+
const rawChunk = buffer.slice(0, boundaryIndex);
|
|
4333
|
+
processSseText(rawChunk);
|
|
4334
|
+
buffer = buffer.slice(boundaryIndex).replace(/^\r?\n\r?\n/, "");
|
|
4335
|
+
boundaryIndex = findEventBoundary(buffer);
|
|
4336
|
+
}
|
|
4337
|
+
}
|
|
4338
|
+
buffer += decoder.decode();
|
|
4339
|
+
if (buffer.trim()) {
|
|
4340
|
+
processSseText(buffer);
|
|
4341
|
+
}
|
|
4342
|
+
} catch (error) {
|
|
4343
|
+
if (closed || error instanceof DOMException && error.name === "AbortError") {
|
|
4344
|
+
return;
|
|
4345
|
+
}
|
|
4346
|
+
reconnectReason = error instanceof Error ? error.message : "stream-error";
|
|
4347
|
+
logRuntimeError("sseConnection", error, {
|
|
4348
|
+
url: safeUrl,
|
|
4349
|
+
transport: "fetch",
|
|
4350
|
+
...typeof error === "object" && error !== null && "sseDetails" in error ? { response: error.sseDetails } : {}
|
|
4351
|
+
});
|
|
4352
|
+
} finally {
|
|
4353
|
+
activeAbortController = null;
|
|
4354
|
+
}
|
|
4355
|
+
scheduleReconnect(reconnectReason);
|
|
4356
|
+
};
|
|
4357
|
+
const connection = {
|
|
4358
|
+
close: () => {
|
|
4359
|
+
closed = true;
|
|
4360
|
+
clearReconnectTimeout();
|
|
4361
|
+
activeAbortController?.abort();
|
|
4362
|
+
activeAbortController = null;
|
|
4363
|
+
}
|
|
4364
|
+
};
|
|
4365
|
+
void connectLoop().catch((error) => {
|
|
4366
|
+
logRuntimeError("sseConnection", error, {
|
|
4367
|
+
url: safeUrl,
|
|
4368
|
+
transport: "fetch"
|
|
4369
|
+
});
|
|
4370
|
+
});
|
|
4371
|
+
return connection;
|
|
4372
|
+
}
|
|
4373
|
+
async handleSseEvent(eventDefinition, rawData, url) {
|
|
4103
4374
|
let parsedMessage = {};
|
|
4104
4375
|
try {
|
|
4105
|
-
parsedMessage =
|
|
4376
|
+
parsedMessage = rawData ? JSON.parse(rawData) : {};
|
|
4106
4377
|
} catch {
|
|
4107
|
-
parsedMessage = { value:
|
|
4378
|
+
parsedMessage = { value: rawData ?? "" };
|
|
4108
4379
|
}
|
|
4109
4380
|
const message = eventDefinition.message ? sanitizeSchemaValue(eventDefinition.message, parsedMessage, `sse.${eventDefinition.name}`) : (() => {
|
|
4110
4381
|
throw new Error(`SSE event ${eventDefinition.name} must define message schema`);
|
|
4111
4382
|
})();
|
|
4112
4383
|
logRuntimeDebug(this.debugLogging, "sse", {
|
|
4113
4384
|
eventName: eventDefinition.name,
|
|
4114
|
-
raw:
|
|
4385
|
+
raw: rawData,
|
|
4115
4386
|
parsedMessage,
|
|
4116
|
-
message
|
|
4387
|
+
message,
|
|
4388
|
+
url
|
|
4117
4389
|
});
|
|
4118
4390
|
if (!eventDefinition.onEvent?.length) {
|
|
4119
4391
|
return;
|
|
@@ -4479,6 +4751,10 @@ var ReferenceRuntime = class {
|
|
|
4479
4751
|
return readPath(state, ["value"]);
|
|
4480
4752
|
case "isHidden":
|
|
4481
4753
|
return state.isHidden ?? true;
|
|
4754
|
+
case "isScrolledToTop":
|
|
4755
|
+
return state.isScrolledToTop ?? true;
|
|
4756
|
+
case "isScrolledToBottom":
|
|
4757
|
+
return state.isScrolledToBottom ?? true;
|
|
4482
4758
|
case "url":
|
|
4483
4759
|
return resolveInlineI18nValue(this.host, state.url ?? "");
|
|
4484
4760
|
case "base64":
|
|
@@ -7720,6 +7996,19 @@ function isGeneratedElementId(value) {
|
|
|
7720
7996
|
function isStepperEdit(node) {
|
|
7721
7997
|
return node.type === "number" || node.type === "float";
|
|
7722
7998
|
}
|
|
7999
|
+
function isScrollableWidgetNode(node) {
|
|
8000
|
+
switch (node.widget) {
|
|
8001
|
+
case "list":
|
|
8002
|
+
case "grid-view":
|
|
8003
|
+
case "listbox":
|
|
8004
|
+
case "combobox":
|
|
8005
|
+
return true;
|
|
8006
|
+
case "container-layout":
|
|
8007
|
+
return normalizeBoolean(node.verticallyScrollable, false);
|
|
8008
|
+
default:
|
|
8009
|
+
return false;
|
|
8010
|
+
}
|
|
8011
|
+
}
|
|
7723
8012
|
function getDateFormat(node) {
|
|
7724
8013
|
return node.format === "yyyy-MM-dd" ? "yyyy-MM-dd" : DEFAULT_DATE_FORMAT;
|
|
7725
8014
|
}
|
|
@@ -8056,6 +8345,7 @@ var RuntimeRenderer = class {
|
|
|
8056
8345
|
this.attachInputBehavior(this.root);
|
|
8057
8346
|
this.attachWidgetEvents(this.root);
|
|
8058
8347
|
restoreElementState(this.root, preservedState, this.stateByKey);
|
|
8348
|
+
this.syncAllScrollStateFlags();
|
|
8059
8349
|
this.applyPendingScrollAction();
|
|
8060
8350
|
this.applyPendingCursorAction();
|
|
8061
8351
|
this.activeContextMenu = adjustContextMenuPosition(this.root, this.activeContextMenu);
|
|
@@ -8674,7 +8964,9 @@ var RuntimeRenderer = class {
|
|
|
8674
8964
|
enabled: normalizeBoolean(node.enabled, true),
|
|
8675
8965
|
definitionSignature: buildDefinitionSignature(node.elements ?? []),
|
|
8676
8966
|
elements: deepClone(node.elements ?? []),
|
|
8677
|
-
selected: toFiniteNumber3(node.item) ?? toFiniteNumber3(node.selected) ?? -1
|
|
8967
|
+
selected: toFiniteNumber3(node.item) ?? toFiniteNumber3(node.selected) ?? -1,
|
|
8968
|
+
isScrolledToTop: true,
|
|
8969
|
+
isScrolledToBottom: true
|
|
8678
8970
|
};
|
|
8679
8971
|
break;
|
|
8680
8972
|
case "listbox":
|
|
@@ -8686,7 +8978,9 @@ var RuntimeRenderer = class {
|
|
|
8686
8978
|
enabled: normalizeBoolean(node.enabled, true),
|
|
8687
8979
|
definitionSignature: buildDefinitionSignature(node.elements ?? []),
|
|
8688
8980
|
listboxElements: deepClone(node.elements ?? []),
|
|
8689
|
-
selected: toFiniteNumber3(node.selected) ?? 0
|
|
8981
|
+
selected: toFiniteNumber3(node.selected) ?? 0,
|
|
8982
|
+
isScrolledToTop: true,
|
|
8983
|
+
isScrolledToBottom: true
|
|
8690
8984
|
};
|
|
8691
8985
|
break;
|
|
8692
8986
|
case "combobox":
|
|
@@ -8698,7 +8992,9 @@ var RuntimeRenderer = class {
|
|
|
8698
8992
|
enabled: normalizeBoolean(node.enabled, true),
|
|
8699
8993
|
definitionSignature: buildDefinitionSignature(node.elements ?? []),
|
|
8700
8994
|
comboboxElements: deepClone(node.elements ?? []),
|
|
8701
|
-
selected: toFiniteNumber3(node.selected) ?? 0
|
|
8995
|
+
selected: toFiniteNumber3(node.selected) ?? 0,
|
|
8996
|
+
isScrolledToTop: true,
|
|
8997
|
+
isScrolledToBottom: true
|
|
8702
8998
|
};
|
|
8703
8999
|
break;
|
|
8704
9000
|
case "radio-group":
|
|
@@ -8750,7 +9046,9 @@ var RuntimeRenderer = class {
|
|
|
8750
9046
|
widget: node.widget,
|
|
8751
9047
|
id: stateId,
|
|
8752
9048
|
name: node.name,
|
|
8753
|
-
enabled: normalizeBoolean(node.enabled, true)
|
|
9049
|
+
enabled: normalizeBoolean(node.enabled, true),
|
|
9050
|
+
isScrolledToTop: isScrollableWidgetNode(node) ? true : void 0,
|
|
9051
|
+
isScrolledToBottom: isScrollableWidgetNode(node) ? true : void 0
|
|
8754
9052
|
};
|
|
8755
9053
|
break;
|
|
8756
9054
|
}
|
|
@@ -9207,6 +9505,19 @@ var RuntimeRenderer = class {
|
|
|
9207
9505
|
redispatchUnderlyingClick: (x, y) => this.redispatchUnderlyingClick(x, y),
|
|
9208
9506
|
runActions: (actions, inputValue, context) => this.actionRuntime.runActions(actions, inputValue, context)
|
|
9209
9507
|
});
|
|
9508
|
+
for (const element of Array.from(root.querySelectorAll("[data-widget-key]"))) {
|
|
9509
|
+
const key = element.dataset.widgetKey;
|
|
9510
|
+
if (!key) {
|
|
9511
|
+
continue;
|
|
9512
|
+
}
|
|
9513
|
+
const node = this.nodeByKey.get(key);
|
|
9514
|
+
if (!node || !isScrollableWidgetNode(node)) {
|
|
9515
|
+
continue;
|
|
9516
|
+
}
|
|
9517
|
+
element.addEventListener("scroll", () => {
|
|
9518
|
+
this.syncScrollStateForElement(element, key);
|
|
9519
|
+
}, { passive: true });
|
|
9520
|
+
}
|
|
9210
9521
|
}
|
|
9211
9522
|
updateOwningListElementDescriptor(element, mutate) {
|
|
9212
9523
|
const listElement = element.closest(".vjt-list-element");
|
|
@@ -9525,6 +9836,7 @@ var RuntimeRenderer = class {
|
|
|
9525
9836
|
}
|
|
9526
9837
|
if (typeof window === "undefined") {
|
|
9527
9838
|
initialElement.scrollTop = Math.max(0, Math.min(targetTop, getMaxScrollTop(initialElement)));
|
|
9839
|
+
this.syncScrollStateForReference(reference, initialElement);
|
|
9528
9840
|
return;
|
|
9529
9841
|
}
|
|
9530
9842
|
if (this.activeScrollAnimationFrameId !== null) {
|
|
@@ -9536,6 +9848,7 @@ var RuntimeRenderer = class {
|
|
|
9536
9848
|
const delta = clampedTargetTop - startTop;
|
|
9537
9849
|
if (Math.abs(delta) < 1) {
|
|
9538
9850
|
initialElement.scrollTop = clampedTargetTop;
|
|
9851
|
+
this.syncScrollStateForReference(reference, initialElement);
|
|
9539
9852
|
return;
|
|
9540
9853
|
}
|
|
9541
9854
|
const durationMs = 300;
|
|
@@ -9551,10 +9864,12 @@ var RuntimeRenderer = class {
|
|
|
9551
9864
|
const progress = Math.min(1, elapsed / durationMs);
|
|
9552
9865
|
const liveTargetTop = Math.max(0, Math.min(clampedTargetTop, getMaxScrollTop(liveElement)));
|
|
9553
9866
|
liveElement.scrollTop = startTop + (liveTargetTop - startTop) * easeInOutCubic(progress);
|
|
9867
|
+
this.syncScrollStateForReference(reference, liveElement);
|
|
9554
9868
|
if (progress < 1) {
|
|
9555
9869
|
this.activeScrollAnimationFrameId = window.requestAnimationFrame(tick);
|
|
9556
9870
|
} else {
|
|
9557
9871
|
liveElement.scrollTop = liveTargetTop;
|
|
9872
|
+
this.syncScrollStateForReference(reference, liveElement);
|
|
9558
9873
|
this.activeScrollAnimationFrameId = null;
|
|
9559
9874
|
}
|
|
9560
9875
|
};
|
|
@@ -9608,6 +9923,48 @@ var RuntimeRenderer = class {
|
|
|
9608
9923
|
}
|
|
9609
9924
|
return null;
|
|
9610
9925
|
}
|
|
9926
|
+
syncAllScrollStateFlags() {
|
|
9927
|
+
if (!(this.root instanceof HTMLElement)) {
|
|
9928
|
+
return;
|
|
9929
|
+
}
|
|
9930
|
+
for (const element of Array.from(this.root.querySelectorAll("[data-widget-key]"))) {
|
|
9931
|
+
const key = element.dataset.widgetKey;
|
|
9932
|
+
if (!key) {
|
|
9933
|
+
continue;
|
|
9934
|
+
}
|
|
9935
|
+
const node = this.nodeByKey.get(key);
|
|
9936
|
+
if (!node || !isScrollableWidgetNode(node)) {
|
|
9937
|
+
continue;
|
|
9938
|
+
}
|
|
9939
|
+
this.syncScrollStateForElement(element, key);
|
|
9940
|
+
}
|
|
9941
|
+
}
|
|
9942
|
+
syncScrollStateForReference(reference, element) {
|
|
9943
|
+
const widgetId = reference.split(".")[0]?.trim();
|
|
9944
|
+
if (!widgetId) {
|
|
9945
|
+
return;
|
|
9946
|
+
}
|
|
9947
|
+
const targetElement = element ?? this.findScrollableWidgetElement(reference);
|
|
9948
|
+
if (!(targetElement instanceof HTMLElement)) {
|
|
9949
|
+
return;
|
|
9950
|
+
}
|
|
9951
|
+
const key = targetElement.dataset.widgetKey ?? widgetId;
|
|
9952
|
+
this.syncScrollStateForElement(targetElement, key);
|
|
9953
|
+
}
|
|
9954
|
+
syncScrollStateForElement(element, key) {
|
|
9955
|
+
const state = this.stateByKey.get(key) ?? this.stateById.get(key);
|
|
9956
|
+
if (!state) {
|
|
9957
|
+
return;
|
|
9958
|
+
}
|
|
9959
|
+
const maxScrollTop = Math.max(0, element.scrollHeight - element.clientHeight);
|
|
9960
|
+
if (maxScrollTop <= 1) {
|
|
9961
|
+
state.isScrolledToTop = true;
|
|
9962
|
+
state.isScrolledToBottom = true;
|
|
9963
|
+
return;
|
|
9964
|
+
}
|
|
9965
|
+
state.isScrolledToTop = element.scrollTop <= 1;
|
|
9966
|
+
state.isScrolledToBottom = element.scrollTop >= maxScrollTop - 1;
|
|
9967
|
+
}
|
|
9611
9968
|
toScrollableIndex(value) {
|
|
9612
9969
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
9613
9970
|
return Math.max(0, Math.trunc(value));
|
package/dist/lib/network.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ActionDefinition, PrimitiveRequestType, RequestMapInput, RequestSchema, SseConfig } from './types.js';
|
|
1
|
+
import type { ActionDefinition, ClosableRuntimeResource, PrimitiveRequestType, RequestMapInput, RequestSchema, SseConfig } from './types.js';
|
|
2
2
|
type ActionRunner = (actions: ActionDefinition[], inputValue: unknown, context: {
|
|
3
3
|
currentValue: unknown;
|
|
4
4
|
responseValue?: unknown;
|
|
@@ -8,7 +8,7 @@ type NetworkRuntimeOptions = {
|
|
|
8
8
|
requestsMap: RequestMapInput;
|
|
9
9
|
sseConfigs: SseConfig[];
|
|
10
10
|
backendUrl?: string;
|
|
11
|
-
eventSources:
|
|
11
|
+
eventSources: ClosableRuntimeResource[];
|
|
12
12
|
runActions: ActionRunner;
|
|
13
13
|
rerenderRoot: () => Promise<void>;
|
|
14
14
|
};
|
|
@@ -28,6 +28,9 @@ export declare class NetworkRuntime {
|
|
|
28
28
|
private createRequestError;
|
|
29
29
|
private toRequestErrorPayload;
|
|
30
30
|
coercePrimitiveSchemaValue(type: PrimitiveRequestType, value: unknown): unknown;
|
|
31
|
+
private createSseConnection;
|
|
32
|
+
private createNativeSseConnection;
|
|
33
|
+
private createFetchSseConnection;
|
|
31
34
|
private handleSseEvent;
|
|
32
35
|
}
|
|
33
36
|
export {};
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -6,6 +6,9 @@ export type HeadingTag = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
|
|
6
6
|
export type WidgetEventName = 'onClick' | 'onUserValueChange' | 'onRefresh' | 'onEnter' | 'onShiftEnter' | 'onControlEnter' | 'onPaste';
|
|
7
7
|
export type SystemEventName = 'onBeforeRender' | 'onAfterRender' | 'onBeforeNavigate' | 'onAfterNavigate' | 'onLayoutSwitchToMobile' | 'onLayoutSwitchToDesktop' | 'onSpeechDetected' | 'onRecordingStarted' | 'onRecordingStopped' | 'onRecordingError' | 'onListeningError' | 'onListeringError' | 'onPlayFinished' | 'onPlayingStopped';
|
|
8
8
|
export type PrimitiveRequestType = 'int' | 'float' | 'boolean' | 'string';
|
|
9
|
+
export type ClosableRuntimeResource = {
|
|
10
|
+
close: () => void;
|
|
11
|
+
};
|
|
9
12
|
export type RouteDefinition = {
|
|
10
13
|
path: string;
|
|
11
14
|
ui: string;
|
|
@@ -128,6 +131,8 @@ export type WidgetState = {
|
|
|
128
131
|
modalHeight?: number;
|
|
129
132
|
activeTab?: number;
|
|
130
133
|
isHidden?: boolean;
|
|
134
|
+
isScrolledToTop?: boolean;
|
|
135
|
+
isScrolledToBottom?: boolean;
|
|
131
136
|
};
|
|
132
137
|
export type RuntimeSnapshot = {
|
|
133
138
|
stateByKey: Array<[string, WidgetState]>;
|
package/package.json
CHANGED
package/vjt-styles.css
CHANGED