@phuong-tran-redoc/document-engine-angular 0.0.33 → 0.0.35

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.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { HostBinding, Input, Directive, inject, Injectable, ElementRef, ViewEncapsulation, ChangeDetectionStrategy, Component, EventEmitter, Output, ChangeDetectorRef, InjectionToken, HostListener, forwardRef, ContentChildren, ViewChild, ViewChildren, NgZone, ViewContainerRef, EnvironmentInjector, NgModule, ApplicationRef, createComponent, Renderer2 } from '@angular/core';
2
+ import { inject, ElementRef, Input, Directive, Injectable, ApplicationRef, createComponent, HostBinding, InjectionToken, Renderer2, ChangeDetectorRef, forwardRef, ViewEncapsulation, ChangeDetectionStrategy, Component, EventEmitter, Output, HostListener, ContentChildren, ViewChild, ViewChildren, NgZone, ViewContainerRef, EnvironmentInjector, NgModule } from '@angular/core';
3
3
  import { Extension, Editor } from '@tiptap/core';
4
4
  import Blockquote from '@tiptap/extension-blockquote';
5
5
  import Bold from '@tiptap/extension-bold';
@@ -22,17 +22,17 @@ import { TextStyleKit } from '@tiptap/extension-text-style';
22
22
  import Underline from '@tiptap/extension-underline';
23
23
  import { Dropcursor, Gapcursor, TrailingNode, UndoRedo, CharacterCount, Placeholder } from '@tiptap/extensions';
24
24
  import { ResetOnEnter, EditableRegion, RestrictedEditing, CustomOrderedList, StyledTableKit, PageBreak, ResetFormat, Indent, ClearContent, TextCase, NotumHeading, DynamicField, Color, normalizeColor, getActiveMarkRange, getSelectedCells, getCombinedCellAttributeValue, TableDefaultAttributes, getCursorCellInfo, getClosestDomElement } from '@phuong-tran-redoc/document-engine-core';
25
+ import { BubbleMenuPlugin } from '@tiptap/extension-bubble-menu';
25
26
  import * as i1 from '@angular/common';
26
- import { CommonModule, DOCUMENT } from '@angular/common';
27
- import { throttle } from 'lodash-es';
27
+ import { DOCUMENT, CommonModule } from '@angular/common';
28
28
  import * as i2 from '@angular/forms';
29
- import { FormsModule, FormControl, ReactiveFormsModule, FormBuilder, Validators, NG_VALUE_ACCESSOR, NonNullableFormBuilder } from '@angular/forms';
29
+ import { NG_VALUE_ACCESSOR, FormsModule, FormControl, ReactiveFormsModule, FormBuilder, Validators, NonNullableFormBuilder } from '@angular/forms';
30
+ import { FloatingMenuPlugin } from '@tiptap/extension-floating-menu';
31
+ import { computePosition, offset, flip, shift, autoUpdate } from '@floating-ui/dom';
32
+ import { throttle } from 'lodash-es';
30
33
  import { DomSanitizer } from '@angular/platform-browser';
31
34
  import { Subject, debounceTime, distinctUntilChanged, takeUntil } from 'rxjs';
32
- import { autoUpdate, computePosition, offset, flip, shift } from '@floating-ui/dom';
33
35
  import { CellSelection } from '@tiptap/pm/tables';
34
- import { BubbleMenuPlugin } from '@tiptap/extension-bubble-menu';
35
- import { FloatingMenuPlugin } from '@tiptap/extension-floating-menu';
36
36
 
