From 2163effbdc307d9a975b7a3505ec48ace111eced Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=85=D0=B8=D0=BF=D0=BE=D0=B2=20=D0=94=D0=BC?= =?UTF-8?q?=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Fri, 6 Feb 2026 16:13:04 +0200 Subject: [PATCH 01/35] feat(bootstrap-addons): remove AVJ from JSON validation, allow to pass SchemaNode directly --- package-lock.json | 82 ++++++------------ .../vanilla-jsoneditor/package.json | 4 +- packages/bootstrap-addons/package.json | 10 +-- .../src/forms/json-input/json-input.ts | 84 +++++++++---------- 4 files changed, 70 insertions(+), 110 deletions(-) diff --git a/package-lock.json b/package-lock.json index a4cbbdf..31e0be1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4153,17 +4153,26 @@ } }, "node_modules/@fortawesome/free-solid-svg-icons": { - "version": "6.7.2", - "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz", - "integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-7.1.0.tgz", + "integrity": "sha512-Udu3K7SzAo9N013qt7qmm22/wo2hADdheXtBfxFTecp+ogsc0caQNRKEb7pkvvagUGOpG9wJC1ViH6WXs8oXIA==", "license": "(CC-BY-4.0 AND MIT)", "dependencies": { - "@fortawesome/fontawesome-common-types": "6.7.2" + "@fortawesome/fontawesome-common-types": "7.1.0" }, "engines": { "node": ">=6" } }, + "node_modules/@fortawesome/free-solid-svg-icons/node_modules/@fortawesome/fontawesome-common-types": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.1.0.tgz", + "integrity": "sha512-l/BQM7fYntsCI//du+6sEnHOP6a74UixFyOYUyz2DLMXKx+6DEhfR3F2NYGE45XH1JJuIamacb4IZs9S0ZOWLA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -46244,11 +46253,10 @@ } }, "node_modules/vanilla-jsoneditor": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/vanilla-jsoneditor/-/vanilla-jsoneditor-3.7.0.tgz", - "integrity": "sha512-Rvm3/22T7IFj5qvnDAvPiW1RLwjJMEgxQC+6J4eoVe2/gDB/tbz1Wb6GbnNuh/42ybO6IdTQK9dXkgqMkWcqZQ==", + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/vanilla-jsoneditor/-/vanilla-jsoneditor-3.11.0.tgz", + "integrity": "sha512-/Zw09Yv8Q2i1yC2q5cDsNtFpyFOQu1Aci4u8dsHwyzhueWnibCMteZz6n1aE9+RGgU92Ni5K4cXTMDUowJPE0A==", "license": "ISC", - "peer": true, "dependencies": { "@codemirror/autocomplete": "^6.18.1", "@codemirror/commands": "^6.7.1", @@ -46258,8 +46266,8 @@ "@codemirror/search": "^6.5.6", "@codemirror/state": "^6.4.1", "@codemirror/view": "^6.34.1", - "@fortawesome/free-regular-svg-icons": "^6.6.0", - "@fortawesome/free-solid-svg-icons": "^6.6.0", + "@fortawesome/free-regular-svg-icons": "^6.6.0 || ^7.0.1", + "@fortawesome/free-solid-svg-icons": "^6.6.0 || ^7.0.1", "@jsonquerylang/jsonquery": "^3.1.1 || ^4.0.0 || ^5.0.0", "@lezer/highlight": "^1.2.1", "@replit/codemirror-indentation-markers": "^6.5.3", @@ -46283,7 +46291,6 @@ "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", "license": "MIT", - "peer": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -48613,77 +48620,38 @@ }, "packages-adapters/vanilla-jsoneditor": { "name": "@ekzo-dev/vanilla-jsoneditor", - "version": "3.10.0", + "version": "3.11.0", "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.14", "immutable-json-patch": "^6.0.1", - "vanilla-jsoneditor": "~3.10.0" + "vanilla-jsoneditor": "~3.11.0" }, "peerDependencies": { "aurelia": "^2.0.0-rc.0" } }, - "packages-adapters/vanilla-jsoneditor/node_modules/diff-sequences": { - "version": "29.6.3", - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "packages-adapters/vanilla-jsoneditor/node_modules/vanilla-jsoneditor": { - "version": "3.10.0", - "license": "ISC", - "dependencies": { - "@codemirror/autocomplete": "^6.18.1", - "@codemirror/commands": "^6.7.1", - "@codemirror/lang-json": "^6.0.1", - "@codemirror/language": "^6.10.3", - "@codemirror/lint": "^6.8.2", - "@codemirror/search": "^6.5.6", - "@codemirror/state": "^6.4.1", - "@codemirror/view": "^6.34.1", - "@fortawesome/free-regular-svg-icons": "^6.6.0 || ^7.0.1", - "@fortawesome/free-solid-svg-icons": "^6.6.0 || ^7.0.1", - "@jsonquerylang/jsonquery": "^3.1.1 || ^4.0.0 || ^5.0.0", - "@lezer/highlight": "^1.2.1", - "@replit/codemirror-indentation-markers": "^6.5.3", - "ajv": "^8.17.1", - "codemirror-wrapped-line-indent": "^1.0.8", - "diff-sequences": "^29.6.3", - "immutable-json-patch": "^6.0.1", - "jmespath": "^0.16.0", - "json-source-map": "^0.6.1", - "jsonpath-plus": "^10.3.0", - "jsonrepair": "^3.0.0", - "lodash-es": "^4.17.21", - "memoize-one": "^6.0.0", - "natural-compare-lite": "^1.4.0", - "svelte": "^5.0.0", - "vanilla-picker": "^2.12.3" - } - }, "packages/bootstrap-addons": { "name": "@ekzo-dev/bootstrap-addons", - "version": "5.3.11", + "version": "5.3.12", "license": "MIT", "dependencies": { "@ekzo-dev/bootstrap": "~5.3.0", "@ekzo-dev/toolkit": "^1.2.4", - "@ekzo-dev/vanilla-jsoneditor": "^3.10.0", - "@fortawesome/free-solid-svg-icons": "^6.5.2", + "@ekzo-dev/vanilla-jsoneditor": "~3.11.0", + "@fortawesome/free-solid-svg-icons": "^7.0.1", "@js-temporal/polyfill": "^0.5.1", "@types/json-schema": "^7.0.14", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", - "json-schema-library": "^10.5.1" + "json-schema-library": "^10.5.2" }, "peerDependencies": { "@popperjs/core": "^2.11.8", "aurelia": "^2.0.0-rc.0", "bootstrap": "~5.3.7", "immutable-json-patch": "^6.0.1", - "vanilla-jsoneditor": "~3.7.0" + "vanilla-jsoneditor": "~3.11.0" } }, "packages/bootstrap-data-table": { diff --git a/packages-adapters/vanilla-jsoneditor/package.json b/packages-adapters/vanilla-jsoneditor/package.json index 1dc8ad0..d4bedf1 100644 --- a/packages-adapters/vanilla-jsoneditor/package.json +++ b/packages-adapters/vanilla-jsoneditor/package.json @@ -1,7 +1,7 @@ { "name": "@ekzo-dev/vanilla-jsoneditor", "description": "Aurelia JSON Editor adapter", - "version": "3.10.0", + "version": "3.11.0", "homepage": "https://github.com/ekzo-dev/aurelia-components/tree/main/packages-adapters/vanilla-json-editor", "repository": { "type": "git", @@ -11,7 +11,7 @@ "dependencies": { "@types/json-schema": "^7.0.14", "immutable-json-patch": "^6.0.1", - "vanilla-jsoneditor": "~3.10.0" + "vanilla-jsoneditor": "~3.11.0" }, "peerDependencies": { "aurelia": "^2.0.0-rc.0" diff --git a/packages/bootstrap-addons/package.json b/packages/bootstrap-addons/package.json index 4ff2fdb..475dd46 100644 --- a/packages/bootstrap-addons/package.json +++ b/packages/bootstrap-addons/package.json @@ -1,7 +1,7 @@ { "name": "@ekzo-dev/bootstrap-addons", "description": "Aurelia Bootstrap additional component", - "version": "5.3.11", + "version": "5.3.12", "homepage": "https://github.com/ekzo-dev/aurelia-components/tree/main/packages/bootstrap-addons", "repository": { "type": "git", @@ -10,20 +10,20 @@ "license": "MIT", "dependencies": { "@ekzo-dev/bootstrap": "~5.3.0", - "@ekzo-dev/vanilla-jsoneditor": "^3.10.0", + "@ekzo-dev/vanilla-jsoneditor": "~3.11.0", "@ekzo-dev/toolkit": "^1.2.4", - "@fortawesome/free-solid-svg-icons": "^6.5.2", + "@fortawesome/free-solid-svg-icons": "^7.0.1", "@types/json-schema": "^7.0.14", "@js-temporal/polyfill": "^0.5.1", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", - "json-schema-library": "^10.5.1" + "json-schema-library": "^10.5.2" }, "peerDependencies": { "aurelia": "^2.0.0-rc.0", "bootstrap": "~5.3.7", "@popperjs/core": "^2.11.8", - "vanilla-jsoneditor": "~3.7.0", + "vanilla-jsoneditor": "~3.11.0", "immutable-json-patch": "^6.0.1" }, "main": "src/index.ts", diff --git a/packages/bootstrap-addons/src/forms/json-input/json-input.ts b/packages/bootstrap-addons/src/forms/json-input/json-input.ts index 4f94131..7160e0b 100644 --- a/packages/bootstrap-addons/src/forms/json-input/json-input.ts +++ b/packages/bootstrap-addons/src/forms/json-input/json-input.ts @@ -17,16 +17,17 @@ import type { Validator, } from 'vanilla-jsoneditor'; +import { ProxyObservable } from '@aurelia/runtime'; import { coerceBoolean } from '@ekzo-dev/toolkit'; import { JsonEditor } from '@ekzo-dev/vanilla-jsoneditor'; import { faUpRightAndDownLeftFromCenter } from '@fortawesome/free-solid-svg-icons/faUpRightAndDownLeftFromCenter'; -import Ajv, { ErrorObject, Options, ValidateFunction } from 'ajv'; +import Ajv, { ErrorObject, Options } from 'ajv'; import Ajv2019 from 'ajv/dist/2019'; import Ajv2020 from 'ajv/dist/2020'; import addFormats from 'ajv-formats'; import { bindable, BindingMode, customElement } from 'aurelia'; import { parsePath } from 'immutable-json-patch'; -import { compileSchema, JsonError, JsonSchema, SchemaNode } from 'json-schema-library'; +import { CompileOptions, compileSchema, JsonError, SchemaNode } from 'json-schema-library'; const patternMap: Record = { '^[A-Za-z_][-A-Za-z0-9._]*$': '^[A-Za-z_][\\-A-Za-z0-9._]*$', @@ -48,11 +49,11 @@ export class BsJsonInput { @bindable(coerceBoolean) disabled: boolean = false; - @bindable({ set: (v) => (v === '' || v === true || v === 'true' ? true : v) }) - jsonSchema?: JSONSchema | boolean; + @bindable({ set: (v: BsJsonInput['jsonSchema'] | string) => (v === '' || v === true || v === 'true' ? true : v) }) + jsonSchema?: JSONSchema | SchemaNode | boolean; @bindable() - ajvOptions: Options = {}; + validatorOptions?: CompileOptions; @bindable() jsonEditorOptions: JSONEditorPropsOptional = {}; @@ -148,52 +149,24 @@ export class BsJsonInput { } get validator(): Validator | undefined { - const { schemaVersion, disabled, ajvOptions, jsonSchema } = this; - // use raw object because proxies don't work with private properties - const rawThis = this['__raw__'] as BsJsonInput; - // use jsonSchema from raw object to pass original (non-proxied) object to AJV - const { jsonSchema: rawJsonSchema } = rawThis; + const { schemaVersion, disabled, jsonSchema } = this; + const rawThis = ProxyObservable.getRaw(this); - if (jsonSchema && typeof jsonSchema === 'object' && !disabled) { - const ajv = rawThis.#initAjv(jsonSchema.$schema as string, ajvOptions); + if (disabled) return; - addFormats(ajv); - let validate: ValidateFunction; - let schema: SchemaNode; - - try { - schema = compileSchema(rawJsonSchema as JsonSchema); - } catch (e) { - console.error('json-schema-library validator compilation error', e); - } - - try { - validate = ajv.compile(rawJsonSchema); - } catch (e) { - console.error('AJV validator compilation error', e); - } + if (jsonSchema && typeof jsonSchema === 'object') { + const schema = rawThis.#initJsonSchemaLibrary(); return (json: unknown): ValidationError[] => { // do not validate empty documents - if (json === undefined) return []; + if (json === undefined || !schema) return []; - let allErrors: ValidationError[] = []; - - if (schema) { - const { errors } = schema.validate(json); - - allErrors = rawThis.#processErrors(errors, json); - } + const { errors } = schema.validate(json); - if (validate) { - validate(json); - allErrors = allErrors.concat(rawThis.#processErrorsAjv(validate.errors, json)); - } - - return allErrors; + return rawThis.#processErrors(errors, json); }; - } else if (schemaVersion != null && !disabled) { - const ajv = rawThis.#initAjv(schemaVersion, ajvOptions); + } else if (schemaVersion != null) { + const ajv = rawThis.#initAjv(schemaVersion); return (json: unknown): ValidationError[] => { // do not validate empty documents @@ -216,7 +189,7 @@ export class BsJsonInput { } } - #initAjv($schema: string, ajvOptions: Options): Ajv { + #initAjv($schema: string): Ajv { // some regexp's in 2019-09/2020-12 meta-schemas are not compatible with 'v' flag, so update them const regExp = (pattern: string) => new RegExp(patternMap[pattern] ?? pattern, 'v'); @@ -228,7 +201,7 @@ export class BsJsonInput { code: { regExp, }, - ...ajvOptions, + allErrors: true, }; let ajv: Ajv; @@ -245,9 +218,28 @@ export class BsJsonInput { ajv = new Ajv(options); } + addFormats(ajv); + return ajv; } + #initJsonSchemaLibrary(): SchemaNode | undefined { + const { jsonSchema } = this; + + // already a SchemaNode + if ((jsonSchema as SchemaNode).evaluationPath) { + return jsonSchema as SchemaNode; + } + + try { + return compileSchema(jsonSchema as JSONSchema, this.validatorOptions); + } catch (e) { + console.error('json-schema-library validator compilation error', e); + + return; + } + } + #getSchemaDefinitions(schema: JSONSchema): JSONSchemaDefinitions { return (schema.$defs ?? schema.definitions) as JSONSchemaDefinitions; } @@ -272,7 +264,7 @@ export class BsJsonInput { return (errors || []).map((error) => ({ path: parsePath(json, error.data.pointer), message: error.message || 'Unknown error', - severity: 'error' as ValidationSeverity, + severity: 'warning' as ValidationSeverity, })); } } From 6478755c29736c8c4abeaa9ee3910acfe814b783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=85=D0=B8=D0=BF=D0=BE=D0=B2=20=D0=94=D0=BC?= =?UTF-8?q?=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Sat, 7 Feb 2026 11:51:38 +0200 Subject: [PATCH 02/35] feat(bootstrap): minimize DOM bindings usage to improve performance --- package-lock.json | 6 +- packages-adapters/bootstrap/package.json | 2 +- .../bootstrap/src/forms/base-field.ts | 57 +++++++++++++++++-- .../src/forms/checkbox/checkbox.html | 12 +--- .../bootstrap/src/forms/checkbox/checkbox.ts | 9 +-- .../bootstrap/src/forms/input/input.html | 35 ++---------- .../bootstrap/src/forms/input/input.ts | 26 +++++++-- .../bootstrap/src/forms/radio/radio.html | 10 +--- .../bootstrap/src/forms/select/select.html | 15 ++--- .../src/forms/textarea/textarea.html | 17 ++---- packages/bootstrap-addons/package.json | 4 +- .../forms/duration-input/duration-input.html | 6 +- .../src/forms/select/select.html | 33 +++++------ .../src/forms/select/select.ts | 5 -- 14 files changed, 119 insertions(+), 118 deletions(-) diff --git a/package-lock.json b/package-lock.json index 31e0be1..25b657b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48529,7 +48529,7 @@ }, "packages-adapters/bootstrap": { "name": "@ekzo-dev/bootstrap", - "version": "5.3.2", + "version": "5.3.3", "license": "MIT", "dependencies": { "@ekzo-dev/toolkit": "^1.1.0", @@ -48633,10 +48633,10 @@ }, "packages/bootstrap-addons": { "name": "@ekzo-dev/bootstrap-addons", - "version": "5.3.12", + "version": "5.3.13", "license": "MIT", "dependencies": { - "@ekzo-dev/bootstrap": "~5.3.0", + "@ekzo-dev/bootstrap": "~5.3.3", "@ekzo-dev/toolkit": "^1.2.4", "@ekzo-dev/vanilla-jsoneditor": "~3.11.0", "@fortawesome/free-solid-svg-icons": "^7.0.1", diff --git a/packages-adapters/bootstrap/package.json b/packages-adapters/bootstrap/package.json index 2143967..bb7d83f 100644 --- a/packages-adapters/bootstrap/package.json +++ b/packages-adapters/bootstrap/package.json @@ -1,7 +1,7 @@ { "name": "@ekzo-dev/bootstrap", "description": "Aurelia Bootstrap adapter", - "version": "5.3.2", + "version": "5.3.3", "homepage": "https://github.com/ekzo-dev/aurelia-components/tree/main/packages-adapters/bootstrap", "repository": { "type": "git", diff --git a/packages-adapters/bootstrap/src/forms/base-field.ts b/packages-adapters/bootstrap/src/forms/base-field.ts index 923be6c..e93efec 100644 --- a/packages-adapters/bootstrap/src/forms/base-field.ts +++ b/packages-adapters/bootstrap/src/forms/base-field.ts @@ -3,6 +3,27 @@ import './common.scss'; import { coerceBoolean, uniqueId } from '@ekzo-dev/toolkit'; import { bindable, ICustomElementViewModel } from 'aurelia'; +const stringProperties = new Set([ + 'name', + 'form', + 'title', + 'autocomplete', + 'placeholder', + 'type', + 'step', + 'min', + 'max', + 'size', + 'minlength', + 'maxlength', + 'pattern', + 'fileAccept', + 'inputmode', + 'rows', +]); +const booleanProperties = new Set(['disabled', 'required', 'readonly', 'multiple']); +const allProperties = [...stringProperties, ...booleanProperties]; + export class BaseField implements ICustomElementViewModel { @bindable() name?: string; @@ -34,16 +55,40 @@ export class BaseField implements ICustomElementViewModel { @bindable() form?: string; + readonly control!: HTMLInputElement; + binding() { if (!this.id) { this.id = uniqueId(); } } - /** - * Пустой метод для работы обработчика в дочерних классах, не удалять! - * Без него дочерние обработчики почему-то не срабатывают - * @param value - */ - valueChanged(value: any): void {} + bound() { + allProperties.forEach((prop) => { + if (this[prop] != null) { + this.propertyChanged(prop, this[prop]); + } + }); + } + + propertyChanged(key: PropertyKey, newValue: unknown, oldValue?: unknown) { + const { control } = this; + const prop = key.toString(); + + if (!control || prop === 'value') return; + + if (stringProperties.has(prop)) { + const isEmpty = newValue == null || newValue === ''; + // TODO: remove after https://github.com/aurelia/aurelia/issues/2383 + const attr = prop === 'fileAccept' ? 'accept' : prop; + + if (isEmpty && oldValue) { + control.removeAttribute(attr); + } else if (!isEmpty) { + control.setAttribute(attr, newValue.toString()); + } + } else if (booleanProperties.has(prop)) { + control[prop] = newValue; + } + } } diff --git a/packages-adapters/bootstrap/src/forms/checkbox/checkbox.html b/packages-adapters/bootstrap/src/forms/checkbox/checkbox.html index 6e662a0..50e86dd 100644 --- a/packages-adapters/bootstrap/src/forms/checkbox/checkbox.html +++ b/packages-adapters/bootstrap/src/forms/checkbox/checkbox.html @@ -1,24 +1,18 @@