@phuong-tran-redoc/document-engine-angular 0.0.41 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,7 @@
1
+ import * as i1 from '@angular/common';
2
+ import { DOCUMENT, CommonModule } from '@angular/common';
1
3
  import * as i0 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';
4
+ import { inject, ElementRef, Input, Directive, Injectable, ApplicationRef, createComponent, HostBinding, InjectionToken, Renderer2, ChangeDetectorRef, forwardRef, ChangeDetectionStrategy, Component, ViewEncapsulation, EventEmitter, Output, HostListener, ContentChildren, ViewChild, ViewChildren, NgZone, ViewContainerRef, EnvironmentInjector, NgModule } from '@angular/core';
3
5
  import { Extension, Editor } from '@tiptap/core';
4
6
  import Blockquote from '@tiptap/extension-blockquote';
5
7
  import Bold from '@tiptap/extension-bold';
@@ -21,10 +23,8 @@ import { TextAlign } from '@tiptap/extension-text-align';
21
23
  import { TextStyleKit } from '@tiptap/extension-text-style';
22
24
  import Underline from '@tiptap/extension-underline';
23
25
  import { Dropcursor, Gapcursor, TrailingNode, UndoRedo, CharacterCount, Placeholder } from '@tiptap/extensions';
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';
26
+ import { ResetOnEnter, EditableRegion, RestrictedEditing, CustomOrderedList, StyledTableKit, ImageRef, PageBreak, ResetFormat, Indent, ClearContent, TextCase, NotumHeading, DynamicField, Color, normalizeColor, getActiveMarkRange, getSelectedCells, getCombinedCellAttributeValue, TableDefaultAttributes, getCursorCellInfo, getClosestDomElement } from '@phuong-tran-redoc/document-engine-core';
25
27
  import { BubbleMenuPlugin } from '@tiptap/extension-bubble-menu';
26
- import * as i1 from '@angular/common';
27
- import { DOCUMENT, CommonModule } from '@angular/common';
28
28
  import * as i2 from '@angular/forms';
29
29
  import { NG_VALUE_ACCESSOR, FormsModule, FormControl, ReactiveFormsModule, FormBuilder, Validators, NonNullableFormBuilder } from '@angular/forms';
30
30
  import { FloatingMenuPlugin } from '@tiptap/extension-floating-menu';
@@ -104,6 +104,7 @@ const DocumentEngineKit = Extension.create({
104
104
  add(options.superscript, Superscript);
105
105
  add(options.textAlign, TextAlign, DEFAULT_TEXT_ALIGN);
106
106
  add(options.image, Image);
107
+ add(options.imageRef, ImageRef);
107
108
  add(options.placeholder, Placeholder, DEFAULT_PLACEHOLDER);
108
109
  add(options.pageBreak, PageBreak);
109
110
  add(options.resetFormat, ResetFormat);
@@ -1033,6 +1034,118 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
1033
1034
  type: Input
1034
1035
  }] } });
1035
1036
 