37
37
  const DocumentEngineKit = Extension.create({
38
38
  name: 'DocumentEngineKit',
@@ -269,188 +269,952 @@ const DefaultEditorConfig = {
269
269
  showPrintButton: false,
270
270
  };
271
271
 
272
- const COLORS = Color.fromArray([
273
- { name: 'Black', value: '#000000', type: 'hex' },
274
- { name: 'Dim gray', value: '#696969', type: 'hex' },
275
- { name: 'Light gray', value: '#d3d3d3', type: 'hex' },
276
- { name: 'White', value: '#ffffff', type: 'hex' },
277
- { name: 'Transparent', value: 'transparent', type: 'hex' },
278
- // row 2
279
- { name: 'Red', value: '#ff0000', type: 'hex' },
280
- { name: 'Orange', value: '#ffa500', type: 'hex' },
281
- { name: 'Yellow', value: '#ffff00', type: 'hex' },
282
- { name: 'Light green', value: '#90ee90', type: 'hex' },
283
- { name: 'Green', value: '#008000', type: 'hex' },
284
- // row 3
285
- { name: 'Aquamarine', value: '#7fffd4', type: 'hex' },
286
- { name: 'Turquoise', value: '#40e0d0', type: 'hex' },
287
- { name: 'Lightblue', value: '#add8e6', type: 'hex' },
288
- { name: 'Blue', value: '#0000ff', type: 'hex' },
289
- { name: 'Purple', value: '#800080', type: 'hex' },
290
- ]);
291
-
292
- /**
293
- * Custom button directive matching React button component styles
294
- * Supports variants: default, destructive, outline, secondary, ghost, link
295
- * Supports sizes: default, sm, lg, icon, icon-sm, icon-lg
296
- *
297
- * Usage:
298
- * - <button documentEngineButton>Text</button> - secondary variant, default size
299
- * - <button documentEngineButton="filled">Text</button> - default variant (primary)
300
- * - <button documentEngineButton variant="outline">Text</button> - outline variant
301
- * - <button documentEngineButton variant="icon" size="icon-sm">Icon</button> - icon button
302
- */
303
- class ButtonDirective {
304
- variant = 'ghost';
305
- size = 'default';
306
- get buttonClasses() {
307
- const classes = ['btn'];
308
- classes.push(`btn--${this.variant}`);
309
- classes.push(`btn--size-${this.size}`);
310
- return classes.join(' ');
272
+ class TiptapBubbleMenuDirective {
273
+ elRef = inject(ElementRef);
274
+ props;
275
+ editor;
276
+ mountTo;
277
+ shouldShow;
278
+ ngOnInit() {
279
+ const editor = this.editor;
280
+ if (!editor)
281
+ throw new Error('Required: Input `editor`');
282
+ const bubbleMenuElement = this.elRef.nativeElement;
283
+ bubbleMenuElement.style.visibility = 'hidden';
284
+ bubbleMenuElement.style.position = 'absolute';
285
+ // Prevent editor blur when interacting with the bubble content
286
+ // bubbleMenuElement.addEventListener('mousedown', this.preventDefault);
287
+ const { pluginKey, appendTo, updateDelay, resizeDelay, options } = this.props;
288
+ editor.registerPlugin(BubbleMenuPlugin({
289
+ pluginKey,
290
+ appendTo,
291
+ updateDelay,
292
+ resizeDelay,
293
+ editor,
294
+ element: bubbleMenuElement,
295
+ options: this.getFloatingUiOptions(options ?? {}, bubbleMenuElement),
296
+ shouldShow: this.shouldShow,
297
+ getReferencedVirtualElement: this.mountTo,
298
+ }));
311
299
  }
312
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: ButtonDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
313
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.10", type: ButtonDirective, isStandalone: true, selector: "button[documentEngineButton]", inputs: { variant: "variant", size: "size" }, host: { attributes: { "type": "button" }, properties: { "class": "this.buttonClasses" } }, ngImport: i0 });
300
+ ngOnDestroy() {
301
+ if (this.editor && this.props?.pluginKey) {
302
+ this.editor.unregisterPlugin(this.props.pluginKey);
303
+ }
304
+ window.requestAnimationFrame(() => {
305
+ if (this.elRef.nativeElement.parentNode) {
306
+ this.elRef.nativeElement.parentNode.removeChild(this.elRef.nativeElement);
307
+ }
308
+ });
309
+ this.elRef.nativeElement.removeEventListener('mousedown', this.preventDefault);
310
+ }
311
+ preventDefault(event) {
312
+ // Allow default behavior for focusable elements so inputs can receive focus
313
+ const target = event.target;
314
+ if (target) {
315
+ const tag = target.tagName;
316
+ const isFocusableTag = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || target.isContentEditable;
317
+ if (isFocusableTag)
318
+ return;
319
+ }
320
+ event.preventDefault();
321
+ }
322
+ getFloatingUiOptions(userOptions, element) {
323
+ // Find arrow element if it exists
324
+ // const arrowElement = element.querySelector('[data-arrow]') as HTMLElement | null;
325
+ // Merge with default Floating UI options
326
+ const defaultOptions = {
327
+ placement: 'bottom',
328
+ flip: {
329
+ fallbackPlacements: ['top', 'bottom', 'bottom-start', 'top-end', 'bottom-end'],
330
+ padding: 8,
331
+ },
332
+ // Enable shift middleware to keep bubble within viewport
333
+ shift: { padding: 8 },
334
+ // Add offset to create space between selection and bubble
335
+ offset: 10,
336
+ // Configure arrow if element exists
337
+ // arrow: arrowElement ? { element: arrowElement } : false,
338
+ };
339
+ return { ...defaultOptions, ...userOptions };
340
+ }
341
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: TiptapBubbleMenuDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
342
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.10", type: TiptapBubbleMenuDirective, isStandalone: true, selector: "tiptap-bubble-menu[editor], [tiptapBubbleMenu][editor]", inputs: { props: "props", editor: "editor", mountTo: "mountTo", shouldShow: "shouldShow" }, ngImport: i0 });
314
343
  }
315
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: ButtonDirective, decorators: [{
344
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: TiptapBubbleMenuDirective, decorators: [{
316
345
  type: Directive,
317
346
  args: [{
318
- selector: 'button[documentEngineButton]',
347
+ selector: 'tiptap-bubble-menu[editor], [tiptapBubbleMenu][editor]',
319
348
  standalone: true,
320
- host: {
321
- type: 'button',
322
- },
323
349
  }]
324
- }], propDecorators: { variant: [{
350
+ }], propDecorators: { props: [{
325
351
  type: Input
326
- }], size: [{
352
+ }], editor: [{
353
+ type: Input
354
+ }], mountTo: [{
355
+ type: Input
356
+ }], shouldShow: [{
327
357
  type: Input
328
- }], buttonClasses: [{
329
- type: HostBinding,
330
- args: ['class']
331
358
  }] } });
332
359
 
333
- const DEFAULT = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></svg>`;
334
- const CHECK = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="100%" height="100%"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z"/></svg>`;
335
- // Official icons
336
- const UNDO = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M280-200v-80h284q63 0 109.5-40T720-420q0-60-46.5-100T564-560H312l104 104-56 56-200-200 200-200 56 56-104 104h252q97 0 166.5 63T800-420q0 94-69.5 157T564-200H280Z"/></svg>`;
337
- const REDO = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M396-200q-97 0-166.5-63T160-420q0-94 69.5-157T396-640h252L544-744l56-56 200 200-200 200-56-56 104-104H396q-63 0-109.5 40T240-420q0 60 46.5 100T396-280h284v80H396Z"/></svg>`;
338
- const FORMAT_SIZE = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M560-160v-520H360v-120h520v120H680v520H560Zm-360 0v-320H80v-120h360v120H320v320H200Z"/></svg>`;
339
- const ARROW_DROP_DOWN = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M480-360 280-560h400L480-360Z"/></svg>`;
340
- const FORMAT_LINE_SPACING = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M240-160 80-320l56-56 64 62v-332l-64 62-56-56 160-160 160 160-56 56-64-62v332l64-62 56 56-160 160Zm240-40v-80h400v80H480Zm0-240v-80h400v80H480Zm0-240v-80h400v80H480Z"/></svg>`;
341
- const MATCH_CASE = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="m131-252 165-440h79l165 440h-76l-39-112H247l-40 112h-76Zm139-176h131l-64-182h-4l-63 182Zm395 186q-51 0-81-27.5T554-342q0-44 34.5-72.5T677-443q23 0 45 4t38 11v-12q0-29-20.5-47T685-505q-23 0-42 9.5T610-468l-47-35q24-29 54.5-43t68.5-14q69 0 103 32.5t34 97.5v178h-63v-37h-4q-14 23-38 35t-53 12Zm12-54q35 0 59.5-24t24.5-56q-14-8-33.5-12.5T689-393q-32 0-50 14t-18 37q0 20 16 33t40 13Z"/></svg>`;
342
- const FORMAT_CLEAR = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor"><path d="m528-546-93-93-121-121h486v120H568l-40 94ZM792-56 460-388l-80 188H249l119-280L56-792l56-56 736 736-56 56Z"/></svg>`;
343
- const FORMAT_COLOR_TEXT = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M80 0v-160h800V0H80Zm140-280 210-560h100l210 560h-96l-50-144H368l-52 144h-96Zm176-224h168l-82-232h-4l-82 232Z"/></svg>`;
344
- const FORMAT_COLOR_FILL = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="m247-904 57-56 343 343q23 23 23 57t-23 57L457-313q-23 23-57 23t-57-23L153-503q-23-23-23-57t23-57l190-191-96-96Zm153 153L209-560h382L400-751Zm360 471q-33 0-56.5-23.5T680-360q0-21 12.5-45t27.5-45q9-12 19-25t21-25q11 12 21 25t19 25q15 21 27.5 45t12.5 45q0 33-23.5 56.5T760-280ZM80 0v-160h800V0H80Z"/></svg>`;
345
- const FORMAT_BOLD = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M272-200v-560h221q65 0 120 40t55 111q0 51-23 78.5T602-491q25 11 55.5 41t30.5 90q0 89-65 124.5T501-200H272Zm121-112h104q48 0 58.5-24.5T566-372q0-11-10.5-35.5T494-432H393v120Zm0-228h93q33 0 48-17t15-38q0-24-17-39t-44-15h-95v109Z"/></svg>`;
346
- const FORMAT_ITALIC = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M200-200v-100h160l120-360H320v-100h400v100H580L460-300h140v100H200Z"/></svg>`;
347
- const FORMAT_UNDERLINE = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M200-120v-80h560v80H200Zm280-160q-101 0-157-63t-56-167v-330h103v336q0 56 28 91t82 35q54 0 82-35t28-91v-336h103v330q0 104-56 167t-157 63Z"/></svg>`;
348
- const FORMAT_STRIKETHROUGH = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M80-400v-80h800v80H80Zm340-160v-120H200v-120h560v120H540v120H420Zm0 400v-160h120v160H420Z"/></svg>`;
349
- const SUBSCRIPT = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M760-160v-80q0-17 11.5-28.5T800-280h80v-40H760v-40h120q17 0 28.5 11.5T920-320v40q0 17-11.5 28.5T880-240h-80v40h120v40H760Zm-525-80 185-291-172-269h106l124 200h4l123-200h107L539-531l186 291H618L482-457h-4L342-240H235Z"/></svg>`;
350
- const SUPERSCRIPT = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M760-600v-80q0-17 11.5-28.5T800-720h80v-40H760v-40h120q17 0 28.5 11.5T920-760v40q0 17-11.5 28.5T880-680h-80v40h120v40H760ZM235-160l185-291-172-269h106l124 200h4l123-200h107L539-451l186 291H618L482-377h-4L342-160H235Z"/></svg>`;
351
- const CODE = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M320-240 80-480l240-240 57 57-184 184 183 183-56 56Zm320 0-57-57 184-184-183-183 56-56 240 240-240 240Z"/></svg>`;
352
- const FORMAT_ALIGN_LEFT = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M120-120v-80h720v80H120Zm0-160v-80h480v80H120Zm0-160v-80h720v80H120Zm0-160v-80h480v80H120Zm0-160v-80h720v80H120Z"/></svg>`;
353
- const FORMAT_ALIGN_CENTER = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M120-120v-80h720v80H120Zm160-160v-80h400v80H280ZM120-440v-80h720v80H120Zm160-160v-80h400v80H280ZM120-760v-80h720v80H120Z"/></svg>`;
354
- const FORMAT_ALIGN_RIGHT = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M120-760v-80h720v80H120Zm240 160v-80h480v80H360ZM120-440v-80h720v80H120Zm240 160v-80h480v80H360ZM120-120v-80h720v80H120Z"/></svg>`;
355
- const FORMAT_ALIGN_JUSTIFY = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M120-120v-80h720v80H120Zm0-160v-80h720v80H120Zm0-160v-80h720v80H120Zm0-160v-80h720v80H120Zm0-160v-80h720v80H120Z"/></svg>`;
356
- const FORMAT_INDENT_DECREASE = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M120-120v-80h720v80H120Zm320-160v-80h400v80H440Zm0-160v-80h400v80H440Zm0-160v-80h400v80H440ZM120-760v-80h720v80H120Zm160 440L120-480l160-160v320Z"/></svg>`;
357
- const FORMAT_INDENT_INCREASE = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M120-120v-80h720v80H120Zm320-160v-80h400v80H440Zm0-160v-80h400v80H440Zm0-160v-80h400v80H440ZM120-760v-80h720v80H120Zm0 440v-320l160 160-160 160Z"/></svg>`;
358
- const FORMAT_LIST_BULLETED = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M360-200v-80h480v80H360Zm0-240v-80h480v80H360Zm0-240v-80h480v80H360ZM200-160q-33 0-56.5-23.5T120-240q0-33 23.5-56.5T200-320q33 0 56.5 23.5T280-240q0 33-23.5 56.5T200-160Zm0-240q-33 0-56.5-23.5T120-480q0-33 23.5-56.5T200-560q33 0 56.5 23.5T280-480q0 33-23.5 56.5T200-400Zm0-240q-33 0-56.5-23.5T120-720q0-33 23.5-56.5T200-800q33 0 56.5 23.5T280-720q0 33-23.5 56.5T200-640Z"/></svg>`;
359
- const FORMAT_LIST_NUMBERED = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M120-80v-60h100v-30h-60v-60h60v-30H120v-60h120q17 0 28.5 11.5T280-280v40q0 17-11.5 28.5T240-200q17 0 28.5 11.5T280-160v40q0 17-11.5 28.5T240-80H120Zm0-280v-110q0-17 11.5-28.5T160-510h60v-30H120v-60h120q17 0 28.5 11.5T280-560v70q0 17-11.5 28.5T240-450h-60v30h100v60H120Zm60-280v-180h-60v-60h120v240h-60Zm180 440v-80h480v80H360Zm0-240v-80h480v80H360Zm0-240v-80h480v80H360Z"/></svg>`;
360
- const FORMAT_QUOTE = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="m228-240 92-160q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 23-5.5 42.5T458-480L320-240h-92Zm360 0 92-160q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 23-5.5 42.5T818-480L680-240h-92ZM320-500q25 0 42.5-17.5T380-560q0-25-17.5-42.5T320-620q-25 0-42.5 17.5T260-560q0 25 17.5 42.5T320-500Zm360 0q25 0 42.5-17.5T740-560q0-25-17.5-42.5T680-620q-25 0-42.5 17.5T620-560q0 25 17.5 42.5T680-500Zm0-60Zm-360 0Z"/></svg>`;
361
- const TABLE_ROWS = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M760-200v-120H200v120h560Zm0-200v-160H200v160h560Zm0-240v-120H200v120h560ZM200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Z"/></svg>`;
362
- const LINK = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M440-280H280q-83 0-141.5-58.5T80-480q0-83 58.5-141.5T280-680h160v80H280q-50 0-85 35t-35 85q0 50 35 85t85 35h160v80ZM320-440v-80h320v80H320Zm200 160v-80h160q50 0 85-35t35-85q0-50-35-85t-85-35H520v-80h160q83 0 141.5 58.5T880-480q0 83-58.5 141.5T680-280H520Z"/></svg>`;
363
- const LINK_OFF = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="m770-302-60-62q40-11 65-42.5t25-73.5q0-50-35-85t-85-35H520v-80h160q83 0 141.5 58.5T880-480q0 57-29.5 105T770-302ZM634-440l-80-80h86v80h-6ZM792-56 56-792l56-56 736 736-56 56ZM440-280H280q-83 0-141.5-58.5T80-480q0-69 42-123t108-71l74 74h-24q-50 0-85 35t-35 85q0 50 35 85t85 35h160v80ZM320-440v-80h65l79 80H320Z"/></svg>`;
364
- const SPECIAL_CHARACTER = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M120-120v-120h200q-84-45-132-125t-48-175q0-142 99-241t241-99q142 0 241 99t99 241q0 95-48 175T640-240h200v120H520v-204q78-14 129-75t51-141q0-92-64-156t-156-64q-92 0-156 64t-64 156q0 80 51 141t129 75v204H120Z"/></svg>`;
365
- const CLEAR_ALL = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M120-280v-80h560v80H120Zm80-160v-80h560v80H200Zm80-160v-80h560v80H280Z"/></svg>`;
366
- const INSERT_PAGE_BREAK = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M240-80q-33 0-56.5-23.5T160-160v-120h80v120h480v-120h80v120q0 33-23.5 56.5T720-80H240Zm-80-440v-280q0-33 23.5-56.5T240-880h320l240 240v120h-80v-80H520v-200H240v280h-80Zm200 160v-80h240v80H360Zm320 0v-80h240v80H680Zm-640 0v-80h240v80H40Zm440-160Zm0 240Z"/></svg>`;
367
- const MARKDOWN = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="m640-360 120-120-42-43-48 48v-125h-60v125l-48-48-42 43 120 120ZM160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm0-80h640v-480H160v480Zm0 0v-480 480Zm60-120h60v-180h40v120h60v-120h40v180h60v-200q0-17-11.5-28.5T440-600H260q-17 0-28.5 11.5T220-560v200Z"/></svg>`;
368
- const DYNAMIC_FORM = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M160-520q-33 0-56.5-23.5T80-600v-120q0-33 23.5-56.5T160-800h360v280H160Zm0-80h280v-120H160v120Zm0 440q-33 0-56.5-23.5T80-240v-120q0-33 23.5-56.5T160-440h440v280H160Zm0-80h360v-120H160v120Zm520 80v-360h-80v-280h280l-80 200h80L680-160ZM190-270h60v-60h-60v60Zm0-360h60v-60h-60v60Zm-30 30v-120 120Zm0 360v-120 120Z"/></svg>`;
369
- const PHOTO = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm0-80h560v-560H200v560Zm40-80h480L570-480 450-320l-90-120-120 160Zm-40 80v-560 560Z"/></svg>`;
370
- const PRINT = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M640-640v-120H320v120h-80v-200h480v200h-80Zm-480 80h640-640Zm560 100q17 0 28.5-11.5T760-500q0-17-11.5-28.5T720-540q-17 0-28.5 11.5T680-500q0 17 11.5 28.5T720-460Zm-80 260v-160H320v160h320Zm80 80H240v-160H80v-240q0-51 35-85.5t85-34.5h560q51 0 85.5 34.5T880-520v240H720v160Zm80-240v-160q0-17-11.5-28.5T760-560H200q-17 0-28.5 11.5T160-520v160h80v-80h480v80h80Z"/></svg>`;
371
- const ARCHIVE = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="m480-240 160-160-56-56-64 64v-168h-80v168l-64-64-56 56 160 160ZM200-640v440h560v-440H200Zm0 520q-33 0-56.5-23.5T120-200v-499q0-14 4.5-27t13.5-24l50-61q11-14 27.5-21.5T250-840h460q18 0 34.5 7.5T772-811l50 61q9 11 13.5 24t4.5 27v499q0 33-23.5 56.5T760-120H200Zm16-600h528l-34-40H250l-34 40Zm264 300Z"/></svg>`;
372
- // Common Icons
373
- const EDIT = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M200-200h57l391-391-57-57-391 391v57Zm-80 80v-170l528-527q12-11 26.5-17t30.5-6q16 0 31 6t26 18l55 56q12 11 17.5 26t5.5 30q0 16-5.5 30.5T817-647L290-120H120Zm640-584-56-56 56 56Zm-141 85-28-29 57 57-29-28Z"/></svg>`;
374
- const SETTINGS = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="m370-80-16-128q-13-5-24.5-12T307-235l-119 50L78-375l103-78q-1-7-1-13.5v-27q0-6.5 1-13.5L78-585l110-190 119 50q11-8 23-15t24-12l16-128h220l16 128q13 5 24.5 12t22.5 15l119-50 110 190-103 78q1 7 1 13.5v27q0 6.5-2 13.5l103 78-110 190-118-50q-11 8-23 15t-24 12L590-80H370Zm70-80h79l14-106q31-8 57.5-23.5T639-327l99 41 39-68-86-65q5-14 7-29.5t2-31.5q0-16-2-31.5t-7-29.5l86-65-39-68-99 42q-22-23-48.5-38.5T533-694l-13-106h-79l-14 106q-31 8-57.5 23.5T321-633l-99-41-39 68 86 64q-5 15-7 30t-2 32q0 16 2 31t7 30l-86 65 39 68 99-42q22 23 48.5 38.5T427-266l13 106Zm42-180q58 0 99-41t41-99q0-58-41-99t-99-41q-59 0-99.5 41T342-480q0 58 40.5 99t99.5 41Zm-2-140Z"/></svg>`;
375
- const SEARCH = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z"/></svg>`;
376
- const SEARCH_OFF = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M280-80q-83 0-141.5-58.5T80-280q0-83 58.5-141.5T280-480q83 0 141.5 58.5T480-280q0 83-58.5 141.5T280-80Zm544-40L568-376q-12-13-25.5-26.5T516-428q38-24 61-64t23-88q0-75-52.5-127.5T420-760q-75 0-127.5 52.5T240-580q0 6 .5 11.5T242-557q-18 2-39.5 8T164-535q-2-11-3-22t-1-23q0-109 75.5-184.5T420-840q109 0 184.5 75.5T680-580q0 43-13.5 81.5T629-428l251 252-56 56Zm-615-61 71-71 70 71 29-28-71-71 71-71-28-28-71 71-71-71-28 28 71 71-71 71 28 28Z"/></svg>`;
377
- const DESCRIPTION = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M320-240h320v-80H320v80Zm0-160h320v-80H320v80ZM240-80q-33 0-56.5-23.5T160-160v-640q0-33 23.5-56.5T240-880h320l240 240v480q0 33-23.5 56.5T720-80H240Zm280-520v-200H240v640h480v-440H520ZM240-800v200-200 640-640Z"/></svg>`;
378
- const CLOSE = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/></svg>`;
379
- const VERTICAL_ALIGN_TOP = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M160-760v-80h640v80H160Zm280 640v-408L336-424l-56-56 200-200 200 200-56 56-104-104v408h-80Z"/></svg>`;
380
- const VERTICAL_ALIGN_CENTER = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M440-80v-168l-64 64-56-56 160-160 160 160-56 56-64-64v168h-80ZM160-440v-80h640v80H160Zm320-120L320-720l56-56 64 64v-168h80v168l64-64 56 56-160 160Z"/></svg>`;
381
- const VERTICAL_ALIGN_BOTTOM = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M160-120v-80h640v80H160Zm320-160L280-480l56-56 104 104v-408h80v408l104-104 56 56-200 200Z"/></svg>`;
382
- // Custom SVG
383
- const RESTRICT_EDIT = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" aria-hidden="true">
384
- <g transform="translate(10 10) scale(0.8) translate(-10 -10)">
385
- <path d="M6.25 16a.75.75 0 1 1 0 1.5H.75a.75.75 0 1 1 0-1.5zm0-5a.75.75 0 1 1 0 1.5H.75a.75.75 0 1 1 0-1.5zm3-5a.75.75 0 0 1 0 1.5H.75a.75.75 0 0 1 0-1.5zm6-5a.75.75 0 1 1 0 1.5H.75a.75.75 0 0 1 0-1.5zm.25 5.5a3.5 3.5 0 0 1 3.143 1.959.75.75 0 0 1-1.36.636A2 2 0 0 0 13.5 10v2H19a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1v-5a1 1 0 0 1 1-1v-2l.005-.192A3.5 3.5 0 0 1 15.5 6.5m0 7.5a.5.5 0 0 0-.492.41L15 14.5v2a.5.5 0 0 0 .992.09L16 16.5v-2a.5.5 0 0 0-.5-.5">
386
- </path>
387
- </g>
388
- </svg>
389
- `;
390
- const TABLE_CELL_PROPERTY = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" aria-hidden="true">
391
- <path d="m11.105 18-.17 1H2.5A1.5 1.5 0 0 1 1 17.5v-15A1.5 1.5 0 0 1 2.5 1h15A1.5 1.5 0 0 1 19 2.5v9.975l-.85-.124-.15-.302V8h-5v4h.021l-.172.351-1.916.28-.151.027c-.287.063-.54.182-.755.341L8 13v5zM2 12h5V8H2zm10-4H8v4h4zM2 2v5h5V2zm0 16h5v-5H2zM13 7h5V2h-5zM8 2v5h4V2z" opacity=".6"></path>
392
- <path d="m15.5 11.5 1.323 2.68 2.957.43-2.14 2.085.505 2.946L15.5 18.25l-2.645 1.39.505-2.945-2.14-2.086 2.957-.43zM13 6a1 1 0 0 1 1 1v3.172a2 2 0 0 0-.293.443l-.858 1.736-1.916.28-.151.027A1.98 1.98 0 0 0 9.315 14H7a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm-1 2H8v4h4z"></path>
393
- </svg>
394
- `;
395
- const TABLE_COLUMN = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" aria-hidden="true">
396
- <path d="M2.5 1h15A1.5 1.5 0 0 1 19 2.5v15a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 1 17.5v-15A1.5 1.5 0 0 1 2.5 1M2 2v16h16V2z" opacity=".6"></path>
397
- <path d="M18 7v1H2V7zm0 5v1H2v-1z" opacity=".6"></path>
398
- <path d="M14 1v18a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1m-2 1H8v4h4zm0 6H8v4h4zm0 6H8v4h4z"></path>
399
- </svg>
400
- `;
401
- const TABLE_MERGE_CELL = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" aria-hidden="true">
402
- <path d="M2.5 1h15A1.5 1.5 0 0 1 19 2.5v15a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 1 17.5v-15A1.5 1.5 0 0 1 2.5 1M2 2v16h16V2z" opacity=".6"></path>
403
- <path d="M7 2h1v16H7zm5 0h1v7h-1zm6 5v1H2V7zM8 12v1H2v-1z" opacity=".6"></path>
404
- <path d="M7 7h12a1 1 0 0 1 1 1v11a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1m1 2v9h10V9z"></path>
405
- </svg>
406
- `;
407
- const TABLE_PROPERTY = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" aria-hidden="true">
408
- <path d="M8 2v5h4V2h1v5h5v1h-5v4h.021l-.172.351-1.916.28-.151.027c-.287.063-.54.182-.755.341L8 13v5H7v-5H2v-1h5V8H2V7h5V2zm4 6H8v4h4z" opacity=".6"></path>
409
- <path d="m15.5 11.5 1.323 2.68 2.957.43-2.14 2.085.505 2.946L15.5 18.25l-2.645 1.39.505-2.945-2.14-2.086 2.957-.43zM17 1a2 2 0 0 1 2 2v9.475l-.85-.124-.857-1.736a2 2 0 0 0-.292-.44L17 3H3v14h7.808l.402.392L10.935 19H3a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2z"></path>
410
- </svg>
411
- `;
412
- const TABLE_ROW = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" aria-hidden="true">
413
- <path d="M2.5 1h15A1.5 1.5 0 0 1 19 2.5v15a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 1 17.5v-15A1.5 1.5 0 0 1 2.5 1M2 2v16h16V2z" opacity=".6"></path>
414
- <path d="M7 2h1v16H7zm5 0h1v16h-1z" opacity=".6"></path>
415
- <path d="M1 6h18a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1m1 2v4h4V8zm6 0v4h4V8zm6 0v4h4V8z"></path>
416
- </svg>
417
- `;
418
-
419
- /**
420
- * Icon registry service for managing SVG icons
421
- * Similar to Angular Material's MatIconRegistry
422
- */
423
- class IconRegistryService {
424
- sanitizer = inject(DomSanitizer);
425
- icons = new Map();
426
- constructor() {
427
- this.registerDefaultIcons();
428
- }
429
- /**
430
- * Register a new icon with its SVG content
431
- */
432
- registerIcon(name, svgContent) {
433
- if (this.icons.has(name)) {
434
- throw new Error(`Icon ${name} already registered`);
435
- }
436
- this.icons.set(name, svgContent);
437
- }
438
- /**
439
- * Get icon SVG content by name
440
- * Returns SafeHtml for use in templates with [innerHTML]
441
- */
442
- getIcon(name) {
443
- const icon = this.icons.get(name);
444
- if (!icon) {
445
- console.warn(`Icon ${name} not registered`);
446
- return this.sanitizer.bypassSecurityTrustHtml(DEFAULT);
447
- }
448
- return this.sanitizer.bypassSecurityTrustHtml(icon);
449
- }
450
- /**
451
- * Get raw icon SVG string by name
452
- * Returns string for direct DOM manipulation
453
- */
360
+ function shouldShowFnFactory(params) {
361
+ const { editor, target, forceOpen } = params;
362
+ const requireFocus = target.requireFocus !== false;
363
+ const allowEmptySelection = target.allowEmptySelection === true;
364
+ return () => {
365
+ if (forceOpen())
366
+ return true;
367
+ if (requireFocus && !editor.isFocused)
368
+ return false;
369
+ // Avoid showing during drag/resize
370
+ const dragging = editor.isDragging;
371
+ if (dragging)
372
+ return false;
373
+ // Avoid showing when selection is empty and mark is required
374
+ if (!allowEmptySelection && editor.state.selection.empty && target.kind === 'mark')
375
+ return false;
376
+ // Avoid showing when extension is not active
377
+ const active = Array.isArray(target.name)
378
+ ? target.name.some((name) => editor.isActive(name))
379
+ : editor.isActive(target.name);
380
+ if (!active)
381
+ return false;
382
+ // Avoid showing when extra predicate is not met
383
+ if (target.extraPredicate && !target.extraPredicate(editor))
384
+ return false;
385
+ return true;
386
+ };
387
+ }
388
+ function selectionChangeHandler(params) {
389
+ const { editor, target, onActive, onInactive, listenOnUpdate } = params;
390
+ const run = () => {
391
+ const isActive = Array.isArray(target.name)
392
+ ? target.name.some((name) => editor.isActive(name))
393
+ : editor.isActive(target.name);
394
+ if (!isActive)
395
+ return onInactive();
396
+ const activeName = Array.isArray(target.name) ? target.name.find((name) => editor.isActive(name)) : target.name;
397
+ const attrs = target.kind === 'mark'
398
+ ? (editor.getAttributes(activeName) ?? {})
399
+ : (editor.state.selection.node?.attrs ?? {});
400
+ onActive(attrs);
401
+ };
402
+ const onSelection = () => run();
403
+ const onUpdate = () => run();
404
+ editor.on('selectionUpdate', onSelection);
405
+ if (listenOnUpdate)
406
+ editor.on('update', onUpdate);
407
+ run(); // initial sync
408
+ return () => {
409
+ editor.off('selectionUpdate', onSelection);
410
+ if (listenOnUpdate)
411
+ editor.off('update', onUpdate);
412
+ };
413
+ }
414
+
415
+ class EventManager {
416
+ document = inject(DOCUMENT);
417
+ keyToEscapeListener = new Map();
418
+ keyToOutsideListener = new Map();
419
+ isAttached(key) {
420
+ return this.keyToOutsideListener.has(key) || this.keyToEscapeListener.has(key);
421
+ }
422
+ attach(key, hostElement, cb, anchorElement) {
423
+ // Avoid duplicate listeners
424
+ this.detach(key);
425
+ const oldOutside = this.keyToOutsideListener.get(key);
426
+ if (!oldOutside) {
427
+ const handler = (event) => {
428
+ const target = event.target;
429
+ const path = event.composedPath?.();
430
+ // Check if click is inside bubble
431
+ const isInsideBubble = !!hostElement && ((!!target && hostElement.contains(target)) || (!!path && path.includes(hostElement)));
432
+ if (isInsideBubble)
433
+ return;
434
+ // Check if click is on the anchor (toolbar button)
435
+ const isInsideAnchor = !!anchorElement && ((!!target && anchorElement.contains(target)) || (!!path && path.includes(anchorElement)));
436
+ if (isInsideAnchor)
437
+ return;
438
+ // Check if click is inside a CDK overlay (e.g., MatSelect dropdown, MatMenu, etc.)
439
+ // These are rendered outside the bubble but should not close it
440
+ if (target instanceof Element) {
441
+ const isInsideOverlay = target.closest('.cdk-overlay-container, .cdk-overlay-pane');
442
+ if (isInsideOverlay)
443
+ return;
444
+ }
445
+ cb(key);
446
+ };
447
+ this.keyToOutsideListener.set(key, handler);
448
+ this.document.addEventListener('pointerdown', handler, true);
449
+ this.document.addEventListener('mousedown', handler, true);
450
+ this.document.addEventListener('click', handler, true);
451
+ }
452
+ const oldEsc = this.keyToEscapeListener.get(key);
453
+ if (!oldEsc) {
454
+ const escHandler = (e) => e.key === 'Escape' && cb(key);
455
+ this.document.addEventListener('keydown', escHandler, true);
456
+ this.keyToEscapeListener.set(key, escHandler);
457
+ }
458
+ }
459
+ detach(key) {
460
+ const outside = this.keyToOutsideListener.get(key);
461
+ if (outside) {
462
+ this.document.removeEventListener('pointerdown', outside, true);
463
+ this.document.removeEventListener('mousedown', outside, true);
464
+ this.document.removeEventListener('click', outside, true);
465
+ this.keyToOutsideListener.delete(key);
466
+ }
467
+ const esc = this.keyToEscapeListener.get(key);
468
+ if (esc) {
469
+ this.document.removeEventListener('keydown', esc, true);
470
+ this.keyToEscapeListener.delete(key);
471
+ }
472
+ }
473
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: EventManager, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
474
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: EventManager, providedIn: 'root' });
475
+ }
476
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: EventManager, decorators: [{
477
+ type: Injectable,
478
+ args: [{ providedIn: 'root' }]
479
+ }] });
480
+
481
+ /**
482
+ * Service to manage focus trapping within an element
483
+ * Traps keyboard focus inside a container element for accessibility
484
+ */
485
+ class FocusTrapService {
486
+ document = inject(DOCUMENT);
487
+ activeTraps = new Map();
488
+ /**
489
+ * Attach focus trap to an element
490
+ * @param element The container element to trap focus within
491
+ * @param autoFocus Whether to automatically focus the first focusable element
492
+ */
493
+ attach(element, autoFocus = true) {
494
+ if (!element)
495
+ return;
496
+ // Detach existing trap if any
497
+ this.detach(element);
498
+ // Get all focusable elements within the container
499
+ const focusableSelectors = [
500
+ 'a[href]',
501
+ 'button:not([disabled])',
502
+ 'textarea:not([disabled])',
503
+ 'input:not([disabled])',
504
+ 'select:not([disabled])',
505
+ '[tabindex]:not([tabindex="-1"])',
506
+ ].join(', ');
507
+ const focusableElements = Array.from(element.querySelectorAll(focusableSelectors));
508
+ if (focusableElements.length === 0)
509
+ return;
510
+ const firstFocusableElement = focusableElements[0];
511
+ const lastFocusableElement = focusableElements[focusableElements.length - 1];
512
+ // Focus the first element if requested
513
+ if (autoFocus && firstFocusableElement) {
514
+ setTimeout(() => {
515
+ firstFocusableElement.focus();
516
+ }, 0);
517
+ }
518
+ // Handle Tab key to trap focus
519
+ const handler = (event) => {
520
+ if (event.key !== 'Tab')
521
+ return;
522
+ if (!element.contains(this.document.activeElement)) {
523
+ event.preventDefault();
524
+ if (event.shiftKey) {
525
+ lastFocusableElement?.focus();
526
+ }
527
+ else {
528
+ firstFocusableElement?.focus();
529
+ }
530
+ return;
531
+ }
532
+ if (event.shiftKey) {
533
+ // Shift + Tab: going backwards
534
+ if (this.document.activeElement === firstFocusableElement) {
535
+ event.preventDefault();
536
+ lastFocusableElement?.focus();
537
+ }
538
+ }
539
+ else {
540
+ // Tab: going forwards
541
+ if (this.document.activeElement === lastFocusableElement) {
542
+ event.preventDefault();
543
+ firstFocusableElement?.focus();
544
+ }
545
+ }
546
+ };
547
+ element.addEventListener('keydown', handler);
548
+ this.activeTraps.set(element, {
549
+ handler,
550
+ firstElement: firstFocusableElement,
551
+ lastElement: lastFocusableElement,
552
+ });
553
+ }
554
+ /**
555
+ * Detach focus trap from an element
556
+ * @param element The element to remove focus trap from
557
+ */
558
+ detach(element) {
559
+ if (!element)
560
+ return;
561
+ const trap = this.activeTraps.get(element);
562
+ if (trap) {
563
+ element.removeEventListener('keydown', trap.handler);
564
+ this.activeTraps.delete(element);
565
+ }
566
+ }
567
+ /**
568
+ * Detach all active focus traps
569
+ */
570
+ detachAll() {
571
+ this.activeTraps.forEach((trap, element) => {
572
+ element.removeEventListener('keydown', trap.handler);
573
+ });
574
+ this.activeTraps.clear();
575
+ }
576
+ /**
577
+ * Re-attach focus trap to an element (useful when content changes)
578
+ * @param element The element to re-attach focus trap to
579
+ * @param autoFocus Whether to automatically focus the first focusable element
580
+ */
581
+ reattach(element, autoFocus = false) {
582
+ this.detach(element);
583
+ this.attach(element, autoFocus);
584
+ }
585
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: FocusTrapService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
586
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: FocusTrapService, providedIn: 'root' });
587
+ }
588
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: FocusTrapService, decorators: [{
589
+ type: Injectable,
590
+ args: [{ providedIn: 'root' }]
591
+ }] });
592
+
593
+ class AngularRenderer {
594
+ applicationRef;
595
+ componentRef;
596
+ constructor(ViewComponent, injector, props) {
597
+ this.applicationRef = injector.get(ApplicationRef);
598
+ this.componentRef = createComponent(ViewComponent, {
599
+ environmentInjector: this.applicationRef.injector,
600
+ elementInjector: injector,
601
+ });
602
+ // set input props to the component
603
+ this.updateProps(props);
604
+ this.applicationRef.attachView(this.componentRef.hostView);
605
+ }
606
+ get instance() {
607
+ return this.componentRef.instance;
608
+ }
609
+ get elementRef() {
610
+ return this.componentRef.injector.get(ElementRef);
611
+ }
612
+ get dom() {
613
+ return this.elementRef.nativeElement;
614
+ }
615
+ updateProps(props) {
616
+ Object.entries(props).forEach(([key, value]) => {
617
+ this.componentRef.setInput(key, value);
618
+ });
619
+ }
620
+ updateAttributes(attributes) {
621
+ Object.keys(attributes).forEach((key) => {
622
+ this.dom.setAttribute(key, attributes[key]);
623
+ });
624
+ }
625
+ detectChanges() {
626
+ this.componentRef.changeDetectorRef.detectChanges();
627
+ }
628
+ destroy() {
629
+ this.componentRef.destroy();
630
+ this.applicationRef.detachView(this.componentRef.hostView);
631
+ }
632
+ }
633
+
634
+ class TiptapDraggableDirective {
635
+ draggable = true;
636
+ handle = '';
637
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: TiptapDraggableDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
638
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.10", type: TiptapDraggableDirective, isStandalone: true, selector: "[tiptapDraggable]", host: { properties: { "attr.draggable": "this.draggable", "attr.data-drag-handle": "this.handle" } }, ngImport: i0 });
639
+ }
640
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: TiptapDraggableDirective, decorators: [{
641
+ type: Directive,
642
+ args: [{
643
+ selector: '[tiptapDraggable]',
644
+ }]
645
+ }], propDecorators: { draggable: [{
646
+ type: HostBinding,
647
+ args: ['attr.draggable']
648
+ }], handle: [{
649
+ type: HostBinding,
650
+ args: ['attr.data-drag-handle']
651
+ }] } });
652
+
653
+ /**
654
+ * Injection token for HTML preprocessor function.
655
+ * Use this token to provide a custom function that processes HTML content
656
+ * before it's set into the TipTap editor.
657
+ *
658
+ * This is optional - if not provided, the editor will use the HTML content as-is.
659
+ *
660
+ * @example
661
+ * ```typescript
662
+ * // In your component or module providers
663
+ * {
664
+ * provide: EDITOR_HTML_PREPROCESSOR,
665
+ * useValue: (html: string) => {
666
+ * // Your custom preprocessing logic
667
+ * return html.replace(/old/g, 'new');
668
+ * }
669
+ * }
670
+ * ```
671
+ */
672
+ const EDITOR_HTML_PREPROCESSOR = new InjectionToken('EDITOR_HTML_PREPROCESSOR');
673
+ /**
674
+ * Injection token for content wrapper class.
675
+ * Use this token to provide a custom class name for the content wrapper element.
676
+ *
677
+ * @example
678
+ * ```typescript
679
+ * // In your component or module providers
680
+ * {
681
+ * provide: EDITOR_CONTENT_WRAPPER_CLASS,
682
+ * useValue: 'my-content-wrapper'
683
+ * }
684
+ * ```
685
+ */
686
+ const EDITOR_CONTENT_WRAPPER_CLASS = new InjectionToken('EDITOR_CONTENT_WRAPPER_CLASS', {
687
+ providedIn: 'root',
688
+ factory: () => null,
689
+ });
690
+
691
+ class TiptapEditorDirective {
692
+ elRef = inject(ElementRef);
693
+ renderer = inject(Renderer2);
694
+ changeDetectorRef = inject(ChangeDetectorRef);
695
+ htmlPreprocessor = inject(EDITOR_HTML_PREPROCESSOR, { optional: true });
696
+ contentWrapperClass = inject(EDITOR_CONTENT_WRAPPER_CLASS, { optional: true });
697
+ _editor;
698
+ initialEditableState;
699
+ set editor(editor) {
700
+ this._editor = editor;
701
+ // Store initial editable state immediately when editor is set
702
+ // This ensures we capture the state before any Forms API calls
703
+ if (this.initialEditableState === undefined && editor) {
704
+ this.initialEditableState = editor.isEditable;
705
+ }
706
+ }
707
+ get editor() {
708
+ if (!this._editor) {
709
+ throw new Error('Editor must be set before use.');
710
+ }
711
+ return this._editor;
712
+ }
713
+ outputFormat = 'html';
714
+ onChange = () => {
715
+ /** */
716
+ };
717
+ onTouched = () => {
718
+ /** */
719
+ };
720
+ // Writes a new value to the element.
721
+ // This methods is called when programmatic changes from model to view are requested.
722
+ writeValue(value) {
723
+ // Apply preprocessor if value is a string (HTML) and preprocessor is available
724
+ let processedValue = value;
725
+ if (typeof value === 'string' && this.htmlPreprocessor) {
726
+ processedValue = this.htmlPreprocessor(value);
727
+ }
728
+ this.editor.chain().setContent(processedValue, { emitUpdate: false }).run();
729
+ }
730
+ // Registers a callback function that is called when the control's value changes in the UI.
731
+ registerOnChange(fn) {
732
+ this.onChange = fn;
733
+ }
734
+ // Registers a callback function that is called by the forms API on initialization to update the form model on blur.
735
+ registerOnTouched(fn) {
736
+ this.onTouched = fn;
737
+ }
738
+ // Called by the forms api to enable or disable the element
739
+ setDisabledState(isDisabled) {
740
+ // Ensure we have captured the initial state (fallback in case setter wasn't called)
741
+ if (this.initialEditableState === undefined && this.editor) {
742
+ this.initialEditableState = this.editor.isEditable;
743
+ }
744
+ // Only apply disabled state if editor was initially editable
745
+ // If editor was configured as readonly (editable: false), preserve that state
746
+ if (this.initialEditableState === true) {
747
+ this.editor.setEditable(!isDisabled);
748
+ }
749
+ // If initialEditableState === false, do nothing - preserve readonly state
750
+ this.renderer.setProperty(this.elRef.nativeElement, 'disabled', isDisabled);
751
+ }
752
+ handleChange = ({ editor, transaction }) => {
753
+ if (!transaction.docChanged)
754
+ return;
755
+ // Needed for ChangeDetectionStrategy.OnPush to get notified about changes
756
+ this.changeDetectorRef.markForCheck();
757
+ if (this.outputFormat === 'html') {
758
+ const htmlContent = editor.getHTML();
759
+ let finalContent = htmlContent;
760
+ if (this.contentWrapperClass) {
761
+ finalContent = `<div class="${this.contentWrapperClass}">${htmlContent}</div>`;
762
+ }
763
+ this.onChange(finalContent);
764
+ return;
765
+ }
766
+ this.onChange(editor.getJSON());
767
+ };
768
+ ngOnInit() {
769
+ const editor = this.editor;
770
+ // take the inner contents and clear the block
771
+ const { innerHTML } = this.elRef.nativeElement;
772
+ this.elRef.nativeElement.innerHTML = '';
773
+ // insert the editor in the dom
774
+ this.elRef.nativeElement.append(...Array.from(editor.options.element?.childNodes || []));
775
+ // update the options for the editor
776
+ editor.setOptions({ element: this.elRef.nativeElement });
777
+ // update content to the editor
778
+ if (innerHTML) {
779
+ // Apply preprocessor to initial content if available
780
+ const processedContent = this.htmlPreprocessor ? this.htmlPreprocessor(innerHTML) : innerHTML;
781
+ editor.chain().setContent(processedContent, { emitUpdate: false }).run();
782
+ }
783
+ // register blur handler to update `touched` property
784
+ editor.on('blur', () => {
785
+ this.onTouched();
786
+ });
787
+ // register update handler to listen to changes on update
788
+ editor.on('update', this.handleChange);
789
+ // Needed for ChangeDetectionStrategy.OnPush to get notified
790
+ editor.on('selectionUpdate', () => this.changeDetectorRef.markForCheck());
791
+ }
792
+ ngAfterViewInit() {
793
+ this.changeDetectorRef.detectChanges();
794
+ }
795
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: TiptapEditorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
796
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.10", type: TiptapEditorDirective, isStandalone: true, selector: "tiptap[editor], [tiptap][editor], tiptap-editor[editor], [tiptapEditor][editor]", inputs: { editor: "editor", outputFormat: "outputFormat" }, host: { classAttribute: "tiptap-editor no-twp" }, providers: [
797
+ {
798
+ provide: NG_VALUE_ACCESSOR,
799
+ useExisting: forwardRef(() => TiptapEditorDirective),
800
+ multi: true,
801
+ },
802
+ ], ngImport: i0 });
803
+ }
804
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: TiptapEditorDirective, decorators: [{
805
+ type: Directive,
806
+ args: [{
807
+ selector: 'tiptap[editor], [tiptap][editor], tiptap-editor[editor], [tiptapEditor][editor]',
808
+ providers: [
809
+ {
810
+ provide: NG_VALUE_ACCESSOR,
811
+ useExisting: forwardRef(() => TiptapEditorDirective),
812
+ multi: true,
813
+ },
814
+ ],
815
+ standalone: true,
816
+ host: {
817
+ ['class']: 'tiptap-editor no-twp',
818
+ },
819
+ }]
820
+ }], propDecorators: { editor: [{
821
+ type: Input
822
+ }], outputFormat: [{
823
+ type: Input
824
+ }] } });
825
+
826
+ class TiptapFloatingMenuDirective {
827
+ elRef;
828
+ constructor(elRef) {
829
+ this.elRef = elRef;
830
+ }
831
+ pluginKey = 'TiptapFloatingMenu';
832
+ editor;
833
+ options = {};
834
+ shouldShow;
835
+ ngOnInit() {
836
+ const editor = this.editor;
837
+ if (!editor) {
838
+ throw new Error('Required: Input `editor`');
839
+ }
840
+ const floatingMenuElement = this.elRef.nativeElement;
841
+ floatingMenuElement.style.visibility = 'hidden';
842
+ floatingMenuElement.style.position = 'absolute';
843
+ editor.registerPlugin(FloatingMenuPlugin({
844
+ pluginKey: this.pluginKey,
845
+ editor,
846
+ element: floatingMenuElement,
847
+ options: this.options,
848
+ shouldShow: this.shouldShow,
849
+ }));
850
+ }
851
+ ngOnDestroy() {
852
+ if (this.editor && this.pluginKey) {
853
+ this.editor.unregisterPlugin(this.pluginKey);
854
+ }
855
+ window.requestAnimationFrame(() => {
856
+ if (this.elRef.nativeElement.parentNode) {
857
+ this.elRef.nativeElement.parentNode.removeChild(this.elRef.nativeElement);
858
+ }
859
+ });
860
+ }
861
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: TiptapFloatingMenuDirective, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive });
862
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.10", type: TiptapFloatingMenuDirective, isStandalone: true, selector: "tiptap-floating-menu[editor], [tiptapFloatingMenu][editor]", inputs: { pluginKey: "pluginKey", editor: "editor", options: "options", shouldShow: "shouldShow" }, ngImport: i0 });
863
+ }
864
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: TiptapFloatingMenuDirective, decorators: [{
865
+ type: Directive,
866
+ args: [{
867
+ selector: 'tiptap-floating-menu[editor], [tiptapFloatingMenu][editor]',
868
+ }]
869
+ }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { pluginKey: [{
870
+ type: Input
871
+ }], editor: [{
872
+ type: Input
873
+ }], options: [{
874
+ type: Input
875
+ }], shouldShow: [{
876
+ type: Input
877
+ }] } });
878
+
879
+ /**
880
+ * Directive for positioning a popover/dropdown relative to a trigger element using floating-ui
881
+ * Handles positioning, visibility, and cleanup automatically
882
+ *
883
+ * Usage:
884
+ * <button #trigger>Open</button>
885
+ * <div [popover]="trigger" [isOpen]="isOpen">
886
+ * Content
887
+ * </div>
888
+ */
889
+ class PopoverDirective {
890
+ elRef = inject(ElementRef);
891
+ cdr = inject(ChangeDetectorRef);
892
+ /**
893
+ * The trigger element reference (ElementRef or HTMLElement)
894
+ */
895
+ popover;
896
+ /**
897
+ * Whether the popover is open
898
+ */
899
+ isOpen = false;
900
+ /**
901
+ * Placement of the popover relative to trigger
902
+ */
903
+ placement = 'bottom-start';
904
+ /**
905
+ * Offset from trigger
906
+ */
907
+ offset = 8;
908
+ /**
909
+ * Padding for flip/shift middleware
910
+ */
911
+ padding = 8;
912
+ cleanupAutoUpdate;
913
+ isPositioned = false;
914
+ viewInitialized = false;
915
+ retryCount = 0;
916
+ maxRetries = 10;
917
+ ngAfterViewInit() {
918
+ this.viewInitialized = true;
919
+ this.updateVisibility();
920
+ }
921
+ ngOnChanges(changes) {
922
+ if (this.viewInitialized && (changes['isOpen'] || changes['popover'])) {
923
+ this.updateVisibility();
924
+ }
925
+ }
926
+ ngOnDestroy() {
927
+ this.cleanup();
928
+ }
929
+ updateVisibility() {
930
+ if (!this.viewInitialized)
931
+ return;
932
+ if (this.isOpen && this.popover) {
933
+ // Wait for next tick to ensure element is rendered
934
+ setTimeout(() => {
935
+ this.setupFloating();
936
+ });
937
+ }
938
+ else {
939
+ this.isPositioned = false;
940
+ this.cleanup();
941
+ }
942
+ }
943
+ setupFloating() {
944
+ // Reset retry count on each attempt
945
+ this.retryCount = 0;
946
+ this._setupFloatingInternal();
947
+ }
948
+ _setupFloatingInternal() {
949
+ let triggerEl;
950
+ if (!this.popover) {
951
+ if (this.retryCount < this.maxRetries) {
952
+ this.retryCount++;
953
+ setTimeout(() => this._setupFloatingInternal(), 10);
954
+ }
955
+ return;
956
+ }
957
+ if (this.popover instanceof ElementRef) {
958
+ triggerEl = this.popover.nativeElement;
959
+ // If ElementRef exists but nativeElement is not yet available, retry
960
+ if (!triggerEl) {
961
+ if (this.retryCount < this.maxRetries) {
962
+ this.retryCount++;
963
+ setTimeout(() => this._setupFloatingInternal(), 10);
964
+ }
965
+ return;
966
+ }
967
+ }
968
+ else {
969
+ triggerEl = this.popover;
970
+ }
971
+ const dropdownEl = this.elRef.nativeElement;
972
+ if (!triggerEl || !dropdownEl) {
973
+ // Retry if elements not ready
974
+ if (this.retryCount < this.maxRetries) {
975
+ this.retryCount++;
976
+ setTimeout(() => this._setupFloatingInternal(), 10);
977
+ }
978
+ return;
979
+ }
980
+ // Compute initial position before showing dropdown to prevent flash
981
+ computePosition(triggerEl, dropdownEl, {
982
+ placement: this.placement,
983
+ middleware: [offset(this.offset), flip(), shift({ padding: this.padding })],
984
+ })
985
+ .then(({ x, y }) => {
986
+ // Set position first, then make visible
987
+ Object.assign(dropdownEl.style, { left: `${x}px`, top: `${y}px` });
988
+ this.isPositioned = true;
989
+ // Trigger change detection to update visibility binding
990
+ this.cdr.markForCheck();
991
+ })
992
+ .catch((error) => {
993
+ console.error('[PopoverDirective] Error computing position', error);
994
+ });
995
+ // Use autoUpdate to automatically reposition on scroll/resize
996
+ this.cleanupAutoUpdate = autoUpdate(triggerEl, dropdownEl, () => {
997
+ computePosition(triggerEl, dropdownEl, {
998
+ placement: this.placement,
999
+ middleware: [offset(this.offset), flip(), shift({ padding: this.padding })],
1000
+ }).then(({ x, y }) => {
1001
+ Object.assign(dropdownEl.style, { left: `${x}px`, top: `${y}px` });
1002
+ });
1003
+ });
1004
+ }
1005
+ cleanup() {
1006
+ if (this.cleanupAutoUpdate) {
1007
+ this.cleanupAutoUpdate();
1008
+ this.cleanupAutoUpdate = undefined;
1009
+ }
1010
+ }
1011
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: PopoverDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1012
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.10", type: PopoverDirective, isStandalone: true, selector: "[popover]", inputs: { popover: "popover", isOpen: "isOpen", placement: "placement", offset: "offset", padding: "padding" }, host: { properties: { "style.position": "\"fixed\"", "style.visibility": "isPositioned ? \"visible\" : \"hidden\"" } }, usesOnChanges: true, ngImport: i0 });
1013
+ }
1014
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: PopoverDirective, decorators: [{
1015
+ type: Directive,
1016
+ args: [{
1017
+ selector: '[popover]',
1018
+ standalone: true,
1019
+ host: {
1020
+ '[style.position]': '"fixed"',
1021
+ '[style.visibility]': 'isPositioned ? "visible" : "hidden"',
1022
+ },
1023
+ }]
1024
+ }], propDecorators: { popover: [{
1025
+ type: Input
1026
+ }], isOpen: [{
1027
+ type: Input
1028
+ }], placement: [{
1029
+ type: Input
1030
+ }], offset: [{
1031
+ type: Input
1032
+ }], padding: [{
1033
+ type: Input
1034
+ }] } });
1035
+
1036
+ const COLORS = Color.fromArray([
1037
+ { name: 'Black', value: '#000000', type: 'hex' },
1038
+ { name: 'Dim gray', value: '#696969', type: 'hex' },
1039
+ { name: 'Light gray', value: '#d3d3d3', type: 'hex' },
1040
+ { name: 'White', value: '#ffffff', type: 'hex' },
1041
+ { name: 'Transparent', value: 'transparent', type: 'hex' },
1042
+ // row 2
1043
+ { name: 'Red', value: '#ff0000', type: 'hex' },
1044
+ { name: 'Orange', value: '#ffa500', type: 'hex' },
1045
+ { name: 'Yellow', value: '#ffff00', type: 'hex' },
1046
+ { name: 'Light green', value: '#90ee90', type: 'hex' },
1047
+ { name: 'Green', value: '#008000', type: 'hex' },
1048
+ // row 3
1049
+ { name: 'Aquamarine', value: '#7fffd4', type: 'hex' },
1050
+ { name: 'Turquoise', value: '#40e0d0', type: 'hex' },
1051
+ { name: 'Lightblue', value: '#add8e6', type: 'hex' },
1052
+ { name: 'Blue', value: '#0000ff', type: 'hex' },
1053
+ { name: 'Purple', value: '#800080', type: 'hex' },
1054
+ ]);
1055
+
1056
+ /**
1057
+ * Custom button directive matching React button component styles
1058
+ * Supports variants: default, destructive, outline, secondary, ghost, link
1059
+ * Supports sizes: default, sm, lg, icon, icon-sm, icon-lg
1060
+ *
1061
+ * Usage:
1062
+ * - <button documentEngineButton>Text</button> - secondary variant, default size
1063
+ * - <button documentEngineButton="filled">Text</button> - default variant (primary)
1064
+ * - <button documentEngineButton variant="outline">Text</button> - outline variant
1065
+ * - <button documentEngineButton variant="icon" size="icon-sm">Icon</button> - icon button
1066
+ */
1067
+ class ButtonDirective {
1068
+ variant = 'ghost';
1069
+ size = 'default';
1070
+ get buttonClasses() {
1071
+ const classes = ['btn'];
1072
+ classes.push(`btn--${this.variant}`);
1073
+ classes.push(`btn--size-${this.size}`);
1074
+ return classes.join(' ');
1075
+ }
1076
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: ButtonDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1077
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.10", type: ButtonDirective, isStandalone: true, selector: "button[documentEngineButton]", inputs: { variant: "variant", size: "size" }, host: { attributes: { "type": "button" }, properties: { "class": "this.buttonClasses" } }, ngImport: i0 });
1078
+ }
1079
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: ButtonDirective, decorators: [{
1080
+ type: Directive,
1081
+ args: [{
1082
+ selector: 'button[documentEngineButton]',
1083
+ standalone: true,
1084
+ host: {
1085
+ type: 'button',
1086
+ },
1087
+ }]
1088
+ }], propDecorators: { variant: [{
1089
+ type: Input
1090
+ }], size: [{
1091
+ type: Input
1092
+ }], buttonClasses: [{
1093
+ type: HostBinding,
1094
+ args: ['class']
1095
+ }] } });
1096
+
1097
+ const DEFAULT = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></svg>`;
1098
+ const CHECK = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="100%" height="100%"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z"/></svg>`;
1099
+ // Official icons
1100
+ const UNDO = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M280-200v-80h284q63 0 109.5-40T720-420q0-60-46.5-100T564-560H312l104 104-56 56-200-200 200-200 56 56-104 104h252q97 0 166.5 63T800-420q0 94-69.5 157T564-200H280Z"/></svg>`;
1101
+ const REDO = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M396-200q-97 0-166.5-63T160-420q0-94 69.5-157T396-640h252L544-744l56-56 200 200-200 200-56-56 104-104H396q-63 0-109.5 40T240-420q0 60 46.5 100T396-280h284v80H396Z"/></svg>`;
1102
+ const FORMAT_SIZE = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M560-160v-520H360v-120h520v120H680v520H560Zm-360 0v-320H80v-120h360v120H320v320H200Z"/></svg>`;
1103
+ const ARROW_DROP_DOWN = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M480-360 280-560h400L480-360Z"/></svg>`;
1104
+ const FORMAT_LINE_SPACING = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M240-160 80-320l56-56 64 62v-332l-64 62-56-56 160-160 160 160-56 56-64-62v332l64-62 56 56-160 160Zm240-40v-80h400v80H480Zm0-240v-80h400v80H480Zm0-240v-80h400v80H480Z"/></svg>`;
1105
+ const MATCH_CASE = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="m131-252 165-440h79l165 440h-76l-39-112H247l-40 112h-76Zm139-176h131l-64-182h-4l-63 182Zm395 186q-51 0-81-27.5T554-342q0-44 34.5-72.5T677-443q23 0 45 4t38 11v-12q0-29-20.5-47T685-505q-23 0-42 9.5T610-468l-47-35q24-29 54.5-43t68.5-14q69 0 103 32.5t34 97.5v178h-63v-37h-4q-14 23-38 35t-53 12Zm12-54q35 0 59.5-24t24.5-56q-14-8-33.5-12.5T689-393q-32 0-50 14t-18 37q0 20 16 33t40 13Z"/></svg>`;
1106
+ const FORMAT_CLEAR = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor"><path d="m528-546-93-93-121-121h486v120H568l-40 94ZM792-56 460-388l-80 188H249l119-280L56-792l56-56 736 736-56 56Z"/></svg>`;
1107
+ const FORMAT_COLOR_TEXT = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M80 0v-160h800V0H80Zm140-280 210-560h100l210 560h-96l-50-144H368l-52 144h-96Zm176-224h168l-82-232h-4l-82 232Z"/></svg>`;
1108
+ const FORMAT_COLOR_FILL = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="m247-904 57-56 343 343q23 23 23 57t-23 57L457-313q-23 23-57 23t-57-23L153-503q-23-23-23-57t23-57l190-191-96-96Zm153 153L209-560h382L400-751Zm360 471q-33 0-56.5-23.5T680-360q0-21 12.5-45t27.5-45q9-12 19-25t21-25q11 12 21 25t19 25q15 21 27.5 45t12.5 45q0 33-23.5 56.5T760-280ZM80 0v-160h800V0H80Z"/></svg>`;
1109
+ const FORMAT_BOLD = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M272-200v-560h221q65 0 120 40t55 111q0 51-23 78.5T602-491q25 11 55.5 41t30.5 90q0 89-65 124.5T501-200H272Zm121-112h104q48 0 58.5-24.5T566-372q0-11-10.5-35.5T494-432H393v120Zm0-228h93q33 0 48-17t15-38q0-24-17-39t-44-15h-95v109Z"/></svg>`;
1110
+ const FORMAT_ITALIC = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M200-200v-100h160l120-360H320v-100h400v100H580L460-300h140v100H200Z"/></svg>`;
1111
+ const FORMAT_UNDERLINE = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M200-120v-80h560v80H200Zm280-160q-101 0-157-63t-56-167v-330h103v336q0 56 28 91t82 35q54 0 82-35t28-91v-336h103v330q0 104-56 167t-157 63Z"/></svg>`;
1112
+ const FORMAT_STRIKETHROUGH = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M80-400v-80h800v80H80Zm340-160v-120H200v-120h560v120H540v120H420Zm0 400v-160h120v160H420Z"/></svg>`;
1113
+ const SUBSCRIPT = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M760-160v-80q0-17 11.5-28.5T800-280h80v-40H760v-40h120q17 0 28.5 11.5T920-320v40q0 17-11.5 28.5T880-240h-80v40h120v40H760Zm-525-80 185-291-172-269h106l124 200h4l123-200h107L539-531l186 291H618L482-457h-4L342-240H235Z"/></svg>`;
1114
+ const SUPERSCRIPT = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M760-600v-80q0-17 11.5-28.5T800-720h80v-40H760v-40h120q17 0 28.5 11.5T920-760v40q0 17-11.5 28.5T880-680h-80v40h120v40H760ZM235-160l185-291-172-269h106l124 200h4l123-200h107L539-451l186 291H618L482-377h-4L342-160H235Z"/></svg>`;
1115
+ const CODE = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M320-240 80-480l240-240 57 57-184 184 183 183-56 56Zm320 0-57-57 184-184-183-183 56-56 240 240-240 240Z"/></svg>`;
1116
+ const FORMAT_ALIGN_LEFT = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M120-120v-80h720v80H120Zm0-160v-80h480v80H120Zm0-160v-80h720v80H120Zm0-160v-80h480v80H120Zm0-160v-80h720v80H120Z"/></svg>`;
1117
+ const FORMAT_ALIGN_CENTER = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M120-120v-80h720v80H120Zm160-160v-80h400v80H280ZM120-440v-80h720v80H120Zm160-160v-80h400v80H280ZM120-760v-80h720v80H120Z"/></svg>`;
1118
+ const FORMAT_ALIGN_RIGHT = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M120-760v-80h720v80H120Zm240 160v-80h480v80H360ZM120-440v-80h720v80H120Zm240 160v-80h480v80H360ZM120-120v-80h720v80H120Z"/></svg>`;
1119
+ const FORMAT_ALIGN_JUSTIFY = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M120-120v-80h720v80H120Zm0-160v-80h720v80H120Zm0-160v-80h720v80H120Zm0-160v-80h720v80H120Zm0-160v-80h720v80H120Z"/></svg>`;
1120
+ const FORMAT_INDENT_DECREASE = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M120-120v-80h720v80H120Zm320-160v-80h400v80H440Zm0-160v-80h400v80H440Zm0-160v-80h400v80H440ZM120-760v-80h720v80H120Zm160 440L120-480l160-160v320Z"/></svg>`;
1121
+ const FORMAT_INDENT_INCREASE = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M120-120v-80h720v80H120Zm320-160v-80h400v80H440Zm0-160v-80h400v80H440Zm0-160v-80h400v80H440ZM120-760v-80h720v80H120Zm0 440v-320l160 160-160 160Z"/></svg>`;
1122
+ const FORMAT_LIST_BULLETED = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M360-200v-80h480v80H360Zm0-240v-80h480v80H360Zm0-240v-80h480v80H360ZM200-160q-33 0-56.5-23.5T120-240q0-33 23.5-56.5T200-320q33 0 56.5 23.5T280-240q0 33-23.5 56.5T200-160Zm0-240q-33 0-56.5-23.5T120-480q0-33 23.5-56.5T200-560q33 0 56.5 23.5T280-480q0 33-23.5 56.5T200-400Zm0-240q-33 0-56.5-23.5T120-720q0-33 23.5-56.5T200-800q33 0 56.5 23.5T280-720q0 33-23.5 56.5T200-640Z"/></svg>`;
1123
+ const FORMAT_LIST_NUMBERED = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M120-80v-60h100v-30h-60v-60h60v-30H120v-60h120q17 0 28.5 11.5T280-280v40q0 17-11.5 28.5T240-200q17 0 28.5 11.5T280-160v40q0 17-11.5 28.5T240-80H120Zm0-280v-110q0-17 11.5-28.5T160-510h60v-30H120v-60h120q17 0 28.5 11.5T280-560v70q0 17-11.5 28.5T240-450h-60v30h100v60H120Zm60-280v-180h-60v-60h120v240h-60Zm180 440v-80h480v80H360Zm0-240v-80h480v80H360Zm0-240v-80h480v80H360Z"/></svg>`;
1124
+ const FORMAT_QUOTE = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="m228-240 92-160q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 23-5.5 42.5T458-480L320-240h-92Zm360 0 92-160q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 23-5.5 42.5T818-480L680-240h-92ZM320-500q25 0 42.5-17.5T380-560q0-25-17.5-42.5T320-620q-25 0-42.5 17.5T260-560q0 25 17.5 42.5T320-500Zm360 0q25 0 42.5-17.5T740-560q0-25-17.5-42.5T680-620q-25 0-42.5 17.5T620-560q0 25 17.5 42.5T680-500Zm0-60Zm-360 0Z"/></svg>`;
1125
+ const TABLE_ROWS = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M760-200v-120H200v120h560Zm0-200v-160H200v160h560Zm0-240v-120H200v120h560ZM200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Z"/></svg>`;
1126
+ const LINK = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M440-280H280q-83 0-141.5-58.5T80-480q0-83 58.5-141.5T280-680h160v80H280q-50 0-85 35t-35 85q0 50 35 85t85 35h160v80ZM320-440v-80h320v80H320Zm200 160v-80h160q50 0 85-35t35-85q0-50-35-85t-85-35H520v-80h160q83 0 141.5 58.5T880-480q0 83-58.5 141.5T680-280H520Z"/></svg>`;
1127
+ const LINK_OFF = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="m770-302-60-62q40-11 65-42.5t25-73.5q0-50-35-85t-85-35H520v-80h160q83 0 141.5 58.5T880-480q0 57-29.5 105T770-302ZM634-440l-80-80h86v80h-6ZM792-56 56-792l56-56 736 736-56 56ZM440-280H280q-83 0-141.5-58.5T80-480q0-69 42-123t108-71l74 74h-24q-50 0-85 35t-35 85q0 50 35 85t85 35h160v80ZM320-440v-80h65l79 80H320Z"/></svg>`;
1128
+ const SPECIAL_CHARACTER = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M120-120v-120h200q-84-45-132-125t-48-175q0-142 99-241t241-99q142 0 241 99t99 241q0 95-48 175T640-240h200v120H520v-204q78-14 129-75t51-141q0-92-64-156t-156-64q-92 0-156 64t-64 156q0 80 51 141t129 75v204H120Z"/></svg>`;
1129
+ const CLEAR_ALL = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M120-280v-80h560v80H120Zm80-160v-80h560v80H200Zm80-160v-80h560v80H280Z"/></svg>`;
1130
+ const INSERT_PAGE_BREAK = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M240-80q-33 0-56.5-23.5T160-160v-120h80v120h480v-120h80v120q0 33-23.5 56.5T720-80H240Zm-80-440v-280q0-33 23.5-56.5T240-880h320l240 240v120h-80v-80H520v-200H240v280h-80Zm200 160v-80h240v80H360Zm320 0v-80h240v80H680Zm-640 0v-80h240v80H40Zm440-160Zm0 240Z"/></svg>`;
1131
+ const MARKDOWN = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="m640-360 120-120-42-43-48 48v-125h-60v125l-48-48-42 43 120 120ZM160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm0-80h640v-480H160v480Zm0 0v-480 480Zm60-120h60v-180h40v120h60v-120h40v180h60v-200q0-17-11.5-28.5T440-600H260q-17 0-28.5 11.5T220-560v200Z"/></svg>`;
1132
+ const DYNAMIC_FORM = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M160-520q-33 0-56.5-23.5T80-600v-120q0-33 23.5-56.5T160-800h360v280H160Zm0-80h280v-120H160v120Zm0 440q-33 0-56.5-23.5T80-240v-120q0-33 23.5-56.5T160-440h440v280H160Zm0-80h360v-120H160v120Zm520 80v-360h-80v-280h280l-80 200h80L680-160ZM190-270h60v-60h-60v60Zm0-360h60v-60h-60v60Zm-30 30v-120 120Zm0 360v-120 120Z"/></svg>`;
1133
+ const PHOTO = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm0-80h560v-560H200v560Zm40-80h480L570-480 450-320l-90-120-120 160Zm-40 80v-560 560Z"/></svg>`;
1134
+ const PRINT = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M640-640v-120H320v120h-80v-200h480v200h-80Zm-480 80h640-640Zm560 100q17 0 28.5-11.5T760-500q0-17-11.5-28.5T720-540q-17 0-28.5 11.5T680-500q0 17 11.5 28.5T720-460Zm-80 260v-160H320v160h320Zm80 80H240v-160H80v-240q0-51 35-85.5t85-34.5h560q51 0 85.5 34.5T880-520v240H720v160Zm80-240v-160q0-17-11.5-28.5T760-560H200q-17 0-28.5 11.5T160-520v160h80v-80h480v80h80Z"/></svg>`;
1135
+ const ARCHIVE = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="m480-240 160-160-56-56-64 64v-168h-80v168l-64-64-56 56 160 160ZM200-640v440h560v-440H200Zm0 520q-33 0-56.5-23.5T120-200v-499q0-14 4.5-27t13.5-24l50-61q11-14 27.5-21.5T250-840h460q18 0 34.5 7.5T772-811l50 61q9 11 13.5 24t4.5 27v499q0 33-23.5 56.5T760-120H200Zm16-600h528l-34-40H250l-34 40Zm264 300Z"/></svg>`;
1136
+ // Common Icons
1137
+ const EDIT = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M200-200h57l391-391-57-57-391 391v57Zm-80 80v-170l528-527q12-11 26.5-17t30.5-6q16 0 31 6t26 18l55 56q12 11 17.5 26t5.5 30q0 16-5.5 30.5T817-647L290-120H120Zm640-584-56-56 56 56Zm-141 85-28-29 57 57-29-28Z"/></svg>`;
1138
+ const SETTINGS = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="m370-80-16-128q-13-5-24.5-12T307-235l-119 50L78-375l103-78q-1-7-1-13.5v-27q0-6.5 1-13.5L78-585l110-190 119 50q11-8 23-15t24-12l16-128h220l16 128q13 5 24.5 12t22.5 15l119-50 110 190-103 78q1 7 1 13.5v27q0 6.5-2 13.5l103 78-110 190-118-50q-11 8-23 15t-24 12L590-80H370Zm70-80h79l14-106q31-8 57.5-23.5T639-327l99 41 39-68-86-65q5-14 7-29.5t2-31.5q0-16-2-31.5t-7-29.5l86-65-39-68-99 42q-22-23-48.5-38.5T533-694l-13-106h-79l-14 106q-31 8-57.5 23.5T321-633l-99-41-39 68 86 64q-5 15-7 30t-2 32q0 16 2 31t7 30l-86 65 39 68 99-42q22 23 48.5 38.5T427-266l13 106Zm42-180q58 0 99-41t41-99q0-58-41-99t-99-41q-59 0-99.5 41T342-480q0 58 40.5 99t99.5 41Zm-2-140Z"/></svg>`;
1139
+ const SEARCH = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z"/></svg>`;
1140
+ const SEARCH_OFF = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M280-80q-83 0-141.5-58.5T80-280q0-83 58.5-141.5T280-480q83 0 141.5 58.5T480-280q0 83-58.5 141.5T280-80Zm544-40L568-376q-12-13-25.5-26.5T516-428q38-24 61-64t23-88q0-75-52.5-127.5T420-760q-75 0-127.5 52.5T240-580q0 6 .5 11.5T242-557q-18 2-39.5 8T164-535q-2-11-3-22t-1-23q0-109 75.5-184.5T420-840q109 0 184.5 75.5T680-580q0 43-13.5 81.5T629-428l251 252-56 56Zm-615-61 71-71 70 71 29-28-71-71 71-71-28-28-71 71-71-71-28 28 71 71-71 71 28 28Z"/></svg>`;
1141
+ const DESCRIPTION = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M320-240h320v-80H320v80Zm0-160h320v-80H320v80ZM240-80q-33 0-56.5-23.5T160-160v-640q0-33 23.5-56.5T240-880h320l240 240v480q0 33-23.5 56.5T720-80H240Zm280-520v-200H240v640h480v-440H520ZM240-800v200-200 640-640Z"/></svg>`;
1142
+ const CLOSE = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/></svg>`;
1143
+ const VERTICAL_ALIGN_TOP = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M160-760v-80h640v80H160Zm280 640v-408L336-424l-56-56 200-200 200 200-56 56-104-104v408h-80Z"/></svg>`;
1144
+ const VERTICAL_ALIGN_CENTER = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M440-80v-168l-64 64-56-56 160-160 160 160-56 56-64-64v168h-80ZM160-440v-80h640v80H160Zm320-120L320-720l56-56 64 64v-168h80v168l64-64 56 56-160 160Z"/></svg>`;
1145
+ const VERTICAL_ALIGN_BOTTOM = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M160-120v-80h640v80H160Zm320-160L280-480l56-56 104 104v-408h80v408l104-104 56 56-200 200Z"/></svg>`;
1146
+ // Custom SVG
1147
+ const RESTRICT_EDIT = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" aria-hidden="true">
1148
+ <g transform="translate(10 10) scale(0.8) translate(-10 -10)">
1149
+ <path d="M6.25 16a.75.75 0 1 1 0 1.5H.75a.75.75 0 1 1 0-1.5zm0-5a.75.75 0 1 1 0 1.5H.75a.75.75 0 1 1 0-1.5zm3-5a.75.75 0 0 1 0 1.5H.75a.75.75 0 0 1 0-1.5zm6-5a.75.75 0 1 1 0 1.5H.75a.75.75 0 0 1 0-1.5zm.25 5.5a3.5 3.5 0 0 1 3.143 1.959.75.75 0 0 1-1.36.636A2 2 0 0 0 13.5 10v2H19a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1v-5a1 1 0 0 1 1-1v-2l.005-.192A3.5 3.5 0 0 1 15.5 6.5m0 7.5a.5.5 0 0 0-.492.41L15 14.5v2a.5.5 0 0 0 .992.09L16 16.5v-2a.5.5 0 0 0-.5-.5">
1150
+ </path>
1151
+ </g>
1152
+ </svg>
1153
+ `;
1154
+ const TABLE_CELL_PROPERTY = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" aria-hidden="true">
1155
+ <path d="m11.105 18-.17 1H2.5A1.5 1.5 0 0 1 1 17.5v-15A1.5 1.5 0 0 1 2.5 1h15A1.5 1.5 0 0 1 19 2.5v9.975l-.85-.124-.15-.302V8h-5v4h.021l-.172.351-1.916.28-.151.027c-.287.063-.54.182-.755.341L8 13v5zM2 12h5V8H2zm10-4H8v4h4zM2 2v5h5V2zm0 16h5v-5H2zM13 7h5V2h-5zM8 2v5h4V2z" opacity=".6"></path>
1156
+ <path d="m15.5 11.5 1.323 2.68 2.957.43-2.14 2.085.505 2.946L15.5 18.25l-2.645 1.39.505-2.945-2.14-2.086 2.957-.43zM13 6a1 1 0 0 1 1 1v3.172a2 2 0 0 0-.293.443l-.858 1.736-1.916.28-.151.027A1.98 1.98 0 0 0 9.315 14H7a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm-1 2H8v4h4z"></path>
1157
+ </svg>
1158
+ `;
1159
+ const TABLE_COLUMN = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" aria-hidden="true">
1160
+ <path d="M2.5 1h15A1.5 1.5 0 0 1 19 2.5v15a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 1 17.5v-15A1.5 1.5 0 0 1 2.5 1M2 2v16h16V2z" opacity=".6"></path>
1161
+ <path d="M18 7v1H2V7zm0 5v1H2v-1z" opacity=".6"></path>
1162
+ <path d="M14 1v18a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1m-2 1H8v4h4zm0 6H8v4h4zm0 6H8v4h4z"></path>
1163
+ </svg>
1164
+ `;
1165
+ const TABLE_MERGE_CELL = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" aria-hidden="true">
1166
+ <path d="M2.5 1h15A1.5 1.5 0 0 1 19 2.5v15a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 1 17.5v-15A1.5 1.5 0 0 1 2.5 1M2 2v16h16V2z" opacity=".6"></path>
1167
+ <path d="M7 2h1v16H7zm5 0h1v7h-1zm6 5v1H2V7zM8 12v1H2v-1z" opacity=".6"></path>
1168
+ <path d="M7 7h12a1 1 0 0 1 1 1v11a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1m1 2v9h10V9z"></path>
1169
+ </svg>
1170
+ `;
1171
+ const TABLE_PROPERTY = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" aria-hidden="true">
1172
+ <path d="M8 2v5h4V2h1v5h5v1h-5v4h.021l-.172.351-1.916.28-.151.027c-.287.063-.54.182-.755.341L8 13v5H7v-5H2v-1h5V8H2V7h5V2zm4 6H8v4h4z" opacity=".6"></path>
1173
+ <path d="m15.5 11.5 1.323 2.68 2.957.43-2.14 2.085.505 2.946L15.5 18.25l-2.645 1.39.505-2.945-2.14-2.086 2.957-.43zM17 1a2 2 0 0 1 2 2v9.475l-.85-.124-.857-1.736a2 2 0 0 0-.292-.44L17 3H3v14h7.808l.402.392L10.935 19H3a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2z"></path>
1174
+ </svg>
1175
+ `;
1176
+ const TABLE_ROW = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" aria-hidden="true">
1177
+ <path d="M2.5 1h15A1.5 1.5 0 0 1 19 2.5v15a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 1 17.5v-15A1.5 1.5 0 0 1 2.5 1M2 2v16h16V2z" opacity=".6"></path>
1178
+ <path d="M7 2h1v16H7zm5 0h1v16h-1z" opacity=".6"></path>
1179
+ <path d="M1 6h18a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1m1 2v4h4V8zm6 0v4h4V8zm6 0v4h4V8z"></path>
1180
+ </svg>
1181
+ `;
1182
+
1183
+ /**
1184
+ * Icon registry service for managing SVG icons
1185
+ * Similar to Angular Material's MatIconRegistry
1186
+ */
1187
+ class IconRegistryService {
1188
+ sanitizer = inject(DomSanitizer);
1189
+ icons = new Map();
1190
+ constructor() {
1191
+ this.registerDefaultIcons();
1192
+ }
1193
+ /**
1194
+ * Register a new icon with its SVG content
1195
+ */
1196
+ registerIcon(name, svgContent) {
1197
+ if (this.icons.has(name)) {
1198
+ throw new Error(`Icon ${name} already registered`);
1199
+ }
1200
+ this.icons.set(name, svgContent);
1201
+ }
1202
+ /**
1203
+ * Get icon SVG content by name
1204
+ * Returns SafeHtml for use in templates with [innerHTML]
1205
+ */
1206
+ getIcon(name) {
1207
+ const icon = this.icons.get(name);
1208
+ if (!icon) {
1209
+ console.warn(`Icon ${name} not registered`);
1210
+ return this.sanitizer.bypassSecurityTrustHtml(DEFAULT);
1211
+ }
1212
+ return this.sanitizer.bypassSecurityTrustHtml(icon);
1213
+ }
1214
+ /**
1215
+ * Get raw icon SVG string by name
1216
+ * Returns string for direct DOM manipulation
1217
+ */
454
1218
  getIconString(name) {
455
1219
  const icon = this.icons.get(name);
456
1220
  if (!icon) {
@@ -2444,214 +3208,57 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
2444
3208
  <button documentEngineSelectOption value="arrows">Arrows</button>
2445
3209
  <button documentEngineSelectOption value="symbols">Symbols</button>
2446
3210
  <button documentEngineSelectOption value="punctuation">Punctuation</button>
2447
- </document-engine-select>
2448
- </div>
2449
-
2450
- <!-- Characters Grid -->
2451
- <div class="special-characters-view__grid-container">
2452
- <div
2453
- class="special-characters-view__grid"
2454
- role="grid"
2455
- aria-label="Special characters grid"
2456
- tabindex="-1"
2457
- (keydown)="onGridKeydown($event)"
2458
- >
2459
- <button
2460
- *ngFor="let char of currentCharacters; let i = index"
2461
- #charButton
2462
- type="button"
2463
- role="gridcell"
2464
- class="special-characters-view__char-button"
2465
- (click)="insertCharacter(char.char)"
2466
- [attr.aria-label]="char.name + (char.code ? ' (' + char.code + ')' : '')"
2467
- [title]="char.name + (char.code ? ' (' + char.code + ')' : '')"
2468
- [attr.data-index]="i"
2469
- >
2470
- {{ char.char }}
2471
- </button>
2472
- </div>
2473
- </div>
2474
-
2475
- <!-- Actions -->
2476
- <div class="special-characters-view__actions">
2477
- <button type="button" documentEngineButton variant="secondary" (click)="close?.()">Close</button>
2478
- </div>
2479
- </div>
2480
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".special-characters-view{display:flex;flex-direction:column;width:100%}.special-characters-view__header{padding:.5rem 1rem;border-bottom:1px solid var(--border);font-weight:500}.special-characters-view__select{padding:.75rem;width:100%}.special-characters-view__grid-container{padding:.75rem;overflow-y:auto;max-height:20rem}.special-characters-view__grid{display:grid;grid-template-columns:repeat(6,1fr);gap:.25rem}.special-characters-view__char-button{width:2.5rem;height:2.5rem;border-radius:.25rem;border:1px solid var(--border);background-color:transparent;cursor:pointer;font-size:1.125rem;font-weight:500;display:flex;align-items:center;justify-content:center;transition:background-color .2s,color .2s;outline:none}.special-characters-view__char-button:hover{background-color:var(--accent);color:var(--accent-foreground)}.special-characters-view__char-button:focus{outline:none;box-shadow:0 0 0 2px var(--ring);box-shadow-offset:2px}.special-characters-view__actions{display:flex;justify-content:flex-end;padding:.75rem;border-top:1px solid var(--border)}\n"] }]
2481
- }], propDecorators: { charButtons: [{
2482
- type: ViewChildren,
2483
- args: ['charButton']
2484
- }] } });
2485
-
2486
- const SpecialCharactersBubbleConfig = {
2487
- pluginKey: 'SpecialCharactersBubbleMenu',
2488
- views: [
2489
- {
2490
- id: 'characters',
2491
- component: SpecialCharactersViewComponent,
2492
- label: 'Special Characters',
2493
- isDefault: true,
2494
- },
2495
- ],
2496
- };
2497
-
2498
- /**
2499
- * Directive for positioning a popover/dropdown relative to a trigger element using floating-ui
2500
- * Handles positioning, visibility, and cleanup automatically
2501
- *
2502
- * Usage:
2503
- * <button #trigger>Open</button>
2504
- * <div [popover]="trigger" [isOpen]="isOpen">
2505
- * Content
2506
- * </div>
2507
- */
2508
- class PopoverDirective {
2509
- elRef = inject(ElementRef);
2510
- cdr = inject(ChangeDetectorRef);
2511
- /**
2512
- * The trigger element reference (ElementRef or HTMLElement)
2513
- */
2514
- popover;
2515
- /**
2516
- * Whether the popover is open
2517
- */
2518
- isOpen = false;
2519
- /**
2520
- * Placement of the popover relative to trigger
2521
- */
2522
- placement = 'bottom-start';
2523
- /**
2524
- * Offset from trigger
2525
- */
2526
- offset = 8;
2527
- /**
2528
- * Padding for flip/shift middleware
2529
- */
2530
- padding = 8;
2531
- cleanupAutoUpdate;
2532
- isPositioned = false;
2533
- viewInitialized = false;
2534
- retryCount = 0;
2535
- maxRetries = 10;
2536
- ngAfterViewInit() {
2537
- this.viewInitialized = true;
2538
- this.updateVisibility();
2539
- }
2540
- ngOnChanges(changes) {
2541
- if (this.viewInitialized && (changes['isOpen'] || changes['popover'])) {
2542
- this.updateVisibility();
2543
- }
2544
- }
2545
- ngOnDestroy() {
2546
- this.cleanup();
2547
- }
2548
- updateVisibility() {
2549
- if (!this.viewInitialized)
2550
- return;
2551
- if (this.isOpen && this.popover) {
2552
- // Wait for next tick to ensure element is rendered
2553
- setTimeout(() => {
2554
- this.setupFloating();
2555
- });
2556
- }
2557
- else {
2558
- this.isPositioned = false;
2559
- this.cleanup();
2560
- }
2561
- }
2562
- setupFloating() {
2563
- // Reset retry count on each attempt
2564
- this.retryCount = 0;
2565
- this._setupFloatingInternal();
2566
- }
2567
- _setupFloatingInternal() {
2568
- let triggerEl;
2569
- if (!this.popover) {
2570
- if (this.retryCount < this.maxRetries) {
2571
- this.retryCount++;
2572
- setTimeout(() => this._setupFloatingInternal(), 10);
2573
- }
2574
- return;
2575
- }
2576
- if (this.popover instanceof ElementRef) {
2577
- triggerEl = this.popover.nativeElement;
2578
- // If ElementRef exists but nativeElement is not yet available, retry
2579
- if (!triggerEl) {
2580
- if (this.retryCount < this.maxRetries) {
2581
- this.retryCount++;
2582
- setTimeout(() => this._setupFloatingInternal(), 10);
2583
- }
2584
- return;
2585
- }
2586
- }
2587
- else {
2588
- triggerEl = this.popover;
2589
- }
2590
- const dropdownEl = this.elRef.nativeElement;
2591
- if (!triggerEl || !dropdownEl) {
2592
- // Retry if elements not ready
2593
- if (this.retryCount < this.maxRetries) {
2594
- this.retryCount++;
2595
- setTimeout(() => this._setupFloatingInternal(), 10);
2596
- }
2597
- return;
2598
- }
2599
- // Compute initial position before showing dropdown to prevent flash
2600
- computePosition(triggerEl, dropdownEl, {
2601
- placement: this.placement,
2602
- middleware: [offset(this.offset), flip(), shift({ padding: this.padding })],
2603
- })
2604
- .then(({ x, y }) => {
2605
- // Set position first, then make visible
2606
- Object.assign(dropdownEl.style, { left: `${x}px`, top: `${y}px` });
2607
- this.isPositioned = true;
2608
- // Trigger change detection to update visibility binding
2609
- this.cdr.markForCheck();
2610
- })
2611
- .catch((error) => {
2612
- console.error('[PopoverDirective] Error computing position', error);
2613
- });
2614
- // Use autoUpdate to automatically reposition on scroll/resize
2615
- this.cleanupAutoUpdate = autoUpdate(triggerEl, dropdownEl, () => {
2616
- computePosition(triggerEl, dropdownEl, {
2617
- placement: this.placement,
2618
- middleware: [offset(this.offset), flip(), shift({ padding: this.padding })],
2619
- }).then(({ x, y }) => {
2620
- Object.assign(dropdownEl.style, { left: `${x}px`, top: `${y}px` });
2621
- });
2622
- });
2623
- }
2624
- cleanup() {
2625
- if (this.cleanupAutoUpdate) {
2626
- this.cleanupAutoUpdate();
2627
- this.cleanupAutoUpdate = undefined;
2628
- }
2629
- }
2630
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: PopoverDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2631
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.10", type: PopoverDirective, isStandalone: true, selector: "[popover]", inputs: { popover: "popover", isOpen: "isOpen", placement: "placement", offset: "offset", padding: "padding" }, host: { properties: { "style.position": "\"fixed\"", "style.visibility": "isPositioned ? \"visible\" : \"hidden\"" } }, usesOnChanges: true, ngImport: i0 });
2632
- }
2633
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: PopoverDirective, decorators: [{
2634
- type: Directive,
2635
- args: [{
2636
- selector: '[popover]',
2637
- standalone: true,
2638
- host: {
2639
- '[style.position]': '"fixed"',
2640
- '[style.visibility]': 'isPositioned ? "visible" : "hidden"',
2641
- },
2642
- }]
2643
- }], propDecorators: { popover: [{
2644
- type: Input
2645
- }], isOpen: [{
2646
- type: Input
2647
- }], placement: [{
2648
- type: Input
2649
- }], offset: [{
2650
- type: Input
2651
- }], padding: [{
2652
- type: Input
3211
+ </document-engine-select>
3212
+ </div>
3213
+
3214
+ <!-- Characters Grid -->
3215
+ <div class="special-characters-view__grid-container">
3216
+ <div
3217
+ class="special-characters-view__grid"
3218
+ role="grid"
3219
+ aria-label="Special characters grid"
3220
+ tabindex="-1"
3221
+ (keydown)="onGridKeydown($event)"
3222
+ >
3223
+ <button
3224
+ *ngFor="let char of currentCharacters; let i = index"
3225
+ #charButton
3226
+ type="button"
3227
+ role="gridcell"
3228
+ class="special-characters-view__char-button"
3229
+ (click)="insertCharacter(char.char)"
3230
+ [attr.aria-label]="char.name + (char.code ? ' (' + char.code + ')' : '')"
3231
+ [title]="char.name + (char.code ? ' (' + char.code + ')' : '')"
3232
+ [attr.data-index]="i"
3233
+ >
3234
+ {{ char.char }}
3235
+ </button>
3236
+ </div>
3237
+ </div>
3238
+
3239
+ <!-- Actions -->
3240
+ <div class="special-characters-view__actions">
3241
+ <button type="button" documentEngineButton variant="secondary" (click)="close?.()">Close</button>
3242
+ </div>
3243
+ </div>
3244
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".special-characters-view{display:flex;flex-direction:column;width:100%}.special-characters-view__header{padding:.5rem 1rem;border-bottom:1px solid var(--border);font-weight:500}.special-characters-view__select{padding:.75rem;width:100%}.special-characters-view__grid-container{padding:.75rem;overflow-y:auto;max-height:20rem}.special-characters-view__grid{display:grid;grid-template-columns:repeat(6,1fr);gap:.25rem}.special-characters-view__char-button{width:2.5rem;height:2.5rem;border-radius:.25rem;border:1px solid var(--border);background-color:transparent;cursor:pointer;font-size:1.125rem;font-weight:500;display:flex;align-items:center;justify-content:center;transition:background-color .2s,color .2s;outline:none}.special-characters-view__char-button:hover{background-color:var(--accent);color:var(--accent-foreground)}.special-characters-view__char-button:focus{outline:none;box-shadow:0 0 0 2px var(--ring);box-shadow-offset:2px}.special-characters-view__actions{display:flex;justify-content:flex-end;padding:.75rem;border-top:1px solid var(--border)}\n"] }]
3245
+ }], propDecorators: { charButtons: [{
3246
+ type: ViewChildren,
3247
+ args: ['charButton']
2653
3248
  }] } });
