From 87fe52b34b01ee32a04c09691ec91a147ccd6b09 Mon Sep 17 00:00:00 2001 From: Kris Date: Mon, 12 Jan 2026 02:34:39 +0530 Subject: [PATCH 1/2] added custom button to in the trains manager --- .../core/src/trait_manager/view/TraitView.ts | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/packages/core/src/trait_manager/view/TraitView.ts b/packages/core/src/trait_manager/view/TraitView.ts index 03c94050d..e385ba088 100644 --- a/packages/core/src/trait_manager/view/TraitView.ts +++ b/packages/core/src/trait_manager/view/TraitView.ts @@ -83,11 +83,11 @@ export default class TraitView extends View { this.removed(); } - init() {} - removed() {} - onRender(props: ReturnType) {} - onUpdate(props: ReturnType) {} - onEvent(props: ReturnType & { event: Event }) {} + init() { } + removed() { } + onRender(props: ReturnType) { } + onUpdate(props: ReturnType) { } + onEvent(props: ReturnType & { event: Event }) { } /** * Fires when the input is changed @@ -261,23 +261,32 @@ export default class TraitView extends View { } render() { - const { $el, pfx, ppfx, model } = this; - const { type, id } = model.attributes; + const { $el, pfx, ppfx, model , config } = this; + const { type, id } = model.attributes; + const {iconAdd } = config; const hasLabel = this.hasLabel && this.hasLabel(); const cls = `${pfx}trait`; delete this.$input; let tmpl = `
${hasLabel ? `
` : ''}
- ${ - this.templateInput - ? isFunction(this.templateInput) - ? this.templateInput(this.getClbOpts()) - : this.templateInput - : '' - } + ${this.templateInput + ? isFunction(this.templateInput) + ? this.templateInput(this.getClbOpts()) + : this.templateInput + : '' + } +
+
+
+
+
+ + $${iconAdd} +
-
`; + + `; $el.empty().append(tmpl); hasLabel && this.renderLabel(); this.renderField(); From 7e6b8736f9c2634094a86d1c56e403b87d216072 Mon Sep 17 00:00:00 2001 From: Kris Date: Wed, 14 Jan 2026 02:47:29 +0530 Subject: [PATCH 2/2] (feat) implement custom attributes input from UI --- docs/api/datasources.md | 21 ++++-- .../core/src/trait_manager/config/config.ts | 2 + .../core/src/trait_manager/view/TraitView.ts | 36 ++++------ .../core/src/trait_manager/view/TraitsView.ts | 70 ++++++++++++++++++- .../specs/trait_manager/view/TraitsView.ts | 42 +++++++++++ 5 files changed, 144 insertions(+), 27 deletions(-) diff --git a/docs/api/datasources.md b/docs/api/datasources.md index 108caa2e3..ae853a24b 100644 --- a/docs/api/datasources.md +++ b/docs/api/datasources.md @@ -113,6 +113,18 @@ const ds = dsm.get('my_data_source_id'); Returns **[DataSource]** Data source. +## getAll + +Return all data sources. + +### Examples + +```javascript +const ds = dsm.getAll(); +``` + +Returns **[Array][8]<[DataSource]>** + ## getValue Get value from data sources by path. @@ -121,6 +133,7 @@ Get value from data sources by path. * `path` **[String][7]** Path to value. * `defValue` **any** Default value if the path is not found. +* `opts` **{context: Record<[string][7], any>?}?** Returns **any** const value = dsm.getValue('ds\_id.record\_id.propName', 'defaultValue'); @@ -139,7 +152,7 @@ Set value in data sources by path. dsm.setValue('ds_id.record_id.propName', 'new value'); ``` -Returns **[Boolean][8]** Returns true if the value was set successfully +Returns **[Boolean][9]** Returns true if the value was set successfully ## remove @@ -183,7 +196,7 @@ data record, and optional property path. Store data sources to a JSON object. -Returns **[Array][9]** Stored data sources. +Returns **[Array][8]** Stored data sources. ## load @@ -209,6 +222,6 @@ Returns **[Object][6]** Loaded data sources. [7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String -[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean +[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array -[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array +[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean diff --git a/packages/core/src/trait_manager/config/config.ts b/packages/core/src/trait_manager/config/config.ts index bd149d1ea..d0baeec1d 100644 --- a/packages/core/src/trait_manager/config/config.ts +++ b/packages/core/src/trait_manager/config/config.ts @@ -20,6 +20,8 @@ export interface TraitManagerConfig { custom?: boolean; optionsTarget?: Record[]; + + customTraitValidationRegex?: RegExp; } const config: () => TraitManagerConfig = () => ({ diff --git a/packages/core/src/trait_manager/view/TraitView.ts b/packages/core/src/trait_manager/view/TraitView.ts index e385ba088..146f918f3 100644 --- a/packages/core/src/trait_manager/view/TraitView.ts +++ b/packages/core/src/trait_manager/view/TraitView.ts @@ -83,11 +83,11 @@ export default class TraitView extends View { this.removed(); } - init() { } - removed() { } - onRender(props: ReturnType) { } - onUpdate(props: ReturnType) { } - onEvent(props: ReturnType & { event: Event }) { } + init() {} + removed() {} + onRender(props: ReturnType) {} + onUpdate(props: ReturnType) {} + onEvent(props: ReturnType & { event: Event }) {} /** * Fires when the input is changed @@ -261,29 +261,21 @@ export default class TraitView extends View { } render() { - const { $el, pfx, ppfx, model , config } = this; - const { type, id } = model.attributes; - const {iconAdd } = config; + const { $el, pfx, ppfx, model, config } = this; + const { type, id } = model.attributes; const hasLabel = this.hasLabel && this.hasLabel(); const cls = `${pfx}trait`; delete this.$input; let tmpl = `
${hasLabel ? `
` : ''}
- ${this.templateInput - ? isFunction(this.templateInput) - ? this.templateInput(this.getClbOpts()) - : this.templateInput - : '' - } -
-
-
-
-
- - $${iconAdd} - + ${ + this.templateInput + ? isFunction(this.templateInput) + ? this.templateInput(this.getClbOpts()) + : this.templateInput + : '' + }
`; diff --git a/packages/core/src/trait_manager/view/TraitsView.ts b/packages/core/src/trait_manager/view/TraitsView.ts index a070cd480..1132d4293 100644 --- a/packages/core/src/trait_manager/view/TraitsView.ts +++ b/packages/core/src/trait_manager/view/TraitsView.ts @@ -1,5 +1,6 @@ import TraitManager from '..'; import CategoryView from '../../abstract/ModuleCategoryView'; +import Component from '../../dom_components/model/Component'; import DomainViews from '../../domain_abstract/view/DomainViews'; import EditorModel from '../../editor/model/Editor'; import Trait from '../model/Trait'; @@ -16,6 +17,7 @@ interface TraitsViewProps { const ATTR_CATEGORIES = 'data-categories'; const ATTR_NO_CATEGORIES = 'data-no-categories'; +const CUSTOM_TRAIT_VALIDATION_REGEX_DEFAULT = new RegExp('^data-[a-z][a-z0-9-]*$'); export default class TraitsView extends DomainViews { reuseView = true; @@ -33,6 +35,55 @@ export default class TraitsView extends DomainViews { itemsView: TraitManager['types']; collection: Traits; + events() { + return { + 'click [data-add-trait]': 'handleAddTrait', + 'keyup [data-add-trait-input]': 'handleInputKeyup', + }; + } + + handleAddTrait() { + const input = this.el.querySelector('[data-add-trait-input]') as HTMLInputElement; + const name = input.value.trim(); + + if (name) { + const component = this.validateTraitName(name); + if (!component) { + return; + } + + const attributes = component.getAttributes(); + if (attributes[name]) { + return; + } + component.addTrait([name]); + component.addAttributes({ [name]: '' }); + + input.value = ''; + } + } + + handleInputKeyup(e: KeyboardEvent) { + if (e.key === 'Enter') { + this.handleAddTrait(); + } + } + + validateTraitName(name: string): Component | undefined { + const component = this.em.getSelected(); + if (component) { + const attributes = component.getAttributes(); + if (attributes[name]) { + return undefined; + } + if (this.config.customTraitValidationRegex) { + return this.config.customTraitValidationRegex.test(name) ? component : undefined; + } + return CUSTOM_TRAIT_VALIDATION_REGEX_DEFAULT.test(name) ? component : undefined; + } + return undefined; + } + constructor(props: TraitsViewProps, itemsView: TraitManager['types']) { super(props); this.itemsView = itemsView; @@ -61,7 +112,14 @@ export default class TraitsView extends DomainViews { const { ppfx, em } = this; const comp = em.getSelected(); this.el.className = `${this.traitContClass}s ${ppfx}one-bg ${ppfx}two-color`; + if (this.collection) { + this.stopListening(this.collection); + } this.collection = comp?.traits || new Traits([], { em }); + const collection = this.collection; + this.listenTo(collection, 'add', (model) => this.add(model)); + this.listenTo(collection, 'reset', this.render); + this.listenTo(collection, 'remove', this.render); this.render(); } @@ -125,14 +183,24 @@ export default class TraitsView extends DomainViews { } render() { - const { el, ppfx, catsClass, traitContClass, classNoCat } = this; + const { el, ppfx, catsClass, traitContClass, classNoCat, config } = this; const frag = document.createDocumentFragment(); delete this.catsEl; delete this.traitsEl; this.renderedCategories = new Map(); + const iconAdd = (config as any).iconAdd || '+'; + el.innerHTML = `
+ `; this.collection.forEach((model) => this.add(model, frag)); diff --git a/packages/core/test/specs/trait_manager/view/TraitsView.ts b/packages/core/test/specs/trait_manager/view/TraitsView.ts index c0e584d90..2e0c598fd 100644 --- a/packages/core/test/specs/trait_manager/view/TraitsView.ts +++ b/packages/core/test/specs/trait_manager/view/TraitsView.ts @@ -1,5 +1,6 @@ import Trait from '../../../../src/trait_manager/model/Trait'; import TraitView from '../../../../src/trait_manager/view/TraitView'; +import TraitsView from '../../../../src/trait_manager/view/TraitsView'; import Component from '../../../../src/dom_components/model/Component'; import EditorModel from '../../../../src/editor/model/Editor'; import Editor from '../../../../src/editor'; @@ -70,3 +71,44 @@ describe('TraitView', () => { expect(target2.get('attributes')).toEqual(eq2); }); }); + +describe('TraitsView', () => { + let em: EditorModel; + let traitsView: TraitsView; + let component: Component; + + beforeEach(() => { + em = new Editor().getModel(); + component = new Component({}, { em, config: em.Components.config }); + em.setSelected(component); + }); + + test('validateTraitName with default regex', () => { + traitsView = new TraitsView({ + collection: [], + editor: em, + config: em.Traits.getConfig(), + }, {}); + + expect(traitsView.validateTraitName('data-test')).toBeTruthy(); + expect(traitsView.validateTraitName('invalid name')).toBeFalsy(); + }); + + test('validateTraitName with custom regex', () => { + const config = { ...em.Traits.getConfig(), customTraitValidationRegex: /^[a-z]+$/ }; + traitsView = new TraitsView({ + collection: [], + editor: em, + config, + }, {}); + + expect(traitsView.validateTraitName('abc')).toBeTruthy(); + expect(traitsView.validateTraitName('abc1')).toBeFalsy(); + }); + + test('validateTraitName returns undefined if attribute exists', () => { + component.addAttributes({ 'data-test': 'value' }); + traitsView = new TraitsView({ collection: [], editor: em, config: em.Traits.getConfig() }, {}); + expect(traitsView.validateTraitName('data-test')).toBeUndefined(); + }); +});