Skip to content

Commit e6cdd80

Browse files
committed
feat(rich-text-input): add secondary tag selector
1 parent fe47b57 commit e6cdd80

6 files changed

Lines changed: 129 additions & 7 deletions

File tree

packages/ng/forms/rich-text-input/plugins/tag/tag.component.html

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<div class="richTextField-toolbar-chips" (keydown.arrowleft)="focusTag($event, -1)" (keydown.arrowRight)="focusTag($event, 1)">
22
<span aria-hidden="true">{{ intl().insertTag }}</span>
3-
@for (tag of tags(); track tag) {
3+
@for (tag of primaryTags(); track tag) {
44
<lu-chip
55
[attr.tabindex]="isDisabled() ? null : $first ? 0 : -1"
66
[disabled]="isDisabled()"
@@ -16,4 +16,21 @@
1616
{{ tag.description }}
1717
</lu-chip>
1818
}
19+
@if (secondaryTags().length) {
20+
<lu-filter-pill [label]="intl().selectTag">
21+
<lu-simple-select
22+
[formControl]="selectedTagControl"
23+
[options]="filteredSecondaryTags()"
24+
(clueChange)="tagSearch.set($event)"
25+
#tagsSelect
26+
>
27+
@if (secondaryHasGroup()) {
28+
<ng-container *luOptionGroup="let group; by: groupByTagGroup; select: tagsSelect">
29+
{{ group.key }}
30+
</ng-container>
31+
}
32+
<ng-container *luOption="let tag; select: tagsSelect">{{ tag.description }}</ng-container>
33+
</lu-simple-select>
34+
</lu-filter-pill>
35+
}
1936
</div>

packages/ng/forms/rich-text-input/plugins/tag/tag.component.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1-
import { ChangeDetectionStrategy, Component, effect, ElementRef, forwardRef, inject, input, OnDestroy, signal, viewChildren, ViewContainerRef } from '@angular/core';
1+
import { ChangeDetectionStrategy, Component, computed, effect, ElementRef, forwardRef, inject, input, OnDestroy, signal, viewChildren, ViewContainerRef } from '@angular/core';
22
import { ChipComponent } from '@lucca-front/ng/chip';
3-
import { intlInputOptions } from '@lucca-front/ng/core';
3+
import { intlInputOptions, isNotNil } from '@lucca-front/ng/core';
44
import { $getNodeByKey, $getRoot, $getSelection, type Klass, type LexicalEditor, type LexicalNode, type NodeKey, SKIP_DOM_SELECTION_TAG } from 'lexical';
55
import { INITIAL_UPDATE_TAG, RICH_TEXT_PLUGIN_COMPONENT, RichTextPluginComponent } from '../../rich-text-input.component';
66
import { LU_RICH_TEXT_INPUT_TRANSLATIONS } from '../../rich-text-input.translate';
77
import { $createTagNode, TagNode } from './tag-node';
88
import type { Tag } from './tag.model';
9+
import { LuSimpleSelectInputComponent } from '@lucca-front/ng/simple-select';
10+
import { FilterPillComponent } from '@lucca-front/ng/filter-pills';
11+
import { LuOptionDirective, LuOptionGroupDirective } from '@lucca-front/ng/core-select';
12+
import { FormControl, ReactiveFormsModule } from '@angular/forms';
13+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
914

1015
const areSetsEqual = (a: Set<string>, b: Set<string>): boolean => a.size === b.size && [...a].every((value) => b.has(value));
1116

@@ -14,7 +19,7 @@ const areSetsEqual = (a: Set<string>, b: Set<string>): boolean => a.size === b.s
1419
changeDetection: ChangeDetectionStrategy.OnPush,
1520
templateUrl: './tag.component.html',
1621
styleUrl: './tag.component.scss',
17-
imports: [ChipComponent],
22+
imports: [ChipComponent, LuSimpleSelectInputComponent, FilterPillComponent, LuOptionGroupDirective, LuOptionDirective, ReactiveFormsModule],
1823
providers: [
1924
{
2025
provide: RICH_TEXT_PLUGIN_COMPONENT,
@@ -31,6 +36,18 @@ export class RichTextPluginTagComponent implements RichTextPluginComponent, OnDe
3136
readonly focusIndex = signal<number>(0);
3237

3338
readonly focusableElements = viewChildren('tagButton', { read: ElementRef });
39+
readonly primaryTags = computed(() => this.tags().filter((t) => !t.secondary));
40+
readonly secondaryTags = computed(() => this.tags().filter((t) => t.secondary));
41+
readonly filteredSecondaryTags = computed(() => {
42+
const search = this.#normalize(this.tagSearch());
43+
return this.secondaryTags().filter((t) => {
44+
const description = t.description ? this.#normalize(t.description) : '';
45+
return !search || description.includes(search);
46+
});
47+
});
48+
readonly secondaryHasGroup = computed(() => this.secondaryTags().some((s) => isNotNil(s.group)));
49+
readonly tagSearch = signal<string>('');
50+
readonly selectedTagControl = new FormControl<Tag | null>(null);
3451

3552
editor: LexicalEditor | null = null;
3653

@@ -65,6 +82,13 @@ export class RichTextPluginTagComponent implements RichTextPluginComponent, OnDe
6582
{ tag: [SKIP_DOM_SELECTION_TAG, INITIAL_UPDATE_TAG] },
6683
);
6784
});
85+
86+
this.selectedTagControl.valueChanges.pipe(takeUntilDestroyed()).subscribe((tag) => {
87+
if (tag) {
88+
this.selectedTagControl.reset();
89+
this.insertTag(tag);
90+
}
91+
});
6892
}
6993

