@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 =
|
|
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) =>
|
|
732
|
-
return raw.filter((s) => !currentTagKeys.has(
|
|
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 =
|
|
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 (
|
|
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) =>
|
|
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) =>
|
|
956
|
+
this.tags.update((prev) => CreateTagHelper.sanitizeTags([...prev, tag]));
|
|
942
957
|
this.tagCreate.emit(this.tags()[this.tags().length - 1]);
|
|
943
958
|
}
|
|
944
|
-
/** @ignore
|
|
945
|
-
|
|
946
|
-
|
|
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"] }] }); }
|