1037
+ /**
1038
+ * Character Count component for document editor footer
1039
+ * Displays character/word count with optional limit and color coding
1040
+ */
1041
+ class CharacterCountComponent {
1042
+ cdr = inject(ChangeDetectorRef);
1043
+ editor;
1044
+ config;
1045
+ mode = 'characters';
1046
+ count = 0;
1047
+ limit = null;
1048
+ updateCount = () => {
1049
+ if (!this.editor) {
1050
+ return;
1051
+ }
1052
+ // Get count from editor storage if available
1053
+ const characterCountStorage = this.editor.storage.characterCount;
1054
+ if (characterCountStorage) {
1055
+ if (this.mode === 'characters') {
1056
+ this.count = characterCountStorage.characters?.() || 0;
1057
+ }
1058
+ else {
1059
+ this.count = characterCountStorage.words?.() || 0;
1060
+ }
1061
+ }
1062
+ else {
1063
+ // Fallback: calculate from text
1064
+ const text = this.editor.getText();
1065
+ if (this.mode === 'characters') {
1066
+ this.count = text.length;
1067
+ }
1068
+ else {
1069
+ this.count = text.trim() ? text.trim().split(/\s+/).length : 0;
1070
+ }
1071
+ }
1072
+ // Get limit from config
1073
+ const characterCountConfig = this.config?.characterCount;
1074
+ if (characterCountConfig && typeof characterCountConfig === 'object') {
1075
+ this.limit = characterCountConfig.limit || null;
1076
+ }
1077
+ else {
1078
+ this.limit = null;
1079
+ }
1080
+ this.cdr.markForCheck();
1081
+ };
1082
+ ngOnInit() {
1083
+ if (!this.editor) {
1084
+ return;
1085
+ }
1086
+ // Subscribe to update events to keep count in sync
1087
+ this.editor.on('update', this.updateCount);
1088
+ this.editor.on('transaction', this.updateCount);
1089
+ // Initialize count once
1090
+ this.updateCount();
1091
+ }
1092
+ ngOnDestroy() {
1093
+ if (this.editor) {
1094
+ this.editor.off('update', this.updateCount);
1095
+ this.editor.off('transaction', this.updateCount);
1096
+ }
1097
+ }
1098
+ isNearLimit() {
1099
+ if (!this.limit) {
1100
+ return false;
1101
+ }
1102
+ // Consider near limit when >= 90% of limit
1103
+ return this.count >= this.limit * 0.9 && this.count < this.limit;
1104
+ }
1105
+ isOverLimit() {
1106
+ if (!this.limit) {
1107
+ return false;
1108
+ }
1109
+ return this.count > this.limit;
1110
+ }
1111
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: CharacterCountComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1112
+ 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 });
1113
+ }
1114
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: CharacterCountComponent, decorators: [{
1115
+ type: Component,
1116
+ 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"] }]
1117
+ }], propDecorators: { editor: [{
1118
+ type: Input
1119
+ }], config: [{
1120
+ type: Input
1121
+ }], mode: [{
1122
+ type: Input
1123
+ }] } });
1124
+
1125
+ /**
1126
+ * Footer component for document editor
1127
+ * Container for footer components like character count
1128
+ */
1129
+ class FooterComponent {
1130
+ editor;
1131
+ config;
1132
+ // @Input() editable = true;
1133
+ hasCharacterCount() {
1134
+ const config = this.config ?? {};
1135
+ return config.characterCount !== false;
1136
+ }
1137
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: FooterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1138
+ 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 });
1139
+ }
1140
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: FooterComponent, decorators: [{
1141
+ type: Component,
1142
+ 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" }]
1143
+ }], propDecorators: { editor: [{
1144
+ type: Input
1145
+ }], config: [{
1146
+ type: Input
1147
+ }] } });
1148
+
1036
1149
  const COLORS = Color.fromArray([
1037
1150
  { name: 'Black', value: '#000000', type: 'hex' },
1038
1151
  { name: 'Dim gray', value: '#696969', type: 'hex' },
@@ -1920,6 +2033,39 @@ const DynamicFieldsBubbleConfig = {
1920
2033
  ],
1921
2034
  };
1922
2035
 
2036
+ /**
2037
+ * Whether a picker result is worth inserting. A `null`/`undefined` result (or one
2038
+ * with neither an `id` nor a `url`) is treated as a cancel — insert nothing.
2039
+ */
2040
+ function isInsertableResult(result) {
2041
+ return !!result && (!!result.id || !!result.url);
2042
+ }
2043
+ /** Read the consumer's `onPick` media hook off the `image` extension options, if any. */
2044
+ function getImagePickHook(editor) {
2045
+ const ext = editor.extensionManager.extensions.find((e) => e.name === 'image');
2046
+ return ext?.options?.onPick;
2047
+ }
2048
+ /**
2049
+ * Insert a picked media result: an `image-ref` node when that node is enabled
2050
+ * (preferred — URL-free), otherwise a plain image from the resolved URL. Returns
2051
+ * `true` when something was actually inserted, `false` when the result had
2052
+ * nothing usable (id-only with no `imageRef` node, and no url) — so the caller can
2053
+ * avoid closing the view as if an insert had happened.
2054
+ */
2055
+ function insertMediaResult(editor, result) {
2056
+ const hasImageRef = editor.extensionManager.extensions.some((e) => e.name === 'imageRef');
2057
+ const chain = editor.chain().focus();
2058
+ if (hasImageRef && result.id) {
2059
+ chain.insertImageRef({ imageId: result.id, caption: result.alt ?? null }).run();
2060
+ return true;
2061
+ }
2062
+ if (result.url) {
2063
+ chain.setImage({ src: result.url, alt: result.alt ?? '' }).run();
2064
+ return true;
2065
+ }
2066
+ return false;
2067
+ }
2068
+
1923
2069
  const urlValidator = (control) => {
1924
2070
  const value = (control.value ?? '').trim();
1925
2071
  if (!value)
@@ -1968,9 +2114,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
1968
2114
  }] } });
1969
2115
 