7094
setEditorInstance(editor: LexicalEditor): void {
@@ -127,4 +151,15 @@ export class RichTextPluginTagComponent implements RichTextPluginComponent, OnDe
127151
ngOnDestroy() {
128152
this.#registeredCommands();
129153
}
154+
155+
groupByTagGroup(tag: Tag) {
156+
return tag.group;
157+
}
158+
159+
#normalize(str: string): string {
160+
return str
161+
.normalize('NFD')
162+
.replace(/\p{Diacritic}/gu, '')
163+
.toLowerCase();
164+
}
130165
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
export type Tag = {
22
key: string;
33
description?: string;
4+
secondary?: boolean;
5+
group?: string;
46
};

packages/ng/forms/rich-text-input/rich-text-input.translate.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export interface ILuRichTextInputLabel {
3434
stylesStrikethrough: string;
3535

3636
insertTag: string;
37+
selectTag: string;
3738
}
3839

3940
export const luRichTextInputTranslations: LuTranslation<ILuRichTextInputLabel> = Translations;

packages/ng/forms/rich-text-input/translations.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
stylesStrikethrough: 'Doorgehaald',
2323
linksPlaceholder: 'https://www.nomDuSite.com',
2424
insertTag: 'Invoegen:',
25+
selectTag: 'Een tag toevoegen',
2526
},
2627
fr: {
2728
clearFormat: 'Effacer la mise en forme',
@@ -46,6 +47,7 @@
4647
stylesStrikethrough: 'Barré',
4748
linksPlaceholder: 'https://www.nomDuSite.com',
4849
insertTag: 'Insérer :',
50+
selectTag: 'Ajouter une balise',
4951
},
5052
en: {
5153
clearFormat: 'Delete formatting',
@@ -70,6 +72,7 @@
7072
stylesStrikethrough: 'Strikethrough',
7173
linksPlaceholder: 'https://www.SiteName.com',
7274
insertTag: 'Insert:',
75+
selectTag: 'Add a tag',
7376
},
7477
de: {
7578
clearFormat: 'Formatierung löschen',
@@ -94,6 +97,7 @@
9497
stylesStrikethrough: 'Durchgestrichen',
9598
linksPlaceholder: 'https://www.NameDerSeite.com',
9699
insertTag: 'Einfügen:',
100+
selectTag: 'Ein Tag hinzufügen',
97101
},
98102
it: {
99103
clearFormat: 'Cancellare la formattazione',
@@ -118,6 +122,7 @@
118122
stylesStrikethrough: 'Barrato',
119123
linksPlaceholder: 'https://www.nomDuSite.com',
120124
insertTag: 'Inserire:',
125+
selectTag: 'Aggiungi un tag',
121126
},
122127
nl: {
123128
clearFormat: 'Opmaak wissen',
@@ -142,6 +147,7 @@
142147
stylesStrikethrough: 'Doorgehaald',
143148
linksPlaceholder: 'https://www.nomDuSite.com',
144149
insertTag: 'Invoegen:',
150+
selectTag: 'Een tag toevoegen',
145151
},
146152
es: {
147153
clearFormat: 'Borrar formato',
@@ -166,6 +172,7 @@
166172
stylesStrikethrough: 'Tachado',
167173
linksPlaceholder: 'https://www.nomDuSite.com',
168174
insertTag: 'Introducir:',
175+
selectTag: 'Añadir una etiqueta',
169176
},
170177
pt: {
171178
clearFormat: 'Eliminar a formatação',
@@ -190,5 +197,6 @@
190197
stylesStrikethrough: 'Barré',
191198
linksPlaceholder: 'https://www.nomeDoSitio.com/',
192199
insertTag: 'Inserir:',
200+
selectTag: 'Adicionar uma etiqueta',
193201
},
194202
};

