-
Notifications
You must be signed in to change notification settings - Fork 6
feat(rich-text-input): add secondary tag selector #5037
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 3 commits
e6cdd80
551f6eb
49771a9
efb7919
7f67f82
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,14 @@ | ||
| import { ChangeDetectionStrategy, Component, effect, ElementRef, forwardRef, inject, input, OnDestroy, signal, viewChildren, ViewContainerRef } from '@angular/core'; | ||
| import { ConnectionPositionPair } from '@angular/cdk/overlay'; | ||
| import { ChangeDetectionStrategy, Component, computed, effect, ElementRef, forwardRef, inject, input, OnDestroy, signal, untracked, viewChild, viewChildren, ViewContainerRef } from '@angular/core'; | ||
| import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; | ||
| import { FormControl, ReactiveFormsModule } from '@angular/forms'; | ||
| import { ButtonComponent } from '@lucca-front/ng/button'; | ||
| import { ChipComponent } from '@lucca-front/ng/chip'; | ||
| import { intlInputOptions } from '@lucca-front/ng/core'; | ||
| import { intlInputOptions, isNotNil } from '@lucca-front/ng/core'; | ||
| import { LuOptionDirective, LuOptionGroupDirective } from '@lucca-front/ng/core-select'; | ||
| import { PopoverDirective } from '@lucca-front/ng/popover2'; | ||
| import { LuSimpleSelectInputComponent } from '@lucca-front/ng/simple-select'; | ||
| import { IconComponent } from '@lucca/prisme/icon'; | ||
| import { $getNodeByKey, $getRoot, $getSelection, type Klass, type LexicalEditor, type LexicalNode, type NodeKey, SKIP_DOM_SELECTION_TAG } from 'lexical'; | ||
| import { INITIAL_UPDATE_TAG, RICH_TEXT_PLUGIN_COMPONENT, RichTextPluginComponent } from '../../rich-text-input.component'; | ||
| import { LU_RICH_TEXT_INPUT_TRANSLATIONS } from '../../rich-text-input.translate'; | ||
|
|
@@ -14,7 +22,7 @@ const areSetsEqual = (a: Set<string>, b: Set<string>): boolean => a.size === b.s | |
| changeDetection: ChangeDetectionStrategy.OnPush, | ||
| templateUrl: './tag.component.html', | ||
| styleUrl: './tag.component.scss', | ||
| imports: [ChipComponent], | ||
| imports: [ButtonComponent, ChipComponent, IconComponent, LuSimpleSelectInputComponent, PopoverDirective, LuOptionGroupDirective, LuOptionDirective, ReactiveFormsModule], | ||
| providers: [ | ||
| { | ||
| provide: RICH_TEXT_PLUGIN_COMPONENT, | ||
|
|
@@ -31,6 +39,28 @@ export class RichTextPluginTagComponent implements RichTextPluginComponent, OnDe | |
| readonly focusIndex = signal<number>(0); | ||
|
|
||
| readonly focusableElements = viewChildren('tagButton', { read: ElementRef }); | ||
| readonly primaryTags = computed(() => this.tags().filter((t) => !t.secondary)); | ||
| readonly secondaryTags = computed(() => this.tags().filter((t) => t.secondary)); | ||
| readonly filteredSecondaryTags = computed(() => { | ||
| const search = this.#normalize(this.tagSearch()); | ||
| return this.secondaryTags().filter((t) => { | ||
| const description = t.description ? this.#normalize(t.description) : ''; | ||
| return !search || description.includes(search); | ||
| }); | ||
| }); | ||
| readonly secondaryHasGroup = computed(() => this.secondaryTags().some((s) => isNotNil(s.group))); | ||
| readonly tagSearch = signal<string>(''); | ||
| readonly selectedTagControl = new FormControl<Tag | null>(null); | ||
|
|
||
| // The select panel only exists once the popover is opened, so these queries resolve lazily. | ||
| readonly tagsSelectRef = viewChild<LuSimpleSelectInputComponent<Tag>>(LuSimpleSelectInputComponent); | ||
| readonly popoverRef = viewChild(PopoverDirective); | ||
|
|
||
| // Anchor the dropdown to the trigger button, opening below it (flipping above if there isn't room). | ||
| readonly popoverPositions: ConnectionPositionPair[] = [ | ||
| new ConnectionPositionPair({ originX: 'start', originY: 'bottom' }, { overlayX: 'start', overlayY: 'top' }, -4, 0), | ||
| new ConnectionPositionPair({ originX: 'start', originY: 'top' }, { overlayX: 'start', overlayY: 'bottom' }, -4, 0), | ||
| ]; | ||
|
|
||
| editor: LexicalEditor | null = null; | ||
|
|
||
|
|
@@ -65,8 +95,38 @@ export class RichTextPluginTagComponent implements RichTextPluginComponent, OnDe | |
| { tag: [SKIP_DOM_SELECTION_TAG, INITIAL_UPDATE_TAG] }, | ||
| ); | ||
| }); | ||
|
|
||
| this.selectedTagControl.valueChanges.pipe(takeUntilDestroyed()).subscribe((tag) => { | ||
| if (tag) { | ||
| this.selectedTagControl.reset(); | ||
| this.insertTag(tag); | ||
| } | ||
| }); | ||
|
|
||
| // Render the select's panel inline inside our own popover (instead of through a filter-pill). | ||
| // The select component only appears once the popover opens, so we wire it up reactively. | ||
| effect(() => { | ||
| const ref = this.tagsSelectRef(); | ||
| if (ref) { | ||
| untracked(() => { | ||
| ref.enableFilterPillMode(); | ||
| ref.registerFilterPillClosePopover(this.closePopover); | ||
|
MarcFairbrother marked this conversation as resolved.
Outdated
|
||
| ref.registerFilterPillUpdatePosition?.(this.updatePosition); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess the select panel is not exported by LF & cannot be used separately from select component? My concern with is that we register the callback but never unregister. Depending on internal implementation, that could lead to memory leaks & event handler duplications. |
||
| ref.onFilterPillOpened?.(); | ||
| }); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| closePopover = () => { | ||
| this.popoverRef()?.close(); | ||
| this.tagsSelectRef()?.onFilterPillClosed?.(); | ||
| }; | ||
|
|
||
| updatePosition = () => { | ||
| this.popoverRef()?.updatePosition(); | ||
| }; | ||
|
|
||
| setEditorInstance(editor: LexicalEditor): void { | ||
| this.editor = editor; | ||
|
|
||
|
|
@@ -127,4 +187,15 @@ export class RichTextPluginTagComponent implements RichTextPluginComponent, OnDe | |
| ngOnDestroy() { | ||
| this.#registeredCommands(); | ||
| } | ||
|
|
||
| groupByTagGroup(tag: Tag) { | ||
| return tag.group; | ||
| } | ||
|
|
||
| #normalize(str: string): string { | ||
| return str | ||
| .normalize('NFD') | ||
| .replace(/\p{Diacritic}/gu, '') | ||
| .toLowerCase(); | ||
| } | ||
|
Comment on lines
+168
to
+173
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have a common utility file for this kind of code? @Supamiu |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,6 @@ | ||
| export type Tag = { | ||
| key: string; | ||
| description?: string; | ||
| secondary?: boolean; | ||
| group?: string; | ||
| }; |
Uh oh!
There was an error while loading. Please reload this page.