@ascentgl/ads-ui 21.108.0 → 21.108.1

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.
@@ -556,6 +556,59 @@ function contrastTextColor(backgroundColor) {
556
556
  return luminance > 0.7 ? '#000' : '#fff';
557
557
  }
558
558
 
559
+ class CreateTagHelper {
560
+ static trimString(tagName) {
561
+ return tagName.trim().toLowerCase();
562
+ }
563
+ /** Builds a comparison key for tag identity: case-insensitive name and color */
564
+ static tagKey(tag) {
565
+ return `${CreateTagHelper.trimString(tag.tag)}::${CreateTagHelper.trimString(tag.color)}`;
566
+ }
567
+ /** Returns the next available numeric ID based on existing tags */
568
+ static getNextId(tags) {
569
+ let max = 0;
570
+ for (const tag of tags) {
571
+ const num = Number(tag.id);
572
+ if (Number.isFinite(num) && num > max) {
573
+ max = num;
574
+ }
575
+ }
576
+ return max + 1;
577
+ }
578
+ /** Applies formatting rules to the tag name: alphanumeric filtering, lowercase, space normalization */
579
+ static formatTagName(tagName, alphanumeric, lowercase) {
580
+ let formattedName = tagName;
581
+ if (alphanumeric) {
582
+ formattedName = formattedName.replace(/[^a-z0-9\s]/gi, '');
583
+ }
584
+ if (lowercase) {
585
+ formattedName = formattedName.toLowerCase();
586
+ }
587
+ if (formattedName.startsWith(' ')) {
588
+ formattedName = formattedName.replace(/^\s+/gm, '');
589
+ }
590
+ if (formattedName.includes(' ')) {
591
+ formattedName = formattedName.replace(/\s\s+/gm, ' ');
592
+ }
593
+ return formattedName;
594
+ }
595
+ /** Assigns new IDs to tags with empty or duplicate IDs. Preserves existing unique IDs.
596
+ * New IDs are generated as the largest existing numeric ID + 1. */
597
+ static sanitizeTags(tags) {
598
+ const seen = new Set();
599
+ let nextId = CreateTagHelper.getNextId(tags);
600
+ return tags.map((tag) => {
601
+ if (!tag.id || seen.has(tag.id)) {
602
+ const id = String(nextId++);
603
+ seen.add(id);
604
+ return { ...tag, id };
605
+ }
606
+ seen.add(tag.id);
607
+ return tag;
608
+ });
609
+ }
610
+ }
611
+
559
612
  class AdsTagContainerComponent {
560
613
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: AdsTagContainerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
561
614
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.6", type: AdsTagContainerComponent, isStandalone: false, selector: "ads-tag-container", ngImport: i0, template: "<div class=\"tag-container\">\r\n <ng-content></ng-content>\r\n</div>\r\n", styles: [".tag-container{display:flex;justify-content:flex-end;align-items:flex-start;gap:16px;flex-wrap:wrap;height:24px}\n"] }); }
@@ -714,7 +767,7 @@ class AdsCreateTagComponent {
714
767
  const seen = new Set();
715
768
  const result = [];
716
769
  for (let i = all.length - 1; i >= 0; i--) {
717
- const key = this.tagKey(all[i]);
770
+ const key = CreateTagHelper.tagKey(all[i]);
718
771
  if (!seen.has(key)) {
719
772
  seen.add(key);
720
773
  result.push(all[i]);
@@ -728,8 +781,8 @@ class AdsCreateTagComponent {
728
781
  const raw = this.rawSuggestions();
729
782
  const currentTags = this.tags();
730
783
  const max = this.maxSuggestions();
731
- const currentTagKeys = new Set(currentTags.map((t) => this.tagKey(t)));
732
- return raw.filter((s) => !currentTagKeys.has(this.tagKey(s))).slice(0, max);
784
+ const currentTagKeys = new Set(currentTags.map((t) => CreateTagHelper.tagKey(t)));
785
+ return raw.filter((s) => !currentTagKeys.has(CreateTagHelper.tagKey(s))).slice(0, max);
733
786
  }, ...(ngDevMode ? [{ debugName: "displayedSuggestions" }] : []));
734
787
  /** @ignore — Whether the tag limit has been reached, used to disable suggestion selection */
735
788
  this.isLimitReached = computed(() => this.tags().length >= this.limit(), ...(ngDevMode ? [{ debugName: "isLimitReached" }] : []));
@@ -794,7 +847,7 @@ class AdsCreateTagComponent {
794
847
  onTagNameInput(event) {
795
848
  const inputEl = event.target;
796
849
  const raw = inputEl.value;
797
- const formatted = this.formatTagName(raw);
850
+ const formatted = CreateTagHelper.formatTagName(raw, this.alphanumeric(), this.lowercase());
798
851
  this.draftTag.update((prev) => ({ ...prev, tag: formatted }));
799
852
  /** Sync the DOM input if formatting changed the value (e.g., stripped special chars) */
800
853
  if (formatted !== raw) {
@@ -859,20 +912,11 @@ class AdsCreateTagComponent {
859
912
  current.tag.length > this.maxlength() ||
860
913
  !current.color ||
861
914
  this.tags().filter((tag) => {
862
- return (this.trimString(tag.tag) === this.trimString(current.tag) &&
915
+ return (CreateTagHelper.trimString(tag.tag) === CreateTagHelper.trimString(current.tag) &&
863
916
  tag.color === current.color &&
864
917
  tag.id !== current.id);
865
918
  }).length > 0);
866
919
  }
867
- /** @ignore */
868
- onTagUpdated(tag) {
869
- /** Immutably replace the matching tag so the model signal emits the update */
870
- this.tags.update((prev) => this.reindexTags(prev.map((t) => (t.id === tag.id ? { ...t, ...tag } : t))));
871
- }
872
- /** @ignore */
873
- onTagCreated(tag) {
874
- this.appendTag(tag);
875
- }
876
920
  /** @ignore — Adds a suggestion to the tags list with a generated id, emits tagCreate,
877
921
  * and optionally closes the overlay based on closeOnSelect. Does nothing if the tag limit is reached. */
878
922
  onSuggestionSelect(suggestion) {
@@ -890,7 +934,7 @@ class AdsCreateTagComponent {
890
934
  /** @ignore */
891
935
  onTagRemove(id) {
892
936
  /** Immutably remove the tag and let the model signal emit the update */
893
- this.tags.update((prev) => this.reindexTags(prev.filter((t) => t.id !== id)));
937
+ this.tags.update((prev) => prev.filter((t) => t.id !== id));
894
938
  this.tagRemove.emit(id);
895
939
  }
896
940
  /** @ignore — Returns white or dark text color based on the perceived luminance of a background color */
@@ -907,43 +951,19 @@ class AdsCreateTagComponent {
907
951
  this.onColorChange(palette[randomIndex]);
908
952
  }
909
953
  }
910
- /** @ignore — Applies formatting rules to the tag name: alphanumeric filtering, lowercase, space normalization */
911
- formatTagName(tagName) {
912
- let formattedName = tagName;
913
- /** if "alphanumeric" is true - allow only alphanumerics and spaces */
914
- if (this.alphanumeric()) {
915
- formattedName = formattedName.replace(/[^a-z0-9\s]/gi, '');
916
- }
917
- /** if "lowercase" is true - turn all letters to lowercase */
918
- if (this.lowercase()) {
919
- formattedName = formattedName.toLowerCase();
920
- }
921
- /** do not allow space at the beginning of the string*/
922
- if (formattedName.startsWith(' ')) {
923
- formattedName = formattedName.replace(/^\s+/gm, '');
924
- }
925
- /** do not allow multi-space sequences*/
926
- if (formattedName.includes(' ')) {
927
- formattedName = formattedName.replace(/\s\s+/gm, ' ');
928
- }
929
- return formattedName;
930
- }
931
- /** @ignore */
932
- trimString(tagName) {
933
- return tagName.trim().toLowerCase();
934
- }
935
- /** @ignore — Builds a comparison key for tag identity: case-insensitive name and color */
936
- tagKey(tag) {
937
- return `${this.trimString(tag.tag)}::${this.trimString(tag.color)}`;
938
- }
939
954
  /** @ignore — Appends a tag to the model signal and emits the tagCreate output */
940
955
  appendTag(tag) {
941
- this.tags.update((prev) => this.reindexTags([...prev, tag]));
956
+ this.tags.update((prev) => CreateTagHelper.sanitizeTags([...prev, tag]));
942
957
  this.tagCreate.emit(this.tags()[this.tags().length - 1]);
943
958
  }
944
- /** @ignore — Re-assigns sequential numeric ids (1, 2, 3, …) to all tags */
945
- reindexTags(tags) {
946
- return tags.map((t, i) => ({ ...t, id: String(i + 1) }));
959
+ /** @ignore */
960
+ onTagUpdated(tag) {
961
+ /** Immutably replace the matching tag so the model signal emits the update */
962
+ this.tags.update((prev) => prev.map((t) => (t.id === tag.id ? { ...t, ...tag } : t)));
963
+ }
964
+ /** @ignore */
965
+ onTagCreated(tag) {
966
+ this.appendTag(tag);
947
967
  }
948
968
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: AdsCreateTagComponent, deps: [{ token: i1.AdsIconRegistry }], target: i0.ɵɵFactoryTarget.Component }); }
949
969
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: AdsCreateTagComponent, isStandalone: false, selector: "ads-create-tag", inputs: { limit: { classPropertyName: "limit", publicName: "limit", isSignal: true, isRequired: false, transformFunction: null }, tags: { classPropertyName: "tags", publicName: "tags", isSignal: true, isRequired: false, transformFunction: null }, palette: { classPropertyName: "palette", publicName: "palette", isSignal: true, isRequired: false, transformFunction: null }, suggestions: { classPropertyName: "suggestions", publicName: "suggestions", isSignal: true, isRequired: false, transformFunction: null }, maxSuggestions: { classPropertyName: "maxSuggestions", publicName: "maxSuggestions", isSignal: true, isRequired: false, transformFunction: null }, closeOnSelect: { classPropertyName: "closeOnSelect", publicName: "closeOnSelect", isSignal: true, isRequired: false, transformFunction: null }, lowercase: { classPropertyName: "lowercase", publicName: "lowercase", isSignal: true, isRequired: false, transformFunction: null }, maxlength: { classPropertyName: "maxlength", publicName: "maxlength", isSignal: true, isRequired: false, transformFunction: null }, alphanumeric: { classPropertyName: "alphanumeric", publicName: "alphanumeric", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { tags: "tagsChange", tagCreate: "tagCreate", tagRemove: "tagRemove" }, host: { listeners: { "window:scroll": "onPageScroll()", "document:scroll": "onPageScroll()" } }, viewQueries: [{ propertyName: "addInput", first: true, predicate: ["addInput"], descendants: true, isSignal: true }, { propertyName: "editInput", first: true, predicate: ["editInput"], descendants: true, isSignal: true }], ngImport: i0, template: "<ads-tag-container>\r\n @for (tag of tags(); track tag.id) {\r\n @if (selectedTag()?.id === tag.id) {\r\n <input\r\n #editInput\r\n class=\"overlay-input\"\r\n cdkOverlayOrigin\r\n #updateTrigger=\"cdkOverlayOrigin\"\r\n [value]=\"draftTag().tag\"\r\n (input)=\"onTagNameInput($event)\"\r\n [attr.maxlength]=\"maxlength()\"\r\n />\r\n <ng-template\r\n cdkConnectedOverlay\r\n [cdkConnectedOverlayOrigin]=\"updateTrigger\"\r\n [cdkConnectedOverlayPositions]=\"positions\"\r\n [cdkConnectedOverlayPush]=\"true\"\r\n [cdkConnectedOverlayOpen]=\"true\"\r\n [cdkConnectedOverlayHasBackdrop]=\"true\"\r\n [cdkConnectedOverlayScrollStrategy]=\"closeScrollStrategy\"\r\n (backdropClick)=\"hide()\"\r\n (detach)=\"hide()\"\r\n >\r\n <div class=\"tag-configuration\">\r\n <div class=\"color-picker\">\r\n @for (color of palette(); track color) {\r\n <div\r\n class=\"color\"\r\n [class.selected]=\"draftTag().color === color\"\r\n [style.background-color]=\"color\"\r\n (click)=\"onColorChange(color)\"\r\n ></div>\r\n }\r\n </div>\r\n <div class=\"actions\">\r\n <ads-button [variant]=\"'secondary'\" size=\"xs\" (click)=\"reset()\">{{ 'Cancel' }}</ads-button>\r\n <ads-button size=\"xs\" [disabled]=\"isValueInvalid()\" (click)=\"updateTag()\">\r\n {{ 'Save' }}\r\n </ads-button>\r\n </div>\r\n </div>\r\n </ng-template>\r\n } @else {\r\n <ads-tag\r\n [tag]=\"tag.tag\"\r\n [color]=\"tag.color\"\r\n [style.--color-dark]=\"tagIconDark\"\r\n [style.--color-white]=\"tagIconLight\"\r\n [id]=\"tag.id\"\r\n (selected)=\"onTagSelect($event)\"\r\n (remove)=\"onTagRemove($event)\"\r\n />\r\n }\r\n }\r\n\r\n <div class=\"tag-container \">\r\n @if (showInput()) {\r\n <input\r\n #addInput\r\n class=\"overlay-input\"\r\n cdkOverlayOrigin\r\n #addTrigger=\"cdkOverlayOrigin\"\r\n [value]=\"draftTag().tag\"\r\n (input)=\"onTagNameInput($event)\"\r\n [attr.maxlength]=\"maxlength()\"\r\n />\r\n <ng-template\r\n cdkConnectedOverlay\r\n [cdkConnectedOverlayOrigin]=\"addTrigger\"\r\n [cdkConnectedOverlayPositions]=\"positions\"\r\n [cdkConnectedOverlayPush]=\"true\"\r\n [cdkConnectedOverlayOpen]=\"true\"\r\n [cdkConnectedOverlayHasBackdrop]=\"true\"\r\n [cdkConnectedOverlayScrollStrategy]=\"closeScrollStrategy\"\r\n (backdropClick)=\"hide()\"\r\n (detach)=\"hide()\"\r\n >\r\n <div class=\"tag-configuration\">\r\n @if (displayedSuggestions().length > 0) {\r\n <div class=\"suggestions-list\">\r\n @for (suggestion of displayedSuggestions(); track suggestion.tag + suggestion.color) {\r\n <div\r\n class=\"suggestion-item\"\r\n [style.background-color]=\"suggestion.color\"\r\n (click)=\"onSuggestionSelect(suggestion)\"\r\n >\r\n <span\r\n class=\"suggestion-text\"\r\n [style.color]=\"contrastTextColor(suggestion.color)\"\r\n #textEl\r\n (mouseenter)=\"textEl.title = textEl.scrollWidth > textEl.clientWidth ? suggestion.tag : ''\"\r\n >{{ suggestion.tag }}</span>\r\n </div>\r\n }\r\n </div>\r\n }\r\n <div class=\"color-picker\">\r\n @for (color of palette(); track color) {\r\n <div\r\n class=\"color\"\r\n [class.selected]=\"draftTag().color === color\"\r\n [style.background-color]=\"color\"\r\n (click)=\"onColorChange(color)\"\r\n ></div>\r\n }\r\n </div>\r\n <div class=\"actions\">\r\n <ads-button [variant]=\"'secondary'\" size=\"xs\" (click)=\"reset()\">{{ 'Cancel' }}</ads-button>\r\n <ads-button size=\"xs\" [disabled]=\"isValueInvalid()\" (click)=\"addTag()\">{{ 'Add' }} </ads-button>\r\n </div>\r\n </div>\r\n </ng-template>\r\n }\r\n\r\n @if (!isLimitReached()) {\r\n <ads-button (click)=\"displayInput()\" size=\"xs\">\r\n {{ 'Add Tag' }}\r\n </ads-button>\r\n }\r\n </div>\r\n</ads-tag-container>\r\n", styles: [".tag-overlay-trigger{height:0;width:150px;display:flex;align-self:flex-start}input{padding:0 8px;box-sizing:border-box;background-color:var(--color-white);border:2px solid var(--color-medium);color:var(--color-dark);height:24px;border-radius:5px;width:150px}input:focus{outline:none}.tag-configuration{box-shadow:0 0 0 1px #98a1b31a,0 15px 35px -5px #11182626,0 5px 15px #00000014;border-radius:5px;background-color:var(--color-white);overflow:hidden}@media(prefers-color-scheme:dark){.tag-configuration{background-color:var(--color-muted)}.actions ::ng-deep ads-button button{background-color:var(--color-muted)!important}}.suggestions-list{max-height:96px;overflow-y:auto;overflow-x:hidden;border-bottom:1px solid var(--color-light)}.suggestion-item{height:32px;display:flex;align-items:center;padding:0 8px;cursor:pointer;overflow:hidden}.suggestion-item:hover:not(.disabled){opacity:.85}.suggestion-item.disabled{cursor:default;opacity:.4}.suggestion-text{font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.color-picker{padding:12px 16px;display:grid;grid-template-columns:repeat(4,1fr);place-items:center;grid-gap:12px}.color-picker .color{position:relative;width:20px;height:20px;border-radius:50%;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease}.color-picker .color:after{content:\"\";position:absolute;inset:-4px;border-radius:50%;border:2px solid var(--color-primary);opacity:0;transform:scale(.8);transition:opacity .2s ease,transform .2s ease;pointer-events:none}.color-picker .color.selected{transform:scale(1.05)}.color-picker .color.selected:after{opacity:1;transform:scale(1)}.actions{border-top:1px solid var(--color-light);padding:8px 16px;display:flex;justify-content:center;gap:12px}.actions ::ng-deep ads-button button{width:52px;min-width:unset}::ng-deep .cdk-overlay-dark-backdrop{opacity:0!important}.tag-container{display:inline-flex;align-items:center;gap:16px;min-height:24px;position:relative}.tag-container ads-button{display:flex}.overlay-input{width:150px}\n"], dependencies: [{ kind: "directive", type: i3.CdkConnectedOverlay, selector: "[cdk-connected-overlay], [connected-overlay], [cdkConnectedOverlay]", inputs: ["cdkConnectedOverlayOrigin", "cdkConnectedOverlayPositions", "cdkConnectedOverlayPositionStrategy", "cdkConnectedOverlayOffsetX", "cdkConnectedOverlayOffsetY", "cdkConnectedOverlayWidth", "cdkConnectedOverlayHeight", "cdkConnectedOverlayMinWidth", "cdkConnectedOverlayMinHeight", "cdkConnectedOverlayBackdropClass", "cdkConnectedOverlayPanelClass", "cdkConnectedOverlayViewportMargin", "cdkConnectedOverlayScrollStrategy", "cdkConnectedOverlayOpen", "cdkConnectedOverlayDisableClose", "cdkConnectedOverlayTransformOriginOn", "cdkConnectedOverlayHasBackdrop", "cdkConnectedOverlayLockPosition", "cdkConnectedOverlayFlexibleDimensions", "cdkConnectedOverlayGrowAfterOpen", "cdkConnectedOverlayPush", "cdkConnectedOverlayDisposeOnNavigation", "cdkConnectedOverlayUsePopover", "cdkConnectedOverlayMatchWidth", "cdkConnectedOverlay"], outputs: ["backdropClick", "positionChange", "attach", "detach", "overlayKeydown", "overlayOutsideClick"], exportAs: ["cdkConnectedOverlay"] }, { kind: "directive", type: i3.CdkOverlayOrigin, selector: "[cdk-overlay-origin], [overlay-origin], [cdkOverlayOrigin]", exportAs: ["cdkOverlayOrigin"] }, { kind: "component", type: AdsButtonComponent, selector: "ads-button", inputs: ["id", "variant", "disabled", "size", "type", "fullWidth"] }, { kind: "component", type: AdsTagContainerComponent, selector: "ads-tag-container" }, { kind: "component", type: AdsTagComponent, selector: "ads-tag", inputs: ["color", "borderColor", "borderWidth", "width", "id", "removable", "clickable", "tag"], outputs: ["remove", "selected"] }] }); }