2654
3249
 
3250
+ const SpecialCharactersBubbleConfig = {
3251
+ pluginKey: 'SpecialCharactersBubbleMenu',
3252
+ views: [
3253
+ {
3254
+ id: 'characters',
3255
+ component: SpecialCharactersViewComponent,
3256
+ label: 'Special Characters',
3257
+ isDefault: true,
3258
+ },
3259
+ ],
3260
+ };
3261
+
2655
3262
  /**
2656
3263
  * Toggle button option directive
2657
3264
  */
@@ -4175,597 +4782,342 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
4175
4782
  ></document-engine-color-picker>
4176
4783
  </div>
4177
4784
  </div>
4178
- </div>
4179
- </div>
4180
-
4181
- <div class="table-style-view__section">
4182
- <div class="table-style-view__section-label">Background</div>
4183
- <div class="table-style-view__field table-style-view__field--color">
4184
- <div class="table-style-view__field-label">Color</div>
4185
-
4186
- <div class="table-style-view__color-input">
4187
- <input documentEngineInput [value]="tableBg" readonly />
4188
-
4189
- <button
4190
- *ngIf="tableBg"
4191
- type="button"
4192
- documentEngineButton
4193
- size="icon"
4194
- variant="ghost"
4195
- class="table-style-view__color-clear"
4196
- (click)="tableBg = null"
4197
- >
4198
- <document-engine-icon name="close"></document-engine-icon>
4199
- </button>
4200
-
4201
- <button
4202
- #bgColorTrigger
4203
- type="button"
4204
- class="table-style-view__color-swatch"
4205
- [style.background-color]="tableBg || 'transparent'"
4206
- (click)="toggleColorPicker('bg')"
4207
- title="Pick color"
4208
- >
4209
- <div *ngIf="tableBg === 'transparent'" class="table-style-view__color-checker"></div>
4210
- </button>
4211
- </div>
4212
-
4213
- <!-- Color picker for background -->
4214
- <div
4215
- *ngIf="showColorPicker === 'bg'"
4216
- [popover]="bgColorTrigger"
4217
- [isOpen]="showColorPicker === 'bg'"
4218
- class="table-style-view__color-picker-dropdown"
4219
- (click)="$event.stopPropagation()"
4220
- >
4221
- <document-engine-color-picker
4222
- [colorPalette]="colorPalette"
4223
- [activeColor]="tableBgObj"
4224
- (colorSelected)="onTableBgSelected($event)"
4225
- (colorRemoved)="onTableBgRemoved()"
4226
- ></document-engine-color-picker>
4227
- </div>
4228
- </div>
4229
- </div>
4230
- </div>
4231
-
4232
- <!-- Actions -->
4233
- <div class="table-style-view__actions">
4234
- <button documentEngineButton variant="ghost" (click)="cancel()">Cancel</button>
4235
- <button documentEngineButton variant="default" (click)="onSave()">Save</button>
4236
- </div>
4237
- </div>
4238
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".table-style-view{display:flex;flex-direction:column;width:32.5rem}.table-style-view__header{padding:.5rem 1rem;border-bottom:1px solid var(--border);font-weight:500}.table-style-view__content{display:flex;flex-direction:column;gap:1rem;padding:.75rem}.table-style-view__section{display:flex;flex-direction:column;gap:.5rem}.table-style-view__section-label{font-weight:700;color:var(--muted-foreground)}.table-style-view__border-grid{display:grid;grid-template-columns:repeat(8,1fr);gap:.5rem}.table-style-view__field{display:flex;flex-direction:column;gap:.25rem;grid-column:span 2}.table-style-view__field--color{grid-column:span 4;position:relative}.table-style-view__field-label{font-size:.75rem;color:var(--muted-foreground)}.table-style-view__color-input{position:relative;display:flex;align-items:center;gap:.25rem}.table-style-view__color-input input{flex:1;padding-right:3rem}.table-style-view__color-clear{position:absolute;right:2.5rem;z-index:1}.table-style-view__color-swatch{position:absolute;right:.5rem;width:1.75rem;height:1.75rem;border-radius:.25rem;border:1px solid var(--border);cursor:pointer;transition:transform .2s;overflow:hidden;z-index:2}.table-style-view__color-swatch:hover{transform:scale(1.1)}.table-style-view__color-checker{position:absolute;inset:0;opacity:.3;background-image:repeating-conic-gradient(#ccc 0deg 90deg,#fff 90deg 180deg);background-size:4px 4px}.table-style-view__color-picker-dropdown{z-index:50;background:var(--popover);border:1px solid var(--border);border-radius:.5rem;box-shadow:var(--shadow-elevation-2);padding:.5rem}.table-style-view__actions{display:flex;align-items:center;justify-content:flex-end;gap:.5rem;padding:.75rem;border-top:1px solid var(--border)}\n"] }]
4239
- }], propDecorators: { borderColorTrigger: [{
4240
- type: ViewChild,
4241
- args: ['borderColorTrigger', { static: false, read: ElementRef }]
4242
- }], bgColorTrigger: [{
4243
- type: ViewChild,
4244
- args: ['bgColorTrigger', { static: false, read: ElementRef }]
4245
- }] } });
4246
-
4247
- const TableBubbleConfig = {
4248
- props: {
4249
- pluginKey: 'TableBubbleMenu',
4250
- mountTo: 'table',
4251
- },
4252
- target: {
4253
- kind: 'node',
4254
- name: ['tableCell', 'tableHeader'],
4255
- allowEmptySelection: true,
4256
- requireFocus: false,
4257
- },
4258
- views: [
4259
- {
4260
- id: 'main',
4261
- component: TableMainViewComponent,
4262
- label: 'Table',
4263
- isDefault: true,
4264
- },
4265
- {
4266
- id: 'table-style',
4267
- component: TableStyleViewComponent,
4268
- label: 'Table Properties',
4269
- },
4270
- {
4271
- id: 'cell-style',
4272
- component: TableCellStyleViewComponent,
4273
- label: 'Cell Properties',
4274
- },
4275
- ],
4276
- };
4277
- const TableCreateBubbleConfig = {
4278
- pluginKey: 'TableCreateBubbleMenu',
4279
- views: [
4280
- {
4281
- id: 'create',
4282
- component: TableCreateViewComponent,
4283
- label: 'Create Table',
4284
- isDefault: true,
4285
- },
4286
- ],
4287
- };
4288
-
4289
- /**
4290
- * Template view for bubble menu
4291
- * Shows templates for insertion into the editor
4292
- * Replaces entire editor content with selected template
4293
- */
4294
- class TemplateViewComponent {
4295
- cdr = inject(ChangeDetectorRef);
4296
- editor;
4297
- close;
4298
- goBack;
4299
- navigateTo;
4300
- searchQuery = '';
4301
- originalTemplates = [];
4302
- // Reactive form control for search
4303
- searchControl = new FormControl('');
4304
- destroy$ = new Subject();
4305
- onActivate(attrs) {
4306
- const templates = attrs['templates'];
4307
- if (!templates)
4308
- throw new Error('Templates are required');
4309
- this.originalTemplates = templates;
4310
- this.searchQuery = '';
4311
- this.searchControl.setValue('');
4312
- this.cdr.markForCheck();
4313
- }
4314
- ngOnInit() {
4315
- // Set up reactive form subscription with debounce
4316
- this.searchControl.valueChanges
4317
- .pipe(debounceTime(200), distinctUntilChanged(), takeUntil(this.destroy$))
4318
- .subscribe((value) => {
4319
- this.searchQuery = value || '';
4320
- this.cdr.markForCheck();
4321
- });
4322
- }
4323
- ngOnDestroy() {
4324
- this.destroy$.next();
4325
- this.destroy$.complete();
4326
- }
4327
- /**
4328
- * Getter that filters templates based on search query
4329
- */
4330
- get filteredTemplates() {
4331
- const query = this.searchQuery.toLowerCase().trim();
4332
- if (!query)
4333
- return this.originalTemplates;
4334
- // Filter templates based on search query
4335
- return this.originalTemplates.filter((template) => template.title.toLowerCase().includes(query) || template.description?.toLowerCase().includes(query));
4336
- }
4337
- /**
4338
- * Insert a template into the editor, replacing entire content
4339
- */
4340
- insertTemplate(template) {
4341
- if (!this.editor)
4342
- return;
4343
- // Replace entire editor content with template data
4344
- this.editor.chain().focus().setContent(template.data).run();
4345
- this.close?.();
4346
- }
4347
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: TemplateViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4348
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.10", type: TemplateViewComponent, isStandalone: true, selector: "document-engine-template-view", ngImport: i0, template: `
4349
- <div class="template-view">
4350
- <!-- Title -->
4351
- <div class="template-view__header">Templates</div>
4352
-
4353
- <!-- Search Bar -->
4354
- <div class="template-view__search">
4355
- <div class="template-view__search-field">
4356
- <label documentEngineLabel for="search-input">Search templates</label>
4357
- <div class="template-view__search-input-wrapper">
4358
- <document-engine-icon name="search" class="template-view__search-icon"></document-engine-icon>
4359
- <input
4360
- documentEngineInput
4361
- id="search-input"
4362
- [formControl]="searchControl"
4363
- placeholder="Search templates..."
4364
- aria-label="Search templates"
4365
- />
4366
- </div>
4367
- </div>
4368
- </div>
4369
-
4370
- <!-- Templates List -->
4371
- <div class="template-view__list-container">
4372
- <div *ngIf="filteredTemplates.length > 0; else noResults" class="template-view__list">
4373
- <button
4374
- *ngFor="let template of filteredTemplates"
4375
- type="button"
4376
- class="template-view__template-item"
4377
- (click)="insertTemplate(template)"
4378
- [attr.aria-label]="template.title + (template.description ? ' - ' + template.description : '')"
4379
- [title]="template.description || template.title"
4380
- >
4381
- <div class="template-view__template-title">{{ template.title }}</div>
4382
- <div *ngIf="template.description" class="template-view__template-description">
4383
- {{ template.description }}
4384
- </div>
4385
- </button>
4785
+ </div>
4386
4786
  </div>
4387
4787
 
4388
- <ng-template #noResults>
4389
- <div class="template-view__empty">
4390
- <document-engine-icon name="search_off" class="template-view__empty-icon"></document-engine-icon>
4391
- <div class="template-view__empty-text">No templates found matching "{{ searchQuery }}"</div>
4392
- </div>
4393
- </ng-template>
4394
- </div>
4788
+ <div class="table-style-view__section">
4789
+ <div class="table-style-view__section-label">Background</div>
4790
+ <div class="table-style-view__field table-style-view__field--color">
4791
+ <div class="table-style-view__field-label">Color</div>
4395
4792
 
4396
- <!-- Actions -->
4397
- <div class="template-view__actions">
4398
- <button type="button" documentEngineButton variant="secondary" (click)="close?.()">Close</button>
4399
- </div>
4400
- </div>
4401
- `, isInline: true, styles: [".template-view{display:flex;flex-direction:column;width:100%;max-width:28rem}.template-view__header{padding:.5rem 1rem;border-bottom:1px solid var(--border);font-weight:500}.template-view__search{padding:.75rem;border-bottom:1px solid var(--border)}.template-view__search-field{display:flex;flex-direction:column;gap:.25rem;width:100%}.template-view__search-input-wrapper{position:relative;display:flex;align-items:center}.template-view__search-icon{position:absolute;left:.75rem;color:var(--muted-foreground);pointer-events:none;z-index:1}.template-view__search-input-wrapper input{padding-left:2.5rem}.template-view__list-container{overflow-y:auto;max-height:20rem}.template-view__list{padding:.5rem 0}.template-view__template-item{display:flex;flex-direction:column;align-items:flex-start;padding:.75rem;width:100%;text-align:left;background-color:transparent;border:none;cursor:pointer;transition:background-color .2s;outline:none}.template-view__template-item:hover{background-color:var(--accent);color:var(--accent-foreground)}.template-view__template-item:focus{outline:none;box-shadow:0 0 0 2px var(--ring);box-shadow-offset:2px}.template-view__template-title{font-weight:500;font-size:.875rem;color:var(--foreground);margin-bottom:.25rem}.template-view__template-description{font-size:.75rem;color:var(--muted-foreground);line-height:1.4}.template-view__empty{padding:2rem 1rem;text-align:center;color:var(--muted-foreground)}.template-view__empty-icon{margin-bottom:.5rem;font-size:2rem;opacity:.5}.template-view__empty-text{font-size:.875rem}.template-view__actions{display:flex;justify-content:flex-end;padding:.75rem;border-top:1px solid var(--border)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: ButtonDirective, selector: "button[documentEngineButton]", inputs: ["variant", "size"] }, { kind: "directive", type: InputDirective, selector: "input[documentEngineInput]", inputs: ["class"] }, { kind: "directive", type: LabelDirective, selector: "label[documentEngineLabel]", inputs: ["class"] }, { kind: "component", type: IconComponent, selector: "document-engine-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4402
- }
4403
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: TemplateViewComponent, decorators: [{
4404
- type: Component,
4405
- args: [{ selector: 'document-engine-template-view', standalone: true, imports: [CommonModule, ReactiveFormsModule, ButtonDirective, InputDirective, LabelDirective, IconComponent], template: `
4406
- <div class="template-view">
4407
- <!-- Title -->
4408
- <div class="template-view__header">Templates</div>
4793
+ <div class="table-style-view__color-input">
4794
+ <input documentEngineInput [value]="tableBg" readonly />
4409
4795
 
4410
- <!-- Search Bar -->
4411
- <div class="template-view__search">
4412
- <div class="template-view__search-field">
4413
- <label documentEngineLabel for="search-input">Search templates</label>
4414
- <div class="template-view__search-input-wrapper">
4415
- <document-engine-icon name="search" class="template-view__search-icon"></document-engine-icon>
4416
- <input
4417
- documentEngineInput
4418
- id="search-input"
4419
- [formControl]="searchControl"
4420
- placeholder="Search templates..."
4421
- aria-label="Search templates"
4422
- />
4423
- </div>
4424
- </div>
4425
- </div>
4796
+ <button
4797
+ *ngIf="tableBg"
4798
+ type="button"
4799
+ documentEngineButton
4800
+ size="icon"
4801
+ variant="ghost"
4802
+ class="table-style-view__color-clear"
4803
+ (click)="tableBg = null"
4804
+ >
4805
+ <document-engine-icon name="close"></document-engine-icon>
4806
+ </button>
4426
4807
 
4427
- <!-- Templates List -->
4428
- <div class="template-view__list-container">
4429
- <div *ngIf="filteredTemplates.length > 0; else noResults" class="template-view__list">
4430
- <button
4431
- *ngFor="let template of filteredTemplates"
4432
- type="button"
4433
- class="template-view__template-item"
4434
- (click)="insertTemplate(template)"
4435
- [attr.aria-label]="template.title + (template.description ? ' - ' + template.description : '')"
4436
- [title]="template.description || template.title"
4437
- >
4438
- <div class="template-view__template-title">{{ template.title }}</div>
4439
- <div *ngIf="template.description" class="template-view__template-description">
4440
- {{ template.description }}
4808
+ <button
4809
+ #bgColorTrigger
4810
+ type="button"
4811
+ class="table-style-view__color-swatch"
4812
+ [style.background-color]="tableBg || 'transparent'"
4813
+ (click)="toggleColorPicker('bg')"
4814
+ title="Pick color"
4815
+ >
4816
+ <div *ngIf="tableBg === 'transparent'" class="table-style-view__color-checker"></div>
4817
+ </button>
4441
4818
  </div>
4442
- </button>
4443
- </div>
4444
4819
 
4445
- <ng-template #noResults>
4446
- <div class="template-view__empty">
4447
- <document-engine-icon name="search_off" class="template-view__empty-icon"></document-engine-icon>
4448
- <div class="template-view__empty-text">No templates found matching "{{ searchQuery }}"</div>
4820
+ <!-- Color picker for background -->
4821
+ <div
4822
+ *ngIf="showColorPicker === 'bg'"
4823
+ [popover]="bgColorTrigger"
4824
+ [isOpen]="showColorPicker === 'bg'"
4825
+ class="table-style-view__color-picker-dropdown"
4826
+ (click)="$event.stopPropagation()"
4827
+ >
4828
+ <document-engine-color-picker
4829
+ [colorPalette]="colorPalette"
4830
+ [activeColor]="tableBgObj"
4831
+ (colorSelected)="onTableBgSelected($event)"
4832
+ (colorRemoved)="onTableBgRemoved()"
4833
+ ></document-engine-color-picker>
4834
+ </div>
4449
4835
  </div>
4450
- </ng-template>
4836
+ </div>
4451
4837
  </div>
4452
4838
 
4453
4839
  <!-- Actions -->
4454
- <div class="template-view__actions">
4455
- <button type="button" documentEngineButton variant="secondary" (click)="close?.()">Close</button>
4840
+ <div class="table-style-view__actions">
4841
+ <button documentEngineButton variant="ghost" (click)="cancel()">Cancel</button>
4842
+ <button documentEngineButton variant="default" (click)="onSave()">Save</button>
4456
4843
  </div>
4457
4844
  </div>
4458
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".template-view{display:flex;flex-direction:column;width:100%;max-width:28rem}.template-view__header{padding:.5rem 1rem;border-bottom:1px solid var(--border);font-weight:500}.template-view__search{padding:.75rem;border-bottom:1px solid var(--border)}.template-view__search-field{display:flex;flex-direction:column;gap:.25rem;width:100%}.template-view__search-input-wrapper{position:relative;display:flex;align-items:center}.template-view__search-icon{position:absolute;left:.75rem;color:var(--muted-foreground);pointer-events:none;z-index:1}.template-view__search-input-wrapper input{padding-left:2.5rem}.template-view__list-container{overflow-y:auto;max-height:20rem}.template-view__list{padding:.5rem 0}.template-view__template-item{display:flex;flex-direction:column;align-items:flex-start;padding:.75rem;width:100%;text-align:left;background-color:transparent;border:none;cursor:pointer;transition:background-color .2s;outline:none}.template-view__template-item:hover{background-color:var(--accent);color:var(--accent-foreground)}.template-view__template-item:focus{outline:none;box-shadow:0 0 0 2px var(--ring);box-shadow-offset:2px}.template-view__template-title{font-weight:500;font-size:.875rem;color:var(--foreground);margin-bottom:.25rem}.template-view__template-description{font-size:.75rem;color:var(--muted-foreground);line-height:1.4}.template-view__empty{padding:2rem 1rem;text-align:center;color:var(--muted-foreground)}.template-view__empty-icon{margin-bottom:.5rem;font-size:2rem;opacity:.5}.template-view__empty-text{font-size:.875rem}.template-view__actions{display:flex;justify-content:flex-end;padding:.75rem;border-top:1px solid var(--border)}\n"] }]
4459
- }] });
4845
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".table-style-view{display:flex;flex-direction:column;width:32.5rem}.table-style-view__header{padding:.5rem 1rem;border-bottom:1px solid var(--border);font-weight:500}.table-style-view__content{display:flex;flex-direction:column;gap:1rem;padding:.75rem}.table-style-view__section{display:flex;flex-direction:column;gap:.5rem}.table-style-view__section-label{font-weight:700;color:var(--muted-foreground)}.table-style-view__border-grid{display:grid;grid-template-columns:repeat(8,1fr);gap:.5rem}.table-style-view__field{display:flex;flex-direction:column;gap:.25rem;grid-column:span 2}.table-style-view__field--color{grid-column:span 4;position:relative}.table-style-view__field-label{font-size:.75rem;color:var(--muted-foreground)}.table-style-view__color-input{position:relative;display:flex;align-items:center;gap:.25rem}.table-style-view__color-input input{flex:1;padding-right:3rem}.table-style-view__color-clear{position:absolute;right:2.5rem;z-index:1}.table-style-view__color-swatch{position:absolute;right:.5rem;width:1.75rem;height:1.75rem;border-radius:.25rem;border:1px solid var(--border);cursor:pointer;transition:transform .2s;overflow:hidden;z-index:2}.table-style-view__color-swatch:hover{transform:scale(1.1)}.table-style-view__color-checker{position:absolute;inset:0;opacity:.3;background-image:repeating-conic-gradient(#ccc 0deg 90deg,#fff 90deg 180deg);background-size:4px 4px}.table-style-view__color-picker-dropdown{z-index:50;background:var(--popover);border:1px solid var(--border);border-radius:.5rem;box-shadow:var(--shadow-elevation-2);padding:.5rem}.table-style-view__actions{display:flex;align-items:center;justify-content:flex-end;gap:.5rem;padding:.75rem;border-top:1px solid var(--border)}\n"] }]
4846
+ }], propDecorators: { borderColorTrigger: [{
4847
+ type: ViewChild,
4848
+ args: ['borderColorTrigger', { static: false, read: ElementRef }]
4849
+ }], bgColorTrigger: [{
4850
+ type: ViewChild,
4851
+ args: ['bgColorTrigger', { static: false, read: ElementRef }]
4852
+ }] } });
4460
4853
 