1970
2116
  /**
1971
- * Temporary image insert view for bubble menu
1972
- * Provides URL input for inserting images via link
1973
- * This is a temporary solution until full upload flow is implemented
2117
+ * Image insert view for the bubble menu.
2118
+ *
2119
+ * Two modes:
2120
+ * - When the editor's `image` config supplies an async `onPick` hook, the view
2121
+ * delegates selection to the consumer's media picker and inserts the result
2122
+ * (an `image-ref` node when that node is enabled, else a plain image).
2123
+ * - Otherwise it falls back to the built-in raw-URL input form.
1974
2124
  */
1975
2125
  class ImageInsertViewComponent {
1976
2126
  fb = inject(FormBuilder);
@@ -1978,11 +2128,22 @@ class ImageInsertViewComponent {
1978
2128
  editor;
1979
2129
  close;
1980
2130
  goBack;
2131
+ /** True while the consumer's `onPick` promise is in flight. */
2132
+ loading = false;
1981
2133
  imageForm = this.fb.nonNullable.group({
1982
2134
  url: this.fb.control('', [Validators.required, urlValidator]),
1983
2135
  alt: this.fb.control(''),
1984
2136
  });
2137
+ /** The consumer's media picker, read off the `image` extension options (if any). */
2138
+ get onPick() {
2139
+ return this.editor ? getImagePickHook(this.editor) : undefined;
2140
+ }
2141
+ /** Whether to show the picker mode (vs the raw-URL fallback form). */
2142
+ get hasPicker() {
2143
+ return typeof this.onPick === 'function';
2144
+ }
1985
2145
  onActivate() {
2146
+ this.loading = false;
1986
2147
  this.imageForm.reset();
1987
2148
  this.imageForm.controls.url.markAsUntouched();
1988
2149
  this.cdr.markForCheck();
@@ -1999,6 +2160,30 @@ class ImageInsertViewComponent {
1999
2160
  this.editor.chain().focus().setImage({ src: url, alt }).run();
2000
2161
  this.close?.();
2001
2162
  }
2163
+ /** Delegate selection to the consumer's media picker, then insert the result. */
2164
+ async pickImage() {
2165
+ const onPick = this.onPick;
2166
+ if (!onPick || !this.editor || this.loading)
2167
+ return;
2168
+ this.loading = true;
2169
+ this.cdr.markForCheck();
2170
+ try {
2171
+ const result = await onPick();
2172
+ // Reject / resolve-nothing is a deliberate no-op (consumer cancelled). Only
2173
+ // close when something was actually inserted — an id-only result with no
2174
+ // `image-ref` node inserts nothing and must leave the view open.
2175
+ if (this.editor && isInsertableResult(result) && insertMediaResult(this.editor, result)) {
2176
+ this.close?.();
2177
+ }
2178
+ }
2179
+ catch {
2180
+ // Swallow: a rejected picker (e.g. dialog dismissed) must not crash the editor.
2181
+ }
2182
+ finally {
2183
+ this.loading = false;
2184
+ this.cdr.markForCheck();
2185
+ }
2186
+ }
2002
2187
  cancel() {
2003
2188
  this.close?.();
2004
2189
  }
@@ -2008,7 +2193,23 @@ class ImageInsertViewComponent {
2008
2193
  <!-- Title -->
2009
2194
  <div class="image-insert-view__header">Insert Image</div>
2010
2195
 
2011
- <!-- Content -->
2196
+ <!-- Picker mode: delegate selection to the consumer's media library -->
2197
+ <ng-container *ngIf="hasPicker; else urlForm">
2198
+ <div class="image-insert-view__content">
2199
+ <p class="image-insert-view__hint">Choose an image from your media library.</p>
2200
+ </div>
2201
+ <div class="image-insert-view__actions">
2202
+ <button type="button" documentEngineButton variant="ghost" [disabled]="loading" (click)="cancel()">
2203
+ Cancel
2204
+ </button>
2205
+ <button type="button" documentEngineButton variant="default" [disabled]="loading" (click)="pickImage()">
2206
+ {{ loading ? 'Loading…' : 'Choose from library' }}
2207
+ </button>
2208
+ </div>
2209
+ </ng-container>
2210
+
2211
+ <!-- Fallback: built-in raw-URL input form -->
2212
+ <ng-template #urlForm>
2012
2213
  <form [formGroup]="imageForm" (ngSubmit)="insertImage()">
2013
2214
  <div class="image-insert-view__content">
2014
2215
  <!-- Image URL input -->
@@ -2048,8 +2249,9 @@ class ImageInsertViewComponent {
2048
2249
  <button type="submit" documentEngineButton variant="default" [disabled]="!canInsert()">Insert Image</button>
2049
2250
  </div>
2050
2251
  </form>
2252
+ </ng-template>
2051
2253
  </div>
2052
- `, isInline: true, styles: [".image-insert-view{display:flex;flex-direction:column;min-width:320px}.image-insert-view__header{padding:.5rem 1rem;border-bottom:1px solid var(--border);font-weight:500;font-size:.875rem}.image-insert-view__content{display:flex;flex-direction:column;gap:.75rem;padding:1rem .5rem}.image-insert-view__field{display:flex;flex-direction:column;gap:.25rem;width:100%}.image-insert-view__actions{display:flex;gap:.5rem;justify-content:flex-end;padding:.5rem;border-top:1px solid var(--border)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { 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.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { 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: ErrorMessageComponent, selector: "document-engine-error-message", inputs: ["class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2254
+ `, isInline: true, styles: [".image-insert-view{display:flex;flex-direction:column;min-width:320px}.image-insert-view__header{padding:.5rem 1rem;border-bottom:1px solid var(--border);font-weight:500;font-size:.875rem}.image-insert-view__content{display:flex;flex-direction:column;gap:.75rem;padding:1rem .5rem}.image-insert-view__hint{margin:0;font-size:.8125rem;color:var(--muted-foreground)}.image-insert-view__field{display:flex;flex-direction:column;gap:.25rem;width:100%}.image-insert-view__actions{display:flex;gap:.5rem;justify-content:flex-end;padding:.5rem;border-top:1px solid var(--border)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { 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.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { 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: ErrorMessageComponent, selector: "document-engine-error-message", inputs: ["class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2053
2255
  }
2054
2256
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: ImageInsertViewComponent, decorators: [{
2055
2257
  type: Component,
@@ -2058,7 +2260,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
2058
2260
  <!-- Title -->
2059
2261
  <div class="image-insert-view__header">Insert Image</div>
2060
2262
 
2061
- <!-- Content -->
2263
+ <!-- Picker mode: delegate selection to the consumer's media library -->
2264
+ <ng-container *ngIf="hasPicker; else urlForm">
2265
+ <div class="image-insert-view__content">
2266
+ <p class="image-insert-view__hint">Choose an image from your media library.</p>
2267
+ </div>
2268
+ <div class="image-insert-view__actions">
2269
+ <button type="button" documentEngineButton variant="ghost" [disabled]="loading" (click)="cancel()">
2270
+ Cancel
2271
+ </button>
2272
+ <button type="button" documentEngineButton variant="default" [disabled]="loading" (click)="pickImage()">
2273
+ {{ loading ? 'Loading…' : 'Choose from library' }}
2274
+ </button>
2275
+ </div>
2276
+ </ng-container>
2277
+
2278
+ <!-- Fallback: built-in raw-URL input form -->
2279
+ <ng-template #urlForm>
2062
2280
  <form [formGroup]="imageForm" (ngSubmit)="insertImage()">
2063
2281
  <div class="image-insert-view__content">
2064
2282
  <!-- Image URL input -->
@@ -2098,8 +2316,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
2098
2316
  <button type="submit" documentEngineButton variant="default" [disabled]="!canInsert()">Insert Image</button>
2099
2317
  </div>
2100
2318
  </form>
2319
+ </ng-template>
2101
2320
  </div>
2102
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".image-insert-view{display:flex;flex-direction:column;min-width:320px}.image-insert-view__header{padding:.5rem 1rem;border-bottom:1px solid var(--border);font-weight:500;font-size:.875rem}.image-insert-view__content{display:flex;flex-direction:column;gap:.75rem;padding:1rem .5rem}.image-insert-view__field{display:flex;flex-direction:column;gap:.25rem;width:100%}.image-insert-view__actions{display:flex;gap:.5rem;justify-content:flex-end;padding:.5rem;border-top:1px solid var(--border)}\n"] }]
2321
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".image-insert-view{display:flex;flex-direction:column;min-width:320px}.image-insert-view__header{padding:.5rem 1rem;border-bottom:1px solid var(--border);font-weight:500;font-size:.875rem}.image-insert-view__content{display:flex;flex-direction:column;gap:.75rem;padding:1rem .5rem}.image-insert-view__hint{margin:0;font-size:.8125rem;color:var(--muted-foreground)}.image-insert-view__field{display:flex;flex-direction:column;gap:.25rem;width:100%}.image-insert-view__actions{display:flex;gap:.5rem;justify-content:flex-end;padding:.5rem;border-top:1px solid var(--border)}\n"] }]
2103
2322
  }] });
