@vortexm/vjt 0.1.1 → 0.1.3
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 +377 -33
- package/dist/lib/action-runtime.d.ts +7 -1
- package/dist/lib/references.d.ts +4 -1
- package/dist/lib/resource-manager.d.ts +4 -1
- package/dist/lib/security.d.ts +13 -0
- package/dist/lib/types.d.ts +5 -1
- package/dist/lib/widgets/bindings.d.ts +1 -1
- package/package.json +4 -4
- package/vjt-styles.css +27 -0
package/dist/index.js
CHANGED
|
@@ -3683,6 +3683,159 @@ function logRuntimeError(scope, error, details) {
|
|
|
3683
3683
|
console.error("[VJT error]", payload);
|
|
3684
3684
|
}
|
|
3685
3685
|
|
|
3686
|
+
// src/lib/security.ts
|
|
3687
|
+
var BLOCKED_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
3688
|
+
var TRUSTED_CONFIG_ONLY_KEYS = /* @__PURE__ */ new Set(["widget", "style", "css", "actions", "events", "onClick", "onRefresh"]);
|
|
3689
|
+
function isPlainObject(value) {
|
|
3690
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3691
|
+
}
|
|
3692
|
+
function escapeHtmlAttribute(value) {
|
|
3693
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
3694
|
+
}
|
|
3695
|
+
function coerceFiniteNumber(value, kind, path) {
|
|
3696
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
3697
|
+
return kind === "int" ? Math.trunc(value) : value;
|
|
3698
|
+
}
|
|
3699
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
3700
|
+
const parsed = kind === "int" ? Number.parseInt(value, 10) : Number.parseFloat(value);
|
|
3701
|
+
if (Number.isFinite(parsed)) {
|
|
3702
|
+
return kind === "int" ? Math.trunc(parsed) : parsed;
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
throw new Error(`Invalid ${kind} at ${path}`);
|
|
3706
|
+
}
|
|
3707
|
+
function isBlockedObjectKey(key) {
|
|
3708
|
+
return BLOCKED_OBJECT_KEYS.has(key);
|
|
3709
|
+
}
|
|
3710
|
+
function hasBlockedReferenceSegment(reference) {
|
|
3711
|
+
return reference.split(".").some((segment) => isBlockedObjectKey(segment));
|
|
3712
|
+
}
|
|
3713
|
+
function isTrustedConfigOnlyKey(key) {
|
|
3714
|
+
return TRUSTED_CONFIG_ONLY_KEYS.has(key);
|
|
3715
|
+
}
|
|
3716
|
+
function templateValueUsesReference(value) {
|
|
3717
|
+
if (typeof value === "string") {
|
|
3718
|
+
return value.includes("$ref:");
|
|
3719
|
+
}
|
|
3720
|
+
if (Array.isArray(value)) {
|
|
3721
|
+
return value.some((entry) => templateValueUsesReference(entry));
|
|
3722
|
+
}
|
|
3723
|
+
if (isPlainObject(value)) {
|
|
3724
|
+
return Object.values(value).some((entry) => templateValueUsesReference(entry));
|
|
3725
|
+
}
|
|
3726
|
+
return false;
|
|
3727
|
+
}
|
|
3728
|
+
function sanitizeConfigStyleMap(styleMap) {
|
|
3729
|
+
const safe = {};
|
|
3730
|
+
for (const [key, value] of Object.entries(styleMap)) {
|
|
3731
|
+
if (isBlockedObjectKey(key) || typeof value !== "string") {
|
|
3732
|
+
continue;
|
|
3733
|
+
}
|
|
3734
|
+
safe[key] = value;
|
|
3735
|
+
}
|
|
3736
|
+
return safe;
|
|
3737
|
+
}
|
|
3738
|
+
function sanitizeUrl(rawValue, options = {}) {
|
|
3739
|
+
if (typeof rawValue !== "string") {
|
|
3740
|
+
return null;
|
|
3741
|
+
}
|
|
3742
|
+
const value = rawValue.trim();
|
|
3743
|
+
if (!value) {
|
|
3744
|
+
return null;
|
|
3745
|
+
}
|
|
3746
|
+
if (/^(javascript|data|vbscript):/i.test(value)) {
|
|
3747
|
+
return null;
|
|
3748
|
+
}
|
|
3749
|
+
if (value.startsWith("//")) {
|
|
3750
|
+
return null;
|
|
3751
|
+
}
|
|
3752
|
+
const schemeMatch = value.match(/^([A-Za-z][A-Za-z\d+\-.]*):/);
|
|
3753
|
+
if (!schemeMatch) {
|
|
3754
|
+
return options.allowRelative !== false ? value : null;
|
|
3755
|
+
}
|
|
3756
|
+
const scheme = schemeMatch[1].toLowerCase();
|
|
3757
|
+
if (scheme === "http" || scheme === "https") {
|
|
3758
|
+
return value;
|
|
3759
|
+
}
|
|
3760
|
+
if (scheme === "mailto" && options.allowMailto) {
|
|
3761
|
+
return value;
|
|
3762
|
+
}
|
|
3763
|
+
return null;
|
|
3764
|
+
}
|
|
3765
|
+
function sanitizeMarkdownSource(markdown) {
|
|
3766
|
+
return markdown.replaceAll("<", "<").replaceAll(">", ">");
|
|
3767
|
+
}
|
|
3768
|
+
function sanitizeGeneratedHtml(html) {
|
|
3769
|
+
const withoutDangerousTags = html.replace(/<\s*(script|style|iframe|object|embed|link|meta|base|form)\b[^>]*>[\s\S]*?<\s*\/\s*\1\s*>/gi, "").replace(/<\s*(script|style|iframe|object|embed|link|meta|base|form)\b[^>]*\/?>/gi, "").replace(/\son[a-z]+\s*=\s*(".*?"|'.*?'|[^\s>]+)/gi, "");
|
|
3770
|
+
return withoutDangerousTags.replace(/\s(href|src)\s*=\s*("([^"]*)"|'([^']*)'|([^\s>]+))/gi, (_match, attributeName, _full, dq, sq, bare) => {
|
|
3771
|
+
const rawUrl = dq ?? sq ?? bare ?? "";
|
|
3772
|
+
const safeUrl = sanitizeUrl(rawUrl, {
|
|
3773
|
+
allowRelative: true,
|
|
3774
|
+
allowMailto: attributeName.toLowerCase() === "href"
|
|
3775
|
+
});
|
|
3776
|
+
const fallback = attributeName.toLowerCase() === "href" ? "#" : "";
|
|
3777
|
+
return ` ${attributeName}="${escapeHtmlAttribute(safeUrl ?? fallback)}"`;
|
|
3778
|
+
});
|
|
3779
|
+
}
|
|
3780
|
+
function sanitizeSchemaValue(schema, value, path = "value") {
|
|
3781
|
+
if (value === void 0 || value === null) {
|
|
3782
|
+
return void 0;
|
|
3783
|
+
}
|
|
3784
|
+
if (schema.type === "object") {
|
|
3785
|
+
if (!isPlainObject(value)) {
|
|
3786
|
+
throw new Error(`Expected object at ${path}`);
|
|
3787
|
+
}
|
|
3788
|
+
const result = {};
|
|
3789
|
+
for (const property of schema.properties ?? []) {
|
|
3790
|
+
if (!property.name || isBlockedObjectKey(property.name)) {
|
|
3791
|
+
continue;
|
|
3792
|
+
}
|
|
3793
|
+
result[property.name] = sanitizeSchemaValue(property, value[property.name], `${path}.${property.name}`);
|
|
3794
|
+
}
|
|
3795
|
+
return result;
|
|
3796
|
+
}
|
|
3797
|
+
if (schema.type === "array") {
|
|
3798
|
+
if (!Array.isArray(value)) {
|
|
3799
|
+
throw new Error(`Expected array at ${path}`);
|
|
3800
|
+
}
|
|
3801
|
+
return value.map((entry, index) => sanitizeSchemaValue(schema.elements, entry, `${path}[${index}]`));
|
|
3802
|
+
}
|
|
3803
|
+
switch (schema.type) {
|
|
3804
|
+
case "int":
|
|
3805
|
+
return coerceFiniteNumber(value, "int", path);
|
|
3806
|
+
case "float":
|
|
3807
|
+
return coerceFiniteNumber(value, "float", path);
|
|
3808
|
+
case "boolean":
|
|
3809
|
+
if (typeof value === "boolean") {
|
|
3810
|
+
return value;
|
|
3811
|
+
}
|
|
3812
|
+
if (value === "true" || value === "1" || value === 1) {
|
|
3813
|
+
return true;
|
|
3814
|
+
}
|
|
3815
|
+
if (value === "false" || value === "0" || value === 0) {
|
|
3816
|
+
return false;
|
|
3817
|
+
}
|
|
3818
|
+
throw new Error(`Invalid boolean at ${path}`);
|
|
3819
|
+
case "string": {
|
|
3820
|
+
const stringValue = typeof value === "string" ? value : typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" ? String(value) : (() => {
|
|
3821
|
+
throw new Error(`Invalid string at ${path}`);
|
|
3822
|
+
})();
|
|
3823
|
+
if (typeof schema.maxLength === "number" && stringValue.length > schema.maxLength) {
|
|
3824
|
+
throw new Error(`String too long at ${path}`);
|
|
3825
|
+
}
|
|
3826
|
+
if (typeof schema.regexp === "string" && schema.regexp) {
|
|
3827
|
+
const regexp = new RegExp(schema.regexp);
|
|
3828
|
+
if (!regexp.test(stringValue)) {
|
|
3829
|
+
throw new Error(`String does not match regexp at ${path}`);
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
return stringValue;
|
|
3833
|
+
}
|
|
3834
|
+
default:
|
|
3835
|
+
return value;
|
|
3836
|
+
}
|
|
3837
|
+
}
|
|
3838
|
+
|
|
3686
3839
|
// src/lib/network.ts
|
|
3687
3840
|
var NetworkRuntime = class {
|
|
3688
3841
|
requestsMap;
|
|
@@ -3707,7 +3860,12 @@ var NetworkRuntime = class {
|
|
|
3707
3860
|
if (!config?.url) {
|
|
3708
3861
|
continue;
|
|
3709
3862
|
}
|
|
3710
|
-
const
|
|
3863
|
+
const safeUrl = sanitizeUrl(config.url, { allowRelative: true });
|
|
3864
|
+
if (!safeUrl) {
|
|
3865
|
+
logRuntimeError("sseConfig", new Error("Blocked unsafe SSE url"), { url: config.url });
|
|
3866
|
+
continue;
|
|
3867
|
+
}
|
|
3868
|
+
const source = new EventSource(safeUrl);
|
|
3711
3869
|
this.eventSources.push(source);
|
|
3712
3870
|
for (const eventDefinition of config.events ?? []) {
|
|
3713
3871
|
if (!eventDefinition?.name) {
|
|
@@ -3717,7 +3875,7 @@ var NetworkRuntime = class {
|
|
|
3717
3875
|
void this.handleSseEvent(eventDefinition, event).catch((error) => {
|
|
3718
3876
|
logRuntimeError("handleSseEvent", error, {
|
|
3719
3877
|
eventName: eventDefinition.name,
|
|
3720
|
-
url:
|
|
3878
|
+
url: safeUrl
|
|
3721
3879
|
});
|
|
3722
3880
|
});
|
|
3723
3881
|
});
|
|
@@ -3745,7 +3903,11 @@ var NetworkRuntime = class {
|
|
|
3745
3903
|
if (!requestUrl) {
|
|
3746
3904
|
throw new Error(`Request ${requestName} has no url in config`);
|
|
3747
3905
|
}
|
|
3748
|
-
const
|
|
3906
|
+
const safeRequestUrl = sanitizeUrl(requestUrl, { allowRelative: true });
|
|
3907
|
+
if (!safeRequestUrl) {
|
|
3908
|
+
throw new Error(`Request ${requestName} has unsafe url`);
|
|
3909
|
+
}
|
|
3910
|
+
const response = await fetch(safeRequestUrl, {
|
|
3749
3911
|
method: "POST",
|
|
3750
3912
|
headers: {
|
|
3751
3913
|
"Content-Type": "application/json"
|
|
@@ -3765,7 +3927,13 @@ var NetworkRuntime = class {
|
|
|
3765
3927
|
} catch {
|
|
3766
3928
|
throw new Error(`Request ${requestName} returned non-JSON response: ${responseText.slice(0, 120)}`);
|
|
3767
3929
|
}
|
|
3768
|
-
|
|
3930
|
+
if (json.error) {
|
|
3931
|
+
throw new Error(`Request ${requestName} returned JSON-RPC error: ${String(json.error.message ?? "unknown error")}`);
|
|
3932
|
+
}
|
|
3933
|
+
if (!definition.response) {
|
|
3934
|
+
throw new Error(`Request ${requestName} must define response schema`);
|
|
3935
|
+
}
|
|
3936
|
+
const result = sanitizeSchemaValue(definition.response, json.result, `response.${requestName}`);
|
|
3769
3937
|
if (definition.onResponse?.length) {
|
|
3770
3938
|
await this.runActions(definition.onResponse, null, {
|
|
3771
3939
|
currentValue: null,
|
|
@@ -3778,7 +3946,7 @@ var NetworkRuntime = class {
|
|
|
3778
3946
|
if (schema.type === "object") {
|
|
3779
3947
|
const result = {};
|
|
3780
3948
|
for (const property of schema.properties ?? []) {
|
|
3781
|
-
if (!property.name) {
|
|
3949
|
+
if (!property.name || isBlockedObjectKey(property.name)) {
|
|
3782
3950
|
continue;
|
|
3783
3951
|
}
|
|
3784
3952
|
result[property.name] = await this.buildSchemaValue(property, currentValue, responseValue);
|
|
@@ -3834,12 +4002,15 @@ var NetworkRuntime = class {
|
|
|
3834
4002
|
}
|
|
3835
4003
|
}
|
|
3836
4004
|
async handleSseEvent(eventDefinition, event) {
|
|
3837
|
-
let
|
|
4005
|
+
let parsedMessage = {};
|
|
3838
4006
|
try {
|
|
3839
|
-
|
|
4007
|
+
parsedMessage = event.data ? JSON.parse(event.data) : {};
|
|
3840
4008
|
} catch {
|
|
3841
|
-
|
|
4009
|
+
parsedMessage = { value: event.data ?? "" };
|
|
3842
4010
|
}
|
|
4011
|
+
const message = eventDefinition.message ? sanitizeSchemaValue(eventDefinition.message, parsedMessage, `sse.${eventDefinition.name}`) : (() => {
|
|
4012
|
+
throw new Error(`SSE event ${eventDefinition.name} must define message schema`);
|
|
4013
|
+
})();
|
|
3843
4014
|
if (!eventDefinition.onEvent?.length) {
|
|
3844
4015
|
return;
|
|
3845
4016
|
}
|
|
@@ -3852,12 +4023,15 @@ var NetworkRuntime = class {
|
|
|
3852
4023
|
};
|
|
3853
4024
|
|
|
3854
4025
|
// src/lib/references.ts
|
|
3855
|
-
function
|
|
4026
|
+
function isPlainObject2(value) {
|
|
3856
4027
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3857
4028
|
}
|
|
3858
4029
|
function readPath(target, path) {
|
|
3859
4030
|
let current = target;
|
|
3860
4031
|
for (const segment of path) {
|
|
4032
|
+
if (isBlockedObjectKey(segment)) {
|
|
4033
|
+
return void 0;
|
|
4034
|
+
}
|
|
3861
4035
|
if (current == null) {
|
|
3862
4036
|
return void 0;
|
|
3863
4037
|
}
|
|
@@ -3869,6 +4043,9 @@ function readPath(target, path) {
|
|
|
3869
4043
|
if (typeof current !== "object") {
|
|
3870
4044
|
return void 0;
|
|
3871
4045
|
}
|
|
4046
|
+
if (!Object.prototype.hasOwnProperty.call(current, segment)) {
|
|
4047
|
+
return void 0;
|
|
4048
|
+
}
|
|
3872
4049
|
current = current[segment];
|
|
3873
4050
|
}
|
|
3874
4051
|
return current;
|
|
@@ -3895,6 +4072,18 @@ function stringifyReferenceValue(value) {
|
|
|
3895
4072
|
}
|
|
3896
4073
|
return JSON.stringify(value);
|
|
3897
4074
|
}
|
|
4075
|
+
function isReferenceValueEmpty(value) {
|
|
4076
|
+
if (value === null || value === void 0) {
|
|
4077
|
+
return true;
|
|
4078
|
+
}
|
|
4079
|
+
if (typeof value === "string") {
|
|
4080
|
+
return value.length === 0;
|
|
4081
|
+
}
|
|
4082
|
+
if (Array.isArray(value)) {
|
|
4083
|
+
return value.length === 0;
|
|
4084
|
+
}
|
|
4085
|
+
return false;
|
|
4086
|
+
}
|
|
3898
4087
|
function getCookieValue(name) {
|
|
3899
4088
|
if (typeof document === "undefined") {
|
|
3900
4089
|
return void 0;
|
|
@@ -3919,16 +4108,27 @@ function getCookieValue(name) {
|
|
|
3919
4108
|
}
|
|
3920
4109
|
return void 0;
|
|
3921
4110
|
}
|
|
3922
|
-
function setCookieValue(name, value) {
|
|
4111
|
+
function setCookieValue(name, value, ttlDays) {
|
|
3923
4112
|
if (typeof document === "undefined") {
|
|
3924
4113
|
return;
|
|
3925
4114
|
}
|
|
3926
|
-
|
|
4115
|
+
const ttlValue = typeof ttlDays === "number" && Number.isFinite(ttlDays) ? ttlDays : null;
|
|
4116
|
+
const expires = ttlValue !== null ? `; max-age=${Math.max(0, Math.round(ttlValue * 24 * 60 * 60))}` : "";
|
|
4117
|
+
document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; path=/${expires}`;
|
|
4118
|
+
}
|
|
4119
|
+
function getUrlParamValue(name) {
|
|
4120
|
+
if (typeof window === "undefined") {
|
|
4121
|
+
return void 0;
|
|
4122
|
+
}
|
|
4123
|
+
return new URLSearchParams(window.location.search).get(name) ?? void 0;
|
|
3927
4124
|
}
|
|
3928
4125
|
var ReferenceRuntime = class {
|
|
3929
4126
|
constructor(host) {
|
|
3930
4127
|
this.host = host;
|
|
3931
4128
|
}
|
|
4129
|
+
isEmptyReference(reference, currentValue, responseValue) {
|
|
4130
|
+
return isReferenceValueEmpty(this.resolveReference(reference, currentValue, responseValue));
|
|
4131
|
+
}
|
|
3932
4132
|
resolveReference(reference, currentValue, responseValue) {
|
|
3933
4133
|
if (reference === "current") {
|
|
3934
4134
|
return currentValue;
|
|
@@ -3951,6 +4151,9 @@ var ReferenceRuntime = class {
|
|
|
3951
4151
|
if (reference.startsWith("app.")) {
|
|
3952
4152
|
return this.host.getAppValue(reference.slice(4));
|
|
3953
4153
|
}
|
|
4154
|
+
if (reference.startsWith("url.")) {
|
|
4155
|
+
return getUrlParamValue(reference.slice(4));
|
|
4156
|
+
}
|
|
3954
4157
|
const [widgetId, ...rest] = reference.split(".");
|
|
3955
4158
|
const state = this.host.stateById.get(widgetId);
|
|
3956
4159
|
if (!state) {
|
|
@@ -3982,7 +4185,7 @@ var ReferenceRuntime = class {
|
|
|
3982
4185
|
}
|
|
3983
4186
|
return readPath(currentValue.descriptor, reference.split("."));
|
|
3984
4187
|
}
|
|
3985
|
-
if (
|
|
4188
|
+
if (isPlainObject2(currentValue) || Array.isArray(currentValue)) {
|
|
3986
4189
|
return readPath(currentValue, reference.split("."));
|
|
3987
4190
|
}
|
|
3988
4191
|
return void 0;
|
|
@@ -4007,7 +4210,7 @@ var ReferenceRuntime = class {
|
|
|
4007
4210
|
if (state.widget === "combobox") {
|
|
4008
4211
|
const index = state.selected ?? 0;
|
|
4009
4212
|
const entry = (state.comboboxElements ?? [])[index];
|
|
4010
|
-
if (
|
|
4213
|
+
if (isPlainObject2(entry)) {
|
|
4011
4214
|
return entry.value ?? entry.text ?? index;
|
|
4012
4215
|
}
|
|
4013
4216
|
return index;
|
|
@@ -4015,7 +4218,7 @@ var ReferenceRuntime = class {
|
|
|
4015
4218
|
if (state.widget === "listbox") {
|
|
4016
4219
|
const index = state.selected ?? 0;
|
|
4017
4220
|
const entry = (state.listboxElements ?? [])[index];
|
|
4018
|
-
if (
|
|
4221
|
+
if (isPlainObject2(entry)) {
|
|
4019
4222
|
return entry.value ?? entry.text ?? index;
|
|
4020
4223
|
}
|
|
4021
4224
|
return index;
|
|
@@ -4064,12 +4267,12 @@ var ReferenceRuntime = class {
|
|
|
4064
4267
|
return readPath(state, field.split("."));
|
|
4065
4268
|
}
|
|
4066
4269
|
}
|
|
4067
|
-
assignReference(reference, inputValue, _currentValue) {
|
|
4068
|
-
if (reference.startsWith("current.")) {
|
|
4270
|
+
assignReference(reference, inputValue, _currentValue, options) {
|
|
4271
|
+
if (reference.startsWith("current.") || hasBlockedReferenceSegment(reference)) {
|
|
4069
4272
|
return;
|
|
4070
4273
|
}
|
|
4071
4274
|
if (reference.startsWith("cookies.")) {
|
|
4072
|
-
setCookieValue(reference.slice(8), stringifyReferenceValue(inputValue));
|
|
4275
|
+
setCookieValue(reference.slice(8), stringifyReferenceValue(inputValue), toFiniteNumber(options?.cookieTtl) ?? void 0);
|
|
4073
4276
|
return;
|
|
4074
4277
|
}
|
|
4075
4278
|
if (reference.startsWith("app.")) {
|
|
@@ -4110,7 +4313,7 @@ var ReferenceRuntime = class {
|
|
|
4110
4313
|
case "elements":
|
|
4111
4314
|
if (state.widget === "list" || state.widget === "grid-view") {
|
|
4112
4315
|
this.clearListElementState(state.key);
|
|
4113
|
-
state.elements = Array.isArray(inputValue) ? inputValue.filter((item) =>
|
|
4316
|
+
state.elements = Array.isArray(inputValue) ? inputValue.filter((item) => isPlainObject2(item) && typeof item.widget === "string") : [];
|
|
4114
4317
|
state.elements.forEach((element, index) => {
|
|
4115
4318
|
const listNode = this.host.nodeById.get(widgetId);
|
|
4116
4319
|
if (listNode?.widget === "list" || listNode?.widget === "grid-view") {
|
|
@@ -4119,10 +4322,10 @@ var ReferenceRuntime = class {
|
|
|
4119
4322
|
});
|
|
4120
4323
|
}
|
|
4121
4324
|
if (state.widget === "listbox") {
|
|
4122
|
-
state.listboxElements = Array.isArray(inputValue) ? inputValue.filter((item) =>
|
|
4325
|
+
state.listboxElements = Array.isArray(inputValue) ? inputValue.filter((item) => isPlainObject2(item)) : [];
|
|
4123
4326
|
}
|
|
4124
4327
|
if (state.widget === "combobox") {
|
|
4125
|
-
state.comboboxElements = Array.isArray(inputValue) ? inputValue.filter((item) =>
|
|
4328
|
+
state.comboboxElements = Array.isArray(inputValue) ? inputValue.filter((item) => isPlainObject2(item)) : [];
|
|
4126
4329
|
}
|
|
4127
4330
|
break;
|
|
4128
4331
|
default:
|
|
@@ -4161,9 +4364,15 @@ var ReferenceRuntime = class {
|
|
|
4161
4364
|
if (Array.isArray(template)) {
|
|
4162
4365
|
return template.map((entry) => this.resolveMappedValue(entry, currentValue, responseValue));
|
|
4163
4366
|
}
|
|
4164
|
-
if (
|
|
4367
|
+
if (isPlainObject2(template)) {
|
|
4165
4368
|
const result = {};
|
|
4166
4369
|
for (const [key, value] of Object.entries(template)) {
|
|
4370
|
+
if (isBlockedObjectKey(key)) {
|
|
4371
|
+
continue;
|
|
4372
|
+
}
|
|
4373
|
+
if (isTrustedConfigOnlyKey(key) && templateValueUsesReference(value)) {
|
|
4374
|
+
continue;
|
|
4375
|
+
}
|
|
4167
4376
|
result[key] = this.resolveMappedValue(value, currentValue, responseValue);
|
|
4168
4377
|
}
|
|
4169
4378
|
return result;
|
|
@@ -4171,7 +4380,7 @@ var ReferenceRuntime = class {
|
|
|
4171
4380
|
return template;
|
|
4172
4381
|
}
|
|
4173
4382
|
isListElementContext(value) {
|
|
4174
|
-
return
|
|
4383
|
+
return isPlainObject2(value) && value.kind === "list-element" && typeof value.listId === "string" && typeof value.index === "number" && isPlainObject2(value.descriptor) && typeof value.descriptor.widget === "string";
|
|
4175
4384
|
}
|
|
4176
4385
|
findNamedWidget(node, name) {
|
|
4177
4386
|
if (!node) {
|
|
@@ -4251,6 +4460,7 @@ var ActionRuntime = class {
|
|
|
4251
4460
|
dispatchWidgetEventImpl;
|
|
4252
4461
|
executeRequest;
|
|
4253
4462
|
resolveReference;
|
|
4463
|
+
isEmptyReference;
|
|
4254
4464
|
assignReference;
|
|
4255
4465
|
resolveMappedValue;
|
|
4256
4466
|
setWidgetEnabled;
|
|
@@ -4258,6 +4468,7 @@ var ActionRuntime = class {
|
|
|
4258
4468
|
setUiReference;
|
|
4259
4469
|
refreshWidgetTree;
|
|
4260
4470
|
renderWidgetTree;
|
|
4471
|
+
navigateTo;
|
|
4261
4472
|
getContextMenuPosition;
|
|
4262
4473
|
setActiveContextMenu;
|
|
4263
4474
|
setActiveModalId;
|
|
@@ -4273,6 +4484,7 @@ var ActionRuntime = class {
|
|
|
4273
4484
|
this.dispatchWidgetEventImpl = options.dispatchWidgetEvent;
|
|
4274
4485
|
this.executeRequest = options.executeRequest;
|
|
4275
4486
|
this.resolveReference = options.resolveReference;
|
|
4487
|
+
this.isEmptyReference = options.isEmptyReference;
|
|
4276
4488
|
this.assignReference = options.assignReference;
|
|
4277
4489
|
this.resolveMappedValue = options.resolveMappedValue;
|
|
4278
4490
|
this.setWidgetEnabled = options.setWidgetEnabled;
|
|
@@ -4280,6 +4492,7 @@ var ActionRuntime = class {
|
|
|
4280
4492
|
this.setUiReference = options.setUiReference;
|
|
4281
4493
|
this.refreshWidgetTree = options.refreshWidgetTree;
|
|
4282
4494
|
this.renderWidgetTree = options.renderWidgetTree;
|
|
4495
|
+
this.navigateTo = options.navigateTo;
|
|
4283
4496
|
this.getContextMenuPosition = options.getContextMenuPosition;
|
|
4284
4497
|
this.setActiveContextMenu = options.setActiveContextMenu;
|
|
4285
4498
|
this.setActiveModalId = options.setActiveModalId;
|
|
@@ -4289,7 +4502,7 @@ var ActionRuntime = class {
|
|
|
4289
4502
|
}
|
|
4290
4503
|
async dispatchWidgetEvent(node, eventName, key, inputValue = null, pointer = null) {
|
|
4291
4504
|
try {
|
|
4292
|
-
if ((eventName === "onClick" || eventName === "onUserValueChange") && !this.isWidgetEnabled(node, key)) {
|
|
4505
|
+
if ((eventName === "onClick" || eventName === "onUserValueChange" || eventName === "onEnter" || eventName === "onShiftEnter" || eventName === "onControlEnter") && !this.isWidgetEnabled(node, key)) {
|
|
4293
4506
|
return;
|
|
4294
4507
|
}
|
|
4295
4508
|
const actions = node.events?.[eventName] ?? (eventName === "onClick" ? this.getInlineActions(node) : void 0);
|
|
@@ -4365,6 +4578,10 @@ var ActionRuntime = class {
|
|
|
4365
4578
|
const requestName = name.slice(8);
|
|
4366
4579
|
return this.executeRequest(requestName, context.currentValue);
|
|
4367
4580
|
}
|
|
4581
|
+
if (name.startsWith("navigate ")) {
|
|
4582
|
+
await this.navigateTo(name.slice(9));
|
|
4583
|
+
return null;
|
|
4584
|
+
}
|
|
4368
4585
|
if (name.startsWith("showMenu ")) {
|
|
4369
4586
|
const menuId = name.slice(9);
|
|
4370
4587
|
const menuState = this.stateById.get(menuId);
|
|
@@ -4418,8 +4635,12 @@ var ActionRuntime = class {
|
|
|
4418
4635
|
if (name.startsWith("equals ")) {
|
|
4419
4636
|
return this.resolveReference(name.slice(7), context.currentValue, context.responseValue) === action.args;
|
|
4420
4637
|
}
|
|
4638
|
+
if (name.startsWith("isEmpty ")) {
|
|
4639
|
+
return this.isEmptyReference(name.slice(8), context.currentValue, context.responseValue);
|
|
4640
|
+
}
|
|
4421
4641
|
if (name.startsWith("set ")) {
|
|
4422
|
-
|
|
4642
|
+
const args = typeof action.args === "object" && action.args !== null && !Array.isArray(action.args) ? action.args : void 0;
|
|
4643
|
+
this.assignReference(name.slice(4), inputValue, context.currentValue, args);
|
|
4423
4644
|
return inputValue;
|
|
4424
4645
|
}
|
|
4425
4646
|
if (name.startsWith("filter ")) {
|
|
@@ -4448,7 +4669,7 @@ function isIfElseArgs(value) {
|
|
|
4448
4669
|
}
|
|
4449
4670
|
|
|
4450
4671
|
// src/lib/dom-state.ts
|
|
4451
|
-
function
|
|
4672
|
+
function isPlainObject3(value) {
|
|
4452
4673
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4453
4674
|
}
|
|
4454
4675
|
function supportsTextSelection(element) {
|
|
@@ -4618,7 +4839,7 @@ function getListElementContextFromElement(element, stateByKey) {
|
|
|
4618
4839
|
}
|
|
4619
4840
|
const listState = stateByKey.get(listId);
|
|
4620
4841
|
const descriptor = listState?.elements?.[index];
|
|
4621
|
-
if (!descriptor || !
|
|
4842
|
+
if (!descriptor || !isPlainObject3(descriptor) || typeof descriptor.widget !== "string") {
|
|
4622
4843
|
return null;
|
|
4623
4844
|
}
|
|
4624
4845
|
return {
|
|
@@ -5135,7 +5356,7 @@ var renderMdText = (env, node, state, key) => {
|
|
|
5135
5356
|
const verticalAlign = VERTICAL_ALIGN_MAP2[node["vert-align"] ?? "center"];
|
|
5136
5357
|
const common = state.id ? ` id="${env.escapeHtml(state.id)}"` : "";
|
|
5137
5358
|
const markdown = env.resolveI18nValue(state.text ?? node.text);
|
|
5138
|
-
const html = markdownConverter.makeHtml(markdown);
|
|
5359
|
+
const html = sanitizeGeneratedHtml(markdownConverter.makeHtml(sanitizeMarkdownSource(markdown)));
|
|
5139
5360
|
const inlineStyle = [
|
|
5140
5361
|
"display:flex",
|
|
5141
5362
|
`align-items:${verticalAlign}`,
|
|
@@ -5241,7 +5462,7 @@ var renderCheckbox = (env, node, state, key) => {
|
|
|
5241
5462
|
|
|
5242
5463
|
// src/lib/widgets/image.ts
|
|
5243
5464
|
var renderImage = (env, node, state, key) => {
|
|
5244
|
-
const url = env.escapeHtml(state.url ?? node.url ?? "");
|
|
5465
|
+
const url = env.escapeHtml(sanitizeUrl(state.url ?? node.url ?? "", { allowRelative: true }) ?? "");
|
|
5245
5466
|
const styleParts = [
|
|
5246
5467
|
"box-sizing:border-box",
|
|
5247
5468
|
"width:100%",
|
|
@@ -5268,7 +5489,7 @@ var renderLink = (env, node, state, key) => {
|
|
|
5268
5489
|
const classes = `vjt-link vjt-link--${type}${enabled ? "" : " is-disabled"}`;
|
|
5269
5490
|
const styleString = env.buildStyleString(node);
|
|
5270
5491
|
const styleAttribute = styleString ? ` style="${env.escapeHtml(styleString)}"` : "";
|
|
5271
|
-
const href = enabled
|
|
5492
|
+
const href = enabled ? sanitizeUrl(state.url ?? node.url, { allowRelative: true, allowMailto: true }) ?? "#" : "#";
|
|
5272
5493
|
const tabIndex = enabled ? "" : ' tabindex="-1" aria-disabled="true"';
|
|
5273
5494
|
return `<a${state.id ? ` id="${env.escapeHtml(state.id)}"` : ""}${env.buildWidgetDataAttributes(key, node)} class="${classes}" data-widget="link" href="${env.escapeHtml(href)}"${tabIndex}${styleAttribute}>${text}</a>`;
|
|
5274
5495
|
};
|
|
@@ -5386,6 +5607,31 @@ function renderContextMenuOverlay(env, menu) {
|
|
|
5386
5607
|
}
|
|
5387
5608
|
|
|
5388
5609
|
// src/lib/widgets/bindings.ts
|
|
5610
|
+
function bindKeyboardSubmitEvents(element, node, key, env) {
|
|
5611
|
+
const hasKeyboardEvents = Boolean(
|
|
5612
|
+
node.events?.onEnter?.length || node.events?.onShiftEnter?.length || node.events?.onControlEnter?.length
|
|
5613
|
+
);
|
|
5614
|
+
if (!hasKeyboardEvents) {
|
|
5615
|
+
return;
|
|
5616
|
+
}
|
|
5617
|
+
element.addEventListener("keydown", (event) => {
|
|
5618
|
+
if (event.key !== "Enter") {
|
|
5619
|
+
return;
|
|
5620
|
+
}
|
|
5621
|
+
let eventName = null;
|
|
5622
|
+
if (event.ctrlKey) {
|
|
5623
|
+
eventName = "onControlEnter";
|
|
5624
|
+
} else if (event.shiftKey) {
|
|
5625
|
+
eventName = "onShiftEnter";
|
|
5626
|
+
} else {
|
|
5627
|
+
eventName = "onEnter";
|
|
5628
|
+
}
|
|
5629
|
+
if (!eventName || !node.events?.[eventName]?.length) {
|
|
5630
|
+
return;
|
|
5631
|
+
}
|
|
5632
|
+
void env.dispatchWidgetEvent(node, eventName, key, env.getActionInputValueForElement(element, node), null);
|
|
5633
|
+
});
|
|
5634
|
+
}
|
|
5389
5635
|
function bindWidgetInputBehavior(root, env) {
|
|
5390
5636
|
for (const input of Array.from(root.querySelectorAll(".vjt-edit"))) {
|
|
5391
5637
|
if (!(input instanceof HTMLInputElement)) {
|
|
@@ -5453,6 +5699,7 @@ function bindWidgetEvents(root, env) {
|
|
|
5453
5699
|
continue;
|
|
5454
5700
|
}
|
|
5455
5701
|
if (element instanceof HTMLInputElement && element.dataset.widget === "edit") {
|
|
5702
|
+
bindKeyboardSubmitEvents(element, node, key, env);
|
|
5456
5703
|
element.addEventListener("input", () => {
|
|
5457
5704
|
const state = env.stateByKey.get(key);
|
|
5458
5705
|
if (state) {
|
|
@@ -5470,6 +5717,7 @@ function bindWidgetEvents(root, env) {
|
|
|
5470
5717
|
continue;
|
|
5471
5718
|
}
|
|
5472
5719
|
if (element instanceof HTMLTextAreaElement && element.dataset.widget === "textarea") {
|
|
5720
|
+
bindKeyboardSubmitEvents(element, node, key, env);
|
|
5473
5721
|
element.addEventListener("input", () => {
|
|
5474
5722
|
const state = env.stateByKey.get(key);
|
|
5475
5723
|
if (state) {
|
|
@@ -5487,6 +5735,7 @@ function bindWidgetEvents(root, env) {
|
|
|
5487
5735
|
continue;
|
|
5488
5736
|
}
|
|
5489
5737
|
if (element instanceof HTMLInputElement && element.dataset.widget === "checkbox") {
|
|
5738
|
+
bindKeyboardSubmitEvents(element, node, key, env);
|
|
5490
5739
|
element.addEventListener("change", () => {
|
|
5491
5740
|
const state = env.stateByKey.get(key);
|
|
5492
5741
|
if (state) {
|
|
@@ -5504,6 +5753,7 @@ function bindWidgetEvents(root, env) {
|
|
|
5504
5753
|
continue;
|
|
5505
5754
|
}
|
|
5506
5755
|
if (element instanceof HTMLSelectElement && (element.dataset.widget === "listbox" || element.dataset.widget === "combobox")) {
|
|
5756
|
+
bindKeyboardSubmitEvents(element, node, key, env);
|
|
5507
5757
|
element.addEventListener("change", () => {
|
|
5508
5758
|
const state = env.stateByKey.get(key);
|
|
5509
5759
|
if (state) {
|
|
@@ -5521,6 +5771,7 @@ function bindWidgetEvents(root, env) {
|
|
|
5521
5771
|
continue;
|
|
5522
5772
|
}
|
|
5523
5773
|
if (element instanceof HTMLInputElement && element.dataset.widget === "radio-group") {
|
|
5774
|
+
bindKeyboardSubmitEvents(element, node, key, env);
|
|
5524
5775
|
element.addEventListener("change", () => {
|
|
5525
5776
|
const state = env.stateByKey.get(key);
|
|
5526
5777
|
if (state) {
|
|
@@ -5800,7 +6051,7 @@ function toFiniteNumber3(value) {
|
|
|
5800
6051
|
}
|
|
5801
6052
|
return null;
|
|
5802
6053
|
}
|
|
5803
|
-
function
|
|
6054
|
+
function isPlainObject4(value) {
|
|
5804
6055
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5805
6056
|
}
|
|
5806
6057
|
function deepClone(value) {
|
|
@@ -5810,6 +6061,9 @@ function sanitizeIdFragment2(value) {
|
|
|
5810
6061
|
const cleaned = value.replaceAll(/[^A-Za-z0-9_-]+/g, "");
|
|
5811
6062
|
return cleaned || "item";
|
|
5812
6063
|
}
|
|
6064
|
+
function buildDefinitionSignature(value) {
|
|
6065
|
+
return JSON.stringify(value) ?? "null";
|
|
6066
|
+
}
|
|
5813
6067
|
function isGeneratedElementId(value) {
|
|
5814
6068
|
return !value.includes(".") && !value.includes(":") && /Element\d+/.test(value);
|
|
5815
6069
|
}
|
|
@@ -5855,6 +6109,7 @@ var RuntimeRenderer = class {
|
|
|
5855
6109
|
actionsMap;
|
|
5856
6110
|
requestsMap;
|
|
5857
6111
|
sseConfigs;
|
|
6112
|
+
systemEvents;
|
|
5858
6113
|
resourceManager;
|
|
5859
6114
|
actionFunctions;
|
|
5860
6115
|
backendUrl;
|
|
@@ -5883,7 +6138,7 @@ var RuntimeRenderer = class {
|
|
|
5883
6138
|
constructor(description, options = {}) {
|
|
5884
6139
|
this.description = deepClone(description);
|
|
5885
6140
|
this.resourceManager = options.resourceManager ?? null;
|
|
5886
|
-
const customStyleMap = options.styleMap ?? this.resourceManager?.getStyleMap() ?? {};
|
|
6141
|
+
const customStyleMap = sanitizeConfigStyleMap(options.styleMap ?? this.resourceManager?.getStyleMap() ?? {});
|
|
5887
6142
|
this.styleMap = {
|
|
5888
6143
|
...DEFAULT_STYLE_MAP,
|
|
5889
6144
|
...customStyleMap
|
|
@@ -5895,6 +6150,7 @@ var RuntimeRenderer = class {
|
|
|
5895
6150
|
this.requestsMap = options.requestsMap ?? this.resourceManager?.getRequestsMap() ?? {};
|
|
5896
6151
|
const resolvedSseConfigs = options.sseConfigs ?? this.resourceManager?.getSseConfigs() ?? [];
|
|
5897
6152
|
this.sseConfigs = Array.isArray(resolvedSseConfigs) ? resolvedSseConfigs : resolvedSseConfigs ? [resolvedSseConfigs] : [];
|
|
6153
|
+
this.systemEvents = options.systemEvents ?? this.resourceManager?.getSystemEvents() ?? {};
|
|
5898
6154
|
this.actionFunctions = options.actionFunctions ?? {};
|
|
5899
6155
|
this.backendUrl = options.backendUrl;
|
|
5900
6156
|
this.initialRuntimeSnapshot = options.runtimeSnapshot ? deepClone(options.runtimeSnapshot) : null;
|
|
@@ -5910,6 +6166,8 @@ var RuntimeRenderer = class {
|
|
|
5910
6166
|
return this.language;
|
|
5911
6167
|
case "theme":
|
|
5912
6168
|
return this.theme;
|
|
6169
|
+
case "urlPath":
|
|
6170
|
+
return typeof window === "undefined" ? "" : window.location.pathname;
|
|
5913
6171
|
default:
|
|
5914
6172
|
return void 0;
|
|
5915
6173
|
}
|
|
@@ -5923,6 +6181,15 @@ var RuntimeRenderer = class {
|
|
|
5923
6181
|
this.theme = typeof value === "boolean" ? value ? "light" : "dark" : value === "light" || value === "dark" ? value : "dark";
|
|
5924
6182
|
document.documentElement.dataset.vjtTheme = this.theme;
|
|
5925
6183
|
break;
|
|
6184
|
+
case "urlPath": {
|
|
6185
|
+
if (typeof window === "undefined") {
|
|
6186
|
+
break;
|
|
6187
|
+
}
|
|
6188
|
+
const nextPath = typeof value === "string" && value.trim() ? value.trim() : "/";
|
|
6189
|
+
const normalizedPath = nextPath.startsWith("/") ? nextPath : `/${nextPath}`;
|
|
6190
|
+
window.history.replaceState(window.history.state, "", `${normalizedPath}${window.location.search}${window.location.hash}`);
|
|
6191
|
+
break;
|
|
6192
|
+
}
|
|
5926
6193
|
default:
|
|
5927
6194
|
break;
|
|
5928
6195
|
}
|
|
@@ -5948,7 +6215,8 @@ var RuntimeRenderer = class {
|
|
|
5948
6215
|
dispatchWidgetEvent: (node, eventName, key, inputValue, pointer) => this.actionRuntime.dispatchWidgetEvent(node, eventName, key, inputValue, pointer),
|
|
5949
6216
|
executeRequest: (requestName, currentValue) => this.networkRuntime.executeRequest(requestName, currentValue),
|
|
5950
6217
|
resolveReference: (reference, currentValue, responseValue) => this.referenceRuntime.resolveReference(reference, currentValue, responseValue),
|
|
5951
|
-
|
|
6218
|
+
isEmptyReference: (reference, currentValue, responseValue) => this.referenceRuntime.isEmptyReference(reference, currentValue, responseValue),
|
|
6219
|
+
assignReference: (reference, inputValue, currentValue, options2) => this.referenceRuntime.assignReference(reference, inputValue, currentValue, options2),
|
|
5952
6220
|
resolveMappedValue: (template, currentValue, responseValue) => this.referenceRuntime.resolveMappedValue(template, currentValue, responseValue),
|
|
5953
6221
|
setWidgetEnabled: (widgetId, enabled) => this.setWidgetEnabled(widgetId, enabled),
|
|
5954
6222
|
clearWidget: (widgetId) => this.clearWidget(widgetId),
|
|
@@ -5968,6 +6236,7 @@ var RuntimeRenderer = class {
|
|
|
5968
6236
|
},
|
|
5969
6237
|
refreshWidgetTree: (widgetId) => this.refreshWidgetTree(widgetId),
|
|
5970
6238
|
renderWidgetTree: (widgetId) => this.renderWidgetTree(widgetId),
|
|
6239
|
+
navigateTo: (url) => this.navigateTo(url),
|
|
5971
6240
|
getContextMenuPosition: (menuId, pointer) => getContextMenuPosition(menuId, pointer ?? null, this.lastPointer, this.nodeById),
|
|
5972
6241
|
setActiveContextMenu: (menu) => {
|
|
5973
6242
|
this.activeContextMenu = menu;
|
|
@@ -5996,7 +6265,9 @@ var RuntimeRenderer = class {
|
|
|
5996
6265
|
this.afterRender = options.afterRender;
|
|
5997
6266
|
this.onRuntimeSnapshot = options.onRuntimeSnapshot ?? this.onRuntimeSnapshot;
|
|
5998
6267
|
const render = async () => {
|
|
6268
|
+
await this.runSystemEvent("onBeforeRender");
|
|
5999
6269
|
await this.rerenderRoot();
|
|
6270
|
+
await this.runSystemEvent("onAfterRender");
|
|
6000
6271
|
};
|
|
6001
6272
|
const renderSync = () => {
|
|
6002
6273
|
void render().catch((error) => {
|
|
@@ -6064,6 +6335,31 @@ var RuntimeRenderer = class {
|
|
|
6064
6335
|
}
|
|
6065
6336
|
return Promise.resolve();
|
|
6066
6337
|
}
|
|
6338
|
+
async runSystemEvent(eventName) {
|
|
6339
|
+
const actions = this.systemEvents[eventName];
|
|
6340
|
+
if (!actions?.length) {
|
|
6341
|
+
return;
|
|
6342
|
+
}
|
|
6343
|
+
await this.actionRuntime.runActions(actions, null, { currentValue: null });
|
|
6344
|
+
}
|
|
6345
|
+
async navigateTo(url) {
|
|
6346
|
+
if (typeof window === "undefined" || !url.trim()) {
|
|
6347
|
+
return;
|
|
6348
|
+
}
|
|
6349
|
+
const safeUrl = sanitizeUrl(url, { allowRelative: true });
|
|
6350
|
+
if (!safeUrl) {
|
|
6351
|
+
throw new Error("Blocked unsafe navigation url");
|
|
6352
|
+
}
|
|
6353
|
+
const nextUrl = new URL(safeUrl, window.location.href);
|
|
6354
|
+
if (nextUrl.origin !== window.location.origin) {
|
|
6355
|
+
window.location.assign(nextUrl.toString());
|
|
6356
|
+
return;
|
|
6357
|
+
}
|
|
6358
|
+
window.history.pushState(window.history.state, "", `${nextUrl.pathname}${nextUrl.search}${nextUrl.hash}`);
|
|
6359
|
+
await this.runSystemEvent("onBeforeNavigate");
|
|
6360
|
+
await this.rerenderRoot();
|
|
6361
|
+
await this.runSystemEvent("onAfterNavigate");
|
|
6362
|
+
}
|
|
6067
6363
|
reindexStaticTree() {
|
|
6068
6364
|
this.nodeByKey.clear();
|
|
6069
6365
|
this.nodeById.clear();
|
|
@@ -6292,6 +6588,7 @@ var RuntimeRenderer = class {
|
|
|
6292
6588
|
existing.id = stateId2;
|
|
6293
6589
|
this.stateById.set(stateId2, existing);
|
|
6294
6590
|
}
|
|
6591
|
+
this.syncStateDefinition(existing, node);
|
|
6295
6592
|
return existing;
|
|
6296
6593
|
}
|
|
6297
6594
|
const stateId = node.id ?? (isGeneratedElementId(key) ? key : void 0);
|
|
@@ -6361,6 +6658,7 @@ var RuntimeRenderer = class {
|
|
|
6361
6658
|
id: stateId,
|
|
6362
6659
|
name: node.name,
|
|
6363
6660
|
enabled: normalizeBoolean(node.enabled, true),
|
|
6661
|
+
definitionSignature: buildDefinitionSignature(node.elements ?? []),
|
|
6364
6662
|
elements: deepClone(node.elements ?? [])
|
|
6365
6663
|
};
|
|
6366
6664
|
break;
|
|
@@ -6371,6 +6669,7 @@ var RuntimeRenderer = class {
|
|
|
6371
6669
|
id: stateId,
|
|
6372
6670
|
name: node.name,
|
|
6373
6671
|
enabled: normalizeBoolean(node.enabled, true),
|
|
6672
|
+
definitionSignature: buildDefinitionSignature(node.elements ?? []),
|
|
6374
6673
|
listboxElements: deepClone(node.elements ?? []),
|
|
6375
6674
|
selected: toFiniteNumber3(node.selected) ?? 0
|
|
6376
6675
|
};
|
|
@@ -6382,6 +6681,7 @@ var RuntimeRenderer = class {
|
|
|
6382
6681
|
id: stateId,
|
|
6383
6682
|
name: node.name,
|
|
6384
6683
|
enabled: normalizeBoolean(node.enabled, true),
|
|
6684
|
+
definitionSignature: buildDefinitionSignature(node.elements ?? []),
|
|
6385
6685
|
comboboxElements: deepClone(node.elements ?? []),
|
|
6386
6686
|
selected: toFiniteNumber3(node.selected) ?? 0
|
|
6387
6687
|
};
|
|
@@ -6445,6 +6745,45 @@ var RuntimeRenderer = class {
|
|
|
6445
6745
|
}
|
|
6446
6746
|
return state;
|
|
6447
6747
|
}
|
|
6748
|
+
syncStateDefinition(state, node) {
|
|
6749
|
+
if (state.widget !== node.widget) {
|
|
6750
|
+
return;
|
|
6751
|
+
}
|
|
6752
|
+
switch (node.widget) {
|
|
6753
|
+
case "list":
|
|
6754
|
+
case "grid-view": {
|
|
6755
|
+
const nextSignature = buildDefinitionSignature(node.elements ?? []);
|
|
6756
|
+
if (state.definitionSignature !== nextSignature) {
|
|
6757
|
+
if (state.widget === "list" || state.widget === "grid-view") {
|
|
6758
|
+
this.clearListElementState(state.key);
|
|
6759
|
+
state.elements = deepClone(node.elements ?? []);
|
|
6760
|
+
state.definitionSignature = nextSignature;
|
|
6761
|
+
}
|
|
6762
|
+
}
|
|
6763
|
+
break;
|
|
6764
|
+
}
|
|
6765
|
+
case "listbox": {
|
|
6766
|
+
const nextSignature = buildDefinitionSignature(node.elements ?? []);
|
|
6767
|
+
if (state.definitionSignature !== nextSignature) {
|
|
6768
|
+
state.listboxElements = deepClone(node.elements ?? []);
|
|
6769
|
+
state.selected = toFiniteNumber3(node.selected) ?? 0;
|
|
6770
|
+
state.definitionSignature = nextSignature;
|
|
6771
|
+
}
|
|
6772
|
+
break;
|
|
6773
|
+
}
|
|
6774
|
+
case "combobox": {
|
|
6775
|
+
const nextSignature = buildDefinitionSignature(node.elements ?? []);
|
|
6776
|
+
if (state.definitionSignature !== nextSignature) {
|
|
6777
|
+
state.comboboxElements = deepClone(node.elements ?? []);
|
|
6778
|
+
state.selected = toFiniteNumber3(node.selected) ?? 0;
|
|
6779
|
+
state.definitionSignature = nextSignature;
|
|
6780
|
+
}
|
|
6781
|
+
break;
|
|
6782
|
+
}
|
|
6783
|
+
default:
|
|
6784
|
+
break;
|
|
6785
|
+
}
|
|
6786
|
+
}
|
|
6448
6787
|
getStateForNode(node, fallbackPath) {
|
|
6449
6788
|
const key = fallbackPath ? this.resolveNodeKey(node, fallbackPath) : node.id ?? this.resolveNodeKey(node, `auto:${node.widget}`);
|
|
6450
6789
|
return this.ensureWidgetState(node, key);
|
|
@@ -6835,7 +7174,7 @@ var RuntimeRenderer = class {
|
|
|
6835
7174
|
return;
|
|
6836
7175
|
}
|
|
6837
7176
|
const descriptor = listState.elements[indexValue];
|
|
6838
|
-
if (!descriptor || !
|
|
7177
|
+
if (!descriptor || !isPlainObject4(descriptor) || typeof descriptor.widget !== "string") {
|
|
6839
7178
|
return;
|
|
6840
7179
|
}
|
|
6841
7180
|
mutate(descriptor);
|
|
@@ -7026,6 +7365,7 @@ var ResourceManager = class {
|
|
|
7026
7365
|
actions = {};
|
|
7027
7366
|
requests = {};
|
|
7028
7367
|
sse = {};
|
|
7368
|
+
systemEvents = {};
|
|
7029
7369
|
i18n = {};
|
|
7030
7370
|
styles = {};
|
|
7031
7371
|
constructor(input = {}) {
|
|
@@ -7035,6 +7375,7 @@ var ResourceManager = class {
|
|
|
7035
7375
|
this.actions = { ...input.actions ?? {} };
|
|
7036
7376
|
this.requests = { ...input.requests ?? {} };
|
|
7037
7377
|
this.sse = { ...input.sse ?? {} };
|
|
7378
|
+
this.systemEvents = { ...input.systemEvents ?? {} };
|
|
7038
7379
|
this.i18n = { ...input.i18n ?? {} };
|
|
7039
7380
|
this.styles = { ...input.styles ?? {} };
|
|
7040
7381
|
}
|
|
@@ -7053,6 +7394,9 @@ var ResourceManager = class {
|
|
|
7053
7394
|
getSseConfigs() {
|
|
7054
7395
|
return Object.values(this.sse);
|
|
7055
7396
|
}
|
|
7397
|
+
getSystemEvents() {
|
|
7398
|
+
return this.systemEvents;
|
|
7399
|
+
}
|
|
7056
7400
|
getI18n() {
|
|
7057
7401
|
return this.i18n;
|
|
7058
7402
|
}
|
|
@@ -19,13 +19,17 @@ type ActionRuntimeOptions = {
|
|
|
19
19
|
} | null) => Promise<void>;
|
|
20
20
|
executeRequest: (requestName: string, currentValue: unknown) => Promise<unknown>;
|
|
21
21
|
resolveReference: (reference: string, currentValue: unknown, responseValue: unknown) => unknown;
|
|
22
|
-
|
|
22
|
+
isEmptyReference: (reference: string, currentValue: unknown, responseValue: unknown) => boolean;
|
|
23
|
+
assignReference: (reference: string, inputValue: unknown, currentValue: unknown, options?: {
|
|
24
|
+
cookieTtl?: unknown;
|
|
25
|
+
}) => void;
|
|
23
26
|
resolveMappedValue: (template: unknown, currentValue: unknown, responseValue: unknown) => unknown;
|
|
24
27
|
setWidgetEnabled: (widgetId: string, enabled: boolean) => void;
|
|
25
28
|
clearWidget: (widgetId: string) => void;
|
|
26
29
|
setUiReference: (widgetId: string, resourceRef: string) => void;
|
|
27
30
|
refreshWidgetTree: (widgetId: string) => Promise<void>;
|
|
28
31
|
renderWidgetTree: (widgetId: string) => Promise<void>;
|
|
32
|
+
navigateTo: (url: string) => Promise<void>;
|
|
29
33
|
getContextMenuPosition: (menuId: string, pointer?: {
|
|
30
34
|
x: number;
|
|
31
35
|
y: number;
|
|
@@ -52,6 +56,7 @@ export declare class ActionRuntime {
|
|
|
52
56
|
private readonly dispatchWidgetEventImpl;
|
|
53
57
|
private readonly executeRequest;
|
|
54
58
|
private readonly resolveReference;
|
|
59
|
+
private readonly isEmptyReference;
|
|
55
60
|
private readonly assignReference;
|
|
56
61
|
private readonly resolveMappedValue;
|
|
57
62
|
private readonly setWidgetEnabled;
|
|
@@ -59,6 +64,7 @@ export declare class ActionRuntime {
|
|
|
59
64
|
private readonly setUiReference;
|
|
60
65
|
private readonly refreshWidgetTree;
|
|
61
66
|
private readonly renderWidgetTree;
|
|
67
|
+
private readonly navigateTo;
|
|
62
68
|
private readonly getContextMenuPosition;
|
|
63
69
|
private readonly setActiveContextMenu;
|
|
64
70
|
private readonly setActiveModalId;
|
package/dist/lib/references.d.ts
CHANGED
|
@@ -13,10 +13,13 @@ type ReferenceRuntimeHost = {
|
|
|
13
13
|
export declare class ReferenceRuntime {
|
|
14
14
|
private readonly host;
|
|
15
15
|
constructor(host: ReferenceRuntimeHost);
|
|
16
|
+
isEmptyReference(reference: string, currentValue: unknown, responseValue: unknown): boolean;
|
|
16
17
|
resolveReference(reference: string, currentValue: unknown, responseValue: unknown): unknown;
|
|
17
18
|
resolveCurrentReference(reference: string, currentValue: unknown): unknown;
|
|
18
19
|
readWidgetField(state: WidgetState, field: string): unknown;
|
|
19
|
-
assignReference(reference: string, inputValue: unknown, _currentValue: unknown
|
|
20
|
+
assignReference(reference: string, inputValue: unknown, _currentValue: unknown, options?: {
|
|
21
|
+
cookieTtl?: unknown;
|
|
22
|
+
}): void;
|
|
20
23
|
clearListElementState(listKey: string): void;
|
|
21
24
|
resolveMappedValue(template: unknown, currentValue: unknown, responseValue: unknown): unknown;
|
|
22
25
|
isListElementContext(value: unknown): value is ListElementContext;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import type { ActionMap, DescriptionNode, I18nMap, RequestMap, SseConfig, StyleMap } from './types.js';
|
|
1
|
+
import type { ActionMap, DescriptionNode, I18nMap, RequestMap, SseConfig, SystemEventsMap, StyleMap } from './types.js';
|
|
2
2
|
export type ResourceManagerInput = {
|
|
3
3
|
ui?: Record<string, DescriptionNode>;
|
|
4
4
|
actions?: ActionMap;
|
|
5
5
|
requests?: RequestMap;
|
|
6
6
|
sse?: Record<string, SseConfig>;
|
|
7
|
+
systemEvents?: SystemEventsMap;
|
|
7
8
|
i18n?: I18nMap;
|
|
8
9
|
styles?: StyleMap;
|
|
9
10
|
};
|
|
@@ -12,6 +13,7 @@ export declare class ResourceManager {
|
|
|
12
13
|
private actions;
|
|
13
14
|
private requests;
|
|
14
15
|
private sse;
|
|
16
|
+
private systemEvents;
|
|
15
17
|
private i18n;
|
|
16
18
|
private styles;
|
|
17
19
|
constructor(input?: ResourceManagerInput);
|
|
@@ -20,6 +22,7 @@ export declare class ResourceManager {
|
|
|
20
22
|
getActionsMap(): ActionMap;
|
|
21
23
|
getRequestsMap(): RequestMap;
|
|
22
24
|
getSseConfigs(): SseConfig[];
|
|
25
|
+
getSystemEvents(): SystemEventsMap;
|
|
23
26
|
getI18n(): I18nMap;
|
|
24
27
|
getStyleMap(): StyleMap;
|
|
25
28
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { RequestSchema, StyleMap } from './types.js';
|
|
2
|
+
export declare function isBlockedObjectKey(key: string): boolean;
|
|
3
|
+
export declare function hasBlockedReferenceSegment(reference: string): boolean;
|
|
4
|
+
export declare function isTrustedConfigOnlyKey(key: string): boolean;
|
|
5
|
+
export declare function templateValueUsesReference(value: unknown): boolean;
|
|
6
|
+
export declare function sanitizeConfigStyleMap(styleMap: StyleMap): StyleMap;
|
|
7
|
+
export declare function sanitizeUrl(rawValue: unknown, options?: {
|
|
8
|
+
allowRelative?: boolean;
|
|
9
|
+
allowMailto?: boolean;
|
|
10
|
+
}): string | null;
|
|
11
|
+
export declare function sanitizeMarkdownSource(markdown: string): string;
|
|
12
|
+
export declare function sanitizeGeneratedHtml(html: string): string;
|
|
13
|
+
export declare function sanitizeSchemaValue(schema: RequestSchema, value: unknown, path?: string): unknown;
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -3,7 +3,8 @@ export type LayoutType = 'vertical' | 'horizontal' | 'grid';
|
|
|
3
3
|
export type TextAlign = 'left' | 'center' | 'right';
|
|
4
4
|
export type VerticalAlign = 'top' | 'center' | 'bottom';
|
|
5
5
|
export type HeadingTag = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
|
6
|
-
export type WidgetEventName = 'onClick' | 'onUserValueChange' | 'onRefresh';
|
|
6
|
+
export type WidgetEventName = 'onClick' | 'onUserValueChange' | 'onRefresh' | 'onEnter' | 'onShiftEnter' | 'onControlEnter';
|
|
7
|
+
export type SystemEventName = 'onBeforeRender' | 'onAfterRender' | 'onBeforeNavigate' | 'onAfterNavigate';
|
|
7
8
|
export type PrimitiveRequestType = 'int' | 'float' | 'boolean' | 'string';
|
|
8
9
|
export type ActionDefinition = {
|
|
9
10
|
action: string;
|
|
@@ -84,11 +85,13 @@ export type I18nMap = Record<string, Record<string, string>>;
|
|
|
84
85
|
export type ActionMap = Record<string, ActionDefinition[]>;
|
|
85
86
|
export type RequestMap = Record<string, RequestDefinition>;
|
|
86
87
|
export type SseConfigInput = SseConfig | SseConfig[];
|
|
88
|
+
export type SystemEventsMap = Partial<Record<SystemEventName, ActionDefinition[]>>;
|
|
87
89
|
export type WidgetState = {
|
|
88
90
|
key: string;
|
|
89
91
|
widget: DescriptionNode['widget'];
|
|
90
92
|
id?: string;
|
|
91
93
|
name?: string;
|
|
94
|
+
definitionSignature?: string;
|
|
92
95
|
currentRef?: string;
|
|
93
96
|
text?: string;
|
|
94
97
|
url?: string;
|
|
@@ -132,6 +135,7 @@ export type RenderJsonOptions = {
|
|
|
132
135
|
actionsMap?: ActionMap;
|
|
133
136
|
requestsMap?: RequestMap;
|
|
134
137
|
sseConfigs?: SseConfigInput;
|
|
138
|
+
systemEvents?: SystemEventsMap;
|
|
135
139
|
actionFunctions?: Record<string, () => unknown>;
|
|
136
140
|
backendUrl?: string;
|
|
137
141
|
resourceManager?: ResourceManager;
|
|
@@ -12,7 +12,7 @@ type BindingEnvironment = {
|
|
|
12
12
|
}) => void;
|
|
13
13
|
updatePointerFromEvent: (event: MouseEvent) => void;
|
|
14
14
|
updateOwningListElementDescriptor: (element: HTMLElement, mutate: (descriptor: DescriptionNode) => void) => void;
|
|
15
|
-
dispatchWidgetEvent: (node: DescriptionNode, eventName: 'onClick' | 'onUserValueChange', key: string, inputValue?: unknown, pointer?: {
|
|
15
|
+
dispatchWidgetEvent: (node: DescriptionNode, eventName: 'onClick' | 'onUserValueChange' | 'onEnter' | 'onShiftEnter' | 'onControlEnter', key: string, inputValue?: unknown, pointer?: {
|
|
16
16
|
x: number;
|
|
17
17
|
y: number;
|
|
18
18
|
} | null) => Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vortexm/vjt",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
"scripts": {
|
|
26
26
|
"build": "npm run build:lib && npm run build:examples && npm run build:types",
|
|
27
27
|
"build:lib": "esbuild src/index.ts --bundle --format=esm --outfile=dist/index.js --loader:.json=json",
|
|
28
|
-
"build:examples": "esbuild
|
|
28
|
+
"build:examples": "esbuild examples/app/examples-app.ts --bundle --format=esm --outfile=dist/examples.js --loader:.json=json",
|
|
29
29
|
"build:types": "tsc -p tsconfig.build.json",
|
|
30
|
-
"watch": "esbuild src/index.ts
|
|
30
|
+
"watch": "esbuild src/index.ts examples/app/examples-app.ts --bundle --format=esm --outdir=dist --loader:.json=json --watch",
|
|
31
31
|
"typecheck": "tsc -p tsconfig.json",
|
|
32
32
|
"lint": "eslint \"src/**/*.ts\"",
|
|
33
|
-
"serve": "
|
|
33
|
+
"serve": "node ./demo/examples-server.mjs",
|
|
34
34
|
"serve:mock-backend": "node ./mock-backend/server.mjs",
|
|
35
35
|
"dev": "concurrently \"npm:watch\" \"npm:serve\" \"npm:serve:mock-backend\"",
|
|
36
36
|
"prepack": "npm run build"
|
package/vjt-styles.css
CHANGED
|
@@ -33,6 +33,33 @@ html {
|
|
|
33
33
|
--vjt-font-heading: Georgia, "Times New Roman", serif;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
:root {
|
|
37
|
+
background: var(--vjt-bg);
|
|
38
|
+
color: var(--vjt-text);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
* {
|
|
42
|
+
box-sizing: border-box;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
html,
|
|
46
|
+
body {
|
|
47
|
+
min-height: 100%;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
body {
|
|
51
|
+
margin: 0;
|
|
52
|
+
min-height: 100vh;
|
|
53
|
+
background:
|
|
54
|
+
radial-gradient(circle at top left, var(--vjt-bg-glow), transparent 28%),
|
|
55
|
+
linear-gradient(180deg, color-mix(in srgb, var(--vjt-bg) 88%, white) 0%, var(--vjt-bg) 100%);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#app {
|
|
59
|
+
width: 100%;
|
|
60
|
+
min-height: 100vh;
|
|
61
|
+
}
|
|
62
|
+
|
|
36
63
|
.vjt-static-text,
|
|
37
64
|
.vjt-md-text,
|
|
38
65
|
.vjt-edit,
|