4461
- const TemplateBubbleConfig = {
4462
- pluginKey: 'TemplateBubbleMenu',
4463
- maxWidth: '400px',
4464
- width: '400px',
4854
+ const TableBubbleConfig = {
4855
+ props: {
4856
+ pluginKey: 'TableBubbleMenu',
4857
+ mountTo: 'table',
4858
+ },
4859
+ target: {
4860
+ kind: 'node',
4861
+ name: ['tableCell', 'tableHeader'],
4862
+ allowEmptySelection: true,
4863
+ requireFocus: false,
4864
+ },
4465
4865
  views: [
4466
4866
  {
4467
- id: 'templates',
4468
- component: TemplateViewComponent,
4469
- label: 'Templates',
4867
+ id: 'main',
4868
+ component: TableMainViewComponent,
4869
+ label: 'Table',
4470
4870
  isDefault: true,
4471
4871
  },
4472
- ],
4473
- };
4474
-
4475
- const FONT_SIZE_OPTIONS = [
4476
- { value: '12px', label: '12px' },
4477
- { value: '14px', label: '14px' },
4478
- { value: '16px', label: '16px' },
4479
- { value: '18px', label: '18px' },
4480
- { value: '20px', label: '20px' },
4481
- ];
4482
- const LINE_HEIGHT_OPTIONS = [
4483
- { value: null, label: 'Default' },
4484
- { value: '1', label: '1' },
4485
- { value: '1.5', label: '1.5' },
4486
- { value: '2', label: '2' },
4487
- { value: '2.5', label: '2.5' },
4488
- { value: '3', label: '3' },
4489
- ];
4490
- const TEXT_CASE_OPTIONS = [
4491
- { value: 'uppercase', label: 'UPPERCASE' },
4492
- { value: 'lowercase', label: 'lowercase' },
4493
- { value: 'capitalize', label: 'Capitalize' },
4494
- ];
4495
- const HEADING_OPTIONS = [
4496
- { value: null, label: 'Normal text', class: '' },
4497
- { value: 1, label: 'Heading 1', class: 'h1' },
4498
- { value: 2, label: 'Heading 2', class: 'h2' },
4499
- { value: 3, label: 'Heading 3', class: 'h3' },
4500
- ];
4501
- const TEXT_ALIGN_OPTIONS = [
4502
- { value: 'left', label: 'Align Left', icon: 'format_align_left' },
4503
- { value: 'center', label: 'Align Center', icon: 'format_align_center' },
4504
- { value: 'right', label: 'Align Right', icon: 'format_align_right' },
4505
- { value: 'justify', label: 'Align Justify', icon: 'format_align_justify' },
4506
- ];
4507
-
4508
- const LIST_STYLE_OPTIONS = [
4509
- { value: 'decimal', label: 'Decimal', example: '1.' },
4510
- { value: 'lower-alpha-dot', label: 'Lower Alpha', example: 'a.' },
4511
- { value: 'lower-alpha-parens', label: 'Lower Alpha (Parentheses)', example: '(a)' },
4512
- { value: 'lower-roman-parens', label: 'Lower Roman', example: '(i)' },
4513
- ];
4514
-
4515
- /**
4516
- * Service to manage focus trapping within an element
4517
- * Traps keyboard focus inside a container element for accessibility
4518
- */
4519
- class FocusTrapService {
4520
- document = inject(DOCUMENT);
4521
- activeTraps = new Map();
4522
- /**
4523
- * Attach focus trap to an element
4524
- * @param element The container element to trap focus within
4525
- * @param autoFocus Whether to automatically focus the first focusable element
4526
- */
4527
- attach(element, autoFocus = true) {
4528
- if (!element)
4529
- return;
4530
- // Detach existing trap if any
4531
- this.detach(element);
4532
- // Get all focusable elements within the container
4533
- const focusableSelectors = [
4534
- 'a[href]',
4535
- 'button:not([disabled])',
4536
- 'textarea:not([disabled])',
4537
- 'input:not([disabled])',
4538
- 'select:not([disabled])',
4539
- '[tabindex]:not([tabindex="-1"])',
4540
- ].join(', ');
4541
- const focusableElements = Array.from(element.querySelectorAll(focusableSelectors));
4542
- if (focusableElements.length === 0)
4543
- return;
4544
- const firstFocusableElement = focusableElements[0];
4545
- const lastFocusableElement = focusableElements[focusableElements.length - 1];
4546
- // Focus the first element if requested
4547
- if (autoFocus && firstFocusableElement) {
4548
- setTimeout(() => {
4549
- firstFocusableElement.focus();
4550
- }, 0);
4551
- }
4552
- // Handle Tab key to trap focus
4553
- const handler = (event) => {
4554
- if (event.key !== 'Tab')
4555
- return;
4556
- if (!element.contains(this.document.activeElement)) {
4557
- event.preventDefault();
4558
- if (event.shiftKey) {
4559
- lastFocusableElement?.focus();
4560
- }
4561
- else {
4562
- firstFocusableElement?.focus();
4563
- }
4564
- return;
4565
- }
4566
- if (event.shiftKey) {
4567
- // Shift + Tab: going backwards
4568
- if (this.document.activeElement === firstFocusableElement) {
4569
- event.preventDefault();
4570
- lastFocusableElement?.focus();
4571
- }
4572
- }
4573
- else {
4574
- // Tab: going forwards
4575
- if (this.document.activeElement === lastFocusableElement) {
4576
- event.preventDefault();
4577
- firstFocusableElement?.focus();
4578
- }
4579
- }
4580
- };
4581
- element.addEventListener('keydown', handler);
4582
- this.activeTraps.set(element, {
4583
- handler,
4584
- firstElement: firstFocusableElement,
4585
- lastElement: lastFocusableElement,
4872
+ {
4873
+ id: 'table-style',
4874
+ component: TableStyleViewComponent,
4875
+ label: 'Table Properties',
4876
+ },
4877
+ {
4878
+ id: 'cell-style',
4879
+ component: TableCellStyleViewComponent,
4880
+ label: 'Cell Properties',
4881
+ },
4882
+ ],
4883
+ };
4884
+ const TableCreateBubbleConfig = {
4885
+ pluginKey: 'TableCreateBubbleMenu',
4886
+ views: [
4887
+ {
4888
+ id: 'create',
4889
+ component: TableCreateViewComponent,
4890
+ label: 'Create Table',
4891
+ isDefault: true,
4892
+ },
4893
+ ],
4894
+ };
4895
+
4896
+ /**
4897
+ * Template view for bubble menu
4898
+ * Shows templates for insertion into the editor
4899
+ * Replaces entire editor content with selected template
4900
+ */
4901
+ class TemplateViewComponent {
4902
+ cdr = inject(ChangeDetectorRef);
4903
+ editor;
4904
+ close;
4905
+ goBack;
4906
+ navigateTo;
4907
+ searchQuery = '';
4908
+ originalTemplates = [];
4909
+ // Reactive form control for search
4910
+ searchControl = new FormControl('');
4911
+ destroy$ = new Subject();
4912
+ onActivate(attrs) {
4913
+ const templates = attrs['templates'];
4914
+ if (!templates)
4915
+ throw new Error('Templates are required');
4916
+ this.originalTemplates = templates;
4917
+ this.searchQuery = '';
4918
+ this.searchControl.setValue('');
4919
+ this.cdr.markForCheck();
4920
+ }
4921
+ ngOnInit() {
4922
+ // Set up reactive form subscription with debounce
4923
+ this.searchControl.valueChanges
4924
+ .pipe(debounceTime(200), distinctUntilChanged(), takeUntil(this.destroy$))
4925
+ .subscribe((value) => {
4926
+ this.searchQuery = value || '';
4927
+ this.cdr.markForCheck();
4586
4928
  });
4587
4929
  }
4588
- /**
4589
- * Detach focus trap from an element
4590
- * @param element The element to remove focus trap from
4591
- */
4592
- detach(element) {
4593
- if (!element)
4594
- return;
4595
- const trap = this.activeTraps.get(element);
4596
- if (trap) {
4597
- element.removeEventListener('keydown', trap.handler);
4598
- this.activeTraps.delete(element);
4599
- }
4930
+ ngOnDestroy() {
4931
+ this.destroy$.next();
4932
+ this.destroy$.complete();
4600
4933
  }
4601
4934
  /**
4602
- * Detach all active focus traps
4935
+ * Getter that filters templates based on search query
4603
4936
  */
4604
- detachAll() {
4605
- this.activeTraps.forEach((trap, element) => {
4606
- element.removeEventListener('keydown', trap.handler);
4607
- });
4608
- this.activeTraps.clear();
4937
+ get filteredTemplates() {
4938
+ const query = this.searchQuery.toLowerCase().trim();
4939
+ if (!query)
4940
+ return this.originalTemplates;
4941
+ // Filter templates based on search query
4942
+ return this.originalTemplates.filter((template) => template.title.toLowerCase().includes(query) || template.description?.toLowerCase().includes(query));
4609
4943
  }
4610
4944
  /**
4611
- * Re-attach focus trap to an element (useful when content changes)
4612
- * @param element The element to re-attach focus trap to
4613
- * @param autoFocus Whether to automatically focus the first focusable element
4945
+ * Insert a template into the editor, replacing entire content
4614
4946
  */
4615
- reattach(element, autoFocus = false) {
4616
- this.detach(element);
4617
- this.attach(element, autoFocus);
4947
+ insertTemplate(template) {
4948
+ if (!this.editor)
4949
+ return;
4950
+ // Replace entire editor content with template data
4951
+ this.editor.chain().focus().setContent(template.data).run();
4952
+ this.close?.();
4618
4953
  }
4619
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: FocusTrapService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4620
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: FocusTrapService, providedIn: 'root' });
4621
- }
4622
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: FocusTrapService, decorators: [{
4623
- type: Injectable,
4624
- args: [{ providedIn: 'root' }]
4625
- }] });
4954
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: TemplateViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4955
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.10", type: TemplateViewComponent, isStandalone: true, selector: "document-engine-template-view", ngImport: i0, template: `
4956
+ <div class="template-view">
4957
+ <!-- Title -->
4958
+ <div class="template-view__header">Templates</div>
4626
4959
 
4627
- function shouldShowFnFactory(params) {
4628
- const { editor, target, forceOpen } = params;
4629
- const requireFocus = target.requireFocus !== false;
4630
- const allowEmptySelection = target.allowEmptySelection === true;
4631
- return () => {
4632
- if (forceOpen())
4633
- return true;
4634
- if (requireFocus && !editor.isFocused)
4635
- return false;
4636
- // Avoid showing during drag/resize
4637
- const dragging = editor.isDragging;
4638
- if (dragging)
4639
- return false;
4640
- // Avoid showing when selection is empty and mark is required
4641
- if (!allowEmptySelection && editor.state.selection.empty && target.kind === 'mark')
4642
- return false;
4643
- // Avoid showing when extension is not active
4644
- const active = Array.isArray(target.name)
4645
- ? target.name.some((name) => editor.isActive(name))
4646
- : editor.isActive(target.name);
4647
- if (!active)
4648
- return false;
4649
- // Avoid showing when extra predicate is not met
4650
- if (target.extraPredicate && !target.extraPredicate(editor))
4651
- return false;
4652
- return true;
4653
- };
4654
- }
4655
- function selectionChangeHandler(params) {
4656
- const { editor, target, onActive, onInactive, listenOnUpdate } = params;
4657
- const run = () => {
4658
- const isActive = Array.isArray(target.name)
4659
- ? target.name.some((name) => editor.isActive(name))
4660
- : editor.isActive(target.name);
4661
- if (!isActive)
4662
- return onInactive();
4663
- const activeName = Array.isArray(target.name) ? target.name.find((name) => editor.isActive(name)) : target.name;
4664
- const attrs = target.kind === 'mark'
4665
- ? (editor.getAttributes(activeName) ?? {})
4666
- : (editor.state.selection.node?.attrs ?? {});
4667
- onActive(attrs);
4668
- };
4669
- const onSelection = () => run();
4670
- const onUpdate = () => run();
4671
- editor.on('selectionUpdate', onSelection);
4672
- if (listenOnUpdate)
4673
- editor.on('update', onUpdate);
4674
- run(); // initial sync
4675
- return () => {
4676
- editor.off('selectionUpdate', onSelection);
4677
- if (listenOnUpdate)
4678
- editor.off('update', onUpdate);
4679
- };
4680
- }
4960
+ <!-- Search Bar -->
4961
+ <div class="template-view__search">
4962
+ <div class="template-view__search-field">
4963
+ <label documentEngineLabel for="search-input">Search templates</label>
4964
+ <div class="template-view__search-input-wrapper">
4965
+ <document-engine-icon name="search" class="template-view__search-icon"></document-engine-icon>
4966
+ <input
4967
+ documentEngineInput
4968
+ id="search-input"
4969
+ [formControl]="searchControl"
4970
+ placeholder="Search templates..."
4971
+ aria-label="Search templates"
4972
+ />
4973
+ </div>
4974
+ </div>
4975
+ </div>
4681
4976
 
4682
- class TiptapBubbleMenuDirective {
4683
- elRef = inject(ElementRef);
4684
- props;
4685
- editor;
4686
- mountTo;
4687
- shouldShow;
4688
- ngOnInit() {
4689
- const editor = this.editor;
4690
- if (!editor)
4691
- throw new Error('Required: Input `editor`');
4692
- const bubbleMenuElement = this.elRef.nativeElement;
4693
- bubbleMenuElement.style.visibility = 'hidden';
4694
- bubbleMenuElement.style.position = 'absolute';
4695
- // Prevent editor blur when interacting with the bubble content
4696
- // bubbleMenuElement.addEventListener('mousedown', this.preventDefault);
4697
- const { pluginKey, appendTo, updateDelay, resizeDelay, options } = this.props;
4698
- editor.registerPlugin(BubbleMenuPlugin({
4699
- pluginKey,
4700
- appendTo,
4701
- updateDelay,
4702
- resizeDelay,
4703
- editor,
4704
- element: bubbleMenuElement,
4705
- options: this.getFloatingUiOptions(options ?? {}, bubbleMenuElement),
4706
- shouldShow: this.shouldShow,
4707
- getReferencedVirtualElement: this.mountTo,
4708
- }));
4709
- }
4710
- ngOnDestroy() {
4711
- if (this.editor && this.props?.pluginKey) {
4712
- this.editor.unregisterPlugin(this.props.pluginKey);
4713
- }
4714
- window.requestAnimationFrame(() => {
4715
- if (this.elRef.nativeElement.parentNode) {
4716
- this.elRef.nativeElement.parentNode.removeChild(this.elRef.nativeElement);
4717
- }
4718
- });
4719
- this.elRef.nativeElement.removeEventListener('mousedown', this.preventDefault);
4720
- }
4721
- preventDefault(event) {
4722
- // Allow default behavior for focusable elements so inputs can receive focus
4723
- const target = event.target;
4724
- if (target) {
4725
- const tag = target.tagName;
4726
- const isFocusableTag = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || target.isContentEditable;
4727
- if (isFocusableTag)
4728
- return;
4729
- }
4730
- event.preventDefault();
4731
- }
4732
- getFloatingUiOptions(userOptions, element) {
4733
- // Find arrow element if it exists
4734
- // const arrowElement = element.querySelector('[data-arrow]') as HTMLElement | null;
4735
- // Merge with default Floating UI options
4736
- const defaultOptions = {
4737
- placement: 'bottom',
4738
- flip: {
4739
- fallbackPlacements: ['top', 'bottom', 'bottom-start', 'top-end', 'bottom-end'],
4740
- padding: 8,
4741
- },
4742
- // Enable shift middleware to keep bubble within viewport
4743
- shift: { padding: 8 },
4744
- // Add offset to create space between selection and bubble
4745
- offset: 10,
4746
- // Configure arrow if element exists
4747
- // arrow: arrowElement ? { element: arrowElement } : false,
4748
- };
4749
- return { ...defaultOptions, ...userOptions };
4750
- }
4751
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: TiptapBubbleMenuDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
4752
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.10", type: TiptapBubbleMenuDirective, isStandalone: true, selector: "tiptap-bubble-menu[editor], [tiptapBubbleMenu][editor]", inputs: { props: "props", editor: "editor", mountTo: "mountTo", shouldShow: "shouldShow" }, ngImport: i0 });
4977
+ <!-- Templates List -->
4978
+ <div class="template-view__list-container">
4979
+ <div *ngIf="filteredTemplates.length > 0; else noResults" class="template-view__list">
4980
+ <button
4981
+ *ngFor="let template of filteredTemplates"
4982
+ type="button"
4983
+ class="template-view__template-item"
4984
+ (click)="insertTemplate(template)"
4985
+ [attr.aria-label]="template.title + (template.description ? ' - ' + template.description : '')"
4986
+ [title]="template.description || template.title"
4987
+ >
4988
+ <div class="template-view__template-title">{{ template.title }}</div>
4989
+ <div *ngIf="template.description" class="template-view__template-description">
4990
+ {{ template.description }}
4991
+ </div>
4992
+ </button>
4993
+ </div>
4994
+
4995
+ <ng-template #noResults>
4996
+ <div class="template-view__empty">
4997
+ <document-engine-icon name="search_off" class="template-view__empty-icon"></document-engine-icon>
4998
+ <div class="template-view__empty-text">No templates found matching "{{ searchQuery }}"</div>
4999
+ </div>
5000
+ </ng-template>
5001
+ </div>
5002
+
5003
+ <!-- Actions -->
5004
+ <div class="template-view__actions">
5005
+ <button type="button" documentEngineButton variant="secondary" (click)="close?.()">Close</button>
5006
+ </div>
5007
+ </div>
5008
+ `, isInline: true, styles: [".template-view{display:flex;flex-direction:column;width:100%;max-width:28rem}.template-view__header{padding:.5rem 1rem;border-bottom:1px solid var(--border);font-weight:500}.template-view__search{padding:.75rem;border-bottom:1px solid var(--border)}.template-view__search-field{display:flex;flex-direction:column;gap:.25rem;width:100%}.template-view__search-input-wrapper{position:relative;display:flex;align-items:center}.template-view__search-icon{position:absolute;left:.75rem;color:var(--muted-foreground);pointer-events:none;z-index:1}.template-view__search-input-wrapper input{padding-left:2.5rem}.template-view__list-container{overflow-y:auto;max-height:20rem}.template-view__list{padding:.5rem 0}.template-view__template-item{display:flex;flex-direction:column;align-items:flex-start;padding:.75rem;width:100%;text-align:left;background-color:transparent;border:none;cursor:pointer;transition:background-color .2s;outline:none}.template-view__template-item:hover{background-color:var(--accent);color:var(--accent-foreground)}.template-view__template-item:focus{outline:none;box-shadow:0 0 0 2px var(--ring);box-shadow-offset:2px}.template-view__template-title{font-weight:500;font-size:.875rem;color:var(--foreground);margin-bottom:.25rem}.template-view__template-description{font-size:.75rem;color:var(--muted-foreground);line-height:1.4}.template-view__empty{padding:2rem 1rem;text-align:center;color:var(--muted-foreground)}.template-view__empty-icon{margin-bottom:.5rem;font-size:2rem;opacity:.5}.template-view__empty-text{font-size:.875rem}.template-view__actions{display:flex;justify-content:flex-end;padding:.75rem;border-top:1px solid var(--border)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: ButtonDirective, selector: "button[documentEngineButton]", inputs: ["variant", "size"] }, { kind: "directive", type: InputDirective, selector: "input[documentEngineInput]", inputs: ["class"] }, { kind: "directive", type: LabelDirective, selector: "label[documentEngineLabel]", inputs: ["class"] }, { kind: "component", type: IconComponent, selector: "document-engine-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4753
5009
  }
4754
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: TiptapBubbleMenuDirective, decorators: [{
4755
- type: Directive,
4756
- args: [{
4757
- selector: 'tiptap-bubble-menu[editor], [tiptapBubbleMenu][editor]',
4758
- standalone: true,
4759
- }]
4760
- }], propDecorators: { props: [{
4761
- type: Input
4762
- }], editor: [{
4763
- type: Input
4764
- }], mountTo: [{
4765
- type: Input
4766
- }], shouldShow: [{
4767
- type: Input
4768
- }] } });
5010
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: TemplateViewComponent, decorators: [{
5011
+ type: Component,
5012
+ args: [{ selector: 'document-engine-template-view', standalone: true, imports: [CommonModule, ReactiveFormsModule, ButtonDirective, InputDirective, LabelDirective, IconComponent], template: `
5013
+ <div class="template-view">
5014
+ <!-- Title -->
5015
+ <div class="template-view__header">Templates</div>
5016
+
5017
+ <!-- Search Bar -->
5018
+ <div class="template-view__search">
5019
+ <div class="template-view__search-field">
5020
+ <label documentEngineLabel for="search-input">Search templates</label>
5021
+ <div class="template-view__search-input-wrapper">
5022
+ <document-engine-icon name="search" class="template-view__search-icon"></document-engine-icon>
5023
+ <input
5024
+ documentEngineInput
5025
+ id="search-input"
5026
+ [formControl]="searchControl"
5027
+ placeholder="Search templates..."
5028
+ aria-label="Search templates"
5029
+ />
5030
+ </div>
5031
+ </div>
5032
+ </div>
5033
+
5034
+ <!-- Templates List -->
5035
+ <div class="template-view__list-container">
5036
+ <div *ngIf="filteredTemplates.length > 0; else noResults" class="template-view__list">
5037
+ <button
5038
+ *ngFor="let template of filteredTemplates"
5039
+ type="button"
5040
+ class="template-view__template-item"
5041
+ (click)="insertTemplate(template)"
5042
+ [attr.aria-label]="template.title + (template.description ? ' - ' + template.description : '')"
5043
+ [title]="template.description || template.title"
5044
+ >
5045
+ <div class="template-view__template-title">{{ template.title }}</div>
5046
+ <div *ngIf="template.description" class="template-view__template-description">
5047
+ {{ template.description }}
5048
+ </div>
5049
+ </button>
5050
+ </div>
5051
+
5052
+ <ng-template #noResults>
5053
+ <div class="template-view__empty">
5054
+ <document-engine-icon name="search_off" class="template-view__empty-icon"></document-engine-icon>
5055
+ <div class="template-view__empty-text">No templates found matching "{{ searchQuery }}"</div>
5056
+ </div>
5057
+ </ng-template>
5058
+ </div>
5059
+
5060
+ <!-- Actions -->
5061
+ <div class="template-view__actions">
5062
+ <button type="button" documentEngineButton variant="secondary" (click)="close?.()">Close</button>
5063
+ </div>
5064
+ </div>
5065
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".template-view{display:flex;flex-direction:column;width:100%;max-width:28rem}.template-view__header{padding:.5rem 1rem;border-bottom:1px solid var(--border);font-weight:500}.template-view__search{padding:.75rem;border-bottom:1px solid var(--border)}.template-view__search-field{display:flex;flex-direction:column;gap:.25rem;width:100%}.template-view__search-input-wrapper{position:relative;display:flex;align-items:center}.template-view__search-icon{position:absolute;left:.75rem;color:var(--muted-foreground);pointer-events:none;z-index:1}.template-view__search-input-wrapper input{padding-left:2.5rem}.template-view__list-container{overflow-y:auto;max-height:20rem}.template-view__list{padding:.5rem 0}.template-view__template-item{display:flex;flex-direction:column;align-items:flex-start;padding:.75rem;width:100%;text-align:left;background-color:transparent;border:none;cursor:pointer;transition:background-color .2s;outline:none}.template-view__template-item:hover{background-color:var(--accent);color:var(--accent-foreground)}.template-view__template-item:focus{outline:none;box-shadow:0 0 0 2px var(--ring);box-shadow-offset:2px}.template-view__template-title{font-weight:500;font-size:.875rem;color:var(--foreground);margin-bottom:.25rem}.template-view__template-description{font-size:.75rem;color:var(--muted-foreground);line-height:1.4}.template-view__empty{padding:2rem 1rem;text-align:center;color:var(--muted-foreground)}.template-view__empty-icon{margin-bottom:.5rem;font-size:2rem;opacity:.5}.template-view__empty-text{font-size:.875rem}.template-view__actions{display:flex;justify-content:flex-end;padding:.75rem;border-top:1px solid var(--border)}\n"] }]
5066
+ }] });
5067
+
5068
+ const TemplateBubbleConfig = {
5069
+ pluginKey: 'TemplateBubbleMenu',
5070
+ maxWidth: '400px',
5071
+ width: '400px',
5072
+ views: [
5073
+ {
5074
+ id: 'templates',
5075
+ component: TemplateViewComponent,
5076
+ label: 'Templates',
5077
+ isDefault: true,
5078
+ },
5079
+ ],
5080
+ };
5081
+
5082
+ const FONT_SIZE_OPTIONS = [
5083
+ { value: '12px', label: '12px' },
5084
+ { value: '14px', label: '14px' },
5085
+ { value: '16px', label: '16px' },
5086
+ { value: '18px', label: '18px' },
5087
+ { value: '20px', label: '20px' },
5088
+ ];
5089
+ const LINE_HEIGHT_OPTIONS = [
5090
+ { value: null, label: 'Default' },
5091
+ { value: '1', label: '1' },
5092
+ { value: '1.5', label: '1.5' },
5093
+ { value: '2', label: '2' },
5094
+ { value: '2.5', label: '2.5' },
5095
+ { value: '3', label: '3' },
5096
+ ];
5097
+ const TEXT_CASE_OPTIONS = [
5098
+ { value: 'uppercase', label: 'UPPERCASE' },
5099
+ { value: 'lowercase', label: 'lowercase' },
5100
+ { value: 'capitalize', label: 'Capitalize' },
5101
+ ];
5102
+ const HEADING_OPTIONS = [
5103
+ { value: null, label: 'Normal text', class: '' },
5104
+ { value: 1, label: 'Heading 1', class: 'h1' },
5105
+ { value: 2, label: 'Heading 2', class: 'h2' },
5106
+ { value: 3, label: 'Heading 3', class: 'h3' },
5107
+ ];
5108
+ const TEXT_ALIGN_OPTIONS = [
5109
+ { value: 'left', label: 'Align Left', icon: 'format_align_left' },
5110
+ { value: 'center', label: 'Align Center', icon: 'format_align_center' },
5111
+ { value: 'right', label: 'Align Right', icon: 'format_align_right' },
5112
+ { value: 'justify', label: 'Align Justify', icon: 'format_align_justify' },
5113
+ ];
5114
+
5115
+ const LIST_STYLE_OPTIONS = [
5116
+ { value: 'decimal', label: 'Decimal', example: '1.' },
5117
+ { value: 'lower-alpha-dot', label: 'Lower Alpha', example: 'a.' },
5118
+ { value: 'lower-alpha-parens', label: 'Lower Alpha (Parentheses)', example: '(a)' },
5119
+ { value: 'lower-roman-parens', label: 'Lower Roman', example: '(i)' },
5120
+ ];
4769
5121
 
4770
5122
  class EditorBubbleMenuComponent {
4771
5123
  editor;
@@ -5086,121 +5438,55 @@ class EditorBubbleMenuComponent {
5086
5438
  }
5087
5439
  return s;
5088
5440
  }