stories/documentation/forms/fields/rich-text/angular/rich-text-input.stories.ts

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,19 @@ export const WithTagPluginWithNoInitialValue: StoryObj<RichTextInputComponent &
133133
description: 'Tag 2',
134134
},
135135
{
136-
key: 'tag3',
137-
description: 'Tag 3',
136+
key: 'tag4',
137+
description: 'Tag 4',
138+
secondary: true,
139+
},
140+
{
141+
key: 'tag5',
142+
description: 'Prénom',
143+
secondary: true,
144+
},
145+
{
146+
key: 'tag6',
147+
description: 'Téléphone',
148+
secondary: true,
138149
},
139150
]" />
140151
</lu-rich-text-input>
@@ -210,6 +221,24 @@ export const WithTagPlugin: StoryObj<RichTextInputComponent & { value: string; d
210221
key: 'tag3',
211222
description: 'Tag 3',
212223
},
224+
{
225+
key: 'tag4',
226+
description: 'Tag 4',
227+
secondary: true,
228+
group: 'Groupe 2'
229+
},
230+
{
231+
key: 'tag6',
232+
description: 'Téléphone',
233+
secondary: true,
234+
group: 'Groupe 2'
235+
},
236+
{
237+
key: 'tag5',
238+
description: 'Prénom',
239+
secondary: true,
240+
group: 'Groupe 1'
241+
},
213242
]" />
214243
</lu-rich-text-input>
215244
</lu-form-field>
@@ -255,6 +284,21 @@ export const WithTagPluginMarkdown: StoryObj<RichTextInputComponent & { value: s
255284
key: 'tag3',
256285
description: 'Tag 3',
257286
},
287+
{
288+
key: 'tag4',
289+
description: 'Tag 4',
290+
secondary: true,
291+
},
292+
{
293+
key: 'tag5',
294+
description: 'Prénom',
295+
secondary: true,
296+
},
297+
{
298+
key: 'tag6',
299+
description: 'Téléphone',
300+
secondary: true,
301+
},
258302
]" />
259303
</lu-rich-text-input>
260304
</lu-form-field>
@@ -265,7 +309,7 @@ export const WithTagPluginMarkdown: StoryObj<RichTextInputComponent & { value: s
265309
};
266310
},
267311
args: {
268-
value: 'Lorem **ipsum** dolor {{tag1}} *italic* {{unregisteredTag}} and regular {{tag2}} trailing text\nLine 2\n\nParagraph 2\n\n\n\nParagraph 3',
312+
value: 'Lorem **ipsum** dolor {{tag1}} *italic* {{unregisteredTag}} and {{tag4}} regular {{tag2}} trailing text\nLine 2\n\nParagraph 2\n\n\n\nParagraph 3',
269313
placeholder: 'Placeholder…',
270314
disabled: false,
271315
required: false,
@@ -348,6 +392,21 @@ export const WithTagPluginMarkdownContentChange: StoryObj<RichTextInputComponent
348392
key: 'tag3',
349393
description: 'Tag 3',
350394
},
395+
{
396+
key: 'tag4',
397+
description: 'Tag 4',
398+
secondary: true,
399+
},
400+
{
401+
key: 'tag5',
402+
description: 'Prénom',
403+
secondary: true,
404+
},
405+
{
406+
key: 'tag6',
407+
description: 'Téléphone',
408+
secondary: true,
409+
},
351410
]"/>
352411
</lu-rich-text-input>
353412
</lu-form-field>

0 commit comments

Comments
 (0)