Skip to content

Commit b5fd6d9

Browse files
authored
feat(block-editor): Introduce Bubble Menu Component for enhanced text… (dotCMS#32500)
## Video https://github.com/user-attachments/assets/9bb58d6f-b5f9-4883-a71c-22e4881c96b6 This pull request introduces a new `BubbleMenuComponent` to replace the deprecated `BubbleMenuDirective` in the block editor module. The changes include adding the new component, its styling, and integration into the existing editor workflow, while removing the old directive and related references. ### New Bubble Menu Implementation: * Added `BubbleMenuComponent` with a rich set of formatting options such as bold, italic, underline, text alignment, and list controls. It uses `ngx-tiptap` for integration with the editor and includes dropdown options for node types like headings, paragraphs, and lists (`bubble-menu.component.ts`, `bubble-menu.component.html`, `bubble-menu.component.scss`). [[1]](diffhunk://#diff-d23b993f9fd90e0ad768747dd7f3a2a779754b32eaf6552646b15bb551a38ee5R1-R147) [[2]](diffhunk://#diff-5504fc3eee34cc6dbd0f0655db4f54a85962c319cb0779196b003183a32924b5R1-R120) [[3]](diffhunk://#diff-7e27151d3ff92b9fe01d3ce1e6265a02eb4cabba8837ddf503fa101476dad397R1-R122) * Integrated the new `BubbleMenuComponent` into the block editor module, replacing the old directive. Updated imports and declarations in `block-editor.module.ts` accordingly. [[1]](diffhunk://#diff-8baf52e51d62118783206bb31ceefa1afcd8a4b9400cfb935614e5a1171d5cf4R1-R2) [[2]](diffhunk://#diff-8baf52e51d62118783206bb31ceefa1afcd8a4b9400cfb935614e5a1171d5cf4R36-L38) [[3]](diffhunk://#diff-8baf52e51d62118783206bb31ceefa1afcd8a4b9400cfb935614e5a1171d5cf4L72-L78) ### Removal of Deprecated Directive: * Removed the deprecated `BubbleMenuDirective` and its associated files, including tests, from the shared directives directory. [[1]](diffhunk://#diff-5eda86f5c86367b5c85f7846a4d9af5815ac282565b9b26379e8797559743522L1-L37) [[2]](diffhunk://#diff-658a47db818d027990396ebc51d202f4d5c7cb0969ccf67e616ff35ee60b7902L1-L59) [[3]](diffhunk://#diff-285bbf72928fbaf78d936f3fd7d43ff9ec25d32bd7e28d1324390afb5a514735L1) ### Updates to Existing Components: * Updated `DotBlockEditorComponent` to include the new `BubbleMenuComponent` and removed references to the old directive. Adjusted the HTML template to integrate the new bubble menu. [[1]](diffhunk://#diff-3628abb3effc8518bbbe60d68908f090cefdff11a43139a2c76f1abf2a8644c3L16-R24) [[2]](diffhunk://#diff-266eab162f8661e695c3e40956692fa3696fbd4e8cd3e0352ea9e01f90b13609L51-L54) [[3]](diffhunk://#diff-266eab162f8661e695c3e40956692fa3696fbd4e8cd3e0352ea9e01f90b13609L452-R451) ### Testing: * Added unit tests for `BubbleMenuComponent` to ensure it initializes correctly and behaves as expected. These changes modernize the block editor's bubble menu functionality, improving maintainability and aligning with updated libraries.
1 parent 4919535 commit b5fd6d9

File tree

60 files changed

+1738
-2652
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+1738
-2652
lines changed

core-web/libs/block-editor/src/lib/block-editor.module.ts

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { TiptapBubbleMenuDirective } from 'ngx-tiptap';
2+
13
import { CommonModule } from '@angular/common';
24
import { APP_INITIALIZER, NgModule } from '@angular/core';
35
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@@ -31,15 +33,11 @@ import {
3133
//Editor
3234
import { DotBlockEditorComponent } from './components/dot-block-editor/dot-block-editor.component';
3335
import { DotEditorCountBarComponent } from './components/dot-editor-count-bar/dot-editor-count-bar.component';
36+
import { DotBubbleMenuComponent } from './elements/dot-bubble-menu/dot-bubble-menu.component';
3437
import {
3538
BubbleFormComponent,
36-
BubbleLinkFormComponent,
37-
BubbleMenuButtonComponent,
38-
BubbleMenuComponent,
3939
DragHandlerComponent,
4040
FloatingButtonComponent,
41-
FormActionsComponent,
42-
SuggestionPageComponent,
4341
UploadPlaceholderComponent
4442
} from './extensions';
4543
import { AssetFormModule } from './extensions/asset-form/asset-form.module';
@@ -69,18 +67,15 @@ const initTranslations = (dotMessageService: DotMessageService) => {
6967
DialogModule,
7068
InputTextareaModule,
7169
PaginatorModule,
72-
DotSpinnerModule
70+
DotSpinnerModule,
71+
DotBubbleMenuComponent,
72+
TiptapBubbleMenuDirective
7373
],
7474
declarations: [
7575
EditorDirective,
7676
ContentletBlockComponent,
7777
DragHandlerComponent,
78-
BubbleMenuComponent,
79-
BubbleMenuButtonComponent,
80-
BubbleLinkFormComponent,
81-
FormActionsComponent,
8278
BubbleFormComponent,
83-
SuggestionPageComponent,
8479
DotBlockEditorComponent,
8580
DotEditorCountBarComponent,
8681
FloatingButtonComponent
@@ -106,8 +101,7 @@ const initTranslations = (dotMessageService: DotMessageService) => {
106101
],
107102
exports: [
108103
EditorDirective,
109-
BubbleMenuComponent,
110-
BubbleLinkFormComponent,
104+
DotBubbleMenuComponent,
111105
ReactiveFormsModule,
112106
SharedModule,
113107
BubbleFormComponent,

core-web/libs/block-editor/src/lib/components/dot-block-editor/dot-block-editor.component.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
[ngClass]="{ 'overflow-hidden': freezeScroll }"
1414
[ngModel]="content" />
1515
</div>
16+
17+
<dot-bubble-menu [editor]="editor" />
18+
1619
@if (showCharData) {
1720
<dot-editor-count-bar
1821
[charLimit]="charLimit"

core-web/libs/block-editor/src/lib/components/dot-block-editor/dot-block-editor.component.scss

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,37 @@
4444
padding: $dot-editor-size (4 * $dot-editor-size);
4545
font: $dot-editor-size;
4646

47+
.ProseMirror-selectednode {
48+
&.dot-image,
49+
.dot-image {
50+
outline: 1px solid $color-palette-primary;
51+
}
52+
}
53+
54+
a:has(img) {
55+
display: block;
56+
width: 50%;
57+
position: relative;
58+
59+
img.dot-image {
60+
max-width: 100%;
61+
position: relative;
62+
}
63+
}
64+
4765
img {
4866
max-width: 100%;
4967
max-height: 100%;
5068
position: relative;
5169

70+
&.dot-image {
71+
display: block;
72+
width: auto;
73+
max-height: 300px;
74+
max-width: 50%;
75+
padding: $spacing-1;
76+
}
77+
5278
&:before {
5379
align-items: center;
5480
background: $color-palette-gray-200;
@@ -289,6 +315,18 @@
289315
border: 1px solid $color-palette-gray-400;
290316
color: $color-palette-primary;
291317
}
318+
319+
.dot-node-center {
320+
margin: 0 auto;
321+
}
322+
323+
.dot-node-right {
324+
margin-left: auto;
325+
}
326+
327+
.dot-node-left {
328+
margin-right: auto;
329+
}
292330
}
293331

294332
.video-container {
@@ -302,22 +340,6 @@
302340
}
303341
}
304342

305-
.image-container {
306-
margin-bottom: $spacing-3;
307-
308-
img {
309-
width: auto;
310-
max-height: 300px;
311-
max-width: 50%;
312-
padding: $spacing-1;
313-
}
314-
&.ProseMirror-selectednode {
315-
img {
316-
outline: 1px solid $color-palette-primary;
317-
}
318-
}
319-
}
320-
321343
.ai-content-container {
322344
&.ProseMirror-selectednode {
323345
background-color: $color-palette-primary-op-20;

core-web/libs/block-editor/src/lib/components/dot-block-editor/dot-block-editor.component.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,6 @@ import {
5050
AssetUploader,
5151
BubbleAssetFormExtension,
5252
BubbleFormExtension,
53-
BubbleLinkFormExtension,
54-
DEFAULT_LANG_ID,
55-
DotBubbleMenuExtension,
5653
DotComands,
5754
DotConfigExtension,
5855
DotFloatingButton,
@@ -71,7 +68,8 @@ import {
7168
formatHTML,
7269
removeInvalidNodes,
7370
RestoreDefaultDOMAttrs,
74-
SetDocAttrStep
71+
SetDocAttrStep,
72+
DEFAULT_LANG_ID
7573
} from '../../shared';
7674

7775
@Component({
@@ -448,8 +446,6 @@ export class DotBlockEditorComponent implements OnInit, OnDestroy, ControlValueA
448446
shouldShowAIExtensions: isAIPluginInstalled
449447
}),
450448
DragHandler(this.viewContainerRef),
451-
BubbleLinkFormExtension(this.viewContainerRef, this.languageId),
452-
DotBubbleMenuExtension(this.#injector, this.viewContainerRef),
453449
BubbleFormExtension(this.viewContainerRef),
454450
DotFloatingButton(this.#injector, this.viewContainerRef),
455451
DotTableCellExtension(this.viewContainerRef),
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import tippy, { Instance } from 'tippy.js';
2+
3+
import { Directive, ElementRef, OnDestroy, OnInit, inject, input } from '@angular/core';
4+
5+
import { Editor, isNodeSelection, posToDOMRect } from '@tiptap/core';
6+
import { BubbleMenuPluginProps } from '@tiptap/extension-bubble-menu';
7+
8+
@Directive({
9+
selector: 'dot-editor-modal[editor], [dotEditorModal][editor]',
10+
standalone: true,
11+
exportAs: 'dotEditorModal'
12+
})
13+
export class EditorModalDirective implements OnInit, OnDestroy {
14+
readonly editor = input.required<Editor>();
15+
readonly tippyOptions = input<BubbleMenuPluginProps['tippyOptions']>({});
16+
17+
private elRef = inject<ElementRef<HTMLElement>>(ElementRef);
18+
private tippy: Instance;
19+
20+
private editorElement: HTMLElement;
21+
22+
ngOnInit(): void {
23+
const { element: editorElement } = this.editor().options;
24+
const editorIsAttached = !!editorElement.parentElement;
25+
26+
if (!editorIsAttached) {
27+
return;
28+
}
29+
30+
this.editorElement = editorElement as HTMLElement;
31+
this.tippy = tippy(editorElement, {
32+
duration: 0,
33+
content: this.elRef.nativeElement,
34+
interactive: true,
35+
trigger: 'manual',
36+
placement: 'bottom-start',
37+
hideOnClick: 'toggle',
38+
getReferenceClientRect: this.getReferenceClientRect.bind(this),
39+
...this.tippyOptions()
40+
});
41+
42+
editorElement.addEventListener('mousedown', () => this.hide());
43+
}
44+
45+
ngOnDestroy(): void {
46+
this.tippy?.destroy();
47+
this.editorElement.removeEventListener('mousedown', () => this.hide());
48+
}
49+
50+
show() {
51+
this.tippy.show();
52+
}
53+
54+
hide() {
55+
this.tippy.hide();
56+
}
57+
58+
toggle() {
59+
this.tippy.state.isVisible ? this.hide() : this.show();
60+
}
61+
62+
private getReferenceClientRect() {
63+
const { state, view } = this.editor();
64+
const { from, to } = state.selection;
65+
66+
// Handle node selections (like images, tables, etc.)
67+
if (isNodeSelection(state.selection)) {
68+
const node = this.getNodeElement(view, from);
69+
if (node) {
70+
// If the node has a bubble menu, return its bounding client rect
71+
const bubbleMenu = document.querySelector('[tiptapbubblemenu]');
72+
73+
if (bubbleMenu) {
74+
return bubbleMenu.getBoundingClientRect();
75+
}
76+
77+
// Otherwise, return the node's bounding client rect
78+
return node.getBoundingClientRect();
79+
}
80+
}
81+
82+
// Handle text selections
83+
return posToDOMRect(view, from, to);
84+
}
85+
86+
private getNodeElement(view: Editor['view'], pos: number): HTMLElement | null {
87+
const node = view.nodeDOM(pos) as HTMLElement;
88+
89+
if (!node) return null;
90+
91+
// Look for node view wrapper and get its first child
92+
const nodeViewWrapper = node.dataset.nodeViewWrapper
93+
? node
94+
: node.querySelector('[data-node-view-wrapper]');
95+
96+
return (nodeViewWrapper?.firstChild as HTMLElement) || node;
97+
}
98+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<div dotEditorModal #popover [tippyOptions]="tippyOptions" [editor]="editor()">
2+
<form class="form" (ngSubmit)="saveImageChanges()" [formGroup]="imageForm">
3+
<div class="form-group">
4+
<label for="src">
5+
Image URL
6+
<span class="required">*</span>
7+
</label>
8+
<input
9+
#input
10+
id="src"
11+
name="src"
12+
type="text"
13+
pInputText
14+
formControlName="src"
15+
autocomplete="off"
16+
aria-label="Image URL" />
17+
</div>
18+
19+
<div class="form-group">
20+
<label for="title">Title</label>
21+
<input
22+
id="title"
23+
name="title"
24+
type="text"
25+
pInputText
26+
formControlName="title"
27+
autocomplete="off"
28+
aria-label="Image title" />
29+
</div>
30+
31+
<div class="form-group">
32+
<label for="alt">Alt Text</label>
33+
<input
34+
id="alt"
35+
name="alt"
36+
type="text"
37+
pInputText
38+
formControlName="alt"
39+
autocomplete="off"
40+
aria-label="Alternative text for the image" />
41+
</div>
42+
43+
<div class="bubble-menu-actions">
44+
<button
45+
type="button"
46+
pButton
47+
class="bubble-menu-button p-button-outlined"
48+
(click)="cancelImageEditing()"
49+
label="Cancel"
50+
aria-label="Cancel editing"></button>
51+
<button
52+
type="submit"
53+
pButton
54+
class="bubble-menu-button bubble-menu-button--primary"
55+
label="Apply"
56+
[disabled]="!imageForm.valid"
57+
aria-label="Apply changes"></button>
58+
</div>
59+
</form>
60+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
@use "variables" as *;
2+
3+
.form {
4+
display: flex;
5+
flex-direction: column;
6+
width: 18.75rem;
7+
gap: $spacing-4;
8+
background-color: $white;
9+
border-radius: $border-radius-md;
10+
padding: $spacing-3;
11+
box-shadow: $shadow-m;
12+
13+
.form-group {
14+
display: flex;
15+
flex-direction: column;
16+
gap: $spacing-1;
17+
18+
label {
19+
font-weight: $font-weight-semi-bold;
20+
margin-bottom: $spacing-0;
21+
22+
.required {
23+
color: $color-palette-red;
24+
}
25+
}
26+
27+
input[pInputText] {
28+
border-radius: $border-radius-md;
29+
border: 1px solid $color-palette-gray-300;
30+
padding: $spacing-1 $spacing-2;
31+
font-size: $font-size-lmd;
32+
33+
&.ng-invalid {
34+
border-color: $color-palette-red;
35+
}
36+
}
37+
}
38+
39+
.bubble-menu-actions {
40+
display: flex;
41+
justify-content: flex-end;
42+
gap: $spacing-1;
43+
}
44+
}

0 commit comments

Comments
 (0)