5089
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: EditorBubbleMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5090
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.10", type: EditorBubbleMenuComponent, isStandalone: true, selector: "document-engine-editor-bubble-menu", inputs: { editor: "editor", config: "config" }, viewQueries: [{ propertyName: "viewContainer", first: true, predicate: ["viewContainer"], descendants: true, read: ViewContainerRef }, { propertyName: "bubbleElement", first: true, predicate: ["bubbleElement"], descendants: true }], ngImport: i0, template: `
5091
- <div
5092
- #bubbleElement
5093
- class="bubble-menu-wrapper rounded border bg-card text-card-foreground border-border shadow-elevation-2 z-10"
5094
- tiptapBubbleMenu
5095
- [editor]="editor"
5096
- [props]="config.props!"
5097
- [mountTo]="mountTo"
5098
- [shouldShow]="shouldShowFn"
5099
- >
5100
- <!-- View component will be dynamically inserted here -->
5101
- <ng-container #viewContainer></ng-container>
5102
- </div>
5103
- `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: TiptapBubbleMenuDirective, selector: "tiptap-bubble-menu[editor], [tiptapBubbleMenu][editor]", inputs: ["props", "editor", "mountTo", "shouldShow"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5104
- }
5105
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: EditorBubbleMenuComponent, decorators: [{
5106
- type: Component,
5107
- args: [{
5108
- selector: 'document-engine-editor-bubble-menu',
5109
- standalone: true,
5110
- imports: [CommonModule, TiptapBubbleMenuDirective],
5111
- template: `
5112
- <div
5113
- #bubbleElement
5114
- class="bubble-menu-wrapper rounded border bg-card text-card-foreground border-border shadow-elevation-2 z-10"
5115
- tiptapBubbleMenu
5116
- [editor]="editor"
5117
- [props]="config.props!"
5118
- [mountTo]="mountTo"
5119
- [shouldShow]="shouldShowFn"
5120
- >
5121
- <!-- View component will be dynamically inserted here -->
5122
- <ng-container #viewContainer></ng-container>
5123
- </div>
5124
- `,
5125
- changeDetection: ChangeDetectionStrategy.OnPush,
5126
- }]
5127
- }], propDecorators: { editor: [{
5128
- type: Input
5129
- }], config: [{
5130
- type: Input
5131
- }], viewContainer: [{
5132
- type: ViewChild,
5133
- args: ['viewContainer', { read: ViewContainerRef }]
5134
- }], bubbleElement: [{
5135
- type: ViewChild,
5136
- args: ['bubbleElement']
5137
- }] } });
5138
-
5139
- class EventManager {
5140
- document = inject(DOCUMENT);
5141
- keyToEscapeListener = new Map();
5142
- keyToOutsideListener = new Map();
5143
- isAttached(key) {
5144
- return this.keyToOutsideListener.has(key) || this.keyToEscapeListener.has(key);
5145
- }
5146
- attach(key, hostElement, cb, anchorElement) {
5147
- // Avoid duplicate listeners
5148
- this.detach(key);
5149
- const oldOutside = this.keyToOutsideListener.get(key);
5150
- if (!oldOutside) {
5151
- const handler = (event) => {
5152
- const target = event.target;
5153
- const path = event.composedPath?.();
5154
- // Check if click is inside bubble
5155
- const isInsideBubble = !!hostElement && ((!!target && hostElement.contains(target)) || (!!path && path.includes(hostElement)));
5156
- if (isInsideBubble)
5157
- return;
5158
- // Check if click is on the anchor (toolbar button)
5159
- const isInsideAnchor = !!anchorElement && ((!!target && anchorElement.contains(target)) || (!!path && path.includes(anchorElement)));
5160
- if (isInsideAnchor)
5161
- return;
5162
- // Check if click is inside a CDK overlay (e.g., MatSelect dropdown, MatMenu, etc.)
5163
- // These are rendered outside the bubble but should not close it
5164
- if (target instanceof Element) {
5165
- const isInsideOverlay = target.closest('.cdk-overlay-container, .cdk-overlay-pane');
5166
- if (isInsideOverlay)
5167
- return;
5168
- }
5169
- cb(key);
5170
- };
5171
- this.keyToOutsideListener.set(key, handler);
5172
- this.document.addEventListener('pointerdown', handler, true);
5173
- this.document.addEventListener('mousedown', handler, true);
5174
- this.document.addEventListener('click', handler, true);
5175
- }
5176
- const oldEsc = this.keyToEscapeListener.get(key);
5177
- if (!oldEsc) {
5178
- const escHandler = (e) => e.key === 'Escape' && cb(key);
5179
- this.document.addEventListener('keydown', escHandler, true);
5180
- this.keyToEscapeListener.set(key, escHandler);
5181
- }
5182
- }
5183
- detach(key) {
5184
- const outside = this.keyToOutsideListener.get(key);
5185
- if (outside) {
5186
- this.document.removeEventListener('pointerdown', outside, true);
5187
- this.document.removeEventListener('mousedown', outside, true);
5188
- this.document.removeEventListener('click', outside, true);
5189
- this.keyToOutsideListener.delete(key);
5190
- }
5191
- const esc = this.keyToEscapeListener.get(key);
5192
- if (esc) {
5193
- this.document.removeEventListener('keydown', esc, true);
5194
- this.keyToEscapeListener.delete(key);
5195
- }
5196
- }
5197
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: EventManager, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
5198
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: EventManager, providedIn: 'root' });
5441
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: EditorBubbleMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5442
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.10", type: EditorBubbleMenuComponent, isStandalone: true, selector: "document-engine-editor-bubble-menu", inputs: { editor: "editor", config: "config" }, viewQueries: [{ propertyName: "viewContainer", first: true, predicate: ["viewContainer"], descendants: true, read: ViewContainerRef }, { propertyName: "bubbleElement", first: true, predicate: ["bubbleElement"], descendants: true }], ngImport: i0, template: `
5443
+ <div
5444
+ #bubbleElement
5445
+ class="bubble-menu-wrapper rounded border bg-card text-card-foreground border-border shadow-elevation-2 z-30"
5446
+ tiptapBubbleMenu
5447
+ [editor]="editor"
5448
+ [props]="config.props!"
5449
+ [mountTo]="mountTo"
5450
+ [shouldShow]="shouldShowFn"
5451
+ >
5452
+ <!-- View component will be dynamically inserted here -->
5453
+ <ng-container #viewContainer></ng-container>
5454
+ </div>
5455
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: TiptapBubbleMenuDirective, selector: "tiptap-bubble-menu[editor], [tiptapBubbleMenu][editor]", inputs: ["props", "editor", "mountTo", "shouldShow"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5199
5456
  }
5200
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: EventManager, decorators: [{
5201
- type: Injectable,
5202
- args: [{ providedIn: 'root' }]
5203
- }] });
5457
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: EditorBubbleMenuComponent, decorators: [{
5458
+ type: Component,
5459
+ args: [{
5460
+ selector: 'document-engine-editor-bubble-menu',
5461
+ standalone: true,
5462
+ imports: [CommonModule, TiptapBubbleMenuDirective],
5463
+ template: `
5464
+ <div
5465
+ #bubbleElement
5466
+ class="bubble-menu-wrapper rounded border bg-card text-card-foreground border-border shadow-elevation-2 z-30"
5467
+ tiptapBubbleMenu
5468
+ [editor]="editor"
5469
+ [props]="config.props!"
5470
+ [mountTo]="mountTo"
5471
+ [shouldShow]="shouldShowFn"
5472
+ >
5473
+ <!-- View component will be dynamically inserted here -->
5474
+ <ng-container #viewContainer></ng-container>
5475
+ </div>
5476
+ `,
5477
+ changeDetection: ChangeDetectionStrategy.OnPush,
5478
+ }]
5479
+ }], propDecorators: { editor: [{
5480
+ type: Input
5481
+ }], config: [{
5482
+ type: Input
5483
+ }], viewContainer: [{
5484
+ type: ViewChild,
5485
+ args: ['viewContainer', { read: ViewContainerRef }]
5486
+ }], bubbleElement: [{
5487
+ type: ViewChild,
5488
+ args: ['bubbleElement']
5489
+ }] } });
5204
5490
 