2104
2323
 
2105
2324
  const ImageBubbleConfig = {
@@ -5340,7 +5559,7 @@ class EditorBubbleMenuComponent {
5340
5559
  // Activate new view
5341
5560
  this.activateView(this.currentViewRef);
5342
5561
  // Trigger change detection for the child component to render immediately
5343
- // This is especially important for Angular v14 compatibility
5562
+ // (the view is created imperatively, so it is outside Angular's normal CD pass)
5344
5563
  this.cdr.markForCheck();
5345
5564
  // Mark bubble as visible for transition handling
5346
5565
  const bubbleElement = this.bubbleElement?.nativeElement;
@@ -6099,118 +6318,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
6099
6318
  args: ['tableCreateBubbleMenu']
6100
6319
  }] } });
6101
6320
 
6102
- /**
6103
- * Character Count component for document editor footer
6104
- * Displays character/word count with optional limit and color coding
6105
- */
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;
6116
- }
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
- }
6126
- }
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
- }
6136
- }
6137
- // Get limit from config
6138
- const characterCountConfig = this.config?.characterCount;
6139
- if (characterCountConfig && typeof characterCountConfig === 'object') {
6140
- this.limit = characterCountConfig.limit || null;
6141
- }
6142
- else {
6143
- this.limit = null;
6144
- }
6145
- this.cdr.markForCheck();
6146
- };
6147
- ngOnInit() {
6148
- if (!this.editor) {
6149
- return;
6150
- }
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();
6156
- }
6157
- ngOnDestroy() {
6158
- if (this.editor) {
6159
- this.editor.off('update', this.updateCount);
6160
- this.editor.off('transaction', this.updateCount);
6161
- }
6162
- }
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 });
6178
- }
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"] }]
6182
- }], propDecorators: { editor: [{
6183
- type: Input
6184
- }], config: [{
6185
- type: Input
6186
- }], mode: [{
6187
- type: Input
6188
- }] } });
6189
-
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;
6201
- }
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
6321
  /**
6215
6322
  * Document Editor Component
6216
6323
  * High-level wrapper that encapsulates TiptapEditor and DocumentEngineKit
@@ -6308,11 +6415,11 @@ class DocumentEditorComponent {
6308
6415
  return merged;
6309
6416
  }
6310
6417
  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 });
6418
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.10", type: DocumentEditorComponent, isStandalone: true, 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: "ngmodule", type: CommonModule }, { 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 });
6312
6419
  }
6313
6420
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: DocumentEditorComponent, decorators: [{
6314
6421
  type: Component,
6315
- args: [{ selector: 'document-engine-editor', standalone: false, changeDetection: ChangeDetectionStrategy.OnPush, host: {
6422
+ args: [{ selector: 'document-engine-editor', imports: [CommonModule, ToolbarComponent, FooterComponent], changeDetection: ChangeDetectionStrategy.OnPush, host: {
6316
6423
  class: 'no-twp',
6317
6424
  }, 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
6425
  }], propDecorators: { config: [{
@@ -6321,16 +6428,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
6321
6428
  type: Output
6322
6429
  }] } });
6323
6430
 
6431
+ /**
6432
+ * Back-compat shim for NgModule-based consumers. `DocumentEditorComponent` is now
6433
+ * standalone (and `@defer`-friendly); standalone consumers should import it (and
6434
+ * `TiptapEditorDirective`) directly instead of this module.
6435
+ */
6324
6436
  class DocumentEditorModule {
6325
6437
  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] });
6438
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.10", ngImport: i0, type: DocumentEditorModule, imports: [DocumentEditorComponent, TiptapEditorDirective], exports: [DocumentEditorComponent, TiptapEditorDirective] });
6439
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: DocumentEditorModule, imports: [DocumentEditorComponent] });
6328
6440
  }
6329
6441
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: DocumentEditorModule, decorators: [{
6330
6442
  type: NgModule,
6331
6443
  args: [{
6332
- declarations: [DocumentEditorComponent],
6333
- imports: [CommonModule, ToolbarComponent, FooterComponent, TiptapEditorDirective],
6444
+ imports: [DocumentEditorComponent, TiptapEditorDirective],
6334
6445
  exports: [DocumentEditorComponent, TiptapEditorDirective],
6335
6446
  }]
6336
6447
  }] });