5205
5491
  /**
5206
5492
  * A bubble menu wrapper specifically for toolbar buttons
@@ -5629,690 +5915,429 @@ class ToolbarComponent {
5629
5915
  else {
5630
5916
  this.editor.chain().focus().unsetFontSize().run();
5631
5917
  }
5632
- this.toolbarService.emit('setFontSize', size);
5633
- }
5634
- setLineHeight(lineHeight) {
5635
- if (lineHeight) {
5636
- this.editor.chain().focus().setLineHeight(lineHeight).run();
5637
- }
5638
- else {
5639
- this.editor.chain().focus().unsetLineHeight().run();
5640
- }
5641
- this.toolbarService.emit('setLineHeight', lineHeight);
5642
- }
5643
- setHeading(level) {
5644
- if (!level) {
5645
- this.editor.chain().focus().removeHeading().run();
5646
- }
5647
- else {
5648
- const levelNumber = parseInt(level, 10);
5649
- this.editor.chain().focus().toggleHeading({ level: levelNumber }).run();
5650
- }
5651
- this.toolbarService.emit('setHeading', level);
5652
- }
5653
- setTextAlign(align) {
5654
- if (align) {
5655
- this.editor
5656
- .chain()
5657
- .focus()
5658
- .setTextAlign(align)
5659
- .run();
5660
- }
5661
- else {
5662
- // Reset to default (left)
5663
- this.editor.chain().focus().setTextAlign('left').run();
5664
- }
5665
- this.toolbarService.emit('setTextAlign', align);
5666
- }
5667
- setListStyle(listStyleType) {
5668
- if (this.editor.isActive('customOrderedList')) {
5669
- // If there's an active ordered list, update its style
5670
- this.editor.chain().focus().setListStyle(listStyleType).run();
5671
- }
5672
- else {
5673
- // If no active list, create a new one with the selected style
5674
- this.editor.chain().focus().toggleOrderedList().run();
5675
- this.editor.chain().focus().setListStyle(listStyleType).run();
5676
- }
5677
- this.toolbarService.emit('setListStyle', listStyleType);
5678
- }
5679
- openColorBubble(event, type) {
5680
- const targetElement = (event.currentTarget || event.target);
5681
- this.colorBubbleMenu?.toggleBubble(targetElement, { type });
5682
- this.toolbarService.emit('openColorBubble', { type });
5683
- }
5684
- toggleLink() {
5685
- if (this.editor.isActive('link')) {
5686
- // If link is active, remove it
5687
- this.editor.chain().focus().unsetLink().run();
5688
- }
5689
- else {
5690
- // If no link, open the bubble menu
5691
- this.linkBubbleWrapper?.showBubble();
5692
- }
5693
- this.toolbarService.emit('toggleLink');
5694
- }
5695
- insertImage(event) {
5696
- const targetElement = (event.currentTarget || event.target);
5697
- this.imageBubbleMenu?.toggleBubble(targetElement);
5698
- this.toolbarService.emit('insertImage');
5699
- }
5700
- openSpecialCharsBubble(event) {
5701
- const targetElement = (event.currentTarget || event.target);
5702
- this.specialCharsBubbleMenu?.toggleBubble(targetElement);
5703
- this.toolbarService.emit('openSpecialCharsBubble');
5704
- }
5705
- openDynamicFieldsBubble(event) {
5706
- const targetElement = (event.currentTarget || event.target);
5707
- this.dynamicFieldsBubbleMenu?.toggleBubble(targetElement, {
5708
- categories: this.config?.dynamicFieldsCategories || [],
5709
- });
5710
- this.toolbarService.emit('openDynamicFieldsBubble');
5711
- }
5712
- openTemplateBubble(event) {
5713
- const targetElement = (event.currentTarget || event.target);
5714
- this.templateBubbleMenu?.toggleBubble(targetElement, {
5715
- templates: this.config?.templates || [],
5716
- });
5717
- this.toolbarService.emit('openTemplateBubble');
5718
- }
5719
- openCreateTableBubble(event) {
5720
- const targetElement = (event.currentTarget || event.target);
5721
- this.tableCreateBubbleMenu?.toggleBubble(targetElement);
5722
- this.toolbarService.emit('openCreateTableBubble');
5723
- }
5724
- // downloadAsMarkdown(filename?: string): void {
5725
- // const fileName = filename ? `${filename}.md` : 'export.md';
5726
- // const markdown = renderToMarkdown({
5727
- // content: this.editor.getJSON(),
5728
- // extensions: this.editor.options.extensions,
5729
- // options: {
5730
- // nodeMapping: {
5731
- // pageBreak: () => 'Page break',
5732
- // },
5733
- // },
5734
- // });
5735
- // const blob = new Blob([markdown], { type: 'text/markdown' });
5736
- // const url = URL.createObjectURL(blob);
5737
- // const a = document.createElement('a');
5738
- // a.href = url;
5739
- // a.download = fileName;
5740
- // a.click();
5741
- // URL.revokeObjectURL(url);
5742
- // }
5743
- trackByValue(index, item) {
5744
- return item.value;
5745
- }
5746
- trackByIndex(index) {
5747
- return index;
5748
- }
5749
- get customToolbarButtons() {
5750
- return this.config?.customToolbarButtons || [];
5751
- }
5752
- handleCustomIconButton(button) {
5753
- if (button.type === 'icon-button') {
5754
- button.callback();
5755
- this.toolbarService.emit('customIconButton', { label: button.label, icon: button.icon });
5756
- }
5757
- }
5758
- handleCustomSelectButton(button, value) {
5759
- if (button.type === 'select-button') {
5760
- button.callback();
5761
- this.toolbarService.emit('customSelectButton', { label: button.label, value });
5762
- }
5763
- }
5764
- printValue() {
5765
- console.log('html', this.editor.getHTML());
5766
- console.log('json', this.editor.getJSON());
5767
- console.log('State', this.editor.state);
5768
- this.toolbarService.emit('printValue');
5769
- }
5770
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: ToolbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5771
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.10", type: ToolbarComponent, isStandalone: true, selector: "document-engine-toolbar", inputs: { editor: "editor", capabilities: "capabilities", config: "config" }, outputs: { action: "action" }, viewQueries: [{ propertyName: "colorBubbleMenu", first: true, predicate: ["colorBubbleMenu"], descendants: true }, { propertyName: "linkBubbleWrapper", first: true, predicate: ["linkBubbleWrapper"], descendants: true }, { propertyName: "imageBubbleMenu", first: true, predicate: ["imageBubbleMenu"], descendants: true }, { propertyName: "specialCharsBubbleMenu", first: true, predicate: ["specialCharsBubbleMenu"], descendants: true }, { propertyName: "dynamicFieldsBubbleMenu", first: true, predicate: ["dynamicFieldsBubbleMenu"], descendants: true }, { propertyName: "templateBubbleMenu", first: true, predicate: ["templateBubbleMenu"], descendants: true }, { propertyName: "tableCreateBubbleMenu", first: true, predicate: ["tableCreateBubbleMenu"], descendants: true }], ngImport: i0, template: "<div class=\"document-engine-document-editor__toolbar\">\n <div class=\"document-engine-document-editor__toolbar-content\">\n <div class=\"document-engine-document-editor__toolbar-left\">\n <!-- History: Undo/Redo -->\n <div *ngIf=\"capabilities.hasHistory\" class=\"document-engine-document-editor__toolbar-group\" data-group=\"history\">\n <button\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canUndo\"\n (click)=\"runAndEmit('undo', editor.chain().focus().undo().run)\"\n title=\"Undo (Ctrl+Z)\"\n >\n <document-engine-icon name=\"undo\"></document-engine-icon>\n </button>\n\n <button\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canRedo\"\n (click)=\"runAndEmit('redo', editor.chain().focus().redo().run)\"\n title=\"Redo\"\n >\n <document-engine-icon name=\"redo\"></document-engine-icon>\n </button>\n </div>\n\n <!-- Text Formatting: Font Size, Line Height, Text Case, Heading -->\n <div\n *ngIf=\"capabilities.hasTextProperty\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"text-property\"\n >\n <!-- Font Size -->\n <document-engine-select\n *ngIf=\"capabilities.hasFontSize\"\n [value]=\"activeFontSize\"\n placeholder=\"Font Size\"\n labelMode=\"static\"\n [disabled]=\"toolbarState.isReadOnly\"\n title=\"Font Size\"\n >\n <document-engine-icon documentEngineSelectLabel name=\"format_size\"></document-engine-icon>\n\n <button documentEngineSelectOption [value]=\"null\">Default</button>\n\n <button\n *ngFor=\"let option of fontSizeOptions; trackBy: trackByValue\"\n documentEngineSelectOption\n [value]=\"option.value\"\n (click)=\"setFontSize(option.value)\"\n >\n {{ option.label }}\n </button>\n </document-engine-select>\n\n <!-- Line Height -->\n <document-engine-select\n *ngIf=\"capabilities.hasLineHeight\"\n [value]=\"activeLineHeight\"\n placeholder=\"Line Height\"\n labelMode=\"static\"\n [disabled]=\"toolbarState.isReadOnly\"\n title=\"Line Height\"\n >\n <document-engine-icon documentEngineSelectLabel name=\"format_line_spacing\"></document-engine-icon>\n\n <button\n *ngFor=\"let option of lineHeightOptions; trackBy: trackByValue\"\n documentEngineSelectOption\n [value]=\"option.value\"\n (click)=\"setLineHeight(option.value)\"\n >\n {{ option.label }}\n </button>\n </document-engine-select>\n\n <!-- Text Case -->\n <document-engine-select\n *ngIf=\"capabilities.hasTextCase\"\n placeholder=\"Text Case\"\n labelMode=\"static\"\n [disabled]=\"toolbarState.isReadOnly\"\n title=\"Text Case\"\n >\n <document-engine-icon documentEngineSelectLabel name=\"match_case\"></document-engine-icon>\n\n <button\n *ngFor=\"let option of textCaseOptions; trackBy: trackByValue\"\n documentEngineSelectOption\n [value]=\"option.value\"\n (click)=\"runAndEmit('setTextCase', editor.chain().focus().textCase(option.value).run)\"\n >\n {{ option.label }}\n </button>\n </document-engine-select>\n\n <!-- Heading -->\n <document-engine-select\n *ngIf=\"capabilities.hasHeading\"\n [value]=\"activeHeading?.toString() || null\"\n placeholder=\"Normal text\"\n [disabled]=\"toolbarState.isReadOnly\"\n title=\"Heading\"\n >\n <button\n *ngFor=\"let option of headingOptions; trackBy: trackByValue\"\n documentEngineSelectOption\n [value]=\"option.value?.toString() || null\"\n [class]=\"option.class\"\n (click)=\"setHeading(option.value?.toString() || null)\"\n >\n {{ option.label }}\n </button>\n </document-engine-select>\n </div>\n\n <!-- Format Utilities: Reset Formatting & Colors -->\n <div\n *ngIf=\"capabilities.hasFormatUtilities\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"format-utilities\"\n >\n <!-- Reset Formatting -->\n <button\n *ngIf=\"capabilities.hasResetFormat\"\n documentEngineButton\n size=\"icon\"\n (click)=\"runAndEmit('resetFormat', editor.chain().focus().resetFormat().run)\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canResetFormat\"\n title=\"Clear Formatting\"\n >\n <document-engine-icon name=\"format_clear\"></document-engine-icon>\n </button>\n\n <!-- Colors: Text/Background -->\n <button\n *ngIf=\"capabilities.hasColors\"\n documentEngineButton\n size=\"icon\"\n title=\"Text Color\"\n [disabled]=\"toolbarState.isReadOnly\"\n (click)=\"openColorBubble($event, 'text')\"\n >\n <document-engine-icon name=\"format_color_text\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasColors\"\n documentEngineButton\n size=\"icon\"\n title=\"Background Color\"\n [disabled]=\"toolbarState.isReadOnly\"\n (click)=\"openColorBubble($event, 'fill')\"\n >\n <document-engine-icon name=\"format_color_fill\"></document-engine-icon>\n </button>\n </div>\n\n <!-- Text Styles: Bold/Italic/Underline/Strike -->\n <div\n *ngIf=\"capabilities.basicInline\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"basic-inline\"\n >\n <button\n *ngIf=\"capabilities.hasBold\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleBold\"\n (click)=\"runAndEmit('toggleBold', editor.chain().focus().toggleBold().run)\"\n [class.active]=\"editor.isActive('bold')\"\n title=\"Bold\"\n >\n <document-engine-icon name=\"format_bold\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasItalic\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleItalic\"\n (click)=\"runAndEmit('toggleItalic', editor.chain().focus().toggleItalic().run)\"\n [class.active]=\"editor.isActive('italic')\"\n title=\"Italic\"\n >\n <document-engine-icon name=\"format_italic\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasUnderline\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleUnderline\"\n (click)=\"runAndEmit('toggleUnderline', editor.chain().focus().toggleUnderline().run)\"\n [class.active]=\"editor.isActive('underline')\"\n title=\"Underline\"\n >\n <document-engine-icon name=\"format_underline\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasStrike\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleStrike\"\n (click)=\"runAndEmit('toggleStrike', editor.chain().focus().toggleStrike().run)\"\n [class.active]=\"editor.isActive('strike')\"\n title=\"Strikethrough\"\n >\n <document-engine-icon name=\"format_strikethrough\"></document-engine-icon>\n </button>\n </div>\n\n <!-- Text Styles: Sub/Super/Code/Link -->\n <div\n *ngIf=\"capabilities.advancedInline\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"advanced-inline\"\n >\n <button\n *ngIf=\"capabilities.hasSubscript\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleSubscript\"\n (click)=\"runAndEmit('toggleSubscript', editor.chain().focus().toggleSubscript().run)\"\n [class.active]=\"editor.isActive('subscript')\"\n title=\"Subscript\"\n >\n <document-engine-icon name=\"subscript\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasSuperscript\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleSuperscript\"\n (click)=\"runAndEmit('toggleSuperscript', editor.chain().focus().toggleSuperscript().run)\"\n [class.active]=\"editor.isActive('superscript')\"\n title=\"Superscript\"\n >\n <document-engine-icon name=\"superscript\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasCode\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleCode\"\n (click)=\"runAndEmit('toggleCode', editor.chain().focus().toggleCode().run)\"\n [class.active]=\"editor.isActive('code')\"\n title=\"Code\"\n >\n <document-engine-icon name=\"code\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasLink\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleLink\"\n (click)=\"toggleLink()\"\n [class.active]=\"editor.isActive('link')\"\n [title]=\"editor.isActive('link') ? 'Unlink' : 'Link'\"\n >\n <document-engine-icon [name]=\"editor.isActive('link') ? 'link_off' : 'link'\"></document-engine-icon>\n </button>\n </div>\n\n <!-- Alignment & Layout: Text Align, Indent, Lists -->\n <div\n *ngIf=\"capabilities.hasAlignmentLayout\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"alignment-layout\"\n >\n <!-- Text Alignment -->\n <document-engine-select\n *ngIf=\"capabilities.hasTextAlign\"\n [value]=\"activeTextAlign\"\n (valueChange)=\"setTextAlign($event)\"\n placeholder=\"Text Align\"\n labelMode=\"static\"\n [disabled]=\"toolbarState.isReadOnly\"\n title=\"Text Align\"\n >\n <document-engine-icon documentEngineSelectLabel [name]=\"activeTextAlignIcon\"></document-engine-icon>\n\n <button\n *ngFor=\"let option of textAlignOptions; trackBy: trackByValue\"\n documentEngineSelectOption\n [value]=\"option.value\"\n >\n <document-engine-icon [name]=\"option.icon\"></document-engine-icon>\n <span>{{ option.label }}</span>\n </button>\n </document-engine-select>\n\n <!-- Indent -->\n <button\n *ngIf=\"capabilities.hasIndent\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canDecreaseIndent\"\n (click)=\"runAndEmit('decreaseIndent', editor.chain().focus().decreaseIndent().run)\"\n title=\"Decrease Indent\"\n >\n <document-engine-icon name=\"format_indent_decrease\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasIndent\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canIncreaseIndent\"\n (click)=\"runAndEmit('increaseIndent', editor.chain().focus().increaseIndent().run)\"\n title=\"Increase Indent\"\n >\n <document-engine-icon name=\"format_indent_increase\"></document-engine-icon>\n </button>\n\n <!-- Lists: Bullet, Numbered (with style) -->\n <button\n *ngIf=\"capabilities.hasLists\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleBulletList\"\n (click)=\"runAndEmit('toggleBulletList', editor.chain().focus().toggleBulletList().run)\"\n [class.active]=\"editor.isActive('bulletList')\"\n title=\"Bullet List\"\n >\n <document-engine-icon name=\"format_list_bulleted\"></document-engine-icon>\n </button>\n\n <!-- Numbered List with Style -->\n <document-engine-select\n *ngIf=\"capabilities.hasLists\"\n placeholder=\"Numbered List\"\n labelMode=\"static\"\n [value]=\"\n editor.isActive('customOrderedList')\n ? editor.getAttributes('customOrderedList')['data-list-style-type'] || 'decimal'\n : null\n \"\n [disabled]=\"toolbarState.isReadOnly\"\n title=\"Numbered List\"\n >\n <document-engine-icon documentEngineSelectLabel name=\"format_list_numbered\"></document-engine-icon>\n\n <button\n *ngFor=\"let option of listStyleOptions; trackBy: trackByValue\"\n documentEngineSelectOption\n [value]=\"option.value\"\n (click)=\"setListStyle(option.value)\"\n [class.active]=\"\n editor.isActive('customOrderedList') &&\n editor.getAttributes('customOrderedList')['data-list-style-type'] === option.value\n \"\n >\n <span>{{ option.example }}</span>\n <span>{{ option.label }}</span>\n </button>\n </document-engine-select>\n </div>\n\n <!-- Block Types: Code Block, Blockquote -->\n <div\n *ngIf=\"capabilities.hasBlockTypes\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"block-types\"\n >\n <!-- Blockquote -->\n <button\n *ngIf=\"capabilities.hasBlockquote\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleBlockquote\"\n (click)=\"runAndEmit('toggleBlockquote', editor.chain().focus().toggleBlockquote().run)\"\n [class.active]=\"editor.isActive('blockquote')\"\n title=\"Quote\"\n >\n <document-engine-icon name=\"format_quote\"></document-engine-icon>\n </button>\n </div>\n\n <!-- Insert Object: Table, Images, Special Chars, Page Break -->\n <div\n *ngIf=\"capabilities.hasInsertObject\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"insert-object\"\n >\n <button\n *ngIf=\"capabilities.hasTable\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly\"\n (click)=\"openCreateTableBubble($event)\"\n [class.active]=\"editor.isActive('table')\"\n title=\"Table\"\n >\n <document-engine-icon name=\"table_rows\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasImages\"\n documentEngineButton\n size=\"icon\"\n title=\"Insert Image\"\n [disabled]=\"toolbarState.isReadOnly\"\n (click)=\"insertImage($event)\"\n >\n <document-engine-icon name=\"photo\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasSpecialChars\"\n documentEngineButton\n size=\"icon\"\n title=\"Special Character\"\n [disabled]=\"toolbarState.isReadOnly\"\n (click)=\"openSpecialCharsBubble($event)\"\n >\n <document-engine-icon name=\"special_character\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasPageBreak\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canInsertPageBreak\"\n (click)=\"runAndEmit('insertPageBreak', editor.chain().focus().insertPageBreak().run)\"\n title=\"Insert page break\"\n >\n <document-engine-icon name=\"insert_page_break\"></document-engine-icon>\n </button>\n </div>\n\n <!-- Business Features: Templates, Dynamic Fields, Editable Region -->\n <div\n *ngIf=\"capabilities.hasBusinessFeatures\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"business-features\"\n >\n <button\n *ngIf=\"capabilities.hasDynamicFields\"\n documentEngineButton\n size=\"icon\"\n title=\"Dynamic Field\"\n [disabled]=\"toolbarState.isReadOnly\"\n (click)=\"openDynamicFieldsBubble($event)\"\n >\n <document-engine-icon name=\"dynamic_form\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasTemplates\"\n documentEngineButton\n size=\"icon\"\n title=\"Template\"\n [disabled]=\"toolbarState.isReadOnly\"\n (click)=\"openTemplateBubble($event)\"\n >\n <document-engine-icon name=\"description\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasEditableRegion\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"!toolbarState.canToggleEditableRegion || toolbarState.isReadOnly\"\n (click)=\"runAndEmit('toggleEditableRegion', editor.chain().focus().toggleEditableRegion().run)\"\n [class.active]=\"editor.isActive('editableRegion')\"\n title=\"Toggle editable region\"\n >\n <document-engine-icon name=\"restrict_edit\"></document-engine-icon>\n </button>\n </div>\n\n <!-- Actions: Clear Content, Markdown -->\n <div *ngIf=\"capabilities.hasActions\" class=\"document-engine-document-editor__toolbar-group\" data-group=\"actions\">\n <button\n *ngIf=\"capabilities.hasClearContent\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canClearContent\"\n (click)=\"runAndEmit('clear', editor.chain().focus().clear().run)\"\n title=\"Clear all content\"\n >\n <document-engine-icon name=\"clear_all\"></document-engine-icon>\n </button>\n\n <!-- <button\n *ngIf=\"capabilities.hasMarkdown\"\n documentEngineButton\n size=\"icon\"\n (click)=\"downloadAsMarkdown()\"\n title=\"Download as Markdown\"\n >\n <document-engine-icon name=\"markdown\"></document-engine-icon>\n </button> -->\n </div>\n\n <!-- Custom Toolbar Buttons -->\n <div\n *ngIf=\"customToolbarButtons.length > 0\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"custom-buttons\"\n >\n <ng-container *ngFor=\"let button of customToolbarButtons; trackBy: trackByIndex\">\n <!-- Icon Button -->\n <button\n *ngIf=\"button.type === 'icon-button'\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly\"\n (click)=\"handleCustomIconButton(button)\"\n [title]=\"button.label || ''\"\n >\n <document-engine-icon [name]=\"button.icon\"></document-engine-icon>\n </button>\n\n <!-- Select Button -->\n <document-engine-select\n *ngIf=\"button.type === 'select-button'\"\n [value]=\"null\"\n labelMode=\"static\"\n [disabled]=\"toolbarState.isReadOnly\"\n [title]=\"button.label\"\n >\n <span documentEngineSelectLabel>{{ button.label }}</span>\n\n <button\n *ngFor=\"let option of button.options; trackBy: trackByValue\"\n documentEngineSelectOption\n [value]=\"option.value\"\n (click)=\"handleCustomSelectButton(button, option.value)\"\n >\n {{ option.label }}\n </button>\n </document-engine-select>\n </ng-container>\n </div>\n </div>\n\n <!-- Right side actions -->\n <div class=\"document-engine-document-editor__toolbar-right\">\n <button\n *ngIf=\"capabilities.hasPrintButton\"\n documentEngineButton\n size=\"icon\"\n (click)=\"printValue()\"\n title=\"Print Value\"\n >\n <document-engine-icon name=\"print\"></document-engine-icon>\n </button>\n </div>\n </div>\n\n <!-- Color Bubble Menu -->\n <document-engine-toolbar-bubble-menu\n #colorBubbleMenu\n [editor]=\"editor\"\n [config]=\"colorBubbleConfig\"\n ></document-engine-toolbar-bubble-menu>\n\n <!-- Link Bubble Menu -->\n <document-engine-editor-bubble-menu\n *ngIf=\"capabilities.hasLink\"\n #linkBubbleWrapper\n [editor]=\"editor\"\n [config]=\"linkBubbleConfig\"\n ></document-engine-editor-bubble-menu>\n\n <!-- Table Bubble Menu -->\n <document-engine-editor-bubble-menu\n *ngIf=\"capabilities.hasTable\"\n #tableBubbleWrapper\n [editor]=\"editor\"\n [config]=\"tableBubbleConfig\"\n ></document-engine-editor-bubble-menu>\n\n <!-- Image Bubble Menu -->\n <document-engine-toolbar-bubble-menu\n *ngIf=\"capabilities.hasImages\"\n #imageBubbleMenu\n [editor]=\"editor\"\n [config]=\"imageBubbleConfig\"\n ></document-engine-toolbar-bubble-menu>\n\n <!-- Special Characters Bubble Menu -->\n <document-engine-toolbar-bubble-menu\n *ngIf=\"capabilities.hasSpecialChars\"\n #specialCharsBubbleMenu\n [editor]=\"editor\"\n [config]=\"specialCharsBubbleConfig\"\n ></document-engine-toolbar-bubble-menu>\n\n <!-- Dynamic Fields Bubble Menu -->\n <document-engine-toolbar-bubble-menu\n *ngIf=\"capabilities.hasDynamicFields\"\n #dynamicFieldsBubbleMenu\n [editor]=\"editor\"\n [config]=\"dynamicFieldsBubbleConfig\"\n ></document-engine-toolbar-bubble-menu>\n\n <!-- Template Bubble Menu -->\n <document-engine-toolbar-bubble-menu\n *ngIf=\"capabilities.hasTemplates\"\n #templateBubbleMenu\n [editor]=\"editor\"\n [config]=\"templateBubbleConfig\"\n ></document-engine-toolbar-bubble-menu>\n\n <!-- Table Create Bubble Menu -->\n <document-engine-toolbar-bubble-menu\n *ngIf=\"capabilities.hasTable\"\n #tableCreateBubbleMenu\n [editor]=\"editor\"\n [config]=\"tableCreateBubbleConfig\"\n ></document-engine-toolbar-bubble-menu>\n</div>\n", styles: [":host{position:sticky;top:0;z-index:20}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: IconComponent, selector: "document-engine-icon", inputs: ["name"] }, { kind: "directive", type: ButtonDirective, selector: "button[documentEngineButton]", inputs: ["variant", "size"] }, { kind: "component", type: SelectComponent, selector: "document-engine-select", inputs: ["value", "disabled", "labelMode", "variant"], outputs: ["valueChange"] }, { kind: "directive", type: SelectLabelDirective, selector: "[documentEngineSelectLabel]" }, { kind: "directive", type: SelectOptionDirective, selector: "[documentEngineSelectOption]", inputs: ["value", "label", "disabled"] }, { kind: "component", type: ToolbarBubbleMenuComponent, selector: "document-engine-toolbar-bubble-menu", inputs: ["editor", "config"] }, { kind: "component", type: EditorBubbleMenuComponent, selector: "document-engine-editor-bubble-menu", inputs: ["editor", "config"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5772
- }
5773
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: ToolbarComponent, decorators: [{
5774
- type: Component,
5775
- args: [{ selector: 'document-engine-toolbar', standalone: true, imports: [
5776
- CommonModule,
5777
- IconComponent,
5778
- ButtonDirective,
5779
- SelectComponent,
5780
- SelectLabelDirective,
5781
- SelectOptionDirective,
5782
- ToolbarBubbleMenuComponent,
5783
- EditorBubbleMenuComponent,
5784
- ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"document-engine-document-editor__toolbar\">\n <div class=\"document-engine-document-editor__toolbar-content\">\n <div class=\"document-engine-document-editor__toolbar-left\">\n <!-- History: Undo/Redo -->\n <div *ngIf=\"capabilities.hasHistory\" class=\"document-engine-document-editor__toolbar-group\" data-group=\"history\">\n <button\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canUndo\"\n (click)=\"runAndEmit('undo', editor.chain().focus().undo().run)\"\n title=\"Undo (Ctrl+Z)\"\n >\n <document-engine-icon name=\"undo\"></document-engine-icon>\n </button>\n\n <button\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canRedo\"\n (click)=\"runAndEmit('redo', editor.chain().focus().redo().run)\"\n title=\"Redo\"\n >\n <document-engine-icon name=\"redo\"></document-engine-icon>\n </button>\n </div>\n\n <!-- Text Formatting: Font Size, Line Height, Text Case, Heading -->\n <div\n *ngIf=\"capabilities.hasTextProperty\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"text-property\"\n >\n <!-- Font Size -->\n <document-engine-select\n *ngIf=\"capabilities.hasFontSize\"\n [value]=\"activeFontSize\"\n placeholder=\"Font Size\"\n labelMode=\"static\"\n [disabled]=\"toolbarState.isReadOnly\"\n title=\"Font Size\"\n >\n <document-engine-icon documentEngineSelectLabel name=\"format_size\"></document-engine-icon>\n\n <button documentEngineSelectOption [value]=\"null\">Default</button>\n\n <button\n *ngFor=\"let option of fontSizeOptions; trackBy: trackByValue\"\n documentEngineSelectOption\n [value]=\"option.value\"\n (click)=\"setFontSize(option.value)\"\n >\n {{ option.label }}\n </button>\n </document-engine-select>\n\n <!-- Line Height -->\n <document-engine-select\n *ngIf=\"capabilities.hasLineHeight\"\n [value]=\"activeLineHeight\"\n placeholder=\"Line Height\"\n labelMode=\"static\"\n [disabled]=\"toolbarState.isReadOnly\"\n title=\"Line Height\"\n >\n <document-engine-icon documentEngineSelectLabel name=\"format_line_spacing\"></document-engine-icon>\n\n <button\n *ngFor=\"let option of lineHeightOptions; trackBy: trackByValue\"\n documentEngineSelectOption\n [value]=\"option.value\"\n (click)=\"setLineHeight(option.value)\"\n >\n {{ option.label }}\n </button>\n </document-engine-select>\n\n <!-- Text Case -->\n <document-engine-select\n *ngIf=\"capabilities.hasTextCase\"\n placeholder=\"Text Case\"\n labelMode=\"static\"\n [disabled]=\"toolbarState.isReadOnly\"\n title=\"Text Case\"\n >\n <document-engine-icon documentEngineSelectLabel name=\"match_case\"></document-engine-icon>\n\n <button\n *ngFor=\"let option of textCaseOptions; trackBy: trackByValue\"\n documentEngineSelectOption\n [value]=\"option.value\"\n (click)=\"runAndEmit('setTextCase', editor.chain().focus().textCase(option.value).run)\"\n >\n {{ option.label }}\n </button>\n </document-engine-select>\n\n <!-- Heading -->\n <document-engine-select\n *ngIf=\"capabilities.hasHeading\"\n [value]=\"activeHeading?.toString() || null\"\n placeholder=\"Normal text\"\n [disabled]=\"toolbarState.isReadOnly\"\n title=\"Heading\"\n >\n <button\n *ngFor=\"let option of headingOptions; trackBy: trackByValue\"\n documentEngineSelectOption\n [value]=\"option.value?.toString() || null\"\n [class]=\"option.class\"\n (click)=\"setHeading(option.value?.toString() || null)\"\n >\n {{ option.label }}\n </button>\n </document-engine-select>\n </div>\n\n <!-- Format Utilities: Reset Formatting & Colors -->\n <div\n *ngIf=\"capabilities.hasFormatUtilities\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"format-utilities\"\n >\n <!-- Reset Formatting -->\n <button\n *ngIf=\"capabilities.hasResetFormat\"\n documentEngineButton\n size=\"icon\"\n (click)=\"runAndEmit('resetFormat', editor.chain().focus().resetFormat().run)\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canResetFormat\"\n title=\"Clear Formatting\"\n >\n <document-engine-icon name=\"format_clear\"></document-engine-icon>\n </button>\n\n <!-- Colors: Text/Background -->\n <button\n *ngIf=\"capabilities.hasColors\"\n documentEngineButton\n size=\"icon\"\n title=\"Text Color\"\n [disabled]=\"toolbarState.isReadOnly\"\n (click)=\"openColorBubble($event, 'text')\"\n >\n <document-engine-icon name=\"format_color_text\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasColors\"\n documentEngineButton\n size=\"icon\"\n title=\"Background Color\"\n [disabled]=\"toolbarState.isReadOnly\"\n (click)=\"openColorBubble($event, 'fill')\"\n >\n <document-engine-icon name=\"format_color_fill\"></document-engine-icon>\n </button>\n </div>\n\n <!-- Text Styles: Bold/Italic/Underline/Strike -->\n <div\n *ngIf=\"capabilities.basicInline\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"basic-inline\"\n >\n <button\n *ngIf=\"capabilities.hasBold\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleBold\"\n (click)=\"runAndEmit('toggleBold', editor.chain().focus().toggleBold().run)\"\n [class.active]=\"editor.isActive('bold')\"\n title=\"Bold\"\n >\n <document-engine-icon name=\"format_bold\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasItalic\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleItalic\"\n (click)=\"runAndEmit('toggleItalic', editor.chain().focus().toggleItalic().run)\"\n [class.active]=\"editor.isActive('italic')\"\n title=\"Italic\"\n >\n <document-engine-icon name=\"format_italic\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasUnderline\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleUnderline\"\n (click)=\"runAndEmit('toggleUnderline', editor.chain().focus().toggleUnderline().run)\"\n [class.active]=\"editor.isActive('underline')\"\n title=\"Underline\"\n >\n <document-engine-icon name=\"format_underline\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasStrike\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleStrike\"\n (click)=\"runAndEmit('toggleStrike', editor.chain().focus().toggleStrike().run)\"\n [class.active]=\"editor.isActive('strike')\"\n title=\"Strikethrough\"\n >\n <document-engine-icon name=\"format_strikethrough\"></document-engine-icon>\n </button>\n </div>\n\n <!-- Text Styles: Sub/Super/Code/Link -->\n <div\n *ngIf=\"capabilities.advancedInline\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"advanced-inline\"\n >\n <button\n *ngIf=\"capabilities.hasSubscript\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleSubscript\"\n (click)=\"runAndEmit('toggleSubscript', editor.chain().focus().toggleSubscript().run)\"\n [class.active]=\"editor.isActive('subscript')\"\n title=\"Subscript\"\n >\n <document-engine-icon name=\"subscript\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasSuperscript\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleSuperscript\"\n (click)=\"runAndEmit('toggleSuperscript', editor.chain().focus().toggleSuperscript().run)\"\n [class.active]=\"editor.isActive('superscript')\"\n title=\"Superscript\"\n >\n <document-engine-icon name=\"superscript\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasCode\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleCode\"\n (click)=\"runAndEmit('toggleCode', editor.chain().focus().toggleCode().run)\"\n [class.active]=\"editor.isActive('code')\"\n title=\"Code\"\n >\n <document-engine-icon name=\"code\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasLink\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleLink\"\n (click)=\"toggleLink()\"\n [class.active]=\"editor.isActive('link')\"\n [title]=\"editor.isActive('link') ? 'Unlink' : 'Link'\"\n >\n <document-engine-icon [name]=\"editor.isActive('link') ? 'link_off' : 'link'\"></document-engine-icon>\n </button>\n </div>\n\n <!-- Alignment & Layout: Text Align, Indent, Lists -->\n <div\n *ngIf=\"capabilities.hasAlignmentLayout\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"alignment-layout\"\n >\n <!-- Text Alignment -->\n <document-engine-select\n *ngIf=\"capabilities.hasTextAlign\"\n [value]=\"activeTextAlign\"\n (valueChange)=\"setTextAlign($event)\"\n placeholder=\"Text Align\"\n labelMode=\"static\"\n [disabled]=\"toolbarState.isReadOnly\"\n title=\"Text Align\"\n >\n <document-engine-icon documentEngineSelectLabel [name]=\"activeTextAlignIcon\"></document-engine-icon>\n\n <button\n *ngFor=\"let option of textAlignOptions; trackBy: trackByValue\"\n documentEngineSelectOption\n [value]=\"option.value\"\n >\n <document-engine-icon [name]=\"option.icon\"></document-engine-icon>\n <span>{{ option.label }}</span>\n </button>\n </document-engine-select>\n\n <!-- Indent -->\n <button\n *ngIf=\"capabilities.hasIndent\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canDecreaseIndent\"\n (click)=\"runAndEmit('decreaseIndent', editor.chain().focus().decreaseIndent().run)\"\n title=\"Decrease Indent\"\n >\n <document-engine-icon name=\"format_indent_decrease\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasIndent\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canIncreaseIndent\"\n (click)=\"runAndEmit('increaseIndent', editor.chain().focus().increaseIndent().run)\"\n title=\"Increase Indent\"\n >\n <document-engine-icon name=\"format_indent_increase\"></document-engine-icon>\n </button>\n\n <!-- Lists: Bullet, Numbered (with style) -->\n <button\n *ngIf=\"capabilities.hasLists\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleBulletList\"\n (click)=\"runAndEmit('toggleBulletList', editor.chain().focus().toggleBulletList().run)\"\n [class.active]=\"editor.isActive('bulletList')\"\n title=\"Bullet List\"\n >\n <document-engine-icon name=\"format_list_bulleted\"></document-engine-icon>\n </button>\n\n <!-- Numbered List with Style -->\n <document-engine-select\n *ngIf=\"capabilities.hasLists\"\n placeholder=\"Numbered List\"\n labelMode=\"static\"\n [value]=\"\n editor.isActive('customOrderedList')\n ? editor.getAttributes('customOrderedList')['data-list-style-type'] || 'decimal'\n : null\n \"\n [disabled]=\"toolbarState.isReadOnly\"\n title=\"Numbered List\"\n >\n <document-engine-icon documentEngineSelectLabel name=\"format_list_numbered\"></document-engine-icon>\n\n <button\n *ngFor=\"let option of listStyleOptions; trackBy: trackByValue\"\n documentEngineSelectOption\n [value]=\"option.value\"\n (click)=\"setListStyle(option.value)\"\n [class.active]=\"\n editor.isActive('customOrderedList') &&\n editor.getAttributes('customOrderedList')['data-list-style-type'] === option.value\n \"\n >\n <span>{{ option.example }}</span>\n <span>{{ option.label }}</span>\n </button>\n </document-engine-select>\n </div>\n\n <!-- Block Types: Code Block, Blockquote -->\n <div\n *ngIf=\"capabilities.hasBlockTypes\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"block-types\"\n >\n <!-- Blockquote -->\n <button\n *ngIf=\"capabilities.hasBlockquote\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleBlockquote\"\n (click)=\"runAndEmit('toggleBlockquote', editor.chain().focus().toggleBlockquote().run)\"\n [class.active]=\"editor.isActive('blockquote')\"\n title=\"Quote\"\n >\n <document-engine-icon name=\"format_quote\"></document-engine-icon>\n </button>\n </div>\n\n <!-- Insert Object: Table, Images, Special Chars, Page Break -->\n <div\n *ngIf=\"capabilities.hasInsertObject\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"insert-object\"\n >\n <button\n *ngIf=\"capabilities.hasTable\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly\"\n (click)=\"openCreateTableBubble($event)\"\n [class.active]=\"editor.isActive('table')\"\n title=\"Table\"\n >\n <document-engine-icon name=\"table_rows\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasImages\"\n documentEngineButton\n size=\"icon\"\n title=\"Insert Image\"\n [disabled]=\"toolbarState.isReadOnly\"\n (click)=\"insertImage($event)\"\n >\n <document-engine-icon name=\"photo\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasSpecialChars\"\n documentEngineButton\n size=\"icon\"\n title=\"Special Character\"\n [disabled]=\"toolbarState.isReadOnly\"\n (click)=\"openSpecialCharsBubble($event)\"\n >\n <document-engine-icon name=\"special_character\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasPageBreak\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canInsertPageBreak\"\n (click)=\"runAndEmit('insertPageBreak', editor.chain().focus().insertPageBreak().run)\"\n title=\"Insert page break\"\n >\n <document-engine-icon name=\"insert_page_break\"></document-engine-icon>\n </button>\n </div>\n\n <!-- Business Features: Templates, Dynamic Fields, Editable Region -->\n <div\n *ngIf=\"capabilities.hasBusinessFeatures\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"business-features\"\n >\n <button\n *ngIf=\"capabilities.hasDynamicFields\"\n documentEngineButton\n size=\"icon\"\n title=\"Dynamic Field\"\n [disabled]=\"toolbarState.isReadOnly\"\n (click)=\"openDynamicFieldsBubble($event)\"\n >\n <document-engine-icon name=\"dynamic_form\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasTemplates\"\n documentEngineButton\n size=\"icon\"\n title=\"Template\"\n [disabled]=\"toolbarState.isReadOnly\"\n (click)=\"openTemplateBubble($event)\"\n >\n <document-engine-icon name=\"description\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasEditableRegion\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"!toolbarState.canToggleEditableRegion || toolbarState.isReadOnly\"\n (click)=\"runAndEmit('toggleEditableRegion', editor.chain().focus().toggleEditableRegion().run)\"\n [class.active]=\"editor.isActive('editableRegion')\"\n title=\"Toggle editable region\"\n >\n <document-engine-icon name=\"restrict_edit\"></document-engine-icon>\n </button>\n </div>\n\n <!-- Actions: Clear Content, Markdown -->\n <div *ngIf=\"capabilities.hasActions\" class=\"document-engine-document-editor__toolbar-group\" data-group=\"actions\">\n <button\n *ngIf=\"capabilities.hasClearContent\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canClearContent\"\n (click)=\"runAndEmit('clear', editor.chain().focus().clear().run)\"\n title=\"Clear all content\"\n >\n <document-engine-icon name=\"clear_all\"></document-engine-icon>\n </button>\n\n <!-- <button\n *ngIf=\"capabilities.hasMarkdown\"\n documentEngineButton\n size=\"icon\"\n (click)=\"downloadAsMarkdown()\"\n title=\"Download as Markdown\"\n >\n <document-engine-icon name=\"markdown\"></document-engine-icon>\n </button> -->\n </div>\n\n <!-- Custom Toolbar Buttons -->\n <div\n *ngIf=\"customToolbarButtons.length > 0\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"custom-buttons\"\n >\n <ng-container *ngFor=\"let button of customToolbarButtons; trackBy: trackByIndex\">\n <!-- Icon Button -->\n <button\n *ngIf=\"button.type === 'icon-button'\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly\"\n (click)=\"handleCustomIconButton(button)\"\n [title]=\"button.label || ''\"\n >\n <document-engine-icon [name]=\"button.icon\"></document-engine-icon>\n </button>\n\n <!-- Select Button -->\n <document-engine-select\n *ngIf=\"button.type === 'select-button'\"\n [value]=\"null\"\n labelMode=\"static\"\n [disabled]=\"toolbarState.isReadOnly\"\n [title]=\"button.label\"\n >\n <span documentEngineSelectLabel>{{ button.label }}</span>\n\n <button\n *ngFor=\"let option of button.options; trackBy: trackByValue\"\n documentEngineSelectOption\n [value]=\"option.value\"\n (click)=\"handleCustomSelectButton(button, option.value)\"\n >\n {{ option.label }}\n </button>\n </document-engine-select>\n </ng-container>\n </div>\n </div>\n\n <!-- Right side actions -->\n <div class=\"document-engine-document-editor__toolbar-right\">\n <button\n *ngIf=\"capabilities.hasPrintButton\"\n documentEngineButton\n size=\"icon\"\n (click)=\"printValue()\"\n title=\"Print Value\"\n >\n <document-engine-icon name=\"print\"></document-engine-icon>\n </button>\n </div>\n </div>\n\n <!-- Color Bubble Menu -->\n <document-engine-toolbar-bubble-menu\n #colorBubbleMenu\n [editor]=\"editor\"\n [config]=\"colorBubbleConfig\"\n ></document-engine-toolbar-bubble-menu>\n\n <!-- Link Bubble Menu -->\n <document-engine-editor-bubble-menu\n *ngIf=\"capabilities.hasLink\"\n #linkBubbleWrapper\n [editor]=\"editor\"\n [config]=\"linkBubbleConfig\"\n ></document-engine-editor-bubble-menu>\n\n <!-- Table Bubble Menu -->\n <document-engine-editor-bubble-menu\n *ngIf=\"capabilities.hasTable\"\n #tableBubbleWrapper\n [editor]=\"editor\"\n [config]=\"tableBubbleConfig\"\n ></document-engine-editor-bubble-menu>\n\n <!-- Image Bubble Menu -->\n <document-engine-toolbar-bubble-menu\n *ngIf=\"capabilities.hasImages\"\n #imageBubbleMenu\n [editor]=\"editor\"\n [config]=\"imageBubbleConfig\"\n ></document-engine-toolbar-bubble-menu>\n\n <!-- Special Characters Bubble Menu -->\n <document-engine-toolbar-bubble-menu\n *ngIf=\"capabilities.hasSpecialChars\"\n #specialCharsBubbleMenu\n [editor]=\"editor\"\n [config]=\"specialCharsBubbleConfig\"\n ></document-engine-toolbar-bubble-menu>\n\n <!-- Dynamic Fields Bubble Menu -->\n <document-engine-toolbar-bubble-menu\n *ngIf=\"capabilities.hasDynamicFields\"\n #dynamicFieldsBubbleMenu\n [editor]=\"editor\"\n [config]=\"dynamicFieldsBubbleConfig\"\n ></document-engine-toolbar-bubble-menu>\n\n <!-- Template Bubble Menu -->\n <document-engine-toolbar-bubble-menu\n *ngIf=\"capabilities.hasTemplates\"\n #templateBubbleMenu\n [editor]=\"editor\"\n [config]=\"templateBubbleConfig\"\n ></document-engine-toolbar-bubble-menu>\n\n <!-- Table Create Bubble Menu -->\n <document-engine-toolbar-bubble-menu\n *ngIf=\"capabilities.hasTable\"\n #tableCreateBubbleMenu\n [editor]=\"editor\"\n [config]=\"tableCreateBubbleConfig\"\n ></document-engine-toolbar-bubble-menu>\n</div>\n", styles: [":host{position:sticky;top:0;z-index:20}\n"] }]
5785
- }], propDecorators: { editor: [{
5786
- type: Input
5787
- }], capabilities: [{
5788
- type: Input
5789
- }], config: [{
5790
- type: Input
5791
- }], action: [{
5792
- type: Output
5793
- }], colorBubbleMenu: [{
5794
- type: ViewChild,
5795
- args: ['colorBubbleMenu']
5796
- }], linkBubbleWrapper: [{
5797
- type: ViewChild,
5798
- args: ['linkBubbleWrapper']
5799
- }], imageBubbleMenu: [{
5800
- type: ViewChild,
5801
- args: ['imageBubbleMenu']
5802
- }], specialCharsBubbleMenu: [{
5803
- type: ViewChild,
5804
- args: ['specialCharsBubbleMenu']
5805
- }], dynamicFieldsBubbleMenu: [{
5806
- type: ViewChild,
5807
- args: ['dynamicFieldsBubbleMenu']
5808
- }], templateBubbleMenu: [{
5809
- type: ViewChild,
5810
- args: ['templateBubbleMenu']
5811
- }], tableCreateBubbleMenu: [{
5812
- type: ViewChild,
5813
- args: ['tableCreateBubbleMenu']
5814
- }] } });
5815
-
5816
- /**
5817
- * Character Count component for document editor footer
5818
- * Displays character/word count with optional limit and color coding
5819
- */
5820
- class CharacterCountComponent {
5821
- cdr = inject(ChangeDetectorRef);
5822
- editor;
5823
- config;
5824
- mode = 'characters';
5825
- count = 0;
5826
- limit = null;
5827
- updateCount = () => {
5828
- if (!this.editor) {
5829
- return;
5830
- }
5831
- // Get count from editor storage if available
5832
- const characterCountStorage = this.editor.storage.characterCount;
5833
- if (characterCountStorage) {
5834
- if (this.mode === 'characters') {
5835
- this.count = characterCountStorage.characters?.() || 0;
5836
- }
5837
- else {
5838
- this.count = characterCountStorage.words?.() || 0;
5839
- }
5918
+ this.toolbarService.emit('setFontSize', size);
5919
+ }
5920
+ setLineHeight(lineHeight) {
5921
+ if (lineHeight) {
5922
+ this.editor.chain().focus().setLineHeight(lineHeight).run();
5840
5923
  }
5841
5924
  else {
5842
- // Fallback: calculate from text
5843
- const text = this.editor.getText();
5844
- if (this.mode === 'characters') {
5845
- this.count = text.length;
5846
- }
5847
- else {
5848
- this.count = text.trim() ? text.trim().split(/\s+/).length : 0;
5849
- }
5925
+ this.editor.chain().focus().unsetLineHeight().run();
5850
5926
  }
5851
- // Get limit from config
5852
- const characterCountConfig = this.config?.characterCount;
5853
- if (characterCountConfig && typeof characterCountConfig === 'object') {
5854
- this.limit = characterCountConfig.limit || null;
5927
+ this.toolbarService.emit('setLineHeight', lineHeight);
5928
+ }
5929
+ setHeading(level) {
5930
+ if (!level) {
5931
+ this.editor.chain().focus().removeHeading().run();
5855
5932
  }
5856
5933
  else {
5857
- this.limit = null;
5858
- }
5859
- this.cdr.markForCheck();
5860
- };
5861
- ngOnInit() {
5862
- if (!this.editor) {
5863
- return;
5934
+ const levelNumber = parseInt(level, 10);
5935
+ this.editor.chain().focus().toggleHeading({ level: levelNumber }).run();
5864
5936
  }
5865
- // Subscribe to update events to keep count in sync
5866
- this.editor.on('update', this.updateCount);
5867
- this.editor.on('transaction', this.updateCount);
5868
- // Initialize count once
5869
- this.updateCount();
5937
+ this.toolbarService.emit('setHeading', level);
5870
5938
  }
5871
- ngOnDestroy() {
5872
- if (this.editor) {
5873
- this.editor.off('update', this.updateCount);
5874
- this.editor.off('transaction', this.updateCount);
5939
+ setTextAlign(align) {
5940
+ if (align) {
5941
+ this.editor
5942
+ .chain()
5943
+ .focus()
5944
+ .setTextAlign(align)
5945
+ .run();
5875
5946
  }
5876
- }
5877
- isNearLimit() {
5878
- if (!this.limit) {
5879
- return false;
5947
+ else {
5948
+ // Reset to default (left)
5949
+ this.editor.chain().focus().setTextAlign('left').run();
5880
5950
  }
5881
- // Consider near limit when >= 90% of limit
5882
- return this.count >= this.limit * 0.9 && this.count < this.limit;
5951
+ this.toolbarService.emit('setTextAlign', align);
5883
5952
  }
5884
- isOverLimit() {
5885
- if (!this.limit) {
5886
- return false;
5953
+ setListStyle(listStyleType) {
5954
+ if (this.editor.isActive('customOrderedList')) {
5955
+ // If there's an active ordered list, update its style
5956
+ this.editor.chain().focus().setListStyle(listStyleType).run();
5887
5957
  }
5888
- return this.count > this.limit;
5889
- }
5890
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: CharacterCountComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5891
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.10", type: CharacterCountComponent, isStandalone: true, selector: "document-engine-character-count", inputs: { editor: "editor", config: "config", mode: "mode" }, ngImport: i0, template: "<div class=\"document-engine-character-count\">\n <span\n class=\"document-engine-character-count__text\"\n [class.document-engine-character-count__text--muted]=\"!isNearLimit() && !isOverLimit()\"\n [class.document-engine-character-count__text--warning]=\"isNearLimit() && !isOverLimit()\"\n [class.document-engine-character-count__text--error]=\"isOverLimit()\"\n >\n <ng-container *ngIf=\"mode === 'characters'\">\n <ng-container *ngIf=\"limit; else noLimit\"> {{ count }} / {{ limit }} characters. </ng-container>\n <ng-template #noLimit> {{ count }} {{ count === 1 ? 'character' : 'characters' }}. </ng-template>\n </ng-container>\n <ng-container *ngIf=\"mode === 'words'\"> {{ count }} {{ count === 1 ? 'word' : 'words' }}. </ng-container>\n </span>\n</div>\n", styles: [".document-engine-character-count{display:flex;justify-content:flex-end}.document-engine-character-count__text{font-size:.875rem;transition:color .2s ease-in-out}.document-engine-character-count__text--muted{color:var(--muted-foreground, #6b7280)}.document-engine-character-count__text--warning{color:var(--warning, #d97706)}.document-engine-character-count__text--error{color:var(--destructive, #dc2626)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5892
- }
5893
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: CharacterCountComponent, decorators: [{
5894
- type: Component,
5895
- args: [{ selector: 'document-engine-character-count', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"document-engine-character-count\">\n <span\n class=\"document-engine-character-count__text\"\n [class.document-engine-character-count__text--muted]=\"!isNearLimit() && !isOverLimit()\"\n [class.document-engine-character-count__text--warning]=\"isNearLimit() && !isOverLimit()\"\n [class.document-engine-character-count__text--error]=\"isOverLimit()\"\n >\n <ng-container *ngIf=\"mode === 'characters'\">\n <ng-container *ngIf=\"limit; else noLimit\"> {{ count }} / {{ limit }} characters. </ng-container>\n <ng-template #noLimit> {{ count }} {{ count === 1 ? 'character' : 'characters' }}. </ng-template>\n </ng-container>\n <ng-container *ngIf=\"mode === 'words'\"> {{ count }} {{ count === 1 ? 'word' : 'words' }}. </ng-container>\n </span>\n</div>\n", styles: [".document-engine-character-count{display:flex;justify-content:flex-end}.document-engine-character-count__text{font-size:.875rem;transition:color .2s ease-in-out}.document-engine-character-count__text--muted{color:var(--muted-foreground, #6b7280)}.document-engine-character-count__text--warning{color:var(--warning, #d97706)}.document-engine-character-count__text--error{color:var(--destructive, #dc2626)}\n"] }]
5896
- }], propDecorators: { editor: [{
5897
- type: Input
5898
- }], config: [{
5899
- type: Input
5900
- }], mode: [{
5901
- type: Input
5902
- }] } });
5903
-
5904
- /**
5905
- * Footer component for document editor
5906
- * Container for footer components like character count
5907
- */
5908
- class FooterComponent {
5909
- editor;
5910
- config;
5911
- // @Input() editable = true;
5912
- hasCharacterCount() {
5913
- const config = this.config ?? {};
5914
- return config.characterCount !== false;
5915
- }
5916
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: FooterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5917
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.10", type: FooterComponent, isStandalone: true, selector: "document-engine-footer", inputs: { editor: "editor", config: "config" }, ngImport: i0, template: "<div class=\"document-engine-document-editor__footer\">\n <div class=\"document-engine-document-editor__footer-content\">\n <div class=\"document-engine-document-editor__footer-left\">\n <!-- Additional footer components can be added here -->\n </div>\n\n <div class=\"document-engine-document-editor__footer-right\">\n <document-engine-character-count\n *ngIf=\"hasCharacterCount()\"\n [editor]=\"editor\"\n [config]=\"config\"\n mode=\"characters\"\n ></document-engine-character-count>\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: CharacterCountComponent, selector: "document-engine-character-count", inputs: ["editor", "config", "mode"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5918
- }
5919
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: FooterComponent, decorators: [{
5920
- type: Component,
5921
- args: [{ selector: 'document-engine-footer', standalone: true, imports: [CommonModule, CharacterCountComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"document-engine-document-editor__footer\">\n <div class=\"document-engine-document-editor__footer-content\">\n <div class=\"document-engine-document-editor__footer-left\">\n <!-- Additional footer components can be added here -->\n </div>\n\n <div class=\"document-engine-document-editor__footer-right\">\n <document-engine-character-count\n *ngIf=\"hasCharacterCount()\"\n [editor]=\"editor\"\n [config]=\"config\"\n mode=\"characters\"\n ></document-engine-character-count>\n </div>\n </div>\n</div>\n" }]
5922
- }], propDecorators: { editor: [{
5923
- type: Input
5924
- }], config: [{
5925
- type: Input
5926
- }] } });
5927
-
5928
- /**
5929
- * Document Editor Component
5930
- * High-level wrapper that encapsulates TiptapEditor and DocumentEngineKit
5931
- * Provides a simple API for consumers without exposing internal dependencies
5932
- *
5933
- * Usage:
5934
- * <document-engine-editor
5935
- * #editor
5936
- * [config]="editorConfig"
5937
- *
5938
- * (editorReady)="editor = docEditor.editor"
5939
- * >
5940
- * <tiptap-editor
5941
- * [editor]="docEditor.editor"
5942
- * [(ngModel)]="content"
5943
- * ></tiptap-editor>
5944
- * </document-engine-editor>
5945
- */
5946
- class DocumentEditorComponent {
5947
- ngZone = inject(NgZone);
5948
- config;
5949
- editorReady = new EventEmitter();
5950
- // Expose editor property for external access (re-export)
5951
- editor;
5952
- // Store merged config for toolbar and helper methods
5953
- get mergedConfig() {
5954
- return this._mergedConfig ?? this.getMergedConfig();
5955
- }
5956
- _mergedConfig;
5957
- // Capabilities instance for toolbar
5958
- capabilities;
5959
- updateCapabilities() {
5960
- this.capabilities = new EditorCapabilities(this.mergedConfig);
5961
- }
5962
- ngOnChanges(changes) {
5963
- if (!changes['config'])
5964
- return;
5965
- if (!this.editor)
5966
- return;
5967
- this._mergedConfig = this.getMergedConfig();
5968
- this.updateCapabilities();
5969
- this.editor.setEditable(this._mergedConfig.editable ?? true);
5970
- }
5971
- ngOnInit() {
5972
- this.initializeEditor();
5958
+ else {
5959
+ // If no active list, create a new one with the selected style
5960
+ this.editor.chain().focus().toggleOrderedList().run();
5961
+ this.editor.chain().focus().setListStyle(listStyleType).run();
5962
+ }
5963
+ this.toolbarService.emit('setListStyle', listStyleType);
5973
5964
  }
5974
- ngOnDestroy() {
5975
- if (this.editor)
5976
- this.editor.destroy();
5965
+ openColorBubble(event, type) {
5966
+ const targetElement = (event.currentTarget || event.target);
5967
+ this.colorBubbleMenu?.toggleBubble(targetElement, { type });
5968
+ this.toolbarService.emit('openColorBubble', { type });
5977
5969
  }
5978
- // Internal methods
5979
- initializeEditor() {
5980
- // Run editor initialization outside Angular zone for better performance
5981
- // Editor operations (especially typing) don't need to trigger Angular change detection
5982
- this.ngZone.runOutsideAngular(() => {
5983
- // Merge user config with default full-featured config
5984
- this._mergedConfig = this.getMergedConfig();
5985
- // Initialize editor with config
5986
- // Initial content will be set by TiptapEditorDirective via writeValue
5987
- this.editor = new Editor({
5988
- editable: this._mergedConfig.editable,
5989
- extensions: [DocumentEngineKit.configure(this._mergedConfig)],
5990
- });
5991
- // Emit editorReady event back inside Angular zone
5992
- this.ngZone.run(() => {
5993
- // Create capabilities instance after editor is initialized
5994
- this.updateCapabilities();
5995
- this.editorReady.emit(this.editor);
5996
- });
5997
- });
5970
+ toggleLink() {
5971
+ if (this.editor.isActive('link')) {
5972
+ // If link is active, remove it
5973
+ this.editor.chain().focus().unsetLink().run();
5974
+ }
5975
+ else {
5976
+ // If no link, open the bubble menu
5977
+ this.linkBubbleWrapper?.showBubble();
5978
+ }
5979
+ this.toolbarService.emit('toggleLink');
5998
5980
  }
5999
- /**
6000
- * Get merged config with all extensions enabled by default
6001
- * User config will override defaults
6002
- */
6003
- getMergedConfig() {
6004
- // Merge user config with defaults, preferring user config when provided
6005
- return this.mergeConfig(DefaultEditorConfig, this.config);
5981
+ insertImage(event) {
5982
+ const targetElement = (event.currentTarget || event.target);
5983
+ this.imageBubbleMenu?.toggleBubble(targetElement);
5984
+ this.toolbarService.emit('insertImage');
6006
5985
  }
6007
- /**
6008
- * Merge two config objects, preferring userConfig values when provided
6009
- */
6010
- mergeConfig(defaultConfig, userConfig) {
6011
- if (!userConfig)
6012
- return defaultConfig;
6013
- const merged = { ...defaultConfig };
6014
- const keys = Object.keys(userConfig);
6015
- for (const key of keys) {
6016
- const value = userConfig[key];
6017
- if (value !== undefined)
6018
- merged[key] = value;
6019
- }
6020
- return merged;
5986
+ openSpecialCharsBubble(event) {
5987
+ const targetElement = (event.currentTarget || event.target);
5988
+ this.specialCharsBubbleMenu?.toggleBubble(targetElement);
5989
+ this.toolbarService.emit('openSpecialCharsBubble');
6021
5990
  }
6022
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: DocumentEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6023
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.10", type: DocumentEditorComponent, isStandalone: false, selector: "document-engine-editor", inputs: { config: "config" }, outputs: { editorReady: "editorReady" }, host: { classAttribute: "no-twp" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"document-engine-document-editor\">\n <document-engine-toolbar\n *ngIf=\"mergedConfig.showToolbar\"\n [editor]=\"editor\"\n [capabilities]=\"capabilities\"\n [config]=\"mergedConfig\"\n ></document-engine-toolbar>\n\n <div class=\"document-engine-document-editor__content\">\n <ng-content></ng-content>\n </div>\n\n <document-engine-footer\n *ngIf=\"editor && mergedConfig.showFooter\"\n [editor]=\"editor\"\n [config]=\"mergedConfig\"\n ></document-engine-footer>\n</div>\n", styles: [":is() *,:is() *:before,:is() *:after{box-sizing:border-box}\n"], dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: ToolbarComponent, selector: "document-engine-toolbar", inputs: ["editor", "capabilities", "config"], outputs: ["action"] }, { kind: "component", type: FooterComponent, selector: "document-engine-footer", inputs: ["editor", "config"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6024
- }
6025
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: DocumentEditorComponent, decorators: [{
6026
- type: Component,
6027
- args: [{ selector: 'document-engine-editor', standalone: false, changeDetection: ChangeDetectionStrategy.OnPush, host: {
6028
- class: 'no-twp',
6029
- }, template: "<div class=\"document-engine-document-editor\">\n <document-engine-toolbar\n *ngIf=\"mergedConfig.showToolbar\"\n [editor]=\"editor\"\n [capabilities]=\"capabilities\"\n [config]=\"mergedConfig\"\n ></document-engine-toolbar>\n\n <div class=\"document-engine-document-editor__content\">\n <ng-content></ng-content>\n </div>\n\n <document-engine-footer\n *ngIf=\"editor && mergedConfig.showFooter\"\n [editor]=\"editor\"\n [config]=\"mergedConfig\"\n ></document-engine-footer>\n</div>\n", styles: [":is() *,:is() *:before,:is() *:after{box-sizing:border-box}\n"] }]
6030
- }], propDecorators: { config: [{
6031
- type: Input
6032
- }], editorReady: [{
6033
- type: Output
6034
- }] } });
6035
-
6036
- class DocumentEditorModule {
6037
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: DocumentEditorModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
6038
- static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.10", ngImport: i0, type: DocumentEditorModule, declarations: [DocumentEditorComponent], imports: [CommonModule, ToolbarComponent, FooterComponent], exports: [DocumentEditorComponent] });
6039
- static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: DocumentEditorModule, imports: [CommonModule, ToolbarComponent, FooterComponent] });
6040
- }
6041
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: DocumentEditorModule, decorators: [{
6042
- type: NgModule,
6043
- args: [{
6044
- declarations: [DocumentEditorComponent],
6045
- imports: [CommonModule, ToolbarComponent, FooterComponent],
6046
- exports: [DocumentEditorComponent],
6047
- }]
6048
- }] });
6049
-
6050
- class AngularRenderer {
6051
- applicationRef;
6052
- componentRef;
6053
- constructor(ViewComponent, injector, props) {
6054
- this.applicationRef = injector.get(ApplicationRef);
6055
- this.componentRef = createComponent(ViewComponent, {
6056
- environmentInjector: this.applicationRef.injector,
6057
- elementInjector: injector,
5991
+ openDynamicFieldsBubble(event) {
5992
+ const targetElement = (event.currentTarget || event.target);
5993
+ this.dynamicFieldsBubbleMenu?.toggleBubble(targetElement, {
5994
+ categories: this.config?.dynamicFieldsCategories || [],
6058
5995
  });
6059
- // set input props to the component
6060
- this.updateProps(props);
6061
- this.applicationRef.attachView(this.componentRef.hostView);
5996
+ this.toolbarService.emit('openDynamicFieldsBubble');
6062
5997
  }
6063
- get instance() {
6064
- return this.componentRef.instance;
5998
+ openTemplateBubble(event) {
5999
+ const targetElement = (event.currentTarget || event.target);
6000
+ this.templateBubbleMenu?.toggleBubble(targetElement, {
6001
+ templates: this.config?.templates || [],
6002
+ });
6003
+ this.toolbarService.emit('openTemplateBubble');
6065
6004
  }
6066
- get elementRef() {
6067
- return this.componentRef.injector.get(ElementRef);
6005
+ openCreateTableBubble(event) {
6006
+ const targetElement = (event.currentTarget || event.target);
6007
+ this.tableCreateBubbleMenu?.toggleBubble(targetElement);
6008
+ this.toolbarService.emit('openCreateTableBubble');
6068
6009
  }
6069
- get dom() {
6070
- return this.elementRef.nativeElement;
6010
+ // downloadAsMarkdown(filename?: string): void {
6011
+ // const fileName = filename ? `${filename}.md` : 'export.md';
6012
+ // const markdown = renderToMarkdown({
6013
+ // content: this.editor.getJSON(),
6014
+ // extensions: this.editor.options.extensions,
6015
+ // options: {
6016
+ // nodeMapping: {
6017
+ // pageBreak: () => 'Page break',
6018
+ // },
6019
+ // },
6020
+ // });
6021
+ // const blob = new Blob([markdown], { type: 'text/markdown' });
6022
+ // const url = URL.createObjectURL(blob);
6023
+ // const a = document.createElement('a');
6024
+ // a.href = url;
6025
+ // a.download = fileName;
6026
+ // a.click();
6027
+ // URL.revokeObjectURL(url);
6028
+ // }
6029
+ trackByValue(index, item) {
6030
+ return item.value;
6071
6031
  }
6072
- updateProps(props) {
6073
- Object.entries(props).forEach(([key, value]) => {
6074
- this.componentRef.setInput(key, value);
6075
- });
6032
+ trackByIndex(index) {
6033
+ return index;
6076
6034
  }
6077
- updateAttributes(attributes) {
6078
- Object.keys(attributes).forEach((key) => {
6079
- this.dom.setAttribute(key, attributes[key]);
6080
- });
6035
+ get customToolbarButtons() {
6036
+ return this.config?.customToolbarButtons || [];
6081
6037
  }
6082
- detectChanges() {
6083
- this.componentRef.changeDetectorRef.detectChanges();
6038
+ handleCustomIconButton(button) {
6039
+ if (button.type === 'icon-button') {
6040
+ button.callback();
6041
+ this.toolbarService.emit('customIconButton', { label: button.label, icon: button.icon });
6042
+ }
6084
6043
  }
6085
- destroy() {
6086
- this.componentRef.destroy();
6087
- this.applicationRef.detachView(this.componentRef.hostView);
6044
+ handleCustomSelectButton(button, value) {
6045
+ if (button.type === 'select-button') {
6046
+ button.callback();
6047
+ this.toolbarService.emit('customSelectButton', { label: button.label, value });
6048
+ }
6088
6049
  }
6050
+ printValue() {
6051
+ console.log('html', this.editor.getHTML());
6052
+ console.log('json', this.editor.getJSON());
6053
+ console.log('State', this.editor.state);
6054
+ this.toolbarService.emit('printValue');
6055
+ }
6056
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: ToolbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6057
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.10", type: ToolbarComponent, isStandalone: true, selector: "document-engine-toolbar", inputs: { editor: "editor", capabilities: "capabilities", config: "config" }, outputs: { action: "action" }, viewQueries: [{ propertyName: "colorBubbleMenu", first: true, predicate: ["colorBubbleMenu"], descendants: true }, { propertyName: "linkBubbleWrapper", first: true, predicate: ["linkBubbleWrapper"], descendants: true }, { propertyName: "imageBubbleMenu", first: true, predicate: ["imageBubbleMenu"], descendants: true }, { propertyName: "specialCharsBubbleMenu", first: true, predicate: ["specialCharsBubbleMenu"], descendants: true }, { propertyName: "dynamicFieldsBubbleMenu", first: true, predicate: ["dynamicFieldsBubbleMenu"], descendants: true }, { propertyName: "templateBubbleMenu", first: true, predicate: ["templateBubbleMenu"], descendants: true }, { propertyName: "tableCreateBubbleMenu", first: true, predicate: ["tableCreateBubbleMenu"], descendants: true }], ngImport: i0, template: "<div class=\"document-engine-document-editor__toolbar\">\n <div class=\"document-engine-document-editor__toolbar-content\">\n <div class=\"document-engine-document-editor__toolbar-left\">\n <!-- History: Undo/Redo -->\n <div *ngIf=\"capabilities.hasHistory\" class=\"document-engine-document-editor__toolbar-group\" data-group=\"history\">\n <button\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canUndo\"\n (click)=\"runAndEmit('undo', editor.chain().focus().undo().run)\"\n title=\"Undo (Ctrl+Z)\"\n >\n <document-engine-icon name=\"undo\"></document-engine-icon>\n </button>\n\n <button\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canRedo\"\n (click)=\"runAndEmit('redo', editor.chain().focus().redo().run)\"\n title=\"Redo\"\n >\n <document-engine-icon name=\"redo\"></document-engine-icon>\n </button>\n </div>\n\n <!-- Text Formatting: Font Size, Line Height, Text Case, Heading -->\n <div\n *ngIf=\"capabilities.hasTextProperty\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"text-property\"\n >\n <!-- Font Size -->\n <document-engine-select\n *ngIf=\"capabilities.hasFontSize\"\n [value]=\"activeFontSize\"\n placeholder=\"Font Size\"\n labelMode=\"static\"\n [disabled]=\"toolbarState.isReadOnly\"\n title=\"Font Size\"\n >\n <document-engine-icon documentEngineSelectLabel name=\"format_size\"></document-engine-icon>\n\n <button documentEngineSelectOption [value]=\"null\">Default</button>\n\n <button\n *ngFor=\"let option of fontSizeOptions; trackBy: trackByValue\"\n documentEngineSelectOption\n [value]=\"option.value\"\n (click)=\"setFontSize(option.value)\"\n >\n {{ option.label }}\n </button>\n </document-engine-select>\n\n <!-- Line Height -->\n <document-engine-select\n *ngIf=\"capabilities.hasLineHeight\"\n [value]=\"activeLineHeight\"\n placeholder=\"Line Height\"\n labelMode=\"static\"\n [disabled]=\"toolbarState.isReadOnly\"\n title=\"Line Height\"\n >\n <document-engine-icon documentEngineSelectLabel name=\"format_line_spacing\"></document-engine-icon>\n\n <button\n *ngFor=\"let option of lineHeightOptions; trackBy: trackByValue\"\n documentEngineSelectOption\n [value]=\"option.value\"\n (click)=\"setLineHeight(option.value)\"\n >\n {{ option.label }}\n </button>\n </document-engine-select>\n\n <!-- Text Case -->\n <document-engine-select\n *ngIf=\"capabilities.hasTextCase\"\n placeholder=\"Text Case\"\n labelMode=\"static\"\n [disabled]=\"toolbarState.isReadOnly\"\n title=\"Text Case\"\n >\n <document-engine-icon documentEngineSelectLabel name=\"match_case\"></document-engine-icon>\n\n <button\n *ngFor=\"let option of textCaseOptions; trackBy: trackByValue\"\n documentEngineSelectOption\n [value]=\"option.value\"\n (click)=\"runAndEmit('setTextCase', editor.chain().focus().textCase(option.value).run)\"\n >\n {{ option.label }}\n </button>\n </document-engine-select>\n\n <!-- Heading -->\n <document-engine-select\n *ngIf=\"capabilities.hasHeading\"\n [value]=\"activeHeading?.toString() || null\"\n placeholder=\"Normal text\"\n [disabled]=\"toolbarState.isReadOnly\"\n title=\"Heading\"\n >\n <button\n *ngFor=\"let option of headingOptions; trackBy: trackByValue\"\n documentEngineSelectOption\n [value]=\"option.value?.toString() || null\"\n [class]=\"option.class\"\n (click)=\"setHeading(option.value?.toString() || null)\"\n >\n {{ option.label }}\n </button>\n </document-engine-select>\n </div>\n\n <!-- Format Utilities: Reset Formatting & Colors -->\n <div\n *ngIf=\"capabilities.hasFormatUtilities\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"format-utilities\"\n >\n <!-- Reset Formatting -->\n <button\n *ngIf=\"capabilities.hasResetFormat\"\n documentEngineButton\n size=\"icon\"\n (click)=\"runAndEmit('resetFormat', editor.chain().focus().resetFormat().run)\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canResetFormat\"\n title=\"Clear Formatting\"\n >\n <document-engine-icon name=\"format_clear\"></document-engine-icon>\n </button>\n\n <!-- Colors: Text/Background -->\n <button\n *ngIf=\"capabilities.hasColors\"\n documentEngineButton\n size=\"icon\"\n title=\"Text Color\"\n [disabled]=\"toolbarState.isReadOnly\"\n (click)=\"openColorBubble($event, 'text')\"\n >\n <document-engine-icon name=\"format_color_text\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasColors\"\n documentEngineButton\n size=\"icon\"\n title=\"Background Color\"\n [disabled]=\"toolbarState.isReadOnly\"\n (click)=\"openColorBubble($event, 'fill')\"\n >\n <document-engine-icon name=\"format_color_fill\"></document-engine-icon>\n </button>\n </div>\n\n <!-- Text Styles: Bold/Italic/Underline/Strike -->\n <div\n *ngIf=\"capabilities.basicInline\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"basic-inline\"\n >\n <button\n *ngIf=\"capabilities.hasBold\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleBold\"\n (click)=\"runAndEmit('toggleBold', editor.chain().focus().toggleBold().run)\"\n [class.active]=\"editor.isActive('bold')\"\n title=\"Bold\"\n >\n <document-engine-icon name=\"format_bold\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasItalic\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleItalic\"\n (click)=\"runAndEmit('toggleItalic', editor.chain().focus().toggleItalic().run)\"\n [class.active]=\"editor.isActive('italic')\"\n title=\"Italic\"\n >\n <document-engine-icon name=\"format_italic\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasUnderline\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleUnderline\"\n (click)=\"runAndEmit('toggleUnderline', editor.chain().focus().toggleUnderline().run)\"\n [class.active]=\"editor.isActive('underline')\"\n title=\"Underline\"\n >\n <document-engine-icon name=\"format_underline\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasStrike\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleStrike\"\n (click)=\"runAndEmit('toggleStrike', editor.chain().focus().toggleStrike().run)\"\n [class.active]=\"editor.isActive('strike')\"\n title=\"Strikethrough\"\n >\n <document-engine-icon name=\"format_strikethrough\"></document-engine-icon>\n </button>\n </div>\n\n <!-- Text Styles: Sub/Super/Code/Link -->\n <div\n *ngIf=\"capabilities.advancedInline\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"advanced-inline\"\n >\n <button\n *ngIf=\"capabilities.hasSubscript\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleSubscript\"\n (click)=\"runAndEmit('toggleSubscript', editor.chain().focus().toggleSubscript().run)\"\n [class.active]=\"editor.isActive('subscript')\"\n title=\"Subscript\"\n >\n <document-engine-icon name=\"subscript\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasSuperscript\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleSuperscript\"\n (click)=\"runAndEmit('toggleSuperscript', editor.chain().focus().toggleSuperscript().run)\"\n [class.active]=\"editor.isActive('superscript')\"\n title=\"Superscript\"\n >\n <document-engine-icon name=\"superscript\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasCode\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleCode\"\n (click)=\"runAndEmit('toggleCode', editor.chain().focus().toggleCode().run)\"\n [class.active]=\"editor.isActive('code')\"\n title=\"Code\"\n >\n <document-engine-icon name=\"code\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasLink\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleLink\"\n (click)=\"toggleLink()\"\n [class.active]=\"editor.isActive('link')\"\n [title]=\"editor.isActive('link') ? 'Unlink' : 'Link'\"\n >\n <document-engine-icon [name]=\"editor.isActive('link') ? 'link_off' : 'link'\"></document-engine-icon>\n </button>\n </div>\n\n <!-- Alignment & Layout: Text Align, Indent -->\n <div\n *ngIf=\"capabilities.hasAlignmentLayout\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"alignment-layout\"\n >\n <!-- Text Alignment -->\n <document-engine-select\n *ngIf=\"capabilities.hasTextAlign\"\n [value]=\"activeTextAlign\"\n (valueChange)=\"setTextAlign($event)\"\n placeholder=\"Text Align\"\n labelMode=\"static\"\n [disabled]=\"toolbarState.isReadOnly\"\n title=\"Text Align\"\n >\n <document-engine-icon documentEngineSelectLabel [name]=\"activeTextAlignIcon\"></document-engine-icon>\n\n <button\n *ngFor=\"let option of textAlignOptions; trackBy: trackByValue\"\n documentEngineSelectOption\n [value]=\"option.value\"\n >\n <document-engine-icon [name]=\"option.icon\"></document-engine-icon>\n <span>{{ option.label }}</span>\n </button>\n </document-engine-select>\n\n <!-- Indent -->\n <button\n *ngIf=\"capabilities.hasIndent\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canDecreaseIndent\"\n (click)=\"runAndEmit('decreaseIndent', editor.chain().focus().decreaseIndent().run)\"\n title=\"Decrease Indent\"\n >\n <document-engine-icon name=\"format_indent_decrease\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasIndent\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canIncreaseIndent\"\n (click)=\"runAndEmit('increaseIndent', editor.chain().focus().increaseIndent().run)\"\n title=\"Increase Indent\"\n >\n <document-engine-icon name=\"format_indent_increase\"></document-engine-icon>\n </button>\n </div>\n\n <!-- Lists -->\n <div\n *ngIf=\"capabilities.hasAlignmentLayout\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"alignment-layout\"\n >\n <!-- Lists: Bullet, Numbered (with style) -->\n <button\n *ngIf=\"capabilities.hasLists\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleBulletList\"\n (click)=\"runAndEmit('toggleBulletList', editor.chain().focus().toggleBulletList().run)\"\n [class.active]=\"editor.isActive('bulletList')\"\n title=\"Bullet List\"\n >\n <document-engine-icon name=\"format_list_bulleted\"></document-engine-icon>\n </button>\n\n <!-- Numbered List with Style -->\n <document-engine-select\n *ngIf=\"capabilities.hasLists\"\n placeholder=\"Numbered List\"\n labelMode=\"static\"\n [value]=\"\n editor.isActive('customOrderedList')\n ? editor.getAttributes('customOrderedList')['data-list-style-type'] || 'decimal'\n : null\n \"\n [disabled]=\"toolbarState.isReadOnly\"\n title=\"Numbered List\"\n >\n <document-engine-icon documentEngineSelectLabel name=\"format_list_numbered\"></document-engine-icon>\n\n <button\n *ngFor=\"let option of listStyleOptions; trackBy: trackByValue\"\n documentEngineSelectOption\n [value]=\"option.value\"\n (click)=\"setListStyle(option.value)\"\n [class.active]=\"\n editor.isActive('customOrderedList') &&\n editor.getAttributes('customOrderedList')['data-list-style-type'] === option.value\n \"\n >\n <span>{{ option.example }}</span>\n <span>{{ option.label }}</span>\n </button>\n </document-engine-select>\n </div>\n\n <!-- Block Types: Code Block, Blockquote -->\n <div\n *ngIf=\"capabilities.hasBlockTypes\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"block-types\"\n >\n <!-- Blockquote -->\n <button\n *ngIf=\"capabilities.hasBlockquote\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleBlockquote\"\n (click)=\"runAndEmit('toggleBlockquote', editor.chain().focus().toggleBlockquote().run)\"\n [class.active]=\"editor.isActive('blockquote')\"\n title=\"Quote\"\n >\n <document-engine-icon name=\"format_quote\"></document-engine-icon>\n </button>\n </div>\n\n <!-- Insert Object: Table, Images, Special Chars, Page Break -->\n <div\n *ngIf=\"capabilities.hasInsertObject\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"insert-object\"\n >\n <button\n *ngIf=\"capabilities.hasTable\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly\"\n (click)=\"openCreateTableBubble($event)\"\n [class.active]=\"editor.isActive('table')\"\n title=\"Table\"\n >\n <document-engine-icon name=\"table_rows\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasImages\"\n documentEngineButton\n size=\"icon\"\n title=\"Insert Image\"\n [disabled]=\"toolbarState.isReadOnly\"\n (click)=\"insertImage($event)\"\n >\n <document-engine-icon name=\"photo\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasSpecialChars\"\n documentEngineButton\n size=\"icon\"\n title=\"Special Character\"\n [disabled]=\"toolbarState.isReadOnly\"\n (click)=\"openSpecialCharsBubble($event)\"\n >\n <document-engine-icon name=\"special_character\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasPageBreak\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canInsertPageBreak\"\n (click)=\"runAndEmit('insertPageBreak', editor.chain().focus().insertPageBreak().run)\"\n title=\"Insert page break\"\n >\n <document-engine-icon name=\"insert_page_break\"></document-engine-icon>\n </button>\n </div>\n\n <!-- Business Features: Templates, Dynamic Fields, Editable Region -->\n <div\n *ngIf=\"capabilities.hasBusinessFeatures\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"business-features\"\n >\n <button\n *ngIf=\"capabilities.hasDynamicFields\"\n documentEngineButton\n size=\"icon\"\n title=\"Dynamic Field\"\n [disabled]=\"toolbarState.isReadOnly\"\n (click)=\"openDynamicFieldsBubble($event)\"\n >\n <document-engine-icon name=\"dynamic_form\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasTemplates\"\n documentEngineButton\n size=\"icon\"\n title=\"Template\"\n [disabled]=\"toolbarState.isReadOnly\"\n (click)=\"openTemplateBubble($event)\"\n >\n <document-engine-icon name=\"description\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasEditableRegion\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"!toolbarState.canToggleEditableRegion || toolbarState.isReadOnly\"\n (click)=\"runAndEmit('toggleEditableRegion', editor.chain().focus().toggleEditableRegion().run)\"\n [class.active]=\"editor.isActive('editableRegion')\"\n title=\"Toggle editable region\"\n >\n <document-engine-icon name=\"restrict_edit\"></document-engine-icon>\n </button>\n </div>\n\n <!-- Actions: Clear Content, Markdown -->\n <div *ngIf=\"capabilities.hasActions\" class=\"document-engine-document-editor__toolbar-group\" data-group=\"actions\">\n <button\n *ngIf=\"capabilities.hasClearContent\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canClearContent\"\n (click)=\"runAndEmit('clear', editor.chain().focus().clear().run)\"\n title=\"Clear all content\"\n >\n <document-engine-icon name=\"clear_all\"></document-engine-icon>\n </button>\n\n <!-- <button\n *ngIf=\"capabilities.hasMarkdown\"\n documentEngineButton\n size=\"icon\"\n (click)=\"downloadAsMarkdown()\"\n title=\"Download as Markdown\"\n >\n <document-engine-icon name=\"markdown\"></document-engine-icon>\n </button> -->\n </div>\n\n <!-- Custom Toolbar Buttons -->\n <div\n *ngIf=\"customToolbarButtons.length > 0\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"custom-buttons\"\n >\n <ng-container *ngFor=\"let button of customToolbarButtons; trackBy: trackByIndex\">\n <!-- Icon Button -->\n <button\n *ngIf=\"button.type === 'icon-button'\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly\"\n (click)=\"handleCustomIconButton(button)\"\n [title]=\"button.label || ''\"\n >\n <document-engine-icon [name]=\"button.icon\"></document-engine-icon>\n </button>\n\n <!-- Select Button -->\n <document-engine-select\n *ngIf=\"button.type === 'select-button'\"\n [value]=\"null\"\n labelMode=\"static\"\n [disabled]=\"toolbarState.isReadOnly\"\n [title]=\"button.label\"\n >\n <span documentEngineSelectLabel>{{ button.label }}</span>\n\n <button\n *ngFor=\"let option of button.options; trackBy: trackByValue\"\n documentEngineSelectOption\n [value]=\"option.value\"\n (click)=\"handleCustomSelectButton(button, option.value)\"\n >\n {{ option.label }}\n </button>\n </document-engine-select>\n </ng-container>\n </div>\n </div>\n\n <!-- Right side actions -->\n <div class=\"document-engine-document-editor__toolbar-right\">\n <button\n *ngIf=\"capabilities.hasPrintButton\"\n documentEngineButton\n size=\"icon\"\n (click)=\"printValue()\"\n title=\"Print Value\"\n >\n <document-engine-icon name=\"print\"></document-engine-icon>\n </button>\n </div>\n </div>\n\n <!-- Color Bubble Menu -->\n <document-engine-toolbar-bubble-menu\n #colorBubbleMenu\n [editor]=\"editor\"\n [config]=\"colorBubbleConfig\"\n ></document-engine-toolbar-bubble-menu>\n\n <!-- Link Bubble Menu -->\n <document-engine-editor-bubble-menu\n *ngIf=\"capabilities.hasLink\"\n #linkBubbleWrapper\n [editor]=\"editor\"\n [config]=\"linkBubbleConfig\"\n ></document-engine-editor-bubble-menu>\n\n <!-- Table Bubble Menu -->\n <document-engine-editor-bubble-menu\n *ngIf=\"capabilities.hasTable\"\n #tableBubbleWrapper\n [editor]=\"editor\"\n [config]=\"tableBubbleConfig\"\n ></document-engine-editor-bubble-menu>\n\n <!-- Image Bubble Menu -->\n <document-engine-toolbar-bubble-menu\n *ngIf=\"capabilities.hasImages\"\n #imageBubbleMenu\n [editor]=\"editor\"\n [config]=\"imageBubbleConfig\"\n ></document-engine-toolbar-bubble-menu>\n\n <!-- Special Characters Bubble Menu -->\n <document-engine-toolbar-bubble-menu\n *ngIf=\"capabilities.hasSpecialChars\"\n #specialCharsBubbleMenu\n [editor]=\"editor\"\n [config]=\"specialCharsBubbleConfig\"\n ></document-engine-toolbar-bubble-menu>\n\n <!-- Dynamic Fields Bubble Menu -->\n <document-engine-toolbar-bubble-menu\n *ngIf=\"capabilities.hasDynamicFields\"\n #dynamicFieldsBubbleMenu\n [editor]=\"editor\"\n [config]=\"dynamicFieldsBubbleConfig\"\n ></document-engine-toolbar-bubble-menu>\n\n <!-- Template Bubble Menu -->\n <document-engine-toolbar-bubble-menu\n *ngIf=\"capabilities.hasTemplates\"\n #templateBubbleMenu\n [editor]=\"editor\"\n [config]=\"templateBubbleConfig\"\n ></document-engine-toolbar-bubble-menu>\n\n <!-- Table Create Bubble Menu -->\n <document-engine-toolbar-bubble-menu\n *ngIf=\"capabilities.hasTable\"\n #tableCreateBubbleMenu\n [editor]=\"editor\"\n [config]=\"tableCreateBubbleConfig\"\n ></document-engine-toolbar-bubble-menu>\n</div>\n", styles: [":host{position:sticky;top:0;z-index:20}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: IconComponent, selector: "document-engine-icon", inputs: ["name"] }, { kind: "directive", type: ButtonDirective, selector: "button[documentEngineButton]", inputs: ["variant", "size"] }, { kind: "component", type: SelectComponent, selector: "document-engine-select", inputs: ["value", "disabled", "labelMode", "variant"], outputs: ["valueChange"] }, { kind: "directive", type: SelectLabelDirective, selector: "[documentEngineSelectLabel]" }, { kind: "directive", type: SelectOptionDirective, selector: "[documentEngineSelectOption]", inputs: ["value", "label", "disabled"] }, { kind: "component", type: ToolbarBubbleMenuComponent, selector: "document-engine-toolbar-bubble-menu", inputs: ["editor", "config"] }, { kind: "component", type: EditorBubbleMenuComponent, selector: "document-engine-editor-bubble-menu", inputs: ["editor", "config"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6089
6058
  }
6090
-
6091
- class TiptapDraggableDirective {
6092
- draggable = true;
6093
- handle = '';
6094
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: TiptapDraggableDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
6095
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.10", type: TiptapDraggableDirective, isStandalone: true, selector: "[tiptapDraggable]", host: { properties: { "attr.draggable": "this.draggable", "attr.data-drag-handle": "this.handle" } }, ngImport: i0 });
6096
- }
6097
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: TiptapDraggableDirective, decorators: [{
6098
- type: Directive,
6099
- args: [{
6100
- selector: '[tiptapDraggable]',
6101
- }]
6102
- }], propDecorators: { draggable: [{
6103
- type: HostBinding,
6104
- args: ['attr.draggable']
6105
- }], handle: [{
6106
- type: HostBinding,
6107
- args: ['attr.data-drag-handle']
6059
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: ToolbarComponent, decorators: [{
6060
+ type: Component,
6061
+ args: [{ selector: 'document-engine-toolbar', standalone: true, imports: [
6062
+ CommonModule,
6063
+ IconComponent,
6064
+ ButtonDirective,
6065
+ SelectComponent,
6066
+ SelectLabelDirective,
6067
+ SelectOptionDirective,
6068
+ ToolbarBubbleMenuComponent,
6069
+ EditorBubbleMenuComponent,
6070
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"document-engine-document-editor__toolbar\">\n <div class=\"document-engine-document-editor__toolbar-content\">\n <div class=\"document-engine-document-editor__toolbar-left\">\n <!-- History: Undo/Redo -->\n <div *ngIf=\"capabilities.hasHistory\" class=\"document-engine-document-editor__toolbar-group\" data-group=\"history\">\n <button\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canUndo\"\n (click)=\"runAndEmit('undo', editor.chain().focus().undo().run)\"\n title=\"Undo (Ctrl+Z)\"\n >\n <document-engine-icon name=\"undo\"></document-engine-icon>\n </button>\n\n <button\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canRedo\"\n (click)=\"runAndEmit('redo', editor.chain().focus().redo().run)\"\n title=\"Redo\"\n >\n <document-engine-icon name=\"redo\"></document-engine-icon>\n </button>\n </div>\n\n <!-- Text Formatting: Font Size, Line Height, Text Case, Heading -->\n <div\n *ngIf=\"capabilities.hasTextProperty\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"text-property\"\n >\n <!-- Font Size -->\n <document-engine-select\n *ngIf=\"capabilities.hasFontSize\"\n [value]=\"activeFontSize\"\n placeholder=\"Font Size\"\n labelMode=\"static\"\n [disabled]=\"toolbarState.isReadOnly\"\n title=\"Font Size\"\n >\n <document-engine-icon documentEngineSelectLabel name=\"format_size\"></document-engine-icon>\n\n <button documentEngineSelectOption [value]=\"null\">Default</button>\n\n <button\n *ngFor=\"let option of fontSizeOptions; trackBy: trackByValue\"\n documentEngineSelectOption\n [value]=\"option.value\"\n (click)=\"setFontSize(option.value)\"\n >\n {{ option.label }}\n </button>\n </document-engine-select>\n\n <!-- Line Height -->\n <document-engine-select\n *ngIf=\"capabilities.hasLineHeight\"\n [value]=\"activeLineHeight\"\n placeholder=\"Line Height\"\n labelMode=\"static\"\n [disabled]=\"toolbarState.isReadOnly\"\n title=\"Line Height\"\n >\n <document-engine-icon documentEngineSelectLabel name=\"format_line_spacing\"></document-engine-icon>\n\n <button\n *ngFor=\"let option of lineHeightOptions; trackBy: trackByValue\"\n documentEngineSelectOption\n [value]=\"option.value\"\n (click)=\"setLineHeight(option.value)\"\n >\n {{ option.label }}\n </button>\n </document-engine-select>\n\n <!-- Text Case -->\n <document-engine-select\n *ngIf=\"capabilities.hasTextCase\"\n placeholder=\"Text Case\"\n labelMode=\"static\"\n [disabled]=\"toolbarState.isReadOnly\"\n title=\"Text Case\"\n >\n <document-engine-icon documentEngineSelectLabel name=\"match_case\"></document-engine-icon>\n\n <button\n *ngFor=\"let option of textCaseOptions; trackBy: trackByValue\"\n documentEngineSelectOption\n [value]=\"option.value\"\n (click)=\"runAndEmit('setTextCase', editor.chain().focus().textCase(option.value).run)\"\n >\n {{ option.label }}\n </button>\n </document-engine-select>\n\n <!-- Heading -->\n <document-engine-select\n *ngIf=\"capabilities.hasHeading\"\n [value]=\"activeHeading?.toString() || null\"\n placeholder=\"Normal text\"\n [disabled]=\"toolbarState.isReadOnly\"\n title=\"Heading\"\n >\n <button\n *ngFor=\"let option of headingOptions; trackBy: trackByValue\"\n documentEngineSelectOption\n [value]=\"option.value?.toString() || null\"\n [class]=\"option.class\"\n (click)=\"setHeading(option.value?.toString() || null)\"\n >\n {{ option.label }}\n </button>\n </document-engine-select>\n </div>\n\n <!-- Format Utilities: Reset Formatting & Colors -->\n <div\n *ngIf=\"capabilities.hasFormatUtilities\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"format-utilities\"\n >\n <!-- Reset Formatting -->\n <button\n *ngIf=\"capabilities.hasResetFormat\"\n documentEngineButton\n size=\"icon\"\n (click)=\"runAndEmit('resetFormat', editor.chain().focus().resetFormat().run)\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canResetFormat\"\n title=\"Clear Formatting\"\n >\n <document-engine-icon name=\"format_clear\"></document-engine-icon>\n </button>\n\n <!-- Colors: Text/Background -->\n <button\n *ngIf=\"capabilities.hasColors\"\n documentEngineButton\n size=\"icon\"\n title=\"Text Color\"\n [disabled]=\"toolbarState.isReadOnly\"\n (click)=\"openColorBubble($event, 'text')\"\n >\n <document-engine-icon name=\"format_color_text\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasColors\"\n documentEngineButton\n size=\"icon\"\n title=\"Background Color\"\n [disabled]=\"toolbarState.isReadOnly\"\n (click)=\"openColorBubble($event, 'fill')\"\n >\n <document-engine-icon name=\"format_color_fill\"></document-engine-icon>\n </button>\n </div>\n\n <!-- Text Styles: Bold/Italic/Underline/Strike -->\n <div\n *ngIf=\"capabilities.basicInline\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"basic-inline\"\n >\n <button\n *ngIf=\"capabilities.hasBold\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleBold\"\n (click)=\"runAndEmit('toggleBold', editor.chain().focus().toggleBold().run)\"\n [class.active]=\"editor.isActive('bold')\"\n title=\"Bold\"\n >\n <document-engine-icon name=\"format_bold\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasItalic\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleItalic\"\n (click)=\"runAndEmit('toggleItalic', editor.chain().focus().toggleItalic().run)\"\n [class.active]=\"editor.isActive('italic')\"\n title=\"Italic\"\n >\n <document-engine-icon name=\"format_italic\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasUnderline\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleUnderline\"\n (click)=\"runAndEmit('toggleUnderline', editor.chain().focus().toggleUnderline().run)\"\n [class.active]=\"editor.isActive('underline')\"\n title=\"Underline\"\n >\n <document-engine-icon name=\"format_underline\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasStrike\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleStrike\"\n (click)=\"runAndEmit('toggleStrike', editor.chain().focus().toggleStrike().run)\"\n [class.active]=\"editor.isActive('strike')\"\n title=\"Strikethrough\"\n >\n <document-engine-icon name=\"format_strikethrough\"></document-engine-icon>\n </button>\n </div>\n\n <!-- Text Styles: Sub/Super/Code/Link -->\n <div\n *ngIf=\"capabilities.advancedInline\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"advanced-inline\"\n >\n <button\n *ngIf=\"capabilities.hasSubscript\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleSubscript\"\n (click)=\"runAndEmit('toggleSubscript', editor.chain().focus().toggleSubscript().run)\"\n [class.active]=\"editor.isActive('subscript')\"\n title=\"Subscript\"\n >\n <document-engine-icon name=\"subscript\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasSuperscript\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleSuperscript\"\n (click)=\"runAndEmit('toggleSuperscript', editor.chain().focus().toggleSuperscript().run)\"\n [class.active]=\"editor.isActive('superscript')\"\n title=\"Superscript\"\n >\n <document-engine-icon name=\"superscript\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasCode\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleCode\"\n (click)=\"runAndEmit('toggleCode', editor.chain().focus().toggleCode().run)\"\n [class.active]=\"editor.isActive('code')\"\n title=\"Code\"\n >\n <document-engine-icon name=\"code\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasLink\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleLink\"\n (click)=\"toggleLink()\"\n [class.active]=\"editor.isActive('link')\"\n [title]=\"editor.isActive('link') ? 'Unlink' : 'Link'\"\n >\n <document-engine-icon [name]=\"editor.isActive('link') ? 'link_off' : 'link'\"></document-engine-icon>\n </button>\n </div>\n\n <!-- Alignment & Layout: Text Align, Indent -->\n <div\n *ngIf=\"capabilities.hasAlignmentLayout\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"alignment-layout\"\n >\n <!-- Text Alignment -->\n <document-engine-select\n *ngIf=\"capabilities.hasTextAlign\"\n [value]=\"activeTextAlign\"\n (valueChange)=\"setTextAlign($event)\"\n placeholder=\"Text Align\"\n labelMode=\"static\"\n [disabled]=\"toolbarState.isReadOnly\"\n title=\"Text Align\"\n >\n <document-engine-icon documentEngineSelectLabel [name]=\"activeTextAlignIcon\"></document-engine-icon>\n\n <button\n *ngFor=\"let option of textAlignOptions; trackBy: trackByValue\"\n documentEngineSelectOption\n [value]=\"option.value\"\n >\n <document-engine-icon [name]=\"option.icon\"></document-engine-icon>\n <span>{{ option.label }}</span>\n </button>\n </document-engine-select>\n\n <!-- Indent -->\n <button\n *ngIf=\"capabilities.hasIndent\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canDecreaseIndent\"\n (click)=\"runAndEmit('decreaseIndent', editor.chain().focus().decreaseIndent().run)\"\n title=\"Decrease Indent\"\n >\n <document-engine-icon name=\"format_indent_decrease\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasIndent\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canIncreaseIndent\"\n (click)=\"runAndEmit('increaseIndent', editor.chain().focus().increaseIndent().run)\"\n title=\"Increase Indent\"\n >\n <document-engine-icon name=\"format_indent_increase\"></document-engine-icon>\n </button>\n </div>\n\n <!-- Lists -->\n <div\n *ngIf=\"capabilities.hasAlignmentLayout\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"alignment-layout\"\n >\n <!-- Lists: Bullet, Numbered (with style) -->\n <button\n *ngIf=\"capabilities.hasLists\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleBulletList\"\n (click)=\"runAndEmit('toggleBulletList', editor.chain().focus().toggleBulletList().run)\"\n [class.active]=\"editor.isActive('bulletList')\"\n title=\"Bullet List\"\n >\n <document-engine-icon name=\"format_list_bulleted\"></document-engine-icon>\n </button>\n\n <!-- Numbered List with Style -->\n <document-engine-select\n *ngIf=\"capabilities.hasLists\"\n placeholder=\"Numbered List\"\n labelMode=\"static\"\n [value]=\"\n editor.isActive('customOrderedList')\n ? editor.getAttributes('customOrderedList')['data-list-style-type'] || 'decimal'\n : null\n \"\n [disabled]=\"toolbarState.isReadOnly\"\n title=\"Numbered List\"\n >\n <document-engine-icon documentEngineSelectLabel name=\"format_list_numbered\"></document-engine-icon>\n\n <button\n *ngFor=\"let option of listStyleOptions; trackBy: trackByValue\"\n documentEngineSelectOption\n [value]=\"option.value\"\n (click)=\"setListStyle(option.value)\"\n [class.active]=\"\n editor.isActive('customOrderedList') &&\n editor.getAttributes('customOrderedList')['data-list-style-type'] === option.value\n \"\n >\n <span>{{ option.example }}</span>\n <span>{{ option.label }}</span>\n </button>\n </document-engine-select>\n </div>\n\n <!-- Block Types: Code Block, Blockquote -->\n <div\n *ngIf=\"capabilities.hasBlockTypes\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"block-types\"\n >\n <!-- Blockquote -->\n <button\n *ngIf=\"capabilities.hasBlockquote\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canToggleBlockquote\"\n (click)=\"runAndEmit('toggleBlockquote', editor.chain().focus().toggleBlockquote().run)\"\n [class.active]=\"editor.isActive('blockquote')\"\n title=\"Quote\"\n >\n <document-engine-icon name=\"format_quote\"></document-engine-icon>\n </button>\n </div>\n\n <!-- Insert Object: Table, Images, Special Chars, Page Break -->\n <div\n *ngIf=\"capabilities.hasInsertObject\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"insert-object\"\n >\n <button\n *ngIf=\"capabilities.hasTable\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly\"\n (click)=\"openCreateTableBubble($event)\"\n [class.active]=\"editor.isActive('table')\"\n title=\"Table\"\n >\n <document-engine-icon name=\"table_rows\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasImages\"\n documentEngineButton\n size=\"icon\"\n title=\"Insert Image\"\n [disabled]=\"toolbarState.isReadOnly\"\n (click)=\"insertImage($event)\"\n >\n <document-engine-icon name=\"photo\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasSpecialChars\"\n documentEngineButton\n size=\"icon\"\n title=\"Special Character\"\n [disabled]=\"toolbarState.isReadOnly\"\n (click)=\"openSpecialCharsBubble($event)\"\n >\n <document-engine-icon name=\"special_character\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasPageBreak\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canInsertPageBreak\"\n (click)=\"runAndEmit('insertPageBreak', editor.chain().focus().insertPageBreak().run)\"\n title=\"Insert page break\"\n >\n <document-engine-icon name=\"insert_page_break\"></document-engine-icon>\n </button>\n </div>\n\n <!-- Business Features: Templates, Dynamic Fields, Editable Region -->\n <div\n *ngIf=\"capabilities.hasBusinessFeatures\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"business-features\"\n >\n <button\n *ngIf=\"capabilities.hasDynamicFields\"\n documentEngineButton\n size=\"icon\"\n title=\"Dynamic Field\"\n [disabled]=\"toolbarState.isReadOnly\"\n (click)=\"openDynamicFieldsBubble($event)\"\n >\n <document-engine-icon name=\"dynamic_form\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasTemplates\"\n documentEngineButton\n size=\"icon\"\n title=\"Template\"\n [disabled]=\"toolbarState.isReadOnly\"\n (click)=\"openTemplateBubble($event)\"\n >\n <document-engine-icon name=\"description\"></document-engine-icon>\n </button>\n\n <button\n *ngIf=\"capabilities.hasEditableRegion\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"!toolbarState.canToggleEditableRegion || toolbarState.isReadOnly\"\n (click)=\"runAndEmit('toggleEditableRegion', editor.chain().focus().toggleEditableRegion().run)\"\n [class.active]=\"editor.isActive('editableRegion')\"\n title=\"Toggle editable region\"\n >\n <document-engine-icon name=\"restrict_edit\"></document-engine-icon>\n </button>\n </div>\n\n <!-- Actions: Clear Content, Markdown -->\n <div *ngIf=\"capabilities.hasActions\" class=\"document-engine-document-editor__toolbar-group\" data-group=\"actions\">\n <button\n *ngIf=\"capabilities.hasClearContent\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly || !toolbarState.canClearContent\"\n (click)=\"runAndEmit('clear', editor.chain().focus().clear().run)\"\n title=\"Clear all content\"\n >\n <document-engine-icon name=\"clear_all\"></document-engine-icon>\n </button>\n\n <!-- <button\n *ngIf=\"capabilities.hasMarkdown\"\n documentEngineButton\n size=\"icon\"\n (click)=\"downloadAsMarkdown()\"\n title=\"Download as Markdown\"\n >\n <document-engine-icon name=\"markdown\"></document-engine-icon>\n </button> -->\n </div>\n\n <!-- Custom Toolbar Buttons -->\n <div\n *ngIf=\"customToolbarButtons.length > 0\"\n class=\"document-engine-document-editor__toolbar-group\"\n data-group=\"custom-buttons\"\n >\n <ng-container *ngFor=\"let button of customToolbarButtons; trackBy: trackByIndex\">\n <!-- Icon Button -->\n <button\n *ngIf=\"button.type === 'icon-button'\"\n documentEngineButton\n size=\"icon\"\n [disabled]=\"toolbarState.isReadOnly\"\n (click)=\"handleCustomIconButton(button)\"\n [title]=\"button.label || ''\"\n >\n <document-engine-icon [name]=\"button.icon\"></document-engine-icon>\n </button>\n\n <!-- Select Button -->\n <document-engine-select\n *ngIf=\"button.type === 'select-button'\"\n [value]=\"null\"\n labelMode=\"static\"\n [disabled]=\"toolbarState.isReadOnly\"\n [title]=\"button.label\"\n >\n <span documentEngineSelectLabel>{{ button.label }}</span>\n\n <button\n *ngFor=\"let option of button.options; trackBy: trackByValue\"\n documentEngineSelectOption\n [value]=\"option.value\"\n (click)=\"handleCustomSelectButton(button, option.value)\"\n >\n {{ option.label }}\n </button>\n </document-engine-select>\n </ng-container>\n </div>\n </div>\n\n <!-- Right side actions -->\n <div class=\"document-engine-document-editor__toolbar-right\">\n <button\n *ngIf=\"capabilities.hasPrintButton\"\n documentEngineButton\n size=\"icon\"\n (click)=\"printValue()\"\n title=\"Print Value\"\n >\n <document-engine-icon name=\"print\"></document-engine-icon>\n </button>\n </div>\n </div>\n\n <!-- Color Bubble Menu -->\n <document-engine-toolbar-bubble-menu\n #colorBubbleMenu\n [editor]=\"editor\"\n [config]=\"colorBubbleConfig\"\n ></document-engine-toolbar-bubble-menu>\n\n <!-- Link Bubble Menu -->\n <document-engine-editor-bubble-menu\n *ngIf=\"capabilities.hasLink\"\n #linkBubbleWrapper\n [editor]=\"editor\"\n [config]=\"linkBubbleConfig\"\n ></document-engine-editor-bubble-menu>\n\n <!-- Table Bubble Menu -->\n <document-engine-editor-bubble-menu\n *ngIf=\"capabilities.hasTable\"\n #tableBubbleWrapper\n [editor]=\"editor\"\n [config]=\"tableBubbleConfig\"\n ></document-engine-editor-bubble-menu>\n\n <!-- Image Bubble Menu -->\n <document-engine-toolbar-bubble-menu\n *ngIf=\"capabilities.hasImages\"\n #imageBubbleMenu\n [editor]=\"editor\"\n [config]=\"imageBubbleConfig\"\n ></document-engine-toolbar-bubble-menu>\n\n <!-- Special Characters Bubble Menu -->\n <document-engine-toolbar-bubble-menu\n *ngIf=\"capabilities.hasSpecialChars\"\n #specialCharsBubbleMenu\n [editor]=\"editor\"\n [config]=\"specialCharsBubbleConfig\"\n ></document-engine-toolbar-bubble-menu>\n\n <!-- Dynamic Fields Bubble Menu -->\n <document-engine-toolbar-bubble-menu\n *ngIf=\"capabilities.hasDynamicFields\"\n #dynamicFieldsBubbleMenu\n [editor]=\"editor\"\n [config]=\"dynamicFieldsBubbleConfig\"\n ></document-engine-toolbar-bubble-menu>\n\n <!-- Template Bubble Menu -->\n <document-engine-toolbar-bubble-menu\n *ngIf=\"capabilities.hasTemplates\"\n #templateBubbleMenu\n [editor]=\"editor\"\n [config]=\"templateBubbleConfig\"\n ></document-engine-toolbar-bubble-menu>\n\n <!-- Table Create Bubble Menu -->\n <document-engine-toolbar-bubble-menu\n *ngIf=\"capabilities.hasTable\"\n #tableCreateBubbleMenu\n [editor]=\"editor\"\n [config]=\"tableCreateBubbleConfig\"\n ></document-engine-toolbar-bubble-menu>\n</div>\n", styles: [":host{position:sticky;top:0;z-index:20}\n"] }]
6071
+ }], propDecorators: { editor: [{
6072
+ type: Input
6073
+ }], capabilities: [{
6074
+ type: Input
6075
+ }], config: [{
6076
+ type: Input
6077
+ }], action: [{
6078
+ type: Output
6079
+ }], colorBubbleMenu: [{
6080
+ type: ViewChild,
6081
+ args: ['colorBubbleMenu']
6082
+ }], linkBubbleWrapper: [{
6083
+ type: ViewChild,
6084
+ args: ['linkBubbleWrapper']
6085
+ }], imageBubbleMenu: [{
6086
+ type: ViewChild,
6087
+ args: ['imageBubbleMenu']
6088
+ }], specialCharsBubbleMenu: [{
6089
+ type: ViewChild,
6090
+ args: ['specialCharsBubbleMenu']
6091
+ }], dynamicFieldsBubbleMenu: [{
6092
+ type: ViewChild,
6093
+ args: ['dynamicFieldsBubbleMenu']
6094
+ }], templateBubbleMenu: [{
6095
+ type: ViewChild,
6096
+ args: ['templateBubbleMenu']
6097
+ }], tableCreateBubbleMenu: [{
6098
+ type: ViewChild,
6099
+ args: ['tableCreateBubbleMenu']
6108
6100
  }] } });
6109
6101
 
6110
6102
  /**
6111
- * Injection token for HTML preprocessor function.
6112
- * Use this token to provide a custom function that processes HTML content
6113
- * before it's set into the TipTap editor.
6114
- *
6115
- * This is optional - if not provided, the editor will use the HTML content as-is.
6116
- *
6117
- * @example
6118
- * ```typescript
6119
- * // In your component or module providers
6120
- * {
6121
- * provide: EDITOR_HTML_PREPROCESSOR,
6122
- * useValue: (html: string) => {
6123
- * // Your custom preprocessing logic
6124
- * return html.replace(/old/g, 'new');
6125
- * }
6126
- * }
6127
- * ```
6103
+ * Character Count component for document editor footer
6104
+ * Displays character/word count with optional limit and color coding
6128
6105
  */
6129
- const EDITOR_HTML_PREPROCESSOR = new InjectionToken('EDITOR_HTML_PREPROCESSOR');
6130
-
6131
- class TiptapEditorDirective {
6132
- elRef = inject(ElementRef);
6133
- renderer = inject(Renderer2);
6134
- changeDetectorRef = inject(ChangeDetectorRef);
6135
- htmlPreprocessor = inject(EDITOR_HTML_PREPROCESSOR, { optional: true });
6136
- _editor;
6137
- initialEditableState;
6138
- set editor(editor) {
6139
- this._editor = editor;
6140
- // Store initial editable state immediately when editor is set
6141
- // This ensures we capture the state before any Forms API calls
6142
- if (this.initialEditableState === undefined && editor) {
6143
- this.initialEditableState = editor.isEditable;
6144
- }
6145
- }
6146
- get editor() {
6147
- if (!this._editor) {
6148
- throw new Error('Editor must be set before use.');
6106
+ class CharacterCountComponent {
6107
+ cdr = inject(ChangeDetectorRef);
6108
+ editor;
6109
+ config;
6110
+ mode = 'characters';
6111
+ count = 0;
6112
+ limit = null;
6113
+ updateCount = () => {
6114
+ if (!this.editor) {
6115
+ return;
6149
6116
  }
6150
- return this._editor;
6151
- }
6152
- outputFormat = 'html';
6153
- onChange = () => {
6154
- /** */
6155
- };
6156
- onTouched = () => {
6157
- /** */
6158
- };
6159
- // Writes a new value to the element.
6160
- // This methods is called when programmatic changes from model to view are requested.
6161
- writeValue(value) {
6162
- // Apply preprocessor if value is a string (HTML) and preprocessor is available
6163
- let processedValue = value;
6164
- if (typeof value === 'string' && this.htmlPreprocessor) {
6165
- processedValue = this.htmlPreprocessor(value);
6117
+ // Get count from editor storage if available
6118
+ const characterCountStorage = this.editor.storage.characterCount;
6119
+ if (characterCountStorage) {
6120
+ if (this.mode === 'characters') {
6121
+ this.count = characterCountStorage.characters?.() || 0;
6122
+ }
6123
+ else {
6124
+ this.count = characterCountStorage.words?.() || 0;
6125
+ }
6166
6126
  }
6167
- this.editor.chain().setContent(processedValue, { emitUpdate: false }).run();
6168
- }
6169
- // Registers a callback function that is called when the control's value changes in the UI.
6170
- registerOnChange(fn) {
6171
- this.onChange = fn;
6172
- }
6173
- // Registers a callback function that is called by the forms API on initialization to update the form model on blur.
6174
- registerOnTouched(fn) {
6175
- this.onTouched = fn;
6176
- }
6177
- // Called by the forms api to enable or disable the element
6178
- setDisabledState(isDisabled) {
6179
- // Ensure we have captured the initial state (fallback in case setter wasn't called)
6180
- if (this.initialEditableState === undefined && this.editor) {
6181
- this.initialEditableState = this.editor.isEditable;
6127
+ else {
6128
+ // Fallback: calculate from text
6129
+ const text = this.editor.getText();
6130
+ if (this.mode === 'characters') {
6131
+ this.count = text.length;
6132
+ }
6133
+ else {
6134
+ this.count = text.trim() ? text.trim().split(/\s+/).length : 0;
6135
+ }
6182
6136
  }
6183
- // Only apply disabled state if editor was initially editable
6184
- // If editor was configured as readonly (editable: false), preserve that state
6185
- if (this.initialEditableState === true) {
6186
- this.editor.setEditable(!isDisabled);
6137
+ // Get limit from config
6138
+ const characterCountConfig = this.config?.characterCount;
6139
+ if (characterCountConfig && typeof characterCountConfig === 'object') {
6140
+ this.limit = characterCountConfig.limit || null;
6187
6141
  }
6188
- // If initialEditableState === false, do nothing - preserve readonly state
6189
- this.renderer.setProperty(this.elRef.nativeElement, 'disabled', isDisabled);
6190
- }
6191
- handleChange = ({ editor, transaction }) => {
6192
- if (!transaction.docChanged)
6193
- return;
6194
- // Needed for ChangeDetectionStrategy.OnPush to get notified about changes
6195
- this.changeDetectorRef.markForCheck();
6196
- if (this.outputFormat === 'html') {
6197
- this.onChange(editor.getHTML());
6198
- return;
6142
+ else {
6143
+ this.limit = null;
6199
6144
  }
6200
- this.onChange(editor.getJSON());
6145
+ this.cdr.markForCheck();
6201
6146
  };
6202
6147
  ngOnInit() {
6203
- const editor = this.editor;
6204
- // take the inner contents and clear the block
6205
- const { innerHTML } = this.elRef.nativeElement;
6206
- this.elRef.nativeElement.innerHTML = '';
6207
- // insert the editor in the dom
6208
- this.elRef.nativeElement.append(...Array.from(editor.options.element?.childNodes || []));
6209
- // update the options for the editor
6210
- editor.setOptions({ element: this.elRef.nativeElement });
6211
- // update content to the editor
6212
- if (innerHTML) {
6213
- // Apply preprocessor to initial content if available
6214
- const processedContent = this.htmlPreprocessor ? this.htmlPreprocessor(innerHTML) : innerHTML;
6215
- editor.chain().setContent(processedContent, { emitUpdate: false }).run();
6148
+ if (!this.editor) {
6149
+ return;
6216
6150
  }
6217
- // register blur handler to update `touched` property
6218
- editor.on('blur', () => {
6219
- this.onTouched();
6220
- });
6221
- // register update handler to listen to changes on update
6222
- editor.on('update', this.handleChange);
6223
- // Needed for ChangeDetectionStrategy.OnPush to get notified
6224
- editor.on('selectionUpdate', () => this.changeDetectorRef.markForCheck());
6151
+ // Subscribe to update events to keep count in sync
6152
+ this.editor.on('update', this.updateCount);
6153
+ this.editor.on('transaction', this.updateCount);
6154
+ // Initialize count once
6155
+ this.updateCount();
6225
6156
  }
6226
- ngAfterViewInit() {
6227
- this.changeDetectorRef.detectChanges();
6157
+ ngOnDestroy() {
6158
+ if (this.editor) {
6159
+ this.editor.off('update', this.updateCount);
6160
+ this.editor.off('transaction', this.updateCount);
6161
+ }
6228
6162
  }
6229
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: TiptapEditorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
6230
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.10", type: TiptapEditorDirective, isStandalone: true, selector: "tiptap[editor], [tiptap][editor], tiptap-editor[editor], [tiptapEditor][editor]", inputs: { editor: "editor", outputFormat: "outputFormat" }, host: { classAttribute: "tiptap-editor no-twp" }, providers: [
6231
- {
6232
- provide: NG_VALUE_ACCESSOR,
6233
- useExisting: forwardRef(() => TiptapEditorDirective),
6234
- multi: true,
6235
- },
6236
- ], ngImport: i0 });
6163
+ isNearLimit() {
6164
+ if (!this.limit) {
6165
+ return false;
6166
+ }
6167
+ // Consider near limit when >= 90% of limit
6168
+ return this.count >= this.limit * 0.9 && this.count < this.limit;
6169
+ }
6170
+ isOverLimit() {
6171
+ if (!this.limit) {
6172
+ return false;
6173
+ }
6174
+ return this.count > this.limit;
6175
+ }
6176
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: CharacterCountComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6177
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.10", type: CharacterCountComponent, isStandalone: true, selector: "document-engine-character-count", inputs: { editor: "editor", config: "config", mode: "mode" }, ngImport: i0, template: "<div class=\"document-engine-character-count\">\n <span\n class=\"document-engine-character-count__text\"\n [class.document-engine-character-count__text--muted]=\"!isNearLimit() && !isOverLimit()\"\n [class.document-engine-character-count__text--warning]=\"isNearLimit() && !isOverLimit()\"\n [class.document-engine-character-count__text--error]=\"isOverLimit()\"\n >\n <ng-container *ngIf=\"mode === 'characters'\">\n <ng-container *ngIf=\"limit; else noLimit\"> {{ count }} / {{ limit }} characters. </ng-container>\n <ng-template #noLimit> {{ count }} {{ count === 1 ? 'character' : 'characters' }}. </ng-template>\n </ng-container>\n <ng-container *ngIf=\"mode === 'words'\"> {{ count }} {{ count === 1 ? 'word' : 'words' }}. </ng-container>\n </span>\n</div>\n", styles: [".document-engine-character-count{display:flex;justify-content:flex-end}.document-engine-character-count__text{font-size:.875rem;transition:color .2s ease-in-out}.document-engine-character-count__text--muted{color:var(--muted-foreground, #6b7280)}.document-engine-character-count__text--warning{color:var(--warning, #d97706)}.document-engine-character-count__text--error{color:var(--destructive, #dc2626)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6237
6178
  }
6238
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: TiptapEditorDirective, decorators: [{
6239
- type: Directive,
6240
- args: [{
6241
- selector: 'tiptap[editor], [tiptap][editor], tiptap-editor[editor], [tiptapEditor][editor]',
6242
- providers: [
6243
- {
6244
- provide: NG_VALUE_ACCESSOR,
6245
- useExisting: forwardRef(() => TiptapEditorDirective),
6246
- multi: true,
6247
- },
6248
- ],
6249
- standalone: true,
6250
- host: {
6251
- ['class']: 'tiptap-editor no-twp',
6252
- },
6253
- }]
6179
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: CharacterCountComponent, decorators: [{
6180
+ type: Component,
6181
+ args: [{ selector: 'document-engine-character-count', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"document-engine-character-count\">\n <span\n class=\"document-engine-character-count__text\"\n [class.document-engine-character-count__text--muted]=\"!isNearLimit() && !isOverLimit()\"\n [class.document-engine-character-count__text--warning]=\"isNearLimit() && !isOverLimit()\"\n [class.document-engine-character-count__text--error]=\"isOverLimit()\"\n >\n <ng-container *ngIf=\"mode === 'characters'\">\n <ng-container *ngIf=\"limit; else noLimit\"> {{ count }} / {{ limit }} characters. </ng-container>\n <ng-template #noLimit> {{ count }} {{ count === 1 ? 'character' : 'characters' }}. </ng-template>\n </ng-container>\n <ng-container *ngIf=\"mode === 'words'\"> {{ count }} {{ count === 1 ? 'word' : 'words' }}. </ng-container>\n </span>\n</div>\n", styles: [".document-engine-character-count{display:flex;justify-content:flex-end}.document-engine-character-count__text{font-size:.875rem;transition:color .2s ease-in-out}.document-engine-character-count__text--muted{color:var(--muted-foreground, #6b7280)}.document-engine-character-count__text--warning{color:var(--warning, #d97706)}.document-engine-character-count__text--error{color:var(--destructive, #dc2626)}\n"] }]
6254
6182
  }], propDecorators: { editor: [{
6255
6183
  type: Input
6256
- }], outputFormat: [{
6184
+ }], config: [{
6185
+ type: Input
6186
+ }], mode: [{
6257
6187
  type: Input
6258
6188
  }] } });
6259
6189
 
6260
- class TiptapFloatingMenuDirective {
6261
- elRef;
6262
- constructor(elRef) {
6263
- this.elRef = elRef;
6190
+ /**
6191
+ * Footer component for document editor
6192
+ * Container for footer components like character count
6193
+ */
6194
+ class FooterComponent {
6195
+ editor;
6196
+ config;
6197
+ // @Input() editable = true;
6198
+ hasCharacterCount() {
6199
+ const config = this.config ?? {};
6200
+ return config.characterCount !== false;
6264
6201
  }
6265
- pluginKey = 'TiptapFloatingMenu';
6202
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: FooterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6203
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.10", type: FooterComponent, isStandalone: true, selector: "document-engine-footer", inputs: { editor: "editor", config: "config" }, ngImport: i0, template: "<div class=\"document-engine-document-editor__footer\">\n <div class=\"document-engine-document-editor__footer-content\">\n <div class=\"document-engine-document-editor__footer-left\">\n <!-- Additional footer components can be added here -->\n </div>\n\n <div class=\"document-engine-document-editor__footer-right\">\n <document-engine-character-count\n *ngIf=\"hasCharacterCount()\"\n [editor]=\"editor\"\n [config]=\"config\"\n mode=\"characters\"\n ></document-engine-character-count>\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: CharacterCountComponent, selector: "document-engine-character-count", inputs: ["editor", "config", "mode"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6204
+ }
6205
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: FooterComponent, decorators: [{
6206
+ type: Component,
6207
+ args: [{ selector: 'document-engine-footer', standalone: true, imports: [CommonModule, CharacterCountComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"document-engine-document-editor__footer\">\n <div class=\"document-engine-document-editor__footer-content\">\n <div class=\"document-engine-document-editor__footer-left\">\n <!-- Additional footer components can be added here -->\n </div>\n\n <div class=\"document-engine-document-editor__footer-right\">\n <document-engine-character-count\n *ngIf=\"hasCharacterCount()\"\n [editor]=\"editor\"\n [config]=\"config\"\n mode=\"characters\"\n ></document-engine-character-count>\n </div>\n </div>\n</div>\n" }]
6208
+ }], propDecorators: { editor: [{
6209
+ type: Input
6210
+ }], config: [{
6211
+ type: Input
6212
+ }] } });
6213
+
6214
+ /**
6215
+ * Document Editor Component
6216
+ * High-level wrapper that encapsulates TiptapEditor and DocumentEngineKit
6217
+ * Provides a simple API for consumers without exposing internal dependencies
6218
+ *
6219
+ * Usage:
6220
+ * <document-engine-editor
6221
+ * #editor
6222
+ * [config]="editorConfig"
6223
+ *
6224
+ * (editorReady)="editor = docEditor.editor"
6225
+ * >
6226
+ * <tiptap-editor
6227
+ * [editor]="docEditor.editor"
6228
+ * [(ngModel)]="content"
6229
+ * ></tiptap-editor>
6230
+ * </document-engine-editor>
6231
+ */
6232
+ class DocumentEditorComponent {
6233
+ ngZone = inject(NgZone);
6234
+ contentWrapperClass = inject(EDITOR_CONTENT_WRAPPER_CLASS, { optional: true });
6235
+ config;
6236
+ editorReady = new EventEmitter();
6237
+ // Expose editor property for external access (re-export)
6266
6238
  editor;
6267
- options = {};
6268
- shouldShow;
6239
+ // Store merged config for toolbar and helper methods
6240
+ get mergedConfig() {
6241
+ return this._mergedConfig ?? this.getMergedConfig();
6242
+ }
6243
+ _mergedConfig;
6244
+ // Capabilities instance for toolbar
6245
+ capabilities;
6246
+ updateCapabilities() {
6247
+ this.capabilities = new EditorCapabilities(this.mergedConfig);
6248
+ }
6249
+ ngOnChanges(changes) {
6250
+ if (!changes['config'])
6251
+ return;
6252
+ if (!this.editor)
6253
+ return;
6254
+ this._mergedConfig = this.getMergedConfig();
6255
+ this.updateCapabilities();
6256
+ this.editor.setEditable(this._mergedConfig.editable ?? true);
6257
+ }
6269
6258
  ngOnInit() {
6270
- const editor = this.editor;
6271
- if (!editor) {
6272
- throw new Error('Required: Input `editor`');
6273
- }
6274
- const floatingMenuElement = this.elRef.nativeElement;
6275
- floatingMenuElement.style.visibility = 'hidden';
6276
- floatingMenuElement.style.position = 'absolute';
6277
- editor.registerPlugin(FloatingMenuPlugin({
6278
- pluginKey: this.pluginKey,
6279
- editor,
6280
- element: floatingMenuElement,
6281
- options: this.options,
6282
- shouldShow: this.shouldShow,
6283
- }));
6259
+ this.initializeEditor();
6284
6260
  }
6285
6261
  ngOnDestroy() {
6286
- if (this.editor && this.pluginKey) {
6287
- this.editor.unregisterPlugin(this.pluginKey);
6288
- }
6289
- window.requestAnimationFrame(() => {
6290
- if (this.elRef.nativeElement.parentNode) {
6291
- this.elRef.nativeElement.parentNode.removeChild(this.elRef.nativeElement);
6292
- }
6262
+ if (this.editor)
6263
+ this.editor.destroy();
6264
+ }
6265
+ // Internal methods
6266
+ initializeEditor() {
6267
+ // Run editor initialization outside Angular zone for better performance
6268
+ // Editor operations (especially typing) don't need to trigger Angular change detection
6269
+ this.ngZone.runOutsideAngular(() => {
6270
+ // Merge user config with default full-featured config
6271
+ this._mergedConfig = this.getMergedConfig();
6272
+ // Initialize editor with config
6273
+ // Initial content will be set by TiptapEditorDirective via writeValue
6274
+ this.editor = new Editor({
6275
+ editable: this._mergedConfig.editable,
6276
+ extensions: [DocumentEngineKit.configure(this._mergedConfig)],
6277
+ editorProps: { attributes: { class: this.contentWrapperClass ?? '' } },
6278
+ });
6279
+ // Emit editorReady event back inside Angular zone
6280
+ this.ngZone.run(() => {
6281
+ // Create capabilities instance after editor is initialized
6282
+ this.updateCapabilities();
6283
+ this.editorReady.emit(this.editor);
6284
+ });
6293
6285
  });
6294
6286
  }
6295
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: TiptapFloatingMenuDirective, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive });
6296
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.10", type: TiptapFloatingMenuDirective, isStandalone: true, selector: "tiptap-floating-menu[editor], [tiptapFloatingMenu][editor]", inputs: { pluginKey: "pluginKey", editor: "editor", options: "options", shouldShow: "shouldShow" }, ngImport: i0 });
6287
+ /**
6288
+ * Get merged config with all extensions enabled by default
6289
+ * User config will override defaults
6290
+ */
6291
+ getMergedConfig() {
6292
+ // Merge user config with defaults, preferring user config when provided
6293
+ return this.mergeConfig(DefaultEditorConfig, this.config);
6294
+ }
6295
+ /**
6296
+ * Merge two config objects, preferring userConfig values when provided
6297
+ */
6298
+ mergeConfig(defaultConfig, userConfig) {
6299
+ if (!userConfig)
6300
+ return defaultConfig;
6301
+ const merged = { ...defaultConfig };
6302
+ const keys = Object.keys(userConfig);
6303
+ for (const key of keys) {
6304
+ const value = userConfig[key];
6305
+ if (value !== undefined)
6306
+ merged[key] = value;
6307
+ }
6308
+ return merged;
6309
+ }
6310
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: DocumentEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6311
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.10", type: DocumentEditorComponent, isStandalone: false, selector: "document-engine-editor", inputs: { config: "config" }, outputs: { editorReady: "editorReady" }, host: { classAttribute: "no-twp" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"document-engine-document-editor\">\n <document-engine-toolbar\n *ngIf=\"mergedConfig.showToolbar\"\n [editor]=\"editor\"\n [capabilities]=\"capabilities\"\n [config]=\"mergedConfig\"\n ></document-engine-toolbar>\n\n <div class=\"document-engine-document-editor__content\">\n <ng-content></ng-content>\n </div>\n\n <document-engine-footer\n *ngIf=\"editor && mergedConfig.showFooter\"\n [editor]=\"editor\"\n [config]=\"mergedConfig\"\n ></document-engine-footer>\n</div>\n", styles: [":is() *,:is() *:before,:is() *:after{box-sizing:border-box}\n"], dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: ToolbarComponent, selector: "document-engine-toolbar", inputs: ["editor", "capabilities", "config"], outputs: ["action"] }, { kind: "component", type: FooterComponent, selector: "document-engine-footer", inputs: ["editor", "config"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6297
6312
  }
6298
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: TiptapFloatingMenuDirective, decorators: [{
6299
- type: Directive,
6300
- args: [{
6301
- selector: 'tiptap-floating-menu[editor], [tiptapFloatingMenu][editor]',
6302
- }]
6303
- }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { pluginKey: [{
6304
- type: Input
6305
- }], editor: [{
6306
- type: Input
6307
- }], options: [{
6308
- type: Input
6309
- }], shouldShow: [{
6313
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: DocumentEditorComponent, decorators: [{
6314
+ type: Component,
6315
+ args: [{ selector: 'document-engine-editor', standalone: false, changeDetection: ChangeDetectionStrategy.OnPush, host: {
6316
+ class: 'no-twp',
6317
+ }, template: "<div class=\"document-engine-document-editor\">\n <document-engine-toolbar\n *ngIf=\"mergedConfig.showToolbar\"\n [editor]=\"editor\"\n [capabilities]=\"capabilities\"\n [config]=\"mergedConfig\"\n ></document-engine-toolbar>\n\n <div class=\"document-engine-document-editor__content\">\n <ng-content></ng-content>\n </div>\n\n <document-engine-footer\n *ngIf=\"editor && mergedConfig.showFooter\"\n [editor]=\"editor\"\n [config]=\"mergedConfig\"\n ></document-engine-footer>\n</div>\n", styles: [":is() *,:is() *:before,:is() *:after{box-sizing:border-box}\n"] }]
6318
+ }], propDecorators: { config: [{
6310
6319
  type: Input
6320
+ }], editorReady: [{
6321
+ type: Output
6311
6322
  }] } });
6312
6323
 
6324
+ class DocumentEditorModule {
6325
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: DocumentEditorModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
6326
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.10", ngImport: i0, type: DocumentEditorModule, declarations: [DocumentEditorComponent], imports: [CommonModule, ToolbarComponent, FooterComponent, TiptapEditorDirective], exports: [DocumentEditorComponent, TiptapEditorDirective] });
6327
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: DocumentEditorModule, imports: [CommonModule, ToolbarComponent, FooterComponent] });
6328
+ }
6329
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: DocumentEditorModule, decorators: [{
6330
+ type: NgModule,
6331
+ args: [{
6332
+ declarations: [DocumentEditorComponent],
6333
+ imports: [CommonModule, ToolbarComponent, FooterComponent, TiptapEditorDirective],
6334
+ exports: [DocumentEditorComponent, TiptapEditorDirective],
6335
+ }]
6336
+ }] });
6337
+
6313
6338
  /**
6314
6339
  * Generated bundle index. Do not edit.
6315
6340
  */
6316
6341
 
6317
- export { ARCHIVE, ARROW_DROP_DOWN, AngularRenderer, ButtonDirective, CHECK, CLEAR_ALL, CLOSE, CODE, CharacterCountComponent, CheckboxComponent, ColorPickerComponent, ColorPickerViewComponent, DEFAULT, DEFAULT_TOOLBAR_STATE, DESCRIPTION, DYNAMIC_FORM, DocumentEditorComponent, DocumentEditorModule, DocumentEngineKit, DynamicFieldsViewComponent, EDIT, EDITOR_HTML_PREPROCESSOR, EditorBubbleMenuComponent, EditorCapabilities, ErrorMessageComponent, EventManager, FORMAT_ALIGN_CENTER, FORMAT_ALIGN_JUSTIFY, FORMAT_ALIGN_LEFT, FORMAT_ALIGN_RIGHT, FORMAT_BOLD, FORMAT_CLEAR, FORMAT_COLOR_FILL, FORMAT_COLOR_TEXT, FORMAT_INDENT_DECREASE, FORMAT_INDENT_INCREASE, FORMAT_ITALIC, FORMAT_LINE_SPACING, FORMAT_LIST_BULLETED, FORMAT_LIST_NUMBERED, FORMAT_QUOTE, FORMAT_SIZE, FORMAT_STRIKETHROUGH, FORMAT_UNDERLINE, FocusTrapService, FooterComponent, INSERT_PAGE_BREAK, IconComponent, IconRegistryService, ImageInsertViewComponent, InputDirective, LINK, LINK_OFF, LabelDirective, LinkEditViewComponent, LinkMainViewComponent, LinkPropertiesViewComponent, MARKDOWN, MATCH_CASE, PHOTO, PRINT, PopoverDirective, REDO, RESTRICT_EDIT, SEARCH, SEARCH_OFF, SETTINGS, SPECIAL_CHARACTER, SUBSCRIPT, SUPERSCRIPT, SelectComponent, SelectLabelDirective, SelectOptionDirective, SpecialCharactersViewComponent, TABLE_CELL_PROPERTY, TABLE_COLUMN, TABLE_MERGE_CELL, TABLE_PROPERTY, TABLE_ROW, TABLE_ROWS, TableCellStyleViewComponent, TableCreateViewComponent, TableMainViewComponent, TableStyleViewComponent, TemplateViewComponent, TiptapBubbleMenuDirective, TiptapDraggableDirective, TiptapEditorDirective, TiptapFloatingMenuDirective, ToggleGroupComponent, ToggleOptionDirective, ToolbarBubbleMenuComponent, ToolbarComponent, ToolbarService, UNDO, VERTICAL_ALIGN_BOTTOM, VERTICAL_ALIGN_CENTER, VERTICAL_ALIGN_TOP, buildToolbarState, selectionChangeHandler, shouldShowFnFactory, urlValidator };
6342
+ export { ARCHIVE, ARROW_DROP_DOWN, AngularRenderer, ButtonDirective, CHECK, CLEAR_ALL, CLOSE, CODE, CharacterCountComponent, CheckboxComponent, ColorPickerComponent, ColorPickerViewComponent, DEFAULT, DEFAULT_TOOLBAR_STATE, DESCRIPTION, DYNAMIC_FORM, DocumentEditorComponent, DocumentEditorModule, DocumentEngineKit, DynamicFieldsViewComponent, EDIT, EDITOR_CONTENT_WRAPPER_CLASS, EDITOR_HTML_PREPROCESSOR, EditorBubbleMenuComponent, EditorCapabilities, ErrorMessageComponent, EventManager, FORMAT_ALIGN_CENTER, FORMAT_ALIGN_JUSTIFY, FORMAT_ALIGN_LEFT, FORMAT_ALIGN_RIGHT, FORMAT_BOLD, FORMAT_CLEAR, FORMAT_COLOR_FILL, FORMAT_COLOR_TEXT, FORMAT_INDENT_DECREASE, FORMAT_INDENT_INCREASE, FORMAT_ITALIC, FORMAT_LINE_SPACING, FORMAT_LIST_BULLETED, FORMAT_LIST_NUMBERED, FORMAT_QUOTE, FORMAT_SIZE, FORMAT_STRIKETHROUGH, FORMAT_UNDERLINE, FocusTrapService, FooterComponent, INSERT_PAGE_BREAK, IconComponent, IconRegistryService, ImageInsertViewComponent, InputDirective, LINK, LINK_OFF, LabelDirective, LinkEditViewComponent, LinkMainViewComponent, LinkPropertiesViewComponent, MARKDOWN, MATCH_CASE, PHOTO, PRINT, PopoverDirective, REDO, RESTRICT_EDIT, SEARCH, SEARCH_OFF, SETTINGS, SPECIAL_CHARACTER, SUBSCRIPT, SUPERSCRIPT, SelectComponent, SelectLabelDirective, SelectOptionDirective, SpecialCharactersViewComponent, TABLE_CELL_PROPERTY, TABLE_COLUMN, TABLE_MERGE_CELL, TABLE_PROPERTY, TABLE_ROW, TABLE_ROWS, TableCellStyleViewComponent, TableCreateViewComponent, TableMainViewComponent, TableStyleViewComponent, TemplateViewComponent, TiptapBubbleMenuDirective, TiptapDraggableDirective, TiptapEditorDirective, TiptapFloatingMenuDirective, ToggleGroupComponent, ToggleOptionDirective, ToolbarBubbleMenuComponent, ToolbarComponent, ToolbarService, UNDO, VERTICAL_ALIGN_BOTTOM, VERTICAL_ALIGN_CENTER, VERTICAL_ALIGN_TOP, buildToolbarState, selectionChangeHandler, shouldShowFnFactory, urlValidator };
6318
6343
  //# sourceMappingURL=phuong-tran-redoc-document-engine-angular.mjs.map