@flogeez/angular-tiptap-editor 0.1.0

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.
@@ -0,0 +1,4240 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, output, Component, signal, computed, Injectable, inject, effect, ViewChild, viewChild, forwardRef } from '@angular/core';
3
+ import { NG_VALUE_ACCESSOR } from '@angular/forms';
4
+ import { Node, nodeInputRule, mergeAttributes, Extension, Editor } from '@tiptap/core';
5
+ import StarterKit from '@tiptap/starter-kit';
6
+ import Placeholder from '@tiptap/extension-placeholder';
7
+ import CharacterCount from '@tiptap/extension-character-count';
8
+ import Underline from '@tiptap/extension-underline';
9
+ import Superscript from '@tiptap/extension-superscript';
10
+ import Subscript from '@tiptap/extension-subscript';
11
+ import TextAlign from '@tiptap/extension-text-align';
12
+ import Link from '@tiptap/extension-link';
13
+ import Highlight from '@tiptap/extension-highlight';
14
+ import OfficePaste from '@intevation/tiptap-extension-office-paste';
15
+ import { Plugin, PluginKey } from '@tiptap/pm/state';
16
+ import { DecorationSet, Decoration } from '@tiptap/pm/view';
17
+ import tippy from 'tippy.js';
18
+ import { Plugin as Plugin$1, PluginKey as PluginKey$1 } from 'prosemirror-state';
19
+
20
+ const ResizableImage = Node.create({
21
+ name: "resizableImage",
22
+ addOptions() {
23
+ return {
24
+ inline: false,
25
+ allowBase64: false,
26
+ HTMLAttributes: {},
27
+ };
28
+ },
29
+ inline() {
30
+ return this.options.inline;
31
+ },
32
+ group() {
33
+ return this.options.inline ? "inline" : "block";
34
+ },
35
+ draggable: true,
36
+ addAttributes() {
37
+ return {
38
+ src: {
39
+ default: null,
40
+ },
41
+ alt: {
42
+ default: null,
43
+ },
44
+ title: {
45
+ default: null,
46
+ },
47
+ width: {
48
+ default: null,
49
+ parseHTML: (element) => {
50
+ const width = element.getAttribute("width");
51
+ return width ? parseInt(width, 10) : null;
52
+ },
53
+ renderHTML: (attributes) => {
54
+ if (!attributes["width"]) {
55
+ return {};
56
+ }
57
+ return {
58
+ width: attributes["width"],
59
+ };
60
+ },
61
+ },
62
+ height: {
63
+ default: null,
64
+ parseHTML: (element) => {
65
+ const height = element.getAttribute("height");
66
+ return height ? parseInt(height, 10) : null;
67
+ },
68
+ renderHTML: (attributes) => {
69
+ if (!attributes["height"]) {
70
+ return {};
71
+ }
72
+ return {
73
+ height: attributes["height"],
74
+ };
75
+ },
76
+ },
77
+ };
78
+ },
79
+ parseHTML() {
80
+ return [
81
+ {
82
+ tag: "img[src]",
83
+ },
84
+ ];
85
+ },
86
+ renderHTML({ HTMLAttributes }) {
87
+ return [
88
+ "img",
89
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
90
+ ];
91
+ },
92
+ addCommands() {
93
+ return {
94
+ setResizableImage: (options) => ({ commands }) => {
95
+ return commands.insertContent({
96
+ type: this.name,
97
+ attrs: options,
98
+ });
99
+ },
100
+ updateResizableImage: (options) => ({ commands }) => {
101
+ return commands.updateAttributes(this.name, options);
102
+ },
103
+ };
104
+ },
105
+ addInputRules() {
106
+ return [
107
+ nodeInputRule({
108
+ find: /!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\)/,
109
+ type: this.type,
110
+ getAttributes: (match) => {
111
+ const [, alt, src, title] = match;
112
+ return { src, alt, title };
113
+ },
114
+ }),
115
+ ];
116
+ },
117
+ addNodeView() {
118
+ return ({ node, getPos, editor }) => {
119
+ const container = document.createElement("div");
120
+ container.className = "resizable-image-container";
121
+ container.style.position = "relative";
122
+ container.style.display = "inline-block";
123
+ const img = document.createElement("img");
124
+ img.src = node.attrs["src"];
125
+ img.alt = node.attrs["alt"] || "";
126
+ img.title = node.attrs["title"] || "";
127
+ img.className = "tiptap-image";
128
+ if (node.attrs["width"])
129
+ img.width = node.attrs["width"];
130
+ if (node.attrs["height"])
131
+ img.height = node.attrs["height"];
132
+ img.parentNode?.insertBefore(container, img);
133
+ container.appendChild(img);
134
+ // Ajouter les contrôles de redimensionnement modernes
135
+ const resizeControls = document.createElement("div");
136
+ resizeControls.className = "resize-controls";
137
+ resizeControls.style.display = "none";
138
+ // Créer les 8 poignées pour un redimensionnement complet
139
+ const handles = ["nw", "n", "ne", "w", "e", "sw", "s", "se"];
140
+ handles.forEach((direction) => {
141
+ const handle = document.createElement("div");
142
+ handle.className = `resize-handle resize-handle-${direction}`;
143
+ handle.setAttribute("data-direction", direction);
144
+ resizeControls.appendChild(handle);
145
+ });
146
+ // Attacher les contrôles au conteneur
147
+ container.appendChild(resizeControls);
148
+ // Variables pour le redimensionnement
149
+ let isResizing = false;
150
+ let startX = 0;
151
+ let startY = 0;
152
+ let startWidth = 0;
153
+ let startHeight = 0;
154
+ let aspectRatio = 1;
155
+ // Calculer le ratio d'aspect
156
+ img.onload = () => {
157
+ aspectRatio = img.naturalWidth / img.naturalHeight;
158
+ };
159
+ // Gestion du redimensionnement
160
+ const handleMouseDown = (e, direction) => {
161
+ e.preventDefault();
162
+ e.stopPropagation();
163
+ isResizing = true;
164
+ startX = e.clientX;
165
+ startY = e.clientY;
166
+ // Utiliser les dimensions actuelles de l'image au lieu des dimensions initiales
167
+ startWidth =
168
+ parseInt(img.getAttribute("width") || "0") ||
169
+ node.attrs["width"] ||
170
+ img.naturalWidth;
171
+ startHeight =
172
+ parseInt(img.getAttribute("height") || "0") ||
173
+ node.attrs["height"] ||
174
+ img.naturalHeight;
175
+ // Ajouter la classe de redimensionnement au body
176
+ document.body.classList.add("resizing");
177
+ const handleMouseMove = (e) => {
178
+ if (!isResizing)
179
+ return;
180
+ const deltaX = e.clientX - startX;
181
+ const deltaY = e.clientY - startY;
182
+ let newWidth = startWidth;
183
+ let newHeight = startHeight;
184
+ // Redimensionnement selon la direction
185
+ switch (direction) {
186
+ case "e":
187
+ newWidth = startWidth + deltaX;
188
+ newHeight = newWidth / aspectRatio;
189
+ break;
190
+ case "w":
191
+ newWidth = startWidth - deltaX;
192
+ newHeight = newWidth / aspectRatio;
193
+ break;
194
+ case "s":
195
+ newHeight = startHeight + deltaY;
196
+ newWidth = newHeight * aspectRatio;
197
+ break;
198
+ case "n":
199
+ newHeight = startHeight - deltaY;
200
+ newWidth = newHeight * aspectRatio;
201
+ break;
202
+ case "se":
203
+ newWidth = startWidth + deltaX;
204
+ newHeight = startHeight + deltaY;
205
+ break;
206
+ case "sw":
207
+ newWidth = startWidth - deltaX;
208
+ newHeight = startHeight + deltaY;
209
+ break;
210
+ case "ne":
211
+ newWidth = startWidth + deltaX;
212
+ newHeight = startHeight - deltaY;
213
+ break;
214
+ case "nw":
215
+ newWidth = startWidth - deltaX;
216
+ newHeight = startHeight - deltaY;
217
+ break;
218
+ }
219
+ // Limites
220
+ newWidth = Math.max(50, Math.min(2000, newWidth));
221
+ newHeight = Math.max(50, Math.min(2000, newHeight));
222
+ // Mettre à jour directement les attributs de l'image
223
+ img.setAttribute("width", Math.round(newWidth).toString());
224
+ img.setAttribute("height", Math.round(newHeight).toString());
225
+ };
226
+ const handleMouseUp = () => {
227
+ isResizing = false;
228
+ document.body.classList.remove("resizing");
229
+ // Mettre à jour le nœud Tiptap avec les nouvelles dimensions
230
+ if (typeof getPos === "function") {
231
+ const finalWidth = parseInt(img.getAttribute("width") || "0");
232
+ const finalHeight = parseInt(img.getAttribute("height") || "0");
233
+ if (finalWidth && finalHeight) {
234
+ editor.commands.updateAttributes("resizableImage", {
235
+ width: finalWidth,
236
+ height: finalHeight,
237
+ });
238
+ }
239
+ }
240
+ document.removeEventListener("mousemove", handleMouseMove);
241
+ document.removeEventListener("mouseup", handleMouseUp);
242
+ };
243
+ document.addEventListener("mousemove", handleMouseMove);
244
+ document.addEventListener("mouseup", handleMouseUp);
245
+ };
246
+ // Ajouter les événements aux poignées
247
+ resizeControls.addEventListener("mousedown", (e) => {
248
+ const target = e.target;
249
+ if (target.classList.contains("resize-handle")) {
250
+ const direction = target.getAttribute("data-direction");
251
+ if (direction) {
252
+ handleMouseDown(e, direction);
253
+ }
254
+ }
255
+ });
256
+ // Gestion des événements
257
+ img.addEventListener("click", () => {
258
+ // Masquer tous les autres contrôles
259
+ document.querySelectorAll(".resize-controls").forEach((control) => {
260
+ control.style.display = "none";
261
+ });
262
+ // Afficher les contrôles de cette image
263
+ resizeControls.style.display = "block";
264
+ img.classList.add("selected");
265
+ });
266
+ // Masquer les contrôles quand on clique ailleurs
267
+ document.addEventListener("click", (e) => {
268
+ const target = e.target;
269
+ if (target &&
270
+ !img.contains(target) &&
271
+ !resizeControls.contains(target)) {
272
+ resizeControls.style.display = "none";
273
+ img.classList.remove("selected");
274
+ }
275
+ });
276
+ return {
277
+ dom: container,
278
+ update: (updatedNode) => {
279
+ if (updatedNode.type.name !== "resizableImage")
280
+ return false;
281
+ img.src = updatedNode.attrs["src"];
282
+ img.alt = updatedNode.attrs["alt"] || "";
283
+ img.title = updatedNode.attrs["title"] || "";
284
+ if (updatedNode.attrs["width"])
285
+ img.width = updatedNode.attrs["width"];
286
+ if (updatedNode.attrs["height"])
287
+ img.height = updatedNode.attrs["height"];
288
+ return true;
289
+ },
290
+ };
291
+ };
292
+ },
293
+ });
294
+
295
+ const UploadProgress = Extension.create({
296
+ name: "uploadProgress",
297
+ addOptions() {
298
+ return {
299
+ isUploading: () => false,
300
+ uploadProgress: () => 0,
301
+ uploadMessage: () => "",
302
+ };
303
+ },
304
+ addProseMirrorPlugins() {
305
+ const options = this.options;
306
+ return [
307
+ new Plugin({
308
+ key: new PluginKey("uploadProgress"),
309
+ state: {
310
+ init() {
311
+ return {
312
+ decorations: DecorationSet.empty,
313
+ isUploading: false,
314
+ uploadPosition: null,
315
+ };
316
+ },
317
+ apply: (tr, state) => {
318
+ const isUploading = options.isUploading();
319
+ // Si l'upload commence, sauvegarder la position
320
+ if (isUploading && !state.isUploading) {
321
+ const uploadPosition = tr.selection.from;
322
+ const uploadProgress = options.uploadProgress();
323
+ const uploadMessage = options.uploadMessage();
324
+ // Créer un élément de progression
325
+ const uploadElement = document.createElement("div");
326
+ uploadElement.className = "upload-progress-widget";
327
+ uploadElement.innerHTML = `
328
+ <div class="upload-skeleton">
329
+ <div class="upload-content">
330
+ <div class="upload-icon">
331
+ <span class="material-symbols-outlined spinning">image</span>
332
+ </div>
333
+ <div class="upload-info">
334
+ <div class="upload-message">${uploadMessage}</div>
335
+ <div class="progress-bar">
336
+ <div class="progress-fill" style="width: ${uploadProgress}%"></div>
337
+ </div>
338
+ <div class="progress-text">${uploadProgress}%</div>
339
+ </div>
340
+ </div>
341
+ </div>
342
+ `;
343
+ // Ajouter les styles si pas déjà fait
344
+ if (!document.querySelector("#upload-progress-styles")) {
345
+ const style = document.createElement("style");
346
+ style.id = "upload-progress-styles";
347
+ style.textContent = `
348
+ .upload-progress-widget {
349
+ display: block;
350
+ margin: 8px 0;
351
+ max-width: 400px;
352
+ }
353
+ .upload-skeleton {
354
+ background: #f8f9fa;
355
+ border: 2px dashed #e2e8f0;
356
+ border-radius: 8px;
357
+ padding: 16px;
358
+ display: flex;
359
+ align-items: center;
360
+ justify-content: center;
361
+ min-height: 120px;
362
+ animation: pulse 2s infinite;
363
+ }
364
+ @keyframes pulse {
365
+ 0%, 100% { background-color: #f8f9fa; }
366
+ 50% { background-color: #f1f5f9; }
367
+ }
368
+ .upload-content {
369
+ display: flex;
370
+ flex-direction: column;
371
+ align-items: center;
372
+ gap: 12px;
373
+ text-align: center;
374
+ }
375
+ .upload-icon {
376
+ display: flex;
377
+ align-items: center;
378
+ justify-content: center;
379
+ width: 48px;
380
+ height: 48px;
381
+ background: #e6f3ff;
382
+ border-radius: 50%;
383
+ color: #3182ce;
384
+ }
385
+ .upload-icon .material-symbols-outlined {
386
+ font-size: 24px;
387
+ }
388
+ .spinning {
389
+ animation: spin 1s linear infinite;
390
+ }
391
+ @keyframes spin {
392
+ from { transform: rotate(0deg); }
393
+ to { transform: rotate(360deg); }
394
+ }
395
+ .upload-info {
396
+ display: flex;
397
+ flex-direction: column;
398
+ gap: 8px;
399
+ width: 100%;
400
+ max-width: 200px;
401
+ }
402
+ .upload-message {
403
+ font-size: 14px;
404
+ color: #4a5568;
405
+ font-weight: 500;
406
+ }
407
+ .progress-bar {
408
+ width: 100%;
409
+ height: 6px;
410
+ background: #e2e8f0;
411
+ border-radius: 3px;
412
+ overflow: hidden;
413
+ }
414
+ .progress-fill {
415
+ height: 100%;
416
+ background: linear-gradient(90deg, #3182ce, #4299e1);
417
+ border-radius: 3px;
418
+ transition: width 0.3s ease;
419
+ }
420
+ .progress-text {
421
+ font-size: 12px;
422
+ color: #718096;
423
+ font-weight: 500;
424
+ }
425
+ `;
426
+ document.head.appendChild(style);
427
+ }
428
+ // Créer une décoration widget à la position sauvegardée
429
+ const decoration = Decoration.widget(uploadPosition, uploadElement, {
430
+ side: 1,
431
+ key: "upload-progress",
432
+ });
433
+ return {
434
+ decorations: DecorationSet.create(tr.doc, [decoration]),
435
+ isUploading: true,
436
+ uploadPosition,
437
+ };
438
+ }
439
+ // Si l'upload continue, mettre à jour le contenu
440
+ if (isUploading &&
441
+ state.isUploading &&
442
+ state.uploadPosition !== null) {
443
+ const uploadProgress = options.uploadProgress();
444
+ const uploadMessage = options.uploadMessage();
445
+ // Mettre à jour le contenu de l'élément existant
446
+ const existingElement = document.querySelector(".upload-progress-widget");
447
+ if (existingElement) {
448
+ existingElement.innerHTML = `
449
+ <div class="upload-skeleton">
450
+ <div class="upload-content">
451
+ <div class="upload-icon">
452
+ <span class="material-symbols-outlined spinning">image</span>
453
+ </div>
454
+ <div class="upload-info">
455
+ <div class="upload-message">${uploadMessage}</div>
456
+ <div class="progress-bar">
457
+ <div class="progress-fill" style="width: ${uploadProgress}%"></div>
458
+ </div>
459
+ <div class="progress-text">${uploadProgress}%</div>
460
+ </div>
461
+ </div>
462
+ </div>
463
+ `;
464
+ }
465
+ return state; // Garder les décorations existantes
466
+ }
467
+ // Si l'upload se termine, supprimer les décorations
468
+ if (!isUploading && state.isUploading) {
469
+ return {
470
+ decorations: DecorationSet.empty,
471
+ isUploading: false,
472
+ uploadPosition: null,
473
+ };
474
+ }
475
+ return state;
476
+ },
477
+ },
478
+ props: {
479
+ decorations(state) {
480
+ const pluginState = this.getState(state);
481
+ return pluginState ? pluginState.decorations : DecorationSet.empty;
482
+ },
483
+ },
484
+ }),
485
+ ];
486
+ },
487
+ });
488
+
489
+ class TiptapButtonComponent {
490
+ constructor() {
491
+ // Inputs
492
+ this.icon = input.required();
493
+ this.title = input.required();
494
+ this.active = input(false);
495
+ this.disabled = input(false);
496
+ this.variant = input("default");
497
+ this.size = input("medium");
498
+ this.iconSize = input("medium");
499
+ // Outputs
500
+ this.onClick = output();
501
+ }
502
+ onMouseDown(event) {
503
+ event.preventDefault();
504
+ }
505
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
506
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.0.0", type: TiptapButtonComponent, isStandalone: true, selector: "tiptap-button", inputs: { icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: true, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: true, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, iconSize: { classPropertyName: "iconSize", publicName: "iconSize", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onClick: "onClick" }, ngImport: i0, template: `
507
+ <button
508
+ class="tiptap-button"
509
+ [class.is-active]="active()"
510
+ [class.is-disabled]="disabled()"
511
+ [class.text-button]="variant() === 'text'"
512
+ [class.danger]="variant() === 'danger'"
513
+ [class.small]="size() === 'small'"
514
+ [class.medium]="size() === 'medium'"
515
+ [class.large]="size() === 'large'"
516
+ [disabled]="disabled()"
517
+ [attr.title]="title()"
518
+ (mousedown)="onMouseDown($event)"
519
+ (click)="onClick.emit($event)"
520
+ type="button"
521
+ >
522
+ <span
523
+ class="material-symbols-outlined"
524
+ [class.icon-small]="iconSize() === 'small'"
525
+ [class.icon-medium]="iconSize() === 'medium'"
526
+ [class.icon-large]="iconSize() === 'large'"
527
+ >{{ icon() }}</span
528
+ >
529
+ <ng-content></ng-content>
530
+ </button>
531
+ `, isInline: true, styles: [".tiptap-button{display:flex;align-items:center;justify-content:center;width:32px;height:32px;border:none;background:transparent;border-radius:8px;cursor:pointer;transition:all .2s cubic-bezier(.4,0,.2,1);color:#64748b;position:relative;overflow:hidden}.tiptap-button:before{content:\"\";position:absolute;inset:0;background:linear-gradient(135deg,#6366f1,#8b5cf6);opacity:0;transition:opacity .2s ease;border-radius:8px}.tiptap-button:hover{color:#6366f1;transform:translateY(-1px)}.tiptap-button:hover:before{opacity:.1}.tiptap-button:active{transform:translateY(0)}.tiptap-button.is-active{color:#6366f1;background:#6366f11a}.tiptap-button.is-active:before{opacity:.15}.tiptap-button.is-active:hover{background:#6366f126}.tiptap-button:disabled{opacity:.5;cursor:not-allowed;pointer-events:none}.tiptap-button:disabled:hover{transform:none;color:#64748b}.tiptap-button:disabled:before{opacity:0}.tiptap-button .material-symbols-outlined{font-size:20px;position:relative;z-index:1}.tiptap-button .material-symbols-outlined.icon-small{font-size:16px}.tiptap-button .material-symbols-outlined.icon-medium{font-size:20px}.tiptap-button .material-symbols-outlined.icon-large{font-size:24px}.tiptap-button.text-button{width:auto;padding:0 12px;font-size:14px;font-weight:500}.tiptap-button.color-button{width:28px;height:28px;border-radius:50%;border:2px solid transparent;transition:all .2s ease}.tiptap-button.color-button:hover{border-color:#e2e8f0;transform:scale(1.1)}.tiptap-button.color-button.is-active{border-color:#6366f1;box-shadow:0 0 0 2px #6366f133}.tiptap-button.primary{background:linear-gradient(135deg,#6366f1,#8b5cf6);color:#fff}.tiptap-button.primary:hover{background:linear-gradient(135deg,#5b21b6,#7c3aed);color:#fff}.tiptap-button.secondary{background:#f1f5f9;color:#64748b}.tiptap-button.secondary:hover{background:#e2e8f0;color:#475569}.tiptap-button.danger{color:#ef4444}.tiptap-button.danger:hover{color:#dc2626;background:#ef44441a}.tiptap-button.danger:before{background:linear-gradient(135deg,#ef4444,#dc2626)}.tiptap-button.small{width:24px;height:24px}.tiptap-button.medium{width:32px;height:32px}.tiptap-button.large{width:40px;height:40px}.tiptap-button.has-badge{position:relative}.tiptap-button .badge{position:absolute;top:-4px;right:-4px;background:#ef4444;color:#fff;font-size:10px;padding:2px 4px;border-radius:8px;min-width:16px;text-align:center;line-height:1}.tiptap-button.has-tooltip{position:relative}.tiptap-button .tooltip{position:absolute;bottom:-30px;left:50%;transform:translate(-50%);background:#000c;color:#fff;padding:4px 8px;border-radius:4px;font-size:12px;white-space:nowrap;opacity:0;visibility:hidden;transition:all .2s ease;z-index:1000}.tiptap-button:hover .tooltip{opacity:1;visibility:visible}@keyframes pulse{0%,to{box-shadow:0 0 #6366f166}50%{box-shadow:0 0 0 4px #6366f100}}.tiptap-button.is-active.pulse{animation:pulse 2s infinite}@media (max-width: 768px){.tiptap-button{width:32px;height:32px}.tiptap-button .material-symbols-outlined{font-size:18px}.tiptap-button.text-button{padding:0 8px;font-size:13px}}\n"] }); }
532
+ }
533
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapButtonComponent, decorators: [{
534
+ type: Component,
535
+ args: [{ selector: "tiptap-button", standalone: true, template: `
536
+ <button
537
+ class="tiptap-button"
538
+ [class.is-active]="active()"
539
+ [class.is-disabled]="disabled()"
540
+ [class.text-button]="variant() === 'text'"
541
+ [class.danger]="variant() === 'danger'"
542
+ [class.small]="size() === 'small'"
543
+ [class.medium]="size() === 'medium'"
544
+ [class.large]="size() === 'large'"
545
+ [disabled]="disabled()"
546
+ [attr.title]="title()"
547
+ (mousedown)="onMouseDown($event)"
548
+ (click)="onClick.emit($event)"
549
+ type="button"
550
+ >
551
+ <span
552
+ class="material-symbols-outlined"
553
+ [class.icon-small]="iconSize() === 'small'"
554
+ [class.icon-medium]="iconSize() === 'medium'"
555
+ [class.icon-large]="iconSize() === 'large'"
556
+ >{{ icon() }}</span
557
+ >
558
+ <ng-content></ng-content>
559
+ </button>
560
+ `, styles: [".tiptap-button{display:flex;align-items:center;justify-content:center;width:32px;height:32px;border:none;background:transparent;border-radius:8px;cursor:pointer;transition:all .2s cubic-bezier(.4,0,.2,1);color:#64748b;position:relative;overflow:hidden}.tiptap-button:before{content:\"\";position:absolute;inset:0;background:linear-gradient(135deg,#6366f1,#8b5cf6);opacity:0;transition:opacity .2s ease;border-radius:8px}.tiptap-button:hover{color:#6366f1;transform:translateY(-1px)}.tiptap-button:hover:before{opacity:.1}.tiptap-button:active{transform:translateY(0)}.tiptap-button.is-active{color:#6366f1;background:#6366f11a}.tiptap-button.is-active:before{opacity:.15}.tiptap-button.is-active:hover{background:#6366f126}.tiptap-button:disabled{opacity:.5;cursor:not-allowed;pointer-events:none}.tiptap-button:disabled:hover{transform:none;color:#64748b}.tiptap-button:disabled:before{opacity:0}.tiptap-button .material-symbols-outlined{font-size:20px;position:relative;z-index:1}.tiptap-button .material-symbols-outlined.icon-small{font-size:16px}.tiptap-button .material-symbols-outlined.icon-medium{font-size:20px}.tiptap-button .material-symbols-outlined.icon-large{font-size:24px}.tiptap-button.text-button{width:auto;padding:0 12px;font-size:14px;font-weight:500}.tiptap-button.color-button{width:28px;height:28px;border-radius:50%;border:2px solid transparent;transition:all .2s ease}.tiptap-button.color-button:hover{border-color:#e2e8f0;transform:scale(1.1)}.tiptap-button.color-button.is-active{border-color:#6366f1;box-shadow:0 0 0 2px #6366f133}.tiptap-button.primary{background:linear-gradient(135deg,#6366f1,#8b5cf6);color:#fff}.tiptap-button.primary:hover{background:linear-gradient(135deg,#5b21b6,#7c3aed);color:#fff}.tiptap-button.secondary{background:#f1f5f9;color:#64748b}.tiptap-button.secondary:hover{background:#e2e8f0;color:#475569}.tiptap-button.danger{color:#ef4444}.tiptap-button.danger:hover{color:#dc2626;background:#ef44441a}.tiptap-button.danger:before{background:linear-gradient(135deg,#ef4444,#dc2626)}.tiptap-button.small{width:24px;height:24px}.tiptap-button.medium{width:32px;height:32px}.tiptap-button.large{width:40px;height:40px}.tiptap-button.has-badge{position:relative}.tiptap-button .badge{position:absolute;top:-4px;right:-4px;background:#ef4444;color:#fff;font-size:10px;padding:2px 4px;border-radius:8px;min-width:16px;text-align:center;line-height:1}.tiptap-button.has-tooltip{position:relative}.tiptap-button .tooltip{position:absolute;bottom:-30px;left:50%;transform:translate(-50%);background:#000c;color:#fff;padding:4px 8px;border-radius:4px;font-size:12px;white-space:nowrap;opacity:0;visibility:hidden;transition:all .2s ease;z-index:1000}.tiptap-button:hover .tooltip{opacity:1;visibility:visible}@keyframes pulse{0%,to{box-shadow:0 0 #6366f166}50%{box-shadow:0 0 0 4px #6366f100}}.tiptap-button.is-active.pulse{animation:pulse 2s infinite}@media (max-width: 768px){.tiptap-button{width:32px;height:32px}.tiptap-button .material-symbols-outlined{font-size:18px}.tiptap-button.text-button{padding:0 8px;font-size:13px}}\n"] }]
561
+ }] });
562
+
563
+ class TiptapSeparatorComponent {
564
+ constructor() {
565
+ this.orientation = input("vertical");
566
+ this.size = input("medium");
567
+ }
568
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapSeparatorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
569
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.0.0", type: TiptapSeparatorComponent, isStandalone: true, selector: "tiptap-separator", inputs: { orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
570
+ <div
571
+ class="tiptap-separator"
572
+ [class.vertical]="orientation() === 'vertical'"
573
+ [class.horizontal]="orientation() === 'horizontal'"
574
+ [class.small]="size() === 'small'"
575
+ [class.medium]="size() === 'medium'"
576
+ [class.large]="size() === 'large'"
577
+ ></div>
578
+ `, isInline: true, styles: [".tiptap-separator{background-color:#e2e8f0;margin:0}.tiptap-separator.vertical{width:1px;height:24px;margin:0 8px}.tiptap-separator.horizontal{height:1px;width:100%;margin:8px 0}.tiptap-separator.small.vertical{height:16px;margin:0 4px}.tiptap-separator.small.horizontal{margin:4px 0}.tiptap-separator.medium.vertical{height:24px;margin:0 8px}.tiptap-separator.medium.horizontal{margin:8px 0}.tiptap-separator.large.vertical{height:32px;margin:0 12px}.tiptap-separator.large.horizontal{margin:12px 0}\n"] }); }
579
+ }
580
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapSeparatorComponent, decorators: [{
581
+ type: Component,
582
+ args: [{ selector: "tiptap-separator", standalone: true, template: `
583
+ <div
584
+ class="tiptap-separator"
585
+ [class.vertical]="orientation() === 'vertical'"
586
+ [class.horizontal]="orientation() === 'horizontal'"
587
+ [class.small]="size() === 'small'"
588
+ [class.medium]="size() === 'medium'"
589
+ [class.large]="size() === 'large'"
590
+ ></div>
591
+ `, styles: [".tiptap-separator{background-color:#e2e8f0;margin:0}.tiptap-separator.vertical{width:1px;height:24px;margin:0 8px}.tiptap-separator.horizontal{height:1px;width:100%;margin:8px 0}.tiptap-separator.small.vertical{height:16px;margin:0 4px}.tiptap-separator.small.horizontal{margin:4px 0}.tiptap-separator.medium.vertical{height:24px;margin:0 8px}.tiptap-separator.medium.horizontal{margin:8px 0}.tiptap-separator.large.vertical{height:32px;margin:0 12px}.tiptap-separator.large.horizontal{margin:12px 0}\n"] }]
592
+ }] });
593
+
594
+ class ImageService {
595
+ constructor() {
596
+ // Signals pour l'état des images
597
+ this.selectedImage = signal(null);
598
+ this.isImageSelected = computed(() => this.selectedImage() !== null);
599
+ this.isResizing = signal(false);
600
+ // Signaux pour l'upload
601
+ this.isUploading = signal(false);
602
+ this.uploadProgress = signal(0);
603
+ this.uploadMessage = signal("");
604
+ // Référence à l'éditeur pour les mises à jour
605
+ this.currentEditor = null;
606
+ }
607
+ // Méthodes pour la gestion des images
608
+ selectImage(editor) {
609
+ if (editor.isActive("resizableImage")) {
610
+ const attrs = editor.getAttributes("resizableImage");
611
+ this.selectedImage.set({
612
+ src: attrs["src"],
613
+ alt: attrs["alt"],
614
+ title: attrs["title"],
615
+ width: attrs["width"],
616
+ height: attrs["height"],
617
+ });
618
+ }
619
+ else {
620
+ this.selectedImage.set(null);
621
+ }
622
+ }
623
+ clearSelection() {
624
+ this.selectedImage.set(null);
625
+ }
626
+ // Méthodes pour manipuler les images
627
+ insertImage(editor, imageData) {
628
+ editor.chain().focus().setResizableImage(imageData).run();
629
+ }
630
+ updateImageAttributes(editor, attributes) {
631
+ if (editor.isActive("resizableImage")) {
632
+ editor
633
+ .chain()
634
+ .focus()
635
+ .updateAttributes("resizableImage", attributes)
636
+ .run();
637
+ this.updateSelectedImage(attributes);
638
+ }
639
+ }
640
+ // Nouvelles méthodes pour le redimensionnement
641
+ resizeImage(editor, options) {
642
+ if (!editor.isActive("resizableImage"))
643
+ return;
644
+ const currentAttrs = editor.getAttributes("resizableImage");
645
+ let newWidth = options.width;
646
+ let newHeight = options.height;
647
+ // Maintenir le ratio d'aspect si demandé
648
+ if (options.maintainAspectRatio !== false &&
649
+ currentAttrs["width"] &&
650
+ currentAttrs["height"]) {
651
+ const aspectRatio = currentAttrs["width"] / currentAttrs["height"];
652
+ if (newWidth && !newHeight) {
653
+ newHeight = Math.round(newWidth / aspectRatio);
654
+ }
655
+ else if (newHeight && !newWidth) {
656
+ newWidth = Math.round(newHeight * aspectRatio);
657
+ }
658
+ }
659
+ // Appliquer des limites minimales
660
+ if (newWidth)
661
+ newWidth = Math.max(50, newWidth);
662
+ if (newHeight)
663
+ newHeight = Math.max(50, newHeight);
664
+ this.updateImageAttributes(editor, {
665
+ width: newWidth,
666
+ height: newHeight,
667
+ });
668
+ }
669
+ // Méthodes pour redimensionner par pourcentage
670
+ resizeImageByPercentage(editor, percentage) {
671
+ if (!editor.isActive("resizableImage"))
672
+ return;
673
+ const currentAttrs = editor.getAttributes("resizableImage");
674
+ if (!currentAttrs["width"] || !currentAttrs["height"])
675
+ return;
676
+ const newWidth = Math.round(currentAttrs["width"] * (percentage / 100));
677
+ const newHeight = Math.round(currentAttrs["height"] * (percentage / 100));
678
+ this.resizeImage(editor, { width: newWidth, height: newHeight });
679
+ }
680
+ // Méthodes pour redimensionner à des tailles prédéfinies
681
+ resizeImageToSmall(editor) {
682
+ this.resizeImage(editor, {
683
+ width: 300,
684
+ height: 200,
685
+ maintainAspectRatio: true,
686
+ });
687
+ }
688
+ resizeImageToMedium(editor) {
689
+ this.resizeImage(editor, {
690
+ width: 500,
691
+ height: 350,
692
+ maintainAspectRatio: true,
693
+ });
694
+ }
695
+ resizeImageToLarge(editor) {
696
+ this.resizeImage(editor, {
697
+ width: 800,
698
+ height: 600,
699
+ maintainAspectRatio: true,
700
+ });
701
+ }
702
+ resizeImageToOriginal(editor) {
703
+ if (!editor.isActive("resizableImage"))
704
+ return;
705
+ const img = new Image();
706
+ img.onload = () => {
707
+ this.resizeImage(editor, {
708
+ width: img.naturalWidth,
709
+ height: img.naturalHeight,
710
+ });
711
+ };
712
+ img.src = editor.getAttributes("resizableImage")["src"];
713
+ }
714
+ // Méthode pour redimensionner librement (sans maintenir le ratio)
715
+ resizeImageFreely(editor, width, height) {
716
+ this.resizeImage(editor, {
717
+ width,
718
+ height,
719
+ maintainAspectRatio: false,
720
+ });
721
+ }
722
+ // Méthode pour obtenir les dimensions actuelles de l'image
723
+ getImageDimensions(editor) {
724
+ if (!editor.isActive("resizableImage"))
725
+ return null;
726
+ const attrs = editor.getAttributes("resizableImage");
727
+ return {
728
+ width: attrs["width"] || 0,
729
+ height: attrs["height"] || 0,
730
+ };
731
+ }
732
+ // Méthode pour obtenir les dimensions naturelles de l'image
733
+ getNaturalImageDimensions(src) {
734
+ return new Promise((resolve, reject) => {
735
+ const img = new Image();
736
+ img.onload = () => {
737
+ resolve({ width: img.naturalWidth, height: img.naturalHeight });
738
+ };
739
+ img.onerror = () => {
740
+ reject(new Error("Impossible de charger l'image"));
741
+ };
742
+ img.src = src;
743
+ });
744
+ }
745
+ deleteImage(editor) {
746
+ if (editor.isActive("resizableImage")) {
747
+ editor.chain().focus().deleteSelection().run();
748
+ this.clearSelection();
749
+ }
750
+ }
751
+ // Méthodes utilitaires
752
+ updateSelectedImage(attributes) {
753
+ const current = this.selectedImage();
754
+ if (current) {
755
+ this.selectedImage.set({ ...current, ...attributes });
756
+ }
757
+ }
758
+ // Validation des images
759
+ validateImage(file, maxSize = 5 * 1024 * 1024) {
760
+ if (!file.type.startsWith("image/")) {
761
+ return { valid: false, error: "Le fichier doit être une image" };
762
+ }
763
+ if (file.size > maxSize) {
764
+ return {
765
+ valid: false,
766
+ error: `L'image est trop volumineuse (max ${maxSize / 1024 / 1024}MB)`,
767
+ };
768
+ }
769
+ return { valid: true };
770
+ }
771
+ // Compression d'image
772
+ async compressImage(file, quality = 0.8, maxWidth = 1920, maxHeight = 1080) {
773
+ return new Promise((resolve, reject) => {
774
+ const canvas = document.createElement("canvas");
775
+ const ctx = canvas.getContext("2d");
776
+ const img = new Image();
777
+ img.onload = () => {
778
+ // Mise à jour du progrès
779
+ if (this.isUploading()) {
780
+ this.uploadProgress.set(40);
781
+ this.uploadMessage.set("Redimensionnement...");
782
+ this.forceEditorUpdate();
783
+ }
784
+ let { width, height } = img;
785
+ // Redimensionner si nécessaire
786
+ if (width > maxWidth || height > maxHeight) {
787
+ const ratio = Math.min(maxWidth / width, maxHeight / height);
788
+ width *= ratio;
789
+ height *= ratio;
790
+ }
791
+ canvas.width = width;
792
+ canvas.height = height;
793
+ // Dessiner l'image redimensionnée
794
+ ctx?.drawImage(img, 0, 0, width, height);
795
+ // Mise à jour du progrès
796
+ if (this.isUploading()) {
797
+ this.uploadProgress.set(60);
798
+ this.uploadMessage.set("Compression...");
799
+ this.forceEditorUpdate();
800
+ }
801
+ // Convertir en base64 avec compression
802
+ canvas.toBlob((blob) => {
803
+ if (blob) {
804
+ const reader = new FileReader();
805
+ reader.onload = (e) => {
806
+ const base64 = e.target?.result;
807
+ if (base64) {
808
+ const result = {
809
+ src: base64,
810
+ name: file.name,
811
+ size: blob.size,
812
+ type: file.type,
813
+ width: Math.round(width),
814
+ height: Math.round(height),
815
+ originalSize: file.size,
816
+ };
817
+ resolve(result);
818
+ }
819
+ else {
820
+ reject(new Error("Erreur lors de la compression"));
821
+ }
822
+ };
823
+ reader.readAsDataURL(blob);
824
+ }
825
+ else {
826
+ reject(new Error("Erreur lors de la compression"));
827
+ }
828
+ }, file.type, quality);
829
+ };
830
+ img.onerror = () => reject(new Error("Erreur lors du chargement de l'image"));
831
+ img.src = URL.createObjectURL(file);
832
+ });
833
+ }
834
+ // Méthode privée générique pour uploader avec progression
835
+ async uploadImageWithProgress(editor, file, insertionStrategy, actionMessage, options) {
836
+ try {
837
+ // Stocker la référence à l'éditeur
838
+ this.currentEditor = editor;
839
+ this.isUploading.set(true);
840
+ this.uploadProgress.set(0);
841
+ this.uploadMessage.set("Validation du fichier...");
842
+ this.forceEditorUpdate();
843
+ // Validation
844
+ const validation = this.validateImage(file);
845
+ if (!validation.valid) {
846
+ throw new Error(validation.error);
847
+ }
848
+ this.uploadProgress.set(20);
849
+ this.uploadMessage.set("Compression en cours...");
850
+ this.forceEditorUpdate();
851
+ // Petit délai pour permettre à l'utilisateur de voir la progression
852
+ await new Promise((resolve) => setTimeout(resolve, 200));
853
+ const result = await this.compressImage(file, options?.quality || 0.8, options?.maxWidth || 1920, options?.maxHeight || 1080);
854
+ this.uploadProgress.set(80);
855
+ this.uploadMessage.set(actionMessage);
856
+ this.forceEditorUpdate();
857
+ // Petit délai pour l'action
858
+ await new Promise((resolve) => setTimeout(resolve, 100));
859
+ // Exécuter la stratégie d'insertion
860
+ insertionStrategy(editor, result);
861
+ // L'action est terminée, maintenant on peut cacher l'indicateur
862
+ this.isUploading.set(false);
863
+ this.uploadProgress.set(0);
864
+ this.uploadMessage.set("");
865
+ this.forceEditorUpdate();
866
+ this.currentEditor = null;
867
+ }
868
+ catch (error) {
869
+ this.isUploading.set(false);
870
+ this.uploadProgress.set(0);
871
+ this.uploadMessage.set("");
872
+ this.forceEditorUpdate();
873
+ this.currentEditor = null;
874
+ console.error("Erreur lors de l'upload d'image:", error);
875
+ throw error;
876
+ }
877
+ }
878
+ // Méthode unifiée pour uploader et insérer une image
879
+ async uploadAndInsertImage(editor, file, options) {
880
+ return this.uploadImageWithProgress(editor, file, (editor, result) => {
881
+ this.insertImage(editor, {
882
+ src: result.src,
883
+ alt: result.name,
884
+ title: `${result.name} (${result.width}×${result.height})`,
885
+ width: result.width,
886
+ height: result.height,
887
+ });
888
+ }, "Insertion dans l'éditeur...", options);
889
+ }
890
+ // Méthode pour forcer la mise à jour de l'éditeur
891
+ forceEditorUpdate() {
892
+ if (this.currentEditor) {
893
+ // Déclencher une transaction vide pour forcer la mise à jour des décorations
894
+ const { tr } = this.currentEditor.state;
895
+ this.currentEditor.view.dispatch(tr);
896
+ }
897
+ }
898
+ // Méthode privée générique pour créer un sélecteur de fichier
899
+ async selectFileAndProcess(editor, uploadMethod, options) {
900
+ return new Promise((resolve, reject) => {
901
+ const input = document.createElement("input");
902
+ input.type = "file";
903
+ input.accept = options?.accept || "image/*";
904
+ input.style.display = "none";
905
+ input.addEventListener("change", async (e) => {
906
+ const file = e.target.files?.[0];
907
+ if (file && file.type.startsWith("image/")) {
908
+ try {
909
+ await uploadMethod(editor, file, options);
910
+ resolve();
911
+ }
912
+ catch (error) {
913
+ reject(error);
914
+ }
915
+ }
916
+ else {
917
+ reject(new Error("Aucun fichier image sélectionné"));
918
+ }
919
+ document.body.removeChild(input);
920
+ });
921
+ input.addEventListener("cancel", () => {
922
+ document.body.removeChild(input);
923
+ reject(new Error("Sélection annulée"));
924
+ });
925
+ document.body.appendChild(input);
926
+ input.click();
927
+ });
928
+ }
929
+ // Méthode pour créer un sélecteur de fichier et uploader une image
930
+ async selectAndUploadImage(editor, options) {
931
+ return this.selectFileAndProcess(editor, this.uploadAndInsertImage.bind(this), options);
932
+ }
933
+ // Méthode pour sélectionner et remplacer une image existante
934
+ async selectAndReplaceImage(editor, options) {
935
+ return this.selectFileAndProcess(editor, this.uploadAndReplaceImage.bind(this), options);
936
+ }
937
+ // Méthode pour remplacer une image existante avec indicateur de progression
938
+ async uploadAndReplaceImage(editor, file, options) {
939
+ // Sauvegarder les attributs de l'image actuelle pour restauration en cas d'échec
940
+ const currentImageAttrs = editor.getAttributes("resizableImage");
941
+ const backupImage = { ...currentImageAttrs };
942
+ try {
943
+ // Supprimer visuellement l'ancienne image immédiatement
944
+ editor.chain().focus().deleteSelection().run();
945
+ // Stocker la référence à l'éditeur
946
+ this.currentEditor = editor;
947
+ this.isUploading.set(true);
948
+ this.uploadProgress.set(0);
949
+ this.uploadMessage.set("Validation du fichier...");
950
+ this.forceEditorUpdate();
951
+ // Validation
952
+ const validation = this.validateImage(file);
953
+ if (!validation.valid) {
954
+ throw new Error(validation.error);
955
+ }
956
+ this.uploadProgress.set(20);
957
+ this.uploadMessage.set("Compression en cours...");
958
+ this.forceEditorUpdate();
959
+ // Petit délai pour permettre à l'utilisateur de voir la progression
960
+ await new Promise((resolve) => setTimeout(resolve, 200));
961
+ const result = await this.compressImage(file, options?.quality || 0.8, options?.maxWidth || 1920, options?.maxHeight || 1080);
962
+ this.uploadProgress.set(80);
963
+ this.uploadMessage.set("Remplacement de l'image...");
964
+ this.forceEditorUpdate();
965
+ // Petit délai pour le remplacement
966
+ await new Promise((resolve) => setTimeout(resolve, 100));
967
+ // Insérer la nouvelle image à la position actuelle
968
+ this.insertImage(editor, {
969
+ src: result.src,
970
+ alt: result.name,
971
+ title: `${result.name} (${result.width}×${result.height})`,
972
+ width: result.width,
973
+ height: result.height,
974
+ });
975
+ // L'image est remplacée, maintenant on peut cacher l'indicateur
976
+ this.isUploading.set(false);
977
+ this.uploadProgress.set(0);
978
+ this.uploadMessage.set("");
979
+ this.forceEditorUpdate();
980
+ this.currentEditor = null;
981
+ }
982
+ catch (error) {
983
+ // En cas d'erreur, restaurer l'image originale
984
+ if (backupImage["src"]) {
985
+ this.insertImage(editor, {
986
+ src: backupImage["src"],
987
+ alt: backupImage["alt"],
988
+ title: backupImage["title"],
989
+ width: backupImage["width"],
990
+ height: backupImage["height"],
991
+ });
992
+ }
993
+ this.isUploading.set(false);
994
+ this.uploadProgress.set(0);
995
+ this.uploadMessage.set("");
996
+ this.forceEditorUpdate();
997
+ this.currentEditor = null;
998
+ console.error("Erreur lors du remplacement d'image:", error);
999
+ throw error;
1000
+ }
1001
+ }
1002
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: ImageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1003
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: ImageService, providedIn: "root" }); }
1004
+ }
1005
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: ImageService, decorators: [{
1006
+ type: Injectable,
1007
+ args: [{
1008
+ providedIn: "root",
1009
+ }]
1010
+ }] });
1011
+
1012
+ const ENGLISH_TRANSLATIONS = {
1013
+ toolbar: {
1014
+ bold: "Bold",
1015
+ italic: "Italic",
1016
+ underline: "Underline",
1017
+ strike: "Strikethrough",
1018
+ code: "Code",
1019
+ superscript: "Superscript",
1020
+ subscript: "Subscript",
1021
+ highlight: "Highlight",
1022
+ heading1: "Heading 1",
1023
+ heading2: "Heading 2",
1024
+ heading3: "Heading 3",
1025
+ bulletList: "Bullet List",
1026
+ orderedList: "Ordered List",
1027
+ blockquote: "Blockquote",
1028
+ alignLeft: "Align Left",
1029
+ alignCenter: "Align Center",
1030
+ alignRight: "Align Right",
1031
+ alignJustify: "Align Justify",
1032
+ link: "Add Link",
1033
+ image: "Add Image",
1034
+ horizontalRule: "Horizontal Rule",
1035
+ undo: "Undo",
1036
+ redo: "Redo",
1037
+ },
1038
+ bubbleMenu: {
1039
+ bold: "Bold",
1040
+ italic: "Italic",
1041
+ underline: "Underline",
1042
+ strike: "Strikethrough",
1043
+ code: "Code",
1044
+ superscript: "Superscript",
1045
+ subscript: "Subscript",
1046
+ highlight: "Highlight",
1047
+ link: "Link",
1048
+ addLink: "Add Link",
1049
+ editLink: "Edit Link",
1050
+ removeLink: "Remove Link",
1051
+ linkUrl: "Link URL",
1052
+ linkText: "Link Text",
1053
+ openLink: "Open Link",
1054
+ },
1055
+ slashCommands: {
1056
+ heading1: {
1057
+ title: "Heading 1",
1058
+ description: "Large section heading",
1059
+ keywords: ["heading", "h1", "title", "1", "header"],
1060
+ },
1061
+ heading2: {
1062
+ title: "Heading 2",
1063
+ description: "Medium section heading",
1064
+ keywords: ["heading", "h2", "title", "2", "header"],
1065
+ },
1066
+ heading3: {
1067
+ title: "Heading 3",
1068
+ description: "Small section heading",
1069
+ keywords: ["heading", "h3", "title", "3", "header"],
1070
+ },
1071
+ bulletList: {
1072
+ title: "Bullet List",
1073
+ description: "Create a bullet list",
1074
+ keywords: ["bullet", "list", "ul", "unordered"],
1075
+ },
1076
+ orderedList: {
1077
+ title: "Ordered List",
1078
+ description: "Create an ordered list",
1079
+ keywords: ["ordered", "list", "ol", "numbered"],
1080
+ },
1081
+ blockquote: {
1082
+ title: "Blockquote",
1083
+ description: "Add a blockquote",
1084
+ keywords: ["quote", "blockquote", "citation"],
1085
+ },
1086
+ code: {
1087
+ title: "Code Block",
1088
+ description: "Add a code block",
1089
+ keywords: ["code", "codeblock", "pre", "programming"],
1090
+ },
1091
+ image: {
1092
+ title: "Image",
1093
+ description: "Insert an image",
1094
+ keywords: ["image", "photo", "picture", "img"],
1095
+ },
1096
+ horizontalRule: {
1097
+ title: "Horizontal Rule",
1098
+ description: "Add a horizontal line",
1099
+ keywords: ["hr", "horizontal", "rule", "line", "separator"],
1100
+ },
1101
+ },
1102
+ imageUpload: {
1103
+ selectImage: "Select Image",
1104
+ uploadingImage: "Uploading image...",
1105
+ uploadProgress: "Upload Progress",
1106
+ uploadError: "Upload Error",
1107
+ uploadSuccess: "Upload Success",
1108
+ imageTooLarge: "Image too large",
1109
+ invalidFileType: "Invalid file type",
1110
+ dragDropText: "Drag and drop images here",
1111
+ changeImage: "Change Image",
1112
+ deleteImage: "Delete Image",
1113
+ resizeSmall: "Small",
1114
+ resizeMedium: "Medium",
1115
+ resizeLarge: "Large",
1116
+ resizeOriginal: "Original",
1117
+ },
1118
+ editor: {
1119
+ placeholder: "Start typing...",
1120
+ characters: "characters",
1121
+ words: "words",
1122
+ imageLoadError: "Error loading image",
1123
+ linkPrompt: "Enter link URL",
1124
+ linkUrlPrompt: "Enter URL",
1125
+ confirmDelete: "Are you sure you want to delete this?",
1126
+ },
1127
+ common: {
1128
+ cancel: "Cancel",
1129
+ confirm: "Confirm",
1130
+ apply: "Apply",
1131
+ delete: "Delete",
1132
+ save: "Save",
1133
+ close: "Close",
1134
+ loading: "Loading",
1135
+ error: "Error",
1136
+ success: "Success",
1137
+ },
1138
+ };
1139
+ const FRENCH_TRANSLATIONS = {
1140
+ toolbar: {
1141
+ bold: "Gras",
1142
+ italic: "Italique",
1143
+ underline: "Souligné",
1144
+ strike: "Barré",
1145
+ code: "Code",
1146
+ superscript: "Exposant",
1147
+ subscript: "Indice",
1148
+ highlight: "Surligner",
1149
+ heading1: "Titre 1",
1150
+ heading2: "Titre 2",
1151
+ heading3: "Titre 3",
1152
+ bulletList: "Liste à puces",
1153
+ orderedList: "Liste numérotée",
1154
+ blockquote: "Citation",
1155
+ alignLeft: "Aligner à gauche",
1156
+ alignCenter: "Centrer",
1157
+ alignRight: "Aligner à droite",
1158
+ alignJustify: "Justifier",
1159
+ link: "Ajouter un lien",
1160
+ image: "Ajouter une image",
1161
+ horizontalRule: "Ligne horizontale",
1162
+ undo: "Annuler",
1163
+ redo: "Refaire",
1164
+ },
1165
+ bubbleMenu: {
1166
+ bold: "Gras",
1167
+ italic: "Italique",
1168
+ underline: "Souligné",
1169
+ strike: "Barré",
1170
+ code: "Code",
1171
+ superscript: "Exposant",
1172
+ subscript: "Indice",
1173
+ highlight: "Surligner",
1174
+ link: "Lien",
1175
+ addLink: "Ajouter un lien",
1176
+ editLink: "Modifier le lien",
1177
+ removeLink: "Supprimer le lien",
1178
+ linkUrl: "URL du lien",
1179
+ linkText: "Texte du lien",
1180
+ openLink: "Ouvrir le lien",
1181
+ },
1182
+ slashCommands: {
1183
+ heading1: {
1184
+ title: "Titre 1",
1185
+ description: "Grand titre de section",
1186
+ keywords: ["heading", "h1", "titre", "title", "1", "header"],
1187
+ },
1188
+ heading2: {
1189
+ title: "Titre 2",
1190
+ description: "Titre de sous-section",
1191
+ keywords: ["heading", "h2", "titre", "title", "2", "header"],
1192
+ },
1193
+ heading3: {
1194
+ title: "Titre 3",
1195
+ description: "Petit titre",
1196
+ keywords: ["heading", "h3", "titre", "title", "3", "header"],
1197
+ },
1198
+ bulletList: {
1199
+ title: "Liste à puces",
1200
+ description: "Créer une liste à puces",
1201
+ keywords: ["bullet", "list", "liste", "puces", "ul"],
1202
+ },
1203
+ orderedList: {
1204
+ title: "Liste numérotée",
1205
+ description: "Créer une liste numérotée",
1206
+ keywords: ["numbered", "list", "liste", "numérotée", "ol", "ordered"],
1207
+ },
1208
+ blockquote: {
1209
+ title: "Citation",
1210
+ description: "Ajouter une citation",
1211
+ keywords: ["quote", "blockquote", "citation"],
1212
+ },
1213
+ code: {
1214
+ title: "Bloc de code",
1215
+ description: "Ajouter un bloc de code",
1216
+ keywords: ["code", "codeblock", "pre", "programmation"],
1217
+ },
1218
+ image: {
1219
+ title: "Image",
1220
+ description: "Insérer une image",
1221
+ keywords: ["image", "photo", "picture", "img"],
1222
+ },
1223
+ horizontalRule: {
1224
+ title: "Ligne horizontale",
1225
+ description: "Ajouter une ligne de séparation",
1226
+ keywords: ["hr", "horizontal", "rule", "ligne", "séparation"],
1227
+ },
1228
+ },
1229
+ imageUpload: {
1230
+ selectImage: "Sélectionner une image",
1231
+ uploadingImage: "Téléchargement de l'image...",
1232
+ uploadProgress: "Progression du téléchargement",
1233
+ uploadError: "Erreur de téléchargement",
1234
+ uploadSuccess: "Téléchargement réussi",
1235
+ imageTooLarge: "Image trop volumineuse",
1236
+ invalidFileType: "Type de fichier invalide",
1237
+ dragDropText: "Glissez et déposez des images ici",
1238
+ changeImage: "Changer l'image",
1239
+ deleteImage: "Supprimer l'image",
1240
+ resizeSmall: "Petit",
1241
+ resizeMedium: "Moyen",
1242
+ resizeLarge: "Grand",
1243
+ resizeOriginal: "Original",
1244
+ },
1245
+ editor: {
1246
+ placeholder: "Commencez à écrire...",
1247
+ characters: "caractères",
1248
+ words: "mots",
1249
+ imageLoadError: "Erreur de chargement de l'image",
1250
+ linkPrompt: "Entrez l'URL du lien",
1251
+ linkUrlPrompt: "Entrez l'URL",
1252
+ confirmDelete: "Êtes-vous sûr de vouloir supprimer ceci ?",
1253
+ },
1254
+ common: {
1255
+ cancel: "Annuler",
1256
+ confirm: "Confirmer",
1257
+ apply: "Appliquer",
1258
+ delete: "Supprimer",
1259
+ save: "Sauvegarder",
1260
+ close: "Fermer",
1261
+ loading: "Chargement",
1262
+ error: "Erreur",
1263
+ success: "Succès",
1264
+ },
1265
+ };
1266
+ class TiptapI18nService {
1267
+ constructor() {
1268
+ this._currentLocale = signal("en");
1269
+ this._translations = signal({
1270
+ en: ENGLISH_TRANSLATIONS,
1271
+ fr: FRENCH_TRANSLATIONS,
1272
+ });
1273
+ // Signaux publics
1274
+ this.currentLocale = this._currentLocale.asReadonly();
1275
+ this.translations = computed(() => this._translations()[this._currentLocale()]);
1276
+ // Méthodes de traduction rapides
1277
+ this.t = computed(() => this.translations());
1278
+ this.toolbar = computed(() => this.translations().toolbar);
1279
+ this.bubbleMenu = computed(() => this.translations().bubbleMenu);
1280
+ this.slashCommands = computed(() => this.translations().slashCommands);
1281
+ this.imageUpload = computed(() => this.translations().imageUpload);
1282
+ this.editor = computed(() => this.translations().editor);
1283
+ this.common = computed(() => this.translations().common);
1284
+ // Détecter automatiquement la langue du navigateur
1285
+ this.detectBrowserLanguage();
1286
+ }
1287
+ setLocale(locale) {
1288
+ this._currentLocale.set(locale);
1289
+ }
1290
+ autoDetectLocale() {
1291
+ this.detectBrowserLanguage();
1292
+ }
1293
+ getSupportedLocales() {
1294
+ return Object.keys(this._translations());
1295
+ }
1296
+ addTranslations(locale, translations) {
1297
+ this._translations.update((current) => ({
1298
+ ...current,
1299
+ [locale]: {
1300
+ ...current[locale],
1301
+ ...translations,
1302
+ },
1303
+ }));
1304
+ }
1305
+ detectBrowserLanguage() {
1306
+ const browserLang = navigator.language.toLowerCase();
1307
+ if (browserLang.startsWith("fr")) {
1308
+ this._currentLocale.set("fr");
1309
+ }
1310
+ else {
1311
+ this._currentLocale.set("en");
1312
+ }
1313
+ }
1314
+ // Méthodes utilitaires pour les composants
1315
+ getToolbarTitle(key) {
1316
+ return this.translations().toolbar[key];
1317
+ }
1318
+ getBubbleMenuTitle(key) {
1319
+ return this.translations().bubbleMenu[key];
1320
+ }
1321
+ getSlashCommand(key) {
1322
+ return this.translations().slashCommands[key];
1323
+ }
1324
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapI18nService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1325
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapI18nService, providedIn: "root" }); }
1326
+ }
1327
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapI18nService, decorators: [{
1328
+ type: Injectable,
1329
+ args: [{
1330
+ providedIn: "root",
1331
+ }]
1332
+ }], ctorParameters: () => [] });
1333
+
1334
+ class EditorCommandsService {
1335
+ // Méthodes pour vérifier l'état actif
1336
+ isActive(editor, name, attributes) {
1337
+ return editor.isActive(name, attributes);
1338
+ }
1339
+ // Méthodes pour vérifier si une commande peut être exécutée
1340
+ canExecute(editor, command) {
1341
+ if (!editor)
1342
+ return false;
1343
+ switch (command) {
1344
+ case "toggleBold":
1345
+ return editor.can().chain().focus().toggleBold().run();
1346
+ case "toggleItalic":
1347
+ return editor.can().chain().focus().toggleItalic().run();
1348
+ case "toggleStrike":
1349
+ return editor.can().chain().focus().toggleStrike().run();
1350
+ case "toggleCode":
1351
+ return editor.can().chain().focus().toggleCode().run();
1352
+ case "toggleUnderline":
1353
+ return editor.can().chain().focus().toggleUnderline().run();
1354
+ case "toggleSuperscript":
1355
+ return editor.can().chain().focus().toggleSuperscript().run();
1356
+ case "toggleSubscript":
1357
+ return editor.can().chain().focus().toggleSubscript().run();
1358
+ case "setTextAlign":
1359
+ return editor.can().chain().focus().setTextAlign("left").run();
1360
+ case "toggleLink":
1361
+ return editor.can().chain().focus().toggleLink({ href: "" }).run();
1362
+ case "insertHorizontalRule":
1363
+ return editor.can().chain().focus().setHorizontalRule().run();
1364
+ case "toggleHighlight":
1365
+ return editor.can().chain().focus().toggleHighlight().run();
1366
+ case "undo":
1367
+ return editor.can().chain().focus().undo().run();
1368
+ case "redo":
1369
+ return editor.can().chain().focus().redo().run();
1370
+ default:
1371
+ return false;
1372
+ }
1373
+ }
1374
+ // Méthodes pour exécuter les commandes
1375
+ toggleBold(editor) {
1376
+ editor.chain().focus().toggleBold().run();
1377
+ }
1378
+ toggleItalic(editor) {
1379
+ editor.chain().focus().toggleItalic().run();
1380
+ }
1381
+ toggleStrike(editor) {
1382
+ editor.chain().focus().toggleStrike().run();
1383
+ }
1384
+ toggleCode(editor) {
1385
+ editor.chain().focus().toggleCode().run();
1386
+ }
1387
+ toggleHeading(editor, level) {
1388
+ editor.chain().focus().toggleHeading({ level }).run();
1389
+ }
1390
+ toggleBulletList(editor) {
1391
+ editor.chain().focus().toggleBulletList().run();
1392
+ }
1393
+ toggleOrderedList(editor) {
1394
+ editor.chain().focus().toggleOrderedList().run();
1395
+ }
1396
+ toggleBlockquote(editor) {
1397
+ editor.chain().focus().toggleBlockquote().run();
1398
+ }
1399
+ undo(editor) {
1400
+ editor.chain().focus().undo().run();
1401
+ }
1402
+ redo(editor) {
1403
+ editor.chain().focus().redo().run();
1404
+ }
1405
+ // Nouvelles méthodes pour les formatages supplémentaires
1406
+ toggleUnderline(editor) {
1407
+ editor.chain().focus().toggleUnderline().run();
1408
+ }
1409
+ toggleSuperscript(editor) {
1410
+ editor.chain().focus().toggleSuperscript().run();
1411
+ }
1412
+ toggleSubscript(editor) {
1413
+ editor.chain().focus().toggleSubscript().run();
1414
+ }
1415
+ setTextAlign(editor, alignment) {
1416
+ editor.chain().focus().setTextAlign(alignment).run();
1417
+ }
1418
+ toggleLink(editor, url) {
1419
+ if (url) {
1420
+ editor.chain().focus().toggleLink({ href: url }).run();
1421
+ }
1422
+ else {
1423
+ // Si pas d'URL fournie, on demande à l'utilisateur
1424
+ const href = window.prompt("URL du lien:");
1425
+ if (href) {
1426
+ editor.chain().focus().toggleLink({ href }).run();
1427
+ }
1428
+ }
1429
+ }
1430
+ insertHorizontalRule(editor) {
1431
+ editor.chain().focus().setHorizontalRule().run();
1432
+ }
1433
+ toggleHighlight(editor, color) {
1434
+ if (color) {
1435
+ editor.chain().focus().toggleHighlight({ color }).run();
1436
+ }
1437
+ else {
1438
+ editor.chain().focus().toggleHighlight().run();
1439
+ }
1440
+ }
1441
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: EditorCommandsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1442
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: EditorCommandsService, providedIn: "root" }); }
1443
+ }
1444
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: EditorCommandsService, decorators: [{
1445
+ type: Injectable,
1446
+ args: [{
1447
+ providedIn: "root",
1448
+ }]
1449
+ }] });
1450
+
1451
+ class TiptapToolbarComponent {
1452
+ constructor(editorCommands) {
1453
+ this.editorCommands = editorCommands;
1454
+ this.editor = input.required();
1455
+ this.config = input.required();
1456
+ // Outputs pour les événements d'image
1457
+ this.imageUploaded = output();
1458
+ this.imageError = output();
1459
+ this.imageService = inject(ImageService);
1460
+ this.i18nService = inject(TiptapI18nService);
1461
+ // Computed values pour les traductions
1462
+ this.t = this.i18nService.toolbar;
1463
+ }
1464
+ isActive(name, attributes) {
1465
+ return this.editorCommands.isActive(this.editor(), name, attributes);
1466
+ }
1467
+ canExecute(command) {
1468
+ return this.editorCommands.canExecute(this.editor(), command);
1469
+ }
1470
+ toggleBold() {
1471
+ this.editorCommands.toggleBold(this.editor());
1472
+ }
1473
+ toggleItalic() {
1474
+ this.editorCommands.toggleItalic(this.editor());
1475
+ }
1476
+ toggleStrike() {
1477
+ this.editorCommands.toggleStrike(this.editor());
1478
+ }
1479
+ toggleCode() {
1480
+ this.editorCommands.toggleCode(this.editor());
1481
+ }
1482
+ toggleHeading(level) {
1483
+ this.editorCommands.toggleHeading(this.editor(), level);
1484
+ }
1485
+ toggleBulletList() {
1486
+ this.editorCommands.toggleBulletList(this.editor());
1487
+ }
1488
+ toggleOrderedList() {
1489
+ this.editorCommands.toggleOrderedList(this.editor());
1490
+ }
1491
+ toggleBlockquote() {
1492
+ this.editorCommands.toggleBlockquote(this.editor());
1493
+ }
1494
+ undo() {
1495
+ this.editorCommands.undo(this.editor());
1496
+ }
1497
+ redo() {
1498
+ this.editorCommands.redo(this.editor());
1499
+ }
1500
+ // Nouvelles méthodes pour les formatages supplémentaires
1501
+ toggleUnderline() {
1502
+ this.editorCommands.toggleUnderline(this.editor());
1503
+ }
1504
+ toggleSuperscript() {
1505
+ this.editorCommands.toggleSuperscript(this.editor());
1506
+ }
1507
+ toggleSubscript() {
1508
+ this.editorCommands.toggleSubscript(this.editor());
1509
+ }
1510
+ setTextAlign(alignment) {
1511
+ this.editorCommands.setTextAlign(this.editor(), alignment);
1512
+ }
1513
+ toggleLink() {
1514
+ this.editorCommands.toggleLink(this.editor());
1515
+ }
1516
+ insertHorizontalRule() {
1517
+ this.editorCommands.insertHorizontalRule(this.editor());
1518
+ }
1519
+ toggleHighlight() {
1520
+ this.editorCommands.toggleHighlight(this.editor());
1521
+ }
1522
+ // Méthode pour insérer une image
1523
+ async insertImage() {
1524
+ try {
1525
+ await this.imageService.selectAndUploadImage(this.editor());
1526
+ }
1527
+ catch (error) {
1528
+ console.error("Erreur lors de l'upload d'image:", error);
1529
+ this.imageError.emit("Erreur lors de l'upload d'image");
1530
+ }
1531
+ }
1532
+ // Méthodes pour les événements d'image (conservées pour compatibilité)
1533
+ onImageSelected(result) {
1534
+ this.imageUploaded.emit(result);
1535
+ }
1536
+ onImageError(error) {
1537
+ this.imageError.emit(error);
1538
+ }
1539
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapToolbarComponent, deps: [{ token: EditorCommandsService }], target: i0.ɵɵFactoryTarget.Component }); }
1540
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.0", type: TiptapToolbarComponent, isStandalone: true, selector: "tiptap-toolbar", inputs: { editor: { classPropertyName: "editor", publicName: "editor", isSignal: true, isRequired: true, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { imageUploaded: "imageUploaded", imageError: "imageError" }, ngImport: i0, template: `
1541
+ <div class="tiptap-toolbar">
1542
+ @if (config().bold) {
1543
+ <tiptap-button
1544
+ icon="format_bold"
1545
+ [title]="t().bold"
1546
+ [active]="isActive('bold')"
1547
+ [disabled]="!canExecute('toggleBold')"
1548
+ (onClick)="toggleBold()"
1549
+ />
1550
+ } @if (config().italic) {
1551
+ <tiptap-button
1552
+ icon="format_italic"
1553
+ [title]="t().italic"
1554
+ [active]="isActive('italic')"
1555
+ [disabled]="!canExecute('toggleItalic')"
1556
+ (onClick)="toggleItalic()"
1557
+ />
1558
+ } @if (config().underline) {
1559
+ <tiptap-button
1560
+ icon="format_underlined"
1561
+ [title]="t().underline"
1562
+ [active]="isActive('underline')"
1563
+ [disabled]="!canExecute('toggleUnderline')"
1564
+ (onClick)="toggleUnderline()"
1565
+ />
1566
+ } @if (config().strike) {
1567
+ <tiptap-button
1568
+ icon="strikethrough_s"
1569
+ [title]="t().strike"
1570
+ [active]="isActive('strike')"
1571
+ [disabled]="!canExecute('toggleStrike')"
1572
+ (onClick)="toggleStrike()"
1573
+ />
1574
+ } @if (config().code) {
1575
+ <tiptap-button
1576
+ icon="code"
1577
+ [title]="t().code"
1578
+ [active]="isActive('code')"
1579
+ [disabled]="!canExecute('toggleCode')"
1580
+ (onClick)="toggleCode()"
1581
+ />
1582
+ } @if (config().superscript) {
1583
+ <tiptap-button
1584
+ icon="superscript"
1585
+ [title]="t().superscript"
1586
+ [active]="isActive('superscript')"
1587
+ [disabled]="!canExecute('toggleSuperscript')"
1588
+ (onClick)="toggleSuperscript()"
1589
+ />
1590
+ } @if (config().subscript) {
1591
+ <tiptap-button
1592
+ icon="subscript"
1593
+ [title]="t().subscript"
1594
+ [active]="isActive('subscript')"
1595
+ [disabled]="!canExecute('toggleSubscript')"
1596
+ (onClick)="toggleSubscript()"
1597
+ />
1598
+ } @if (config().highlight) {
1599
+ <tiptap-button
1600
+ icon="highlight"
1601
+ [title]="t().highlight"
1602
+ [active]="isActive('highlight')"
1603
+ [disabled]="!canExecute('toggleHighlight')"
1604
+ (onClick)="toggleHighlight()"
1605
+ />
1606
+ } @if (config().separator && (config().heading1 || config().heading2 ||
1607
+ config().heading3)) {
1608
+ <tiptap-separator />
1609
+ } @if (config().heading1) {
1610
+ <tiptap-button
1611
+ icon="format_h1"
1612
+ [title]="t().heading1"
1613
+ variant="text"
1614
+ [active]="isActive('heading', { level: 1 })"
1615
+ (onClick)="toggleHeading(1)"
1616
+ />
1617
+ } @if (config().heading2) {
1618
+ <tiptap-button
1619
+ icon="format_h2"
1620
+ [title]="t().heading2"
1621
+ variant="text"
1622
+ [active]="isActive('heading', { level: 2 })"
1623
+ (onClick)="toggleHeading(2)"
1624
+ />
1625
+ } @if (config().heading3) {
1626
+ <tiptap-button
1627
+ icon="format_h3"
1628
+ [title]="t().heading3"
1629
+ variant="text"
1630
+ [active]="isActive('heading', { level: 3 })"
1631
+ (onClick)="toggleHeading(3)"
1632
+ />
1633
+ } @if (config().separator && (config().bulletList || config().orderedList
1634
+ || config().blockquote)) {
1635
+ <tiptap-separator />
1636
+ } @if (config().bulletList) {
1637
+ <tiptap-button
1638
+ icon="format_list_bulleted"
1639
+ [title]="t().bulletList"
1640
+ [active]="isActive('bulletList')"
1641
+ (onClick)="toggleBulletList()"
1642
+ />
1643
+ } @if (config().orderedList) {
1644
+ <tiptap-button
1645
+ icon="format_list_numbered"
1646
+ [title]="t().orderedList"
1647
+ [active]="isActive('orderedList')"
1648
+ (onClick)="toggleOrderedList()"
1649
+ />
1650
+ } @if (config().blockquote) {
1651
+ <tiptap-button
1652
+ icon="format_quote"
1653
+ [title]="t().blockquote"
1654
+ [active]="isActive('blockquote')"
1655
+ (onClick)="toggleBlockquote()"
1656
+ />
1657
+ } @if (config().separator && (config().alignLeft || config().alignCenter
1658
+ || config().alignRight || config().alignJustify)) {
1659
+ <tiptap-separator />
1660
+ } @if (config().alignLeft) {
1661
+ <tiptap-button
1662
+ icon="format_align_left"
1663
+ [title]="t().alignLeft"
1664
+ [active]="isActive('textAlign', { textAlign: 'left' })"
1665
+ (onClick)="setTextAlign('left')"
1666
+ />
1667
+ } @if (config().alignCenter) {
1668
+ <tiptap-button
1669
+ icon="format_align_center"
1670
+ [title]="t().alignCenter"
1671
+ [active]="isActive('textAlign', { textAlign: 'center' })"
1672
+ (onClick)="setTextAlign('center')"
1673
+ />
1674
+ } @if (config().alignRight) {
1675
+ <tiptap-button
1676
+ icon="format_align_right"
1677
+ [title]="t().alignRight"
1678
+ [active]="isActive('textAlign', { textAlign: 'right' })"
1679
+ (onClick)="setTextAlign('right')"
1680
+ />
1681
+ } @if (config().alignJustify) {
1682
+ <tiptap-button
1683
+ icon="format_align_justify"
1684
+ [title]="t().alignJustify"
1685
+ [active]="isActive('textAlign', { textAlign: 'justify' })"
1686
+ (onClick)="setTextAlign('justify')"
1687
+ />
1688
+ } @if (config().separator && (config().link || config().horizontalRule)) {
1689
+ <tiptap-separator />
1690
+ } @if (config().link) {
1691
+ <tiptap-button
1692
+ icon="link"
1693
+ [title]="t().link"
1694
+ [active]="isActive('link')"
1695
+ (onClick)="toggleLink()"
1696
+ />
1697
+ } @if (config().horizontalRule) {
1698
+ <tiptap-button
1699
+ icon="horizontal_rule"
1700
+ [title]="t().horizontalRule"
1701
+ (onClick)="insertHorizontalRule()"
1702
+ />
1703
+ } @if (config().separator && config().image) {
1704
+ <tiptap-separator />
1705
+ } @if (config().image) {
1706
+ <tiptap-button
1707
+ icon="image"
1708
+ [title]="t().image"
1709
+ (onClick)="insertImage()"
1710
+ />
1711
+ } @if (config().separator && (config().undo || config().redo)) {
1712
+ <tiptap-separator />
1713
+ } @if (config().undo) {
1714
+ <tiptap-button
1715
+ icon="undo"
1716
+ [title]="t().undo"
1717
+ [disabled]="!canExecute('undo')"
1718
+ (onClick)="undo()"
1719
+ />
1720
+ } @if (config().redo) {
1721
+ <tiptap-button
1722
+ icon="redo"
1723
+ [title]="t().redo"
1724
+ [disabled]="!canExecute('redo')"
1725
+ (onClick)="redo()"
1726
+ />
1727
+ }
1728
+ </div>
1729
+ `, isInline: true, styles: [".tiptap-toolbar{display:flex;align-items:center;gap:4px;padding:4px 8px;background:#f8f9fa;border-bottom:1px solid #e2e8f0;flex-wrap:wrap;min-height:32px;position:relative}.toolbar-group{display:flex;align-items:center;gap:2px;padding:0 4px}.toolbar-separator{width:1px;height:24px;background:#e2e8f0;margin:0 4px}@media (max-width: 768px){.tiptap-toolbar{padding:6px 8px;gap:2px}.toolbar-group{gap:1px}}@keyframes toolbarSlideIn{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}.tiptap-toolbar{animation:toolbarSlideIn .3s cubic-bezier(.4,0,.2,1)}\n"], dependencies: [{ kind: "component", type: TiptapButtonComponent, selector: "tiptap-button", inputs: ["icon", "title", "active", "disabled", "variant", "size", "iconSize"], outputs: ["onClick"] }, { kind: "component", type: TiptapSeparatorComponent, selector: "tiptap-separator", inputs: ["orientation", "size"] }] }); }
1730
+ }
1731
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapToolbarComponent, decorators: [{
1732
+ type: Component,
1733
+ args: [{ selector: "tiptap-toolbar", standalone: true, imports: [TiptapButtonComponent, TiptapSeparatorComponent], template: `
1734
+ <div class="tiptap-toolbar">
1735
+ @if (config().bold) {
1736
+ <tiptap-button
1737
+ icon="format_bold"
1738
+ [title]="t().bold"
1739
+ [active]="isActive('bold')"
1740
+ [disabled]="!canExecute('toggleBold')"
1741
+ (onClick)="toggleBold()"
1742
+ />
1743
+ } @if (config().italic) {
1744
+ <tiptap-button
1745
+ icon="format_italic"
1746
+ [title]="t().italic"
1747
+ [active]="isActive('italic')"
1748
+ [disabled]="!canExecute('toggleItalic')"
1749
+ (onClick)="toggleItalic()"
1750
+ />
1751
+ } @if (config().underline) {
1752
+ <tiptap-button
1753
+ icon="format_underlined"
1754
+ [title]="t().underline"
1755
+ [active]="isActive('underline')"
1756
+ [disabled]="!canExecute('toggleUnderline')"
1757
+ (onClick)="toggleUnderline()"
1758
+ />
1759
+ } @if (config().strike) {
1760
+ <tiptap-button
1761
+ icon="strikethrough_s"
1762
+ [title]="t().strike"
1763
+ [active]="isActive('strike')"
1764
+ [disabled]="!canExecute('toggleStrike')"
1765
+ (onClick)="toggleStrike()"
1766
+ />
1767
+ } @if (config().code) {
1768
+ <tiptap-button
1769
+ icon="code"
1770
+ [title]="t().code"
1771
+ [active]="isActive('code')"
1772
+ [disabled]="!canExecute('toggleCode')"
1773
+ (onClick)="toggleCode()"
1774
+ />
1775
+ } @if (config().superscript) {
1776
+ <tiptap-button
1777
+ icon="superscript"
1778
+ [title]="t().superscript"
1779
+ [active]="isActive('superscript')"
1780
+ [disabled]="!canExecute('toggleSuperscript')"
1781
+ (onClick)="toggleSuperscript()"
1782
+ />
1783
+ } @if (config().subscript) {
1784
+ <tiptap-button
1785
+ icon="subscript"
1786
+ [title]="t().subscript"
1787
+ [active]="isActive('subscript')"
1788
+ [disabled]="!canExecute('toggleSubscript')"
1789
+ (onClick)="toggleSubscript()"
1790
+ />
1791
+ } @if (config().highlight) {
1792
+ <tiptap-button
1793
+ icon="highlight"
1794
+ [title]="t().highlight"
1795
+ [active]="isActive('highlight')"
1796
+ [disabled]="!canExecute('toggleHighlight')"
1797
+ (onClick)="toggleHighlight()"
1798
+ />
1799
+ } @if (config().separator && (config().heading1 || config().heading2 ||
1800
+ config().heading3)) {
1801
+ <tiptap-separator />
1802
+ } @if (config().heading1) {
1803
+ <tiptap-button
1804
+ icon="format_h1"
1805
+ [title]="t().heading1"
1806
+ variant="text"
1807
+ [active]="isActive('heading', { level: 1 })"
1808
+ (onClick)="toggleHeading(1)"
1809
+ />
1810
+ } @if (config().heading2) {
1811
+ <tiptap-button
1812
+ icon="format_h2"
1813
+ [title]="t().heading2"
1814
+ variant="text"
1815
+ [active]="isActive('heading', { level: 2 })"
1816
+ (onClick)="toggleHeading(2)"
1817
+ />
1818
+ } @if (config().heading3) {
1819
+ <tiptap-button
1820
+ icon="format_h3"
1821
+ [title]="t().heading3"
1822
+ variant="text"
1823
+ [active]="isActive('heading', { level: 3 })"
1824
+ (onClick)="toggleHeading(3)"
1825
+ />
1826
+ } @if (config().separator && (config().bulletList || config().orderedList
1827
+ || config().blockquote)) {
1828
+ <tiptap-separator />
1829
+ } @if (config().bulletList) {
1830
+ <tiptap-button
1831
+ icon="format_list_bulleted"
1832
+ [title]="t().bulletList"
1833
+ [active]="isActive('bulletList')"
1834
+ (onClick)="toggleBulletList()"
1835
+ />
1836
+ } @if (config().orderedList) {
1837
+ <tiptap-button
1838
+ icon="format_list_numbered"
1839
+ [title]="t().orderedList"
1840
+ [active]="isActive('orderedList')"
1841
+ (onClick)="toggleOrderedList()"
1842
+ />
1843
+ } @if (config().blockquote) {
1844
+ <tiptap-button
1845
+ icon="format_quote"
1846
+ [title]="t().blockquote"
1847
+ [active]="isActive('blockquote')"
1848
+ (onClick)="toggleBlockquote()"
1849
+ />
1850
+ } @if (config().separator && (config().alignLeft || config().alignCenter
1851
+ || config().alignRight || config().alignJustify)) {
1852
+ <tiptap-separator />
1853
+ } @if (config().alignLeft) {
1854
+ <tiptap-button
1855
+ icon="format_align_left"
1856
+ [title]="t().alignLeft"
1857
+ [active]="isActive('textAlign', { textAlign: 'left' })"
1858
+ (onClick)="setTextAlign('left')"
1859
+ />
1860
+ } @if (config().alignCenter) {
1861
+ <tiptap-button
1862
+ icon="format_align_center"
1863
+ [title]="t().alignCenter"
1864
+ [active]="isActive('textAlign', { textAlign: 'center' })"
1865
+ (onClick)="setTextAlign('center')"
1866
+ />
1867
+ } @if (config().alignRight) {
1868
+ <tiptap-button
1869
+ icon="format_align_right"
1870
+ [title]="t().alignRight"
1871
+ [active]="isActive('textAlign', { textAlign: 'right' })"
1872
+ (onClick)="setTextAlign('right')"
1873
+ />
1874
+ } @if (config().alignJustify) {
1875
+ <tiptap-button
1876
+ icon="format_align_justify"
1877
+ [title]="t().alignJustify"
1878
+ [active]="isActive('textAlign', { textAlign: 'justify' })"
1879
+ (onClick)="setTextAlign('justify')"
1880
+ />
1881
+ } @if (config().separator && (config().link || config().horizontalRule)) {
1882
+ <tiptap-separator />
1883
+ } @if (config().link) {
1884
+ <tiptap-button
1885
+ icon="link"
1886
+ [title]="t().link"
1887
+ [active]="isActive('link')"
1888
+ (onClick)="toggleLink()"
1889
+ />
1890
+ } @if (config().horizontalRule) {
1891
+ <tiptap-button
1892
+ icon="horizontal_rule"
1893
+ [title]="t().horizontalRule"
1894
+ (onClick)="insertHorizontalRule()"
1895
+ />
1896
+ } @if (config().separator && config().image) {
1897
+ <tiptap-separator />
1898
+ } @if (config().image) {
1899
+ <tiptap-button
1900
+ icon="image"
1901
+ [title]="t().image"
1902
+ (onClick)="insertImage()"
1903
+ />
1904
+ } @if (config().separator && (config().undo || config().redo)) {
1905
+ <tiptap-separator />
1906
+ } @if (config().undo) {
1907
+ <tiptap-button
1908
+ icon="undo"
1909
+ [title]="t().undo"
1910
+ [disabled]="!canExecute('undo')"
1911
+ (onClick)="undo()"
1912
+ />
1913
+ } @if (config().redo) {
1914
+ <tiptap-button
1915
+ icon="redo"
1916
+ [title]="t().redo"
1917
+ [disabled]="!canExecute('redo')"
1918
+ (onClick)="redo()"
1919
+ />
1920
+ }
1921
+ </div>
1922
+ `, styles: [".tiptap-toolbar{display:flex;align-items:center;gap:4px;padding:4px 8px;background:#f8f9fa;border-bottom:1px solid #e2e8f0;flex-wrap:wrap;min-height:32px;position:relative}.toolbar-group{display:flex;align-items:center;gap:2px;padding:0 4px}.toolbar-separator{width:1px;height:24px;background:#e2e8f0;margin:0 4px}@media (max-width: 768px){.tiptap-toolbar{padding:6px 8px;gap:2px}.toolbar-group{gap:1px}}@keyframes toolbarSlideIn{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}.tiptap-toolbar{animation:toolbarSlideIn .3s cubic-bezier(.4,0,.2,1)}\n"] }]
1923
+ }], ctorParameters: () => [{ type: EditorCommandsService }] });
1924
+
1925
+ class TiptapImageUploadComponent {
1926
+ constructor() {
1927
+ // Inputs
1928
+ this.config = input({
1929
+ maxSize: 5, // 5MB par défaut
1930
+ maxWidth: 1920, // largeur max par défaut
1931
+ maxHeight: 1080, // hauteur max par défaut
1932
+ allowedTypes: ["image/jpeg", "image/png", "image/gif", "image/webp"],
1933
+ enableDragDrop: true,
1934
+ showPreview: true,
1935
+ multiple: false,
1936
+ compressImages: true,
1937
+ quality: 0.8,
1938
+ });
1939
+ // Outputs
1940
+ this.imageSelected = output();
1941
+ this.error = output();
1942
+ // Signals internes
1943
+ this.isDragOver = signal(false);
1944
+ this.isUploading = signal(false);
1945
+ this.uploadProgress = signal(0);
1946
+ this.previewImage = signal(null);
1947
+ this.previewInfo = signal("");
1948
+ this.errorMessage = signal(null);
1949
+ // Computed
1950
+ this.acceptedTypes = computed(() => {
1951
+ const types = this.config().allowedTypes || ["image/*"];
1952
+ return types.join(",");
1953
+ });
1954
+ }
1955
+ triggerFileInput() {
1956
+ const input = document.querySelector('input[type="file"]');
1957
+ if (input) {
1958
+ input.click();
1959
+ }
1960
+ }
1961
+ onFileSelected(event) {
1962
+ const input = event.target;
1963
+ const files = input.files;
1964
+ if (files && files.length > 0) {
1965
+ this.processFiles(Array.from(files));
1966
+ }
1967
+ // Reset input
1968
+ input.value = "";
1969
+ }
1970
+ onDragOver(event) {
1971
+ event.preventDefault();
1972
+ event.stopPropagation();
1973
+ this.isDragOver.set(true);
1974
+ }
1975
+ onDrop(event) {
1976
+ event.preventDefault();
1977
+ event.stopPropagation();
1978
+ this.isDragOver.set(false);
1979
+ const files = event.dataTransfer?.files;
1980
+ if (files && files.length > 0) {
1981
+ this.processFiles(Array.from(files));
1982
+ }
1983
+ }
1984
+ onDragLeave(event) {
1985
+ event.preventDefault();
1986
+ event.stopPropagation();
1987
+ this.isDragOver.set(false);
1988
+ }
1989
+ processFiles(files) {
1990
+ const config = this.config();
1991
+ const maxSize = (config.maxSize || 5) * 1024 * 1024; // Convertir en bytes
1992
+ const allowedTypes = config.allowedTypes || ["image/*"];
1993
+ // Vérifier le nombre de fichiers
1994
+ if (!config.multiple && files.length > 1) {
1995
+ this.showError("Veuillez sélectionner une seule image");
1996
+ return;
1997
+ }
1998
+ // Traiter chaque fichier
1999
+ files.forEach((file) => {
2000
+ // Vérifier le type
2001
+ if (!this.isValidFileType(file, allowedTypes)) {
2002
+ this.showError(`Type de fichier non supporté: ${file.name}`);
2003
+ return;
2004
+ }
2005
+ // Vérifier la taille
2006
+ if (file.size > maxSize) {
2007
+ this.showError(`Fichier trop volumineux: ${file.name} (max ${config.maxSize}MB)`);
2008
+ return;
2009
+ }
2010
+ // Traiter l'image avec compression si nécessaire
2011
+ this.processImage(file);
2012
+ });
2013
+ }
2014
+ isValidFileType(file, allowedTypes) {
2015
+ if (allowedTypes.includes("image/*")) {
2016
+ return file.type.startsWith("image/");
2017
+ }
2018
+ return allowedTypes.includes(file.type);
2019
+ }
2020
+ processImage(file) {
2021
+ this.isUploading.set(true);
2022
+ this.uploadProgress.set(10);
2023
+ const config = this.config();
2024
+ const originalSize = file.size;
2025
+ // Créer un canvas pour la compression
2026
+ const canvas = document.createElement("canvas");
2027
+ const ctx = canvas.getContext("2d");
2028
+ const img = new Image();
2029
+ img.onload = () => {
2030
+ this.uploadProgress.set(30);
2031
+ // Vérifier les dimensions
2032
+ const maxWidth = config.maxWidth || 1920;
2033
+ const maxHeight = config.maxHeight || 1080;
2034
+ let { width, height } = img;
2035
+ // Redimensionner si nécessaire
2036
+ if (width > maxWidth || height > maxHeight) {
2037
+ const ratio = Math.min(maxWidth / width, maxHeight / height);
2038
+ width *= ratio;
2039
+ height *= ratio;
2040
+ }
2041
+ canvas.width = width;
2042
+ canvas.height = height;
2043
+ // Dessiner l'image redimensionnée
2044
+ ctx?.drawImage(img, 0, 0, width, height);
2045
+ this.uploadProgress.set(70);
2046
+ // Convertir en base64 avec compression
2047
+ const quality = config.quality || 0.8;
2048
+ const mimeType = file.type;
2049
+ canvas.toBlob((blob) => {
2050
+ this.uploadProgress.set(90);
2051
+ if (blob) {
2052
+ const reader = new FileReader();
2053
+ reader.onload = (e) => {
2054
+ const base64 = e.target?.result;
2055
+ if (base64) {
2056
+ const result = {
2057
+ src: base64,
2058
+ name: file.name,
2059
+ size: blob.size,
2060
+ type: file.type,
2061
+ width: Math.round(width),
2062
+ height: Math.round(height),
2063
+ originalSize: originalSize,
2064
+ };
2065
+ // Afficher la prévisualisation si activée
2066
+ if (config.showPreview) {
2067
+ this.previewImage.set(base64);
2068
+ this.previewInfo.set(`${result.width}×${result.height} • ${this.formatFileSize(blob.size)}`);
2069
+ }
2070
+ // Émettre l'événement
2071
+ this.imageSelected.emit(result);
2072
+ this.clearError();
2073
+ }
2074
+ this.uploadProgress.set(100);
2075
+ setTimeout(() => {
2076
+ this.isUploading.set(false);
2077
+ this.uploadProgress.set(0);
2078
+ }, 500);
2079
+ };
2080
+ reader.readAsDataURL(blob);
2081
+ }
2082
+ else {
2083
+ this.showError("Erreur lors de la compression de l'image");
2084
+ this.isUploading.set(false);
2085
+ this.uploadProgress.set(0);
2086
+ }
2087
+ }, mimeType, quality);
2088
+ };
2089
+ img.onerror = () => {
2090
+ this.showError("Erreur lors du chargement de l'image");
2091
+ this.isUploading.set(false);
2092
+ this.uploadProgress.set(0);
2093
+ };
2094
+ img.src = URL.createObjectURL(file);
2095
+ }
2096
+ formatFileSize(bytes) {
2097
+ if (bytes === 0)
2098
+ return "0 B";
2099
+ const k = 1024;
2100
+ const sizes = ["B", "KB", "MB", "GB"];
2101
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
2102
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
2103
+ }
2104
+ showError(message) {
2105
+ this.errorMessage.set(message);
2106
+ this.error.emit(message);
2107
+ this.isUploading.set(false);
2108
+ this.uploadProgress.set(0);
2109
+ // Auto-clear après 5 secondes
2110
+ setTimeout(() => {
2111
+ this.clearError();
2112
+ }, 5000);
2113
+ }
2114
+ clearError() {
2115
+ this.errorMessage.set(null);
2116
+ }
2117
+ clearPreview() {
2118
+ this.previewImage.set(null);
2119
+ this.previewInfo.set("");
2120
+ }
2121
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapImageUploadComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2122
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.0", type: TiptapImageUploadComponent, isStandalone: true, selector: "tiptap-image-upload", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { imageSelected: "imageSelected", error: "error" }, ngImport: i0, template: `
2123
+ <div class="image-upload-container">
2124
+ <!-- Bouton d'upload -->
2125
+ <tiptap-button
2126
+ icon="image"
2127
+ title="Ajouter une image"
2128
+ [disabled]="isUploading()"
2129
+ (onClick)="triggerFileInput()"
2130
+ />
2131
+
2132
+ <!-- Input file caché -->
2133
+ <input
2134
+ #fileInput
2135
+ type="file"
2136
+ [accept]="acceptedTypes()"
2137
+ [multiple]="config().multiple"
2138
+ (change)="onFileSelected($event)"
2139
+ style="display: none;"
2140
+ />
2141
+
2142
+ <!-- Zone de drag & drop -->
2143
+ @if (config().enableDragDrop && isDragOver()) {
2144
+ <div
2145
+ class="drag-overlay"
2146
+ (dragover)="onDragOver($event)"
2147
+ (drop)="onDrop($event)"
2148
+ (dragleave)="onDragLeave($event)"
2149
+ >
2150
+ <div class="drag-content">
2151
+ <span class="material-symbols-outlined">cloud_upload</span>
2152
+ <p>Déposez votre image ici</p>
2153
+ </div>
2154
+ </div>
2155
+ }
2156
+
2157
+ <!-- Barre de progression -->
2158
+ @if (isUploading() && uploadProgress() > 0) {
2159
+ <div class="upload-progress">
2160
+ <div class="progress-bar">
2161
+ <div class="progress-fill" [style.width.%]="uploadProgress()"></div>
2162
+ </div>
2163
+ <div class="progress-text">{{ uploadProgress() }}%</div>
2164
+ </div>
2165
+ }
2166
+
2167
+ <!-- Prévisualisation -->
2168
+ @if (config().showPreview && previewImage()) {
2169
+ <div class="image-preview">
2170
+ <img [src]="previewImage()" alt="Prévisualisation" />
2171
+ <div class="preview-info">
2172
+ <span>{{ previewInfo() }}</span>
2173
+ </div>
2174
+ <button
2175
+ class="preview-close"
2176
+ (click)="clearPreview()"
2177
+ title="Fermer la prévisualisation"
2178
+ >
2179
+ <span class="material-symbols-outlined">close</span>
2180
+ </button>
2181
+ </div>
2182
+ }
2183
+
2184
+ <!-- Messages d'erreur -->
2185
+ @if (errorMessage()) {
2186
+ <div class="error-message">
2187
+ <span class="material-symbols-outlined">error</span>
2188
+ {{ errorMessage() }}
2189
+ </div>
2190
+ }
2191
+ </div>
2192
+ `, isInline: true, styles: [".image-upload-container{position:relative;display:inline-block}.drag-overlay{position:fixed;inset:0;background:#3182ce1a;border:2px dashed #3182ce;border-radius:6px;display:flex;align-items:center;justify-content:center;z-index:1000;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.drag-content{text-align:center;color:#3182ce;font-weight:600}.drag-content .material-symbols-outlined{font-size:48px;margin-bottom:16px}.drag-content p{margin:0;font-size:18px}.upload-progress{position:absolute;top:100%;left:0;background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:12px;margin-top:8px;z-index:100;min-width:200px;box-shadow:0 4px 12px #00000026}.progress-bar{width:100%;height:6px;background:#e2e8f0;border-radius:3px;overflow:hidden;margin-bottom:8px}.progress-fill{height:100%;background:#3182ce;border-radius:3px;transition:width .3s ease}.progress-text{font-size:12px;color:#4a5568;text-align:center}.image-preview{position:absolute;top:100%;left:0;background:#fff;border:1px solid #e2e8f0;border-radius:8px;box-shadow:0 4px 12px #00000026;padding:8px;margin-top:8px;z-index:100;min-width:200px}.image-preview img{max-width:200px;max-height:150px;border-radius:4px;display:block}.preview-info{margin-top:8px;font-size:11px;color:#718096;text-align:center}.preview-close{position:absolute;top:4px;right:4px;background:#000000b3;color:#fff;border:none;border-radius:50%;width:24px;height:24px;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:14px}.preview-close:hover{background:#000000e6}.error-message{position:absolute;top:100%;left:0;background:#fed7d7;color:#c53030;border:1px solid #feb2b2;border-radius:6px;padding:8px 12px;margin-top:8px;font-size:12px;display:flex;align-items:center;gap:6px;z-index:100;min-width:200px}.error-message .material-symbols-outlined{font-size:16px}\n"], dependencies: [{ kind: "component", type: TiptapButtonComponent, selector: "tiptap-button", inputs: ["icon", "title", "active", "disabled", "variant", "size", "iconSize"], outputs: ["onClick"] }] }); }
2193
+ }
2194
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapImageUploadComponent, decorators: [{
2195
+ type: Component,
2196
+ args: [{ selector: "tiptap-image-upload", standalone: true, imports: [TiptapButtonComponent], template: `
2197
+ <div class="image-upload-container">
2198
+ <!-- Bouton d'upload -->
2199
+ <tiptap-button
2200
+ icon="image"
2201
+ title="Ajouter une image"
2202
+ [disabled]="isUploading()"
2203
+ (onClick)="triggerFileInput()"
2204
+ />
2205
+
2206
+ <!-- Input file caché -->
2207
+ <input
2208
+ #fileInput
2209
+ type="file"
2210
+ [accept]="acceptedTypes()"
2211
+ [multiple]="config().multiple"
2212
+ (change)="onFileSelected($event)"
2213
+ style="display: none;"
2214
+ />
2215
+
2216
+ <!-- Zone de drag & drop -->
2217
+ @if (config().enableDragDrop && isDragOver()) {
2218
+ <div
2219
+ class="drag-overlay"
2220
+ (dragover)="onDragOver($event)"
2221
+ (drop)="onDrop($event)"
2222
+ (dragleave)="onDragLeave($event)"
2223
+ >
2224
+ <div class="drag-content">
2225
+ <span class="material-symbols-outlined">cloud_upload</span>
2226
+ <p>Déposez votre image ici</p>
2227
+ </div>
2228
+ </div>
2229
+ }
2230
+
2231
+ <!-- Barre de progression -->
2232
+ @if (isUploading() && uploadProgress() > 0) {
2233
+ <div class="upload-progress">
2234
+ <div class="progress-bar">
2235
+ <div class="progress-fill" [style.width.%]="uploadProgress()"></div>
2236
+ </div>
2237
+ <div class="progress-text">{{ uploadProgress() }}%</div>
2238
+ </div>
2239
+ }
2240
+
2241
+ <!-- Prévisualisation -->
2242
+ @if (config().showPreview && previewImage()) {
2243
+ <div class="image-preview">
2244
+ <img [src]="previewImage()" alt="Prévisualisation" />
2245
+ <div class="preview-info">
2246
+ <span>{{ previewInfo() }}</span>
2247
+ </div>
2248
+ <button
2249
+ class="preview-close"
2250
+ (click)="clearPreview()"
2251
+ title="Fermer la prévisualisation"
2252
+ >
2253
+ <span class="material-symbols-outlined">close</span>
2254
+ </button>
2255
+ </div>
2256
+ }
2257
+
2258
+ <!-- Messages d'erreur -->
2259
+ @if (errorMessage()) {
2260
+ <div class="error-message">
2261
+ <span class="material-symbols-outlined">error</span>
2262
+ {{ errorMessage() }}
2263
+ </div>
2264
+ }
2265
+ </div>
2266
+ `, styles: [".image-upload-container{position:relative;display:inline-block}.drag-overlay{position:fixed;inset:0;background:#3182ce1a;border:2px dashed #3182ce;border-radius:6px;display:flex;align-items:center;justify-content:center;z-index:1000;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.drag-content{text-align:center;color:#3182ce;font-weight:600}.drag-content .material-symbols-outlined{font-size:48px;margin-bottom:16px}.drag-content p{margin:0;font-size:18px}.upload-progress{position:absolute;top:100%;left:0;background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:12px;margin-top:8px;z-index:100;min-width:200px;box-shadow:0 4px 12px #00000026}.progress-bar{width:100%;height:6px;background:#e2e8f0;border-radius:3px;overflow:hidden;margin-bottom:8px}.progress-fill{height:100%;background:#3182ce;border-radius:3px;transition:width .3s ease}.progress-text{font-size:12px;color:#4a5568;text-align:center}.image-preview{position:absolute;top:100%;left:0;background:#fff;border:1px solid #e2e8f0;border-radius:8px;box-shadow:0 4px 12px #00000026;padding:8px;margin-top:8px;z-index:100;min-width:200px}.image-preview img{max-width:200px;max-height:150px;border-radius:4px;display:block}.preview-info{margin-top:8px;font-size:11px;color:#718096;text-align:center}.preview-close{position:absolute;top:4px;right:4px;background:#000000b3;color:#fff;border:none;border-radius:50%;width:24px;height:24px;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:14px}.preview-close:hover{background:#000000e6}.error-message{position:absolute;top:100%;left:0;background:#fed7d7;color:#c53030;border:1px solid #feb2b2;border-radius:6px;padding:8px 12px;margin-top:8px;font-size:12px;display:flex;align-items:center;gap:6px;z-index:100;min-width:200px}.error-message .material-symbols-outlined{font-size:16px}\n"] }]
2267
+ }] });
2268
+
2269
+ class TiptapBubbleMenuComponent {
2270
+ // Effect comme propriété de classe pour éviter l'erreur d'injection context
2271
+ constructor() {
2272
+ this.editor = input.required();
2273
+ this.config = input({
2274
+ bold: true,
2275
+ italic: true,
2276
+ underline: true,
2277
+ strike: true,
2278
+ code: true,
2279
+ superscript: false,
2280
+ subscript: false,
2281
+ highlight: true,
2282
+ link: true,
2283
+ separator: true,
2284
+ });
2285
+ this.tippyInstance = null;
2286
+ this.updateTimeout = null;
2287
+ this.bubbleMenuConfig = computed(() => ({
2288
+ bold: true,
2289
+ italic: true,
2290
+ underline: true,
2291
+ strike: true,
2292
+ code: true,
2293
+ superscript: false,
2294
+ subscript: false,
2295
+ highlight: true,
2296
+ link: true,
2297
+ separator: true,
2298
+ ...this.config(),
2299
+ }));
2300
+ this.updateMenu = () => {
2301
+ // Debounce pour éviter les appels trop fréquents
2302
+ if (this.updateTimeout) {
2303
+ clearTimeout(this.updateTimeout);
2304
+ }
2305
+ this.updateTimeout = setTimeout(() => {
2306
+ const ed = this.editor();
2307
+ if (!ed)
2308
+ return;
2309
+ const { from, to } = ed.state.selection;
2310
+ const hasTextSelection = from !== to;
2311
+ const isImageSelected = ed.isActive("image") || ed.isActive("resizableImage");
2312
+ // Ne montrer le menu texte que si :
2313
+ // - Il y a une sélection de texte
2314
+ // - Aucune image n'est sélectionnée (priorité aux images)
2315
+ // - L'éditeur est éditable
2316
+ const shouldShow = hasTextSelection && !isImageSelected && ed.isEditable;
2317
+ if (shouldShow) {
2318
+ this.showTippy();
2319
+ }
2320
+ else {
2321
+ this.hideTippy();
2322
+ }
2323
+ }, 10);
2324
+ };
2325
+ this.handleBlur = () => {
2326
+ // Masquer le menu quand l'éditeur perd le focus
2327
+ setTimeout(() => {
2328
+ this.hideTippy();
2329
+ }, 100);
2330
+ };
2331
+ effect(() => {
2332
+ const ed = this.editor();
2333
+ if (!ed)
2334
+ return;
2335
+ // Nettoyer les anciens listeners
2336
+ ed.off("selectionUpdate", this.updateMenu);
2337
+ ed.off("transaction", this.updateMenu);
2338
+ ed.off("focus", this.updateMenu);
2339
+ ed.off("blur", this.handleBlur);
2340
+ // Ajouter les nouveaux listeners
2341
+ ed.on("selectionUpdate", this.updateMenu);
2342
+ ed.on("transaction", this.updateMenu);
2343
+ ed.on("focus", this.updateMenu);
2344
+ ed.on("blur", this.handleBlur);
2345
+ // Ne pas appeler updateMenu() ici pour éviter l'affichage prématuré
2346
+ // Il sera appelé automatiquement quand l'éditeur sera prêt
2347
+ });
2348
+ }
2349
+ ngOnInit() {
2350
+ // Initialiser Tippy de manière synchrone après que le component soit ready
2351
+ this.initTippy();
2352
+ }
2353
+ ngOnDestroy() {
2354
+ const ed = this.editor();
2355
+ if (ed) {
2356
+ ed.off("selectionUpdate", this.updateMenu);
2357
+ ed.off("transaction", this.updateMenu);
2358
+ ed.off("focus", this.updateMenu);
2359
+ ed.off("blur", this.handleBlur);
2360
+ }
2361
+ // Nettoyer les timeouts
2362
+ if (this.updateTimeout) {
2363
+ clearTimeout(this.updateTimeout);
2364
+ }
2365
+ // Nettoyer Tippy
2366
+ if (this.tippyInstance) {
2367
+ this.tippyInstance.destroy();
2368
+ this.tippyInstance = null;
2369
+ }
2370
+ }
2371
+ initTippy() {
2372
+ // Attendre que l'élément soit disponible
2373
+ if (!this.menuRef?.nativeElement) {
2374
+ setTimeout(() => this.initTippy(), 50);
2375
+ return;
2376
+ }
2377
+ const menuElement = this.menuRef.nativeElement;
2378
+ // S'assurer qu'il n'y a pas déjà une instance
2379
+ if (this.tippyInstance) {
2380
+ this.tippyInstance.destroy();
2381
+ }
2382
+ // Créer l'instance Tippy
2383
+ this.tippyInstance = tippy(document.body, {
2384
+ content: menuElement,
2385
+ trigger: "manual",
2386
+ placement: "top-start",
2387
+ appendTo: () => document.body,
2388
+ interactive: true,
2389
+ arrow: false,
2390
+ offset: [0, 8],
2391
+ hideOnClick: false,
2392
+ onShow: (instance) => {
2393
+ // S'assurer que les autres menus sont fermés
2394
+ this.hideOtherMenus();
2395
+ },
2396
+ getReferenceClientRect: () => this.getSelectionRect(),
2397
+ // Améliorer le positionnement avec scroll
2398
+ popperOptions: {
2399
+ modifiers: [
2400
+ {
2401
+ name: "preventOverflow",
2402
+ options: {
2403
+ boundary: "viewport",
2404
+ padding: 8,
2405
+ },
2406
+ },
2407
+ {
2408
+ name: "flip",
2409
+ options: {
2410
+ fallbackPlacements: ["bottom-start", "top-end", "bottom-end"],
2411
+ },
2412
+ },
2413
+ ],
2414
+ },
2415
+ });
2416
+ // Maintenant que Tippy est initialisé, faire un premier check
2417
+ this.updateMenu();
2418
+ }
2419
+ getSelectionRect() {
2420
+ const selection = window.getSelection();
2421
+ if (!selection || selection.rangeCount === 0) {
2422
+ return new DOMRect(0, 0, 0, 0);
2423
+ }
2424
+ const range = selection.getRangeAt(0);
2425
+ return range.getBoundingClientRect();
2426
+ }
2427
+ hideOtherMenus() {
2428
+ // Cette méthode peut être étendue pour fermer d'autres menus si nécessaire
2429
+ // Pour l'instant, elle sert de placeholder pour une future coordination entre menus
2430
+ }
2431
+ showTippy() {
2432
+ if (!this.tippyInstance)
2433
+ return;
2434
+ // Mettre à jour la position
2435
+ this.tippyInstance.setProps({
2436
+ getReferenceClientRect: () => this.getSelectionRect(),
2437
+ });
2438
+ this.tippyInstance.show();
2439
+ }
2440
+ hideTippy() {
2441
+ if (this.tippyInstance) {
2442
+ this.tippyInstance.hide();
2443
+ }
2444
+ }
2445
+ isActive(mark) {
2446
+ const ed = this.editor();
2447
+ return ed?.isActive(mark) || false;
2448
+ }
2449
+ onCommand(command, event) {
2450
+ event.preventDefault();
2451
+ const ed = this.editor();
2452
+ if (!ed)
2453
+ return;
2454
+ switch (command) {
2455
+ case "bold":
2456
+ ed.chain().focus().toggleBold().run();
2457
+ break;
2458
+ case "italic":
2459
+ ed.chain().focus().toggleItalic().run();
2460
+ break;
2461
+ case "underline":
2462
+ ed.chain().focus().toggleUnderline().run();
2463
+ break;
2464
+ case "strike":
2465
+ ed.chain().focus().toggleStrike().run();
2466
+ break;
2467
+ case "code":
2468
+ ed.chain().focus().toggleCode().run();
2469
+ break;
2470
+ case "superscript":
2471
+ ed.chain().focus().toggleSuperscript().run();
2472
+ break;
2473
+ case "subscript":
2474
+ ed.chain().focus().toggleSubscript().run();
2475
+ break;
2476
+ case "highlight":
2477
+ ed.chain().focus().toggleHighlight().run();
2478
+ break;
2479
+ case "link":
2480
+ const href = window.prompt("URL du lien:");
2481
+ if (href) {
2482
+ ed.chain().focus().toggleLink({ href }).run();
2483
+ }
2484
+ break;
2485
+ }
2486
+ }
2487
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapBubbleMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2488
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.0", type: TiptapBubbleMenuComponent, isStandalone: true, selector: "tiptap-bubble-menu", inputs: { editor: { classPropertyName: "editor", publicName: "editor", isSignal: true, isRequired: true, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "menuRef", first: true, predicate: ["menuRef"], descendants: true }], ngImport: i0, template: `
2489
+ <div #menuRef class="bubble-menu">
2490
+ @if (bubbleMenuConfig().bold) {
2491
+ <tiptap-button
2492
+ icon="format_bold"
2493
+ title="Gras"
2494
+ [active]="isActive('bold')"
2495
+ (click)="onCommand('bold', $event)"
2496
+ ></tiptap-button>
2497
+ } @if (bubbleMenuConfig().italic) {
2498
+ <tiptap-button
2499
+ icon="format_italic"
2500
+ title="Italique"
2501
+ [active]="isActive('italic')"
2502
+ (click)="onCommand('italic', $event)"
2503
+ ></tiptap-button>
2504
+ } @if (bubbleMenuConfig().underline) {
2505
+ <tiptap-button
2506
+ icon="format_underlined"
2507
+ title="Souligné"
2508
+ [active]="isActive('underline')"
2509
+ (click)="onCommand('underline', $event)"
2510
+ ></tiptap-button>
2511
+ } @if (bubbleMenuConfig().strike) {
2512
+ <tiptap-button
2513
+ icon="strikethrough_s"
2514
+ title="Barré"
2515
+ [active]="isActive('strike')"
2516
+ (click)="onCommand('strike', $event)"
2517
+ ></tiptap-button>
2518
+ } @if (bubbleMenuConfig().superscript) {
2519
+ <tiptap-button
2520
+ icon="superscript"
2521
+ title="Exposant"
2522
+ [active]="isActive('superscript')"
2523
+ (click)="onCommand('superscript', $event)"
2524
+ ></tiptap-button>
2525
+ } @if (bubbleMenuConfig().subscript) {
2526
+ <tiptap-button
2527
+ icon="subscript"
2528
+ title="Indice"
2529
+ [active]="isActive('subscript')"
2530
+ (click)="onCommand('subscript', $event)"
2531
+ ></tiptap-button>
2532
+ } @if (bubbleMenuConfig().highlight) {
2533
+ <tiptap-button
2534
+ icon="highlight"
2535
+ title="Surbrillance"
2536
+ [active]="isActive('highlight')"
2537
+ (click)="onCommand('highlight', $event)"
2538
+ ></tiptap-button>
2539
+ } @if (bubbleMenuConfig().separator && (bubbleMenuConfig().code ||
2540
+ bubbleMenuConfig().link)) {
2541
+ <div class="tiptap-separator"></div>
2542
+ } @if (bubbleMenuConfig().code) {
2543
+ <tiptap-button
2544
+ icon="code"
2545
+ title="Code"
2546
+ [active]="isActive('code')"
2547
+ (click)="onCommand('code', $event)"
2548
+ ></tiptap-button>
2549
+ } @if (bubbleMenuConfig().link) {
2550
+ <tiptap-button
2551
+ icon="link"
2552
+ title="Lien"
2553
+ [active]="isActive('link')"
2554
+ (click)="onCommand('link', $event)"
2555
+ ></tiptap-button>
2556
+ }
2557
+ </div>
2558
+ `, isInline: true, dependencies: [{ kind: "component", type: TiptapButtonComponent, selector: "tiptap-button", inputs: ["icon", "title", "active", "disabled", "variant", "size", "iconSize"], outputs: ["onClick"] }] }); }
2559
+ }
2560
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapBubbleMenuComponent, decorators: [{
2561
+ type: Component,
2562
+ args: [{ selector: "tiptap-bubble-menu", standalone: true, imports: [TiptapButtonComponent], template: `
2563
+ <div #menuRef class="bubble-menu">
2564
+ @if (bubbleMenuConfig().bold) {
2565
+ <tiptap-button
2566
+ icon="format_bold"
2567
+ title="Gras"
2568
+ [active]="isActive('bold')"
2569
+ (click)="onCommand('bold', $event)"
2570
+ ></tiptap-button>
2571
+ } @if (bubbleMenuConfig().italic) {
2572
+ <tiptap-button
2573
+ icon="format_italic"
2574
+ title="Italique"
2575
+ [active]="isActive('italic')"
2576
+ (click)="onCommand('italic', $event)"
2577
+ ></tiptap-button>
2578
+ } @if (bubbleMenuConfig().underline) {
2579
+ <tiptap-button
2580
+ icon="format_underlined"
2581
+ title="Souligné"
2582
+ [active]="isActive('underline')"
2583
+ (click)="onCommand('underline', $event)"
2584
+ ></tiptap-button>
2585
+ } @if (bubbleMenuConfig().strike) {
2586
+ <tiptap-button
2587
+ icon="strikethrough_s"
2588
+ title="Barré"
2589
+ [active]="isActive('strike')"
2590
+ (click)="onCommand('strike', $event)"
2591
+ ></tiptap-button>
2592
+ } @if (bubbleMenuConfig().superscript) {
2593
+ <tiptap-button
2594
+ icon="superscript"
2595
+ title="Exposant"
2596
+ [active]="isActive('superscript')"
2597
+ (click)="onCommand('superscript', $event)"
2598
+ ></tiptap-button>
2599
+ } @if (bubbleMenuConfig().subscript) {
2600
+ <tiptap-button
2601
+ icon="subscript"
2602
+ title="Indice"
2603
+ [active]="isActive('subscript')"
2604
+ (click)="onCommand('subscript', $event)"
2605
+ ></tiptap-button>
2606
+ } @if (bubbleMenuConfig().highlight) {
2607
+ <tiptap-button
2608
+ icon="highlight"
2609
+ title="Surbrillance"
2610
+ [active]="isActive('highlight')"
2611
+ (click)="onCommand('highlight', $event)"
2612
+ ></tiptap-button>
2613
+ } @if (bubbleMenuConfig().separator && (bubbleMenuConfig().code ||
2614
+ bubbleMenuConfig().link)) {
2615
+ <div class="tiptap-separator"></div>
2616
+ } @if (bubbleMenuConfig().code) {
2617
+ <tiptap-button
2618
+ icon="code"
2619
+ title="Code"
2620
+ [active]="isActive('code')"
2621
+ (click)="onCommand('code', $event)"
2622
+ ></tiptap-button>
2623
+ } @if (bubbleMenuConfig().link) {
2624
+ <tiptap-button
2625
+ icon="link"
2626
+ title="Lien"
2627
+ [active]="isActive('link')"
2628
+ (click)="onCommand('link', $event)"
2629
+ ></tiptap-button>
2630
+ }
2631
+ </div>
2632
+ ` }]
2633
+ }], ctorParameters: () => [], propDecorators: { menuRef: [{
2634
+ type: ViewChild,
2635
+ args: ["menuRef", { static: false }]
2636
+ }] } });
2637
+
2638
+ class TiptapImageBubbleMenuComponent {
2639
+ constructor() {
2640
+ this.editor = input.required();
2641
+ this.config = input({
2642
+ changeImage: true,
2643
+ resizeSmall: true,
2644
+ resizeMedium: true,
2645
+ resizeLarge: true,
2646
+ resizeOriginal: true,
2647
+ deleteImage: true,
2648
+ separator: true,
2649
+ });
2650
+ this.tippyInstance = null;
2651
+ this.imageService = inject(ImageService);
2652
+ this.updateTimeout = null;
2653
+ this.imageBubbleMenuConfig = computed(() => ({
2654
+ changeImage: true,
2655
+ resizeSmall: true,
2656
+ resizeMedium: true,
2657
+ resizeLarge: true,
2658
+ resizeOriginal: true,
2659
+ deleteImage: true,
2660
+ separator: true,
2661
+ ...this.config(),
2662
+ }));
2663
+ this.hasResizeButtons = computed(() => {
2664
+ const config = this.imageBubbleMenuConfig();
2665
+ return (config.resizeSmall ||
2666
+ config.resizeMedium ||
2667
+ config.resizeLarge ||
2668
+ config.resizeOriginal);
2669
+ });
2670
+ this.updateMenu = () => {
2671
+ // Debounce pour éviter les appels trop fréquents
2672
+ if (this.updateTimeout) {
2673
+ clearTimeout(this.updateTimeout);
2674
+ }
2675
+ this.updateTimeout = setTimeout(() => {
2676
+ const ed = this.editor();
2677
+ if (!ed)
2678
+ return;
2679
+ const isImageSelected = ed.isActive("resizableImage") || ed.isActive("image");
2680
+ const { from, to } = ed.state.selection;
2681
+ const hasTextSelection = from !== to;
2682
+ // Ne montrer le menu image que si :
2683
+ // - Une image est sélectionnée
2684
+ // - L'éditeur est éditable
2685
+ const shouldShow = isImageSelected && ed.isEditable;
2686
+ if (shouldShow) {
2687
+ this.showTippy();
2688
+ }
2689
+ else {
2690
+ this.hideTippy();
2691
+ }
2692
+ }, 10);
2693
+ };
2694
+ this.handleBlur = () => {
2695
+ // Masquer le menu quand l'éditeur perd le focus
2696
+ setTimeout(() => {
2697
+ this.hideTippy();
2698
+ }, 100);
2699
+ };
2700
+ effect(() => {
2701
+ const ed = this.editor();
2702
+ if (!ed)
2703
+ return;
2704
+ // Nettoyer les anciens listeners
2705
+ ed.off("selectionUpdate", this.updateMenu);
2706
+ ed.off("transaction", this.updateMenu);
2707
+ ed.off("focus", this.updateMenu);
2708
+ ed.off("blur", this.handleBlur);
2709
+ // Ajouter les nouveaux listeners
2710
+ ed.on("selectionUpdate", this.updateMenu);
2711
+ ed.on("transaction", this.updateMenu);
2712
+ ed.on("focus", this.updateMenu);
2713
+ ed.on("blur", this.handleBlur);
2714
+ // Ne pas appeler updateMenu() ici pour éviter l'affichage prématuré
2715
+ // Il sera appelé automatiquement quand l'éditeur sera prêt
2716
+ });
2717
+ }
2718
+ ngOnInit() {
2719
+ // Initialiser Tippy de manière synchrone après que le component soit ready
2720
+ this.initTippy();
2721
+ }
2722
+ ngOnDestroy() {
2723
+ const ed = this.editor();
2724
+ if (ed) {
2725
+ ed.off("selectionUpdate", this.updateMenu);
2726
+ ed.off("transaction", this.updateMenu);
2727
+ ed.off("focus", this.updateMenu);
2728
+ ed.off("blur", this.handleBlur);
2729
+ }
2730
+ // Nettoyer les timeouts
2731
+ if (this.updateTimeout) {
2732
+ clearTimeout(this.updateTimeout);
2733
+ }
2734
+ // Nettoyer Tippy
2735
+ if (this.tippyInstance) {
2736
+ this.tippyInstance.destroy();
2737
+ this.tippyInstance = null;
2738
+ }
2739
+ }
2740
+ initTippy() {
2741
+ // Attendre que l'élément soit disponible
2742
+ if (!this.menuRef?.nativeElement) {
2743
+ setTimeout(() => this.initTippy(), 50);
2744
+ return;
2745
+ }
2746
+ const menuElement = this.menuRef.nativeElement;
2747
+ // S'assurer qu'il n'y a pas déjà une instance
2748
+ if (this.tippyInstance) {
2749
+ this.tippyInstance.destroy();
2750
+ }
2751
+ // Créer l'instance Tippy
2752
+ this.tippyInstance = tippy(document.body, {
2753
+ content: menuElement,
2754
+ trigger: "manual",
2755
+ placement: "top-start",
2756
+ appendTo: () => document.body,
2757
+ interactive: true,
2758
+ arrow: false,
2759
+ offset: [0, 8],
2760
+ hideOnClick: false,
2761
+ onShow: (instance) => {
2762
+ // S'assurer que les autres menus sont fermés
2763
+ this.hideOtherMenus();
2764
+ },
2765
+ getReferenceClientRect: () => this.getImageRect(),
2766
+ // Améliorer le positionnement avec scroll
2767
+ popperOptions: {
2768
+ modifiers: [
2769
+ {
2770
+ name: "preventOverflow",
2771
+ options: {
2772
+ boundary: "viewport",
2773
+ padding: 8,
2774
+ },
2775
+ },
2776
+ {
2777
+ name: "flip",
2778
+ options: {
2779
+ fallbackPlacements: ["bottom-start", "top-end", "bottom-end"],
2780
+ },
2781
+ },
2782
+ ],
2783
+ },
2784
+ });
2785
+ // Maintenant que Tippy est initialisé, faire un premier check
2786
+ this.updateMenu();
2787
+ }
2788
+ getImageRect() {
2789
+ const ed = this.editor();
2790
+ if (!ed)
2791
+ return new DOMRect(0, 0, 0, 0);
2792
+ // Trouver l'image sélectionnée dans le DOM
2793
+ const { from } = ed.state.selection;
2794
+ // Fonction pour trouver toutes les images dans l'éditeur
2795
+ const getAllImages = () => {
2796
+ const editorElement = ed.view.dom;
2797
+ return Array.from(editorElement.querySelectorAll("img"));
2798
+ };
2799
+ // Fonction pour trouver l'image à la position spécifique
2800
+ const findImageAtPosition = () => {
2801
+ const allImages = getAllImages();
2802
+ for (const img of allImages) {
2803
+ try {
2804
+ // Obtenir la position ProseMirror de cette image
2805
+ const imgPos = ed.view.posAtDOM(img, 0);
2806
+ // Vérifier si cette image correspond à la position sélectionnée
2807
+ if (Math.abs(imgPos - from) <= 1) {
2808
+ return img;
2809
+ }
2810
+ }
2811
+ catch (error) {
2812
+ // Continuer si on ne peut pas obtenir la position de cette image
2813
+ continue;
2814
+ }
2815
+ }
2816
+ return null;
2817
+ };
2818
+ // Chercher l'image à la position exacte
2819
+ const imageElement = findImageAtPosition();
2820
+ if (imageElement) {
2821
+ return imageElement.getBoundingClientRect();
2822
+ }
2823
+ return new DOMRect(0, 0, 0, 0);
2824
+ }
2825
+ hideOtherMenus() {
2826
+ // Cette méthode peut être étendue pour fermer d'autres menus si nécessaire
2827
+ // Pour l'instant, elle sert de placeholder pour une future coordination entre menus
2828
+ }
2829
+ showTippy() {
2830
+ if (!this.tippyInstance)
2831
+ return;
2832
+ // Mettre à jour la position
2833
+ this.tippyInstance.setProps({
2834
+ getReferenceClientRect: () => this.getImageRect(),
2835
+ });
2836
+ this.tippyInstance.show();
2837
+ }
2838
+ hideTippy() {
2839
+ if (this.tippyInstance) {
2840
+ this.tippyInstance.hide();
2841
+ }
2842
+ }
2843
+ onCommand(command, event) {
2844
+ event.preventDefault();
2845
+ const ed = this.editor();
2846
+ if (!ed)
2847
+ return;
2848
+ switch (command) {
2849
+ case "changeImage":
2850
+ this.changeImage();
2851
+ break;
2852
+ case "resizeSmall":
2853
+ this.imageService.resizeImageToSmall(ed);
2854
+ break;
2855
+ case "resizeMedium":
2856
+ this.imageService.resizeImageToMedium(ed);
2857
+ break;
2858
+ case "resizeLarge":
2859
+ this.imageService.resizeImageToLarge(ed);
2860
+ break;
2861
+ case "resizeOriginal":
2862
+ this.imageService.resizeImageToOriginal(ed);
2863
+ break;
2864
+ case "deleteImage":
2865
+ this.deleteImage();
2866
+ break;
2867
+ }
2868
+ }
2869
+ async changeImage() {
2870
+ const ed = this.editor();
2871
+ if (!ed)
2872
+ return;
2873
+ try {
2874
+ // Utiliser la méthode spécifique pour remplacer une image existante
2875
+ await this.imageService.selectAndReplaceImage(ed, {
2876
+ quality: 0.8,
2877
+ maxWidth: 1920,
2878
+ maxHeight: 1080,
2879
+ accept: "image/*",
2880
+ });
2881
+ }
2882
+ catch (error) {
2883
+ console.error("Erreur lors du changement d'image:", error);
2884
+ }
2885
+ }
2886
+ deleteImage() {
2887
+ const ed = this.editor();
2888
+ if (ed) {
2889
+ ed.chain().focus().deleteSelection().run();
2890
+ }
2891
+ }
2892
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapImageBubbleMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2893
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.0", type: TiptapImageBubbleMenuComponent, isStandalone: true, selector: "tiptap-image-bubble-menu", inputs: { editor: { classPropertyName: "editor", publicName: "editor", isSignal: true, isRequired: true, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "menuRef", first: true, predicate: ["menuRef"], descendants: true }], ngImport: i0, template: `
2894
+ <div #menuRef class="bubble-menu">
2895
+ @if (imageBubbleMenuConfig().changeImage) {
2896
+ <tiptap-button
2897
+ icon="drive_file_rename_outline"
2898
+ title="Changer l'image"
2899
+ (click)="onCommand('changeImage', $event)"
2900
+ ></tiptap-button>
2901
+ } @if (imageBubbleMenuConfig().separator && hasResizeButtons()) {
2902
+ <tiptap-separator></tiptap-separator>
2903
+ } @if (imageBubbleMenuConfig().resizeSmall) {
2904
+ <tiptap-button
2905
+ icon="crop_square"
2906
+ iconSize="small"
2907
+ title="Petite (300×200)"
2908
+ (click)="onCommand('resizeSmall', $event)"
2909
+ ></tiptap-button>
2910
+ } @if (imageBubbleMenuConfig().resizeMedium) {
2911
+ <tiptap-button
2912
+ icon="crop_square"
2913
+ iconSize="medium"
2914
+ title="Moyenne (500×350)"
2915
+ (click)="onCommand('resizeMedium', $event)"
2916
+ ></tiptap-button>
2917
+ } @if (imageBubbleMenuConfig().resizeLarge) {
2918
+ <tiptap-button
2919
+ icon="crop_square"
2920
+ iconSize="large"
2921
+ title="Grande (800×600)"
2922
+ (click)="onCommand('resizeLarge', $event)"
2923
+ ></tiptap-button>
2924
+ } @if (imageBubbleMenuConfig().resizeOriginal) {
2925
+ <tiptap-button
2926
+ icon="photo_size_select_actual"
2927
+ title="Taille originale"
2928
+ (click)="onCommand('resizeOriginal', $event)"
2929
+ ></tiptap-button>
2930
+ } @if (imageBubbleMenuConfig().separator &&
2931
+ imageBubbleMenuConfig().deleteImage) {
2932
+ <tiptap-separator></tiptap-separator>
2933
+ } @if (imageBubbleMenuConfig().deleteImage) {
2934
+ <tiptap-button
2935
+ icon="delete"
2936
+ title="Supprimer l'image"
2937
+ variant="danger"
2938
+ (click)="onCommand('deleteImage', $event)"
2939
+ ></tiptap-button>
2940
+ }
2941
+ </div>
2942
+ `, isInline: true, dependencies: [{ kind: "component", type: TiptapButtonComponent, selector: "tiptap-button", inputs: ["icon", "title", "active", "disabled", "variant", "size", "iconSize"], outputs: ["onClick"] }, { kind: "component", type: TiptapSeparatorComponent, selector: "tiptap-separator", inputs: ["orientation", "size"] }] }); }
2943
+ }
2944
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapImageBubbleMenuComponent, decorators: [{
2945
+ type: Component,
2946
+ args: [{ selector: "tiptap-image-bubble-menu", standalone: true, imports: [TiptapButtonComponent, TiptapSeparatorComponent], template: `
2947
+ <div #menuRef class="bubble-menu">
2948
+ @if (imageBubbleMenuConfig().changeImage) {
2949
+ <tiptap-button
2950
+ icon="drive_file_rename_outline"
2951
+ title="Changer l'image"
2952
+ (click)="onCommand('changeImage', $event)"
2953
+ ></tiptap-button>
2954
+ } @if (imageBubbleMenuConfig().separator && hasResizeButtons()) {
2955
+ <tiptap-separator></tiptap-separator>
2956
+ } @if (imageBubbleMenuConfig().resizeSmall) {
2957
+ <tiptap-button
2958
+ icon="crop_square"
2959
+ iconSize="small"
2960
+ title="Petite (300×200)"
2961
+ (click)="onCommand('resizeSmall', $event)"
2962
+ ></tiptap-button>
2963
+ } @if (imageBubbleMenuConfig().resizeMedium) {
2964
+ <tiptap-button
2965
+ icon="crop_square"
2966
+ iconSize="medium"
2967
+ title="Moyenne (500×350)"
2968
+ (click)="onCommand('resizeMedium', $event)"
2969
+ ></tiptap-button>
2970
+ } @if (imageBubbleMenuConfig().resizeLarge) {
2971
+ <tiptap-button
2972
+ icon="crop_square"
2973
+ iconSize="large"
2974
+ title="Grande (800×600)"
2975
+ (click)="onCommand('resizeLarge', $event)"
2976
+ ></tiptap-button>
2977
+ } @if (imageBubbleMenuConfig().resizeOriginal) {
2978
+ <tiptap-button
2979
+ icon="photo_size_select_actual"
2980
+ title="Taille originale"
2981
+ (click)="onCommand('resizeOriginal', $event)"
2982
+ ></tiptap-button>
2983
+ } @if (imageBubbleMenuConfig().separator &&
2984
+ imageBubbleMenuConfig().deleteImage) {
2985
+ <tiptap-separator></tiptap-separator>
2986
+ } @if (imageBubbleMenuConfig().deleteImage) {
2987
+ <tiptap-button
2988
+ icon="delete"
2989
+ title="Supprimer l'image"
2990
+ variant="danger"
2991
+ (click)="onCommand('deleteImage', $event)"
2992
+ ></tiptap-button>
2993
+ }
2994
+ </div>
2995
+ ` }]
2996
+ }], ctorParameters: () => [], propDecorators: { menuRef: [{
2997
+ type: ViewChild,
2998
+ args: ["menuRef", { static: false }]
2999
+ }] } });
3000
+
3001
+ const DEFAULT_SLASH_COMMANDS = [
3002
+ {
3003
+ title: "Titre 1",
3004
+ description: "Grand titre de section",
3005
+ icon: "format_h1",
3006
+ keywords: ["heading", "h1", "titre", "title", "1"],
3007
+ command: (editor) => editor.chain().focus().toggleHeading({ level: 1 }).run(),
3008
+ },
3009
+ {
3010
+ title: "Titre 2",
3011
+ description: "Titre de sous-section",
3012
+ icon: "format_h2",
3013
+ keywords: ["heading", "h2", "titre", "title", "2"],
3014
+ command: (editor) => editor.chain().focus().toggleHeading({ level: 2 }).run(),
3015
+ },
3016
+ {
3017
+ title: "Titre 3",
3018
+ description: "Petit titre",
3019
+ icon: "format_h3",
3020
+ keywords: ["heading", "h3", "titre", "title", "3"],
3021
+ command: (editor) => editor.chain().focus().toggleHeading({ level: 3 }).run(),
3022
+ },
3023
+ {
3024
+ title: "Liste à puces",
3025
+ description: "Créer une liste à puces",
3026
+ icon: "format_list_bulleted",
3027
+ keywords: ["bullet", "list", "liste", "puces", "ul"],
3028
+ command: (editor) => editor.chain().focus().toggleBulletList().run(),
3029
+ },
3030
+ {
3031
+ title: "Liste numérotée",
3032
+ description: "Créer une liste numérotée",
3033
+ icon: "format_list_numbered",
3034
+ keywords: ["numbered", "list", "liste", "numérotée", "ol", "ordered"],
3035
+ command: (editor) => editor.chain().focus().toggleOrderedList().run(),
3036
+ },
3037
+ {
3038
+ title: "Citation",
3039
+ description: "Ajouter une citation",
3040
+ icon: "format_quote",
3041
+ keywords: ["quote", "blockquote", "citation"],
3042
+ command: (editor) => editor.chain().focus().toggleBlockquote().run(),
3043
+ },
3044
+ {
3045
+ title: "Code",
3046
+ description: "Bloc de code",
3047
+ icon: "code",
3048
+ keywords: ["code", "codeblock", "pre"],
3049
+ command: (editor) => editor.chain().focus().toggleCodeBlock().run(),
3050
+ },
3051
+ {
3052
+ title: "Image",
3053
+ description: "Insérer une image",
3054
+ icon: "image",
3055
+ keywords: ["image", "photo", "picture", "img"],
3056
+ command: (editor) => {
3057
+ // Créer un input file temporaire pour sélectionner une image
3058
+ const input = document.createElement("input");
3059
+ input.type = "file";
3060
+ input.accept = "image/*";
3061
+ input.style.display = "none";
3062
+ input.addEventListener("change", async (e) => {
3063
+ const file = e.target.files?.[0];
3064
+ if (file && file.type.startsWith("image/")) {
3065
+ try {
3066
+ // Utiliser la méthode de compression unifiée
3067
+ const canvas = document.createElement("canvas");
3068
+ const ctx = canvas.getContext("2d");
3069
+ const img = new Image();
3070
+ img.onload = () => {
3071
+ // Vérifier les dimensions (max 1920x1080)
3072
+ const maxWidth = 1920;
3073
+ const maxHeight = 1080;
3074
+ let { width, height } = img;
3075
+ // Redimensionner si nécessaire
3076
+ if (width > maxWidth || height > maxHeight) {
3077
+ const ratio = Math.min(maxWidth / width, maxHeight / height);
3078
+ width *= ratio;
3079
+ height *= ratio;
3080
+ }
3081
+ canvas.width = width;
3082
+ canvas.height = height;
3083
+ // Dessiner l'image redimensionnée
3084
+ ctx?.drawImage(img, 0, 0, width, height);
3085
+ // Convertir en base64 avec compression
3086
+ canvas.toBlob((blob) => {
3087
+ if (blob) {
3088
+ const reader = new FileReader();
3089
+ reader.onload = (e) => {
3090
+ const base64 = e.target?.result;
3091
+ if (base64) {
3092
+ // Utiliser setResizableImage avec toutes les propriétés
3093
+ editor
3094
+ .chain()
3095
+ .focus()
3096
+ .setResizableImage({
3097
+ src: base64,
3098
+ alt: file.name,
3099
+ title: `${file.name} (${Math.round(width)}×${Math.round(height)})`,
3100
+ width: Math.round(width),
3101
+ height: Math.round(height),
3102
+ })
3103
+ .run();
3104
+ }
3105
+ };
3106
+ reader.readAsDataURL(blob);
3107
+ }
3108
+ }, file.type, 0.8 // qualité de compression
3109
+ );
3110
+ };
3111
+ img.onerror = () => {
3112
+ console.error("Erreur lors du chargement de l'image");
3113
+ };
3114
+ img.src = URL.createObjectURL(file);
3115
+ }
3116
+ catch (error) {
3117
+ console.error("Erreur lors de l'upload:", error);
3118
+ }
3119
+ }
3120
+ document.body.removeChild(input);
3121
+ });
3122
+ document.body.appendChild(input);
3123
+ input.click();
3124
+ },
3125
+ },
3126
+ {
3127
+ title: "Ligne horizontale",
3128
+ description: "Ajouter une ligne de séparation",
3129
+ icon: "horizontal_rule",
3130
+ keywords: ["hr", "horizontal", "rule", "ligne", "séparation"],
3131
+ command: (editor) => editor.chain().focus().setHorizontalRule().run(),
3132
+ },
3133
+ ];
3134
+ class TiptapSlashCommandsComponent {
3135
+ constructor() {
3136
+ this.editor = input.required();
3137
+ this.config = input({
3138
+ commands: DEFAULT_SLASH_COMMANDS,
3139
+ });
3140
+ // Output pour l'upload d'image
3141
+ this.imageUploadRequested = output();
3142
+ this.tippyInstance = null;
3143
+ this.imageService = inject(ImageService);
3144
+ // État local
3145
+ this.isActive = false;
3146
+ this.currentQuery = signal("");
3147
+ this.slashRange = null;
3148
+ // Signal pour l'index sélectionné
3149
+ this.selectedIndex = signal(0);
3150
+ this.commands = computed(() => {
3151
+ const configCommands = this.config().commands || DEFAULT_SLASH_COMMANDS;
3152
+ // Remplacer la commande image par une version qui utilise l'output
3153
+ return configCommands.map((command) => {
3154
+ if (command.icon === "image") {
3155
+ return {
3156
+ ...command,
3157
+ command: (editor) => {
3158
+ // Créer un input file temporaire
3159
+ const input = document.createElement("input");
3160
+ input.type = "file";
3161
+ input.accept = "image/*";
3162
+ input.style.display = "none";
3163
+ input.addEventListener("change", (e) => {
3164
+ const file = e.target.files?.[0];
3165
+ if (file && file.type.startsWith("image/")) {
3166
+ this.imageUploadRequested.emit(file);
3167
+ }
3168
+ document.body.removeChild(input);
3169
+ });
3170
+ document.body.appendChild(input);
3171
+ input.click();
3172
+ },
3173
+ };
3174
+ }
3175
+ return command;
3176
+ });
3177
+ });
3178
+ this.filteredCommands = computed(() => {
3179
+ const query = this.currentQuery().toLowerCase();
3180
+ const commands = this.commands();
3181
+ if (!query) {
3182
+ return commands;
3183
+ }
3184
+ return commands.filter((command) => command.title.toLowerCase().includes(query) ||
3185
+ command.description.toLowerCase().includes(query) ||
3186
+ command.keywords.some((keyword) => keyword.toLowerCase().includes(query)));
3187
+ });
3188
+ this.updateMenu = () => {
3189
+ const ed = this.editor();
3190
+ if (!ed)
3191
+ return;
3192
+ const { from } = ed.state.selection;
3193
+ // Vérifier si on a tapé '/' au début d'une ligne ou après un espace
3194
+ const textBefore = ed.state.doc.textBetween(Math.max(0, from - 20), from, "\n");
3195
+ const slashMatch = textBefore.match(/(?:^|\s)\/([^\/\s]*)$/);
3196
+ if (slashMatch) {
3197
+ const query = slashMatch[1] || "";
3198
+ const wasActive = this.isActive;
3199
+ this.currentQuery.set(query);
3200
+ this.slashRange = {
3201
+ from: from - slashMatch[0].length + slashMatch[0].indexOf("/"),
3202
+ to: from,
3203
+ };
3204
+ // Si le menu vient de devenir actif, réinitialiser l'index
3205
+ if (!wasActive) {
3206
+ this.selectedIndex.set(0);
3207
+ }
3208
+ this.isActive = true;
3209
+ this.showTippy();
3210
+ }
3211
+ else {
3212
+ this.isActive = false;
3213
+ this.hideTippy();
3214
+ }
3215
+ };
3216
+ this.handleBlur = () => {
3217
+ setTimeout(() => this.hideTippy(), 100);
3218
+ };
3219
+ this.handleKeyDown = (event) => {
3220
+ // Ne gérer les touches que si le menu est actif
3221
+ if (!this.isActive || this.filteredCommands().length === 0) {
3222
+ return;
3223
+ }
3224
+ switch (event.key) {
3225
+ case "ArrowDown":
3226
+ event.preventDefault();
3227
+ event.stopPropagation();
3228
+ const nextIndex = (this.selectedIndex() + 1) % this.filteredCommands().length;
3229
+ this.selectedIndex.set(nextIndex);
3230
+ this.scrollToSelected();
3231
+ break;
3232
+ case "ArrowUp":
3233
+ event.preventDefault();
3234
+ event.stopPropagation();
3235
+ const prevIndex = this.selectedIndex() === 0
3236
+ ? this.filteredCommands().length - 1
3237
+ : this.selectedIndex() - 1;
3238
+ this.selectedIndex.set(prevIndex);
3239
+ this.scrollToSelected();
3240
+ break;
3241
+ case "Enter":
3242
+ event.preventDefault();
3243
+ event.stopPropagation();
3244
+ const selectedCommand = this.filteredCommands()[this.selectedIndex()];
3245
+ if (selectedCommand) {
3246
+ this.executeCommand(selectedCommand);
3247
+ }
3248
+ break;
3249
+ case "Escape":
3250
+ event.preventDefault();
3251
+ event.stopPropagation();
3252
+ this.isActive = false;
3253
+ this.hideTippy();
3254
+ // Optionnel : supprimer le "/" tapé
3255
+ const ed = this.editor();
3256
+ if (ed && this.slashRange) {
3257
+ const { tr } = ed.state;
3258
+ tr.delete(this.slashRange.from, this.slashRange.to);
3259
+ ed.view.dispatch(tr);
3260
+ }
3261
+ break;
3262
+ }
3263
+ };
3264
+ effect(() => {
3265
+ const ed = this.editor();
3266
+ if (!ed)
3267
+ return;
3268
+ // Nettoyer les anciens listeners
3269
+ ed.off("selectionUpdate", this.updateMenu);
3270
+ ed.off("transaction", this.updateMenu);
3271
+ ed.off("focus", this.updateMenu);
3272
+ ed.off("blur", this.handleBlur);
3273
+ // Ajouter les nouveaux listeners
3274
+ ed.on("selectionUpdate", this.updateMenu);
3275
+ ed.on("transaction", this.updateMenu);
3276
+ ed.on("focus", this.updateMenu);
3277
+ ed.on("blur", this.handleBlur);
3278
+ // Utiliser le système de plugins ProseMirror pour intercepter les touches
3279
+ this.addKeyboardPlugin(ed);
3280
+ // Ne pas appeler updateMenu() ici pour éviter l'affichage prématuré
3281
+ // Il sera appelé automatiquement quand l'éditeur sera prêt
3282
+ });
3283
+ }
3284
+ ngOnInit() {
3285
+ this.initTippy();
3286
+ }
3287
+ ngOnDestroy() {
3288
+ const ed = this.editor();
3289
+ if (ed) {
3290
+ ed.off("selectionUpdate", this.updateMenu);
3291
+ ed.off("transaction", this.updateMenu);
3292
+ ed.off("focus", this.updateMenu);
3293
+ ed.off("blur", this.handleBlur);
3294
+ }
3295
+ if (this.tippyInstance) {
3296
+ this.tippyInstance.destroy();
3297
+ this.tippyInstance = null;
3298
+ }
3299
+ }
3300
+ initTippy() {
3301
+ if (!this.menuRef?.nativeElement) {
3302
+ setTimeout(() => this.initTippy(), 50);
3303
+ return;
3304
+ }
3305
+ const menuElement = this.menuRef.nativeElement;
3306
+ if (this.tippyInstance) {
3307
+ this.tippyInstance.destroy();
3308
+ }
3309
+ this.tippyInstance = tippy(document.body, {
3310
+ content: menuElement,
3311
+ trigger: "manual",
3312
+ placement: "bottom-start",
3313
+ appendTo: () => document.body,
3314
+ interactive: true,
3315
+ arrow: false,
3316
+ offset: [0, 8],
3317
+ hideOnClick: true,
3318
+ getReferenceClientRect: () => this.getSlashRect(),
3319
+ // Améliorer le positionnement avec scroll
3320
+ popperOptions: {
3321
+ modifiers: [
3322
+ {
3323
+ name: "preventOverflow",
3324
+ options: {
3325
+ boundary: "viewport",
3326
+ padding: 8,
3327
+ },
3328
+ },
3329
+ {
3330
+ name: "flip",
3331
+ options: {
3332
+ fallbackPlacements: ["top-start", "bottom-end", "top-end"],
3333
+ },
3334
+ },
3335
+ ],
3336
+ },
3337
+ });
3338
+ // Maintenant que Tippy est initialisé, faire un premier check
3339
+ this.updateMenu();
3340
+ }
3341
+ getSlashRect() {
3342
+ const ed = this.editor();
3343
+ if (!ed || !this.slashRange) {
3344
+ return new DOMRect(0, 0, 0, 0);
3345
+ }
3346
+ try {
3347
+ // Utiliser les coordonnées ProseMirror pour plus de précision
3348
+ const coords = ed.view.coordsAtPos(this.slashRange.from);
3349
+ return new DOMRect(coords.left, coords.top, 0, coords.bottom - coords.top);
3350
+ }
3351
+ catch (error) {
3352
+ console.warn("Erreur lors du calcul des coordonnées:", error);
3353
+ // Fallback sur window.getSelection
3354
+ const selection = window.getSelection();
3355
+ if (!selection || selection.rangeCount === 0) {
3356
+ return new DOMRect(0, 0, 0, 0);
3357
+ }
3358
+ const range = selection.getRangeAt(0);
3359
+ return range.getBoundingClientRect();
3360
+ }
3361
+ }
3362
+ scrollToSelected() {
3363
+ // Faire défiler vers l'élément sélectionné
3364
+ if (this.menuRef?.nativeElement) {
3365
+ const selectedItem = this.menuRef.nativeElement.querySelector(".slash-command-item.selected");
3366
+ if (selectedItem) {
3367
+ selectedItem.scrollIntoView({ block: "nearest", behavior: "smooth" });
3368
+ }
3369
+ }
3370
+ }
3371
+ showTippy() {
3372
+ if (this.tippyInstance && this.filteredCommands().length > 0) {
3373
+ this.tippyInstance.show();
3374
+ }
3375
+ }
3376
+ hideTippy() {
3377
+ if (this.tippyInstance) {
3378
+ this.tippyInstance.hide();
3379
+ }
3380
+ }
3381
+ executeCommand(command) {
3382
+ const ed = this.editor();
3383
+ if (!ed || !this.slashRange)
3384
+ return;
3385
+ // Supprimer le texte slash
3386
+ const { tr } = ed.state;
3387
+ tr.delete(this.slashRange.from, this.slashRange.to);
3388
+ ed.view.dispatch(tr);
3389
+ // Cacher le menu
3390
+ this.hideTippy();
3391
+ // Exécuter la commande
3392
+ setTimeout(() => {
3393
+ command.command(ed);
3394
+ }, 10);
3395
+ }
3396
+ addKeyboardPlugin(ed) {
3397
+ // Ajouter un plugin ProseMirror pour intercepter les événements clavier
3398
+ const keyboardPlugin = new Plugin$1({
3399
+ key: new PluginKey$1("slash-commands-keyboard"),
3400
+ props: {
3401
+ handleKeyDown: (view, event) => {
3402
+ // Ne gérer que si le menu est actif
3403
+ if (!this.isActive || this.filteredCommands().length === 0) {
3404
+ return false;
3405
+ }
3406
+ switch (event.key) {
3407
+ case "ArrowDown":
3408
+ event.preventDefault();
3409
+ const nextIndex = (this.selectedIndex() + 1) % this.filteredCommands().length;
3410
+ this.selectedIndex.set(nextIndex);
3411
+ this.scrollToSelected();
3412
+ return true;
3413
+ case "ArrowUp":
3414
+ event.preventDefault();
3415
+ const prevIndex = this.selectedIndex() === 0
3416
+ ? this.filteredCommands().length - 1
3417
+ : this.selectedIndex() - 1;
3418
+ this.selectedIndex.set(prevIndex);
3419
+ this.scrollToSelected();
3420
+ return true;
3421
+ case "Enter":
3422
+ event.preventDefault();
3423
+ const selectedCommand = this.filteredCommands()[this.selectedIndex()];
3424
+ if (selectedCommand) {
3425
+ this.executeCommand(selectedCommand);
3426
+ }
3427
+ return true;
3428
+ case "Escape":
3429
+ event.preventDefault();
3430
+ this.isActive = false;
3431
+ this.hideTippy();
3432
+ // Supprimer le "/" tapé
3433
+ if (this.slashRange) {
3434
+ const { tr } = view.state;
3435
+ tr.delete(this.slashRange.from, this.slashRange.to);
3436
+ view.dispatch(tr);
3437
+ }
3438
+ return true;
3439
+ }
3440
+ return false;
3441
+ },
3442
+ },
3443
+ });
3444
+ // Ajouter le plugin à l'éditeur
3445
+ ed.view.updateState(ed.view.state.reconfigure({
3446
+ plugins: [keyboardPlugin, ...ed.view.state.plugins],
3447
+ }));
3448
+ }
3449
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapSlashCommandsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3450
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.0", type: TiptapSlashCommandsComponent, isStandalone: true, selector: "tiptap-slash-commands", inputs: { editor: { classPropertyName: "editor", publicName: "editor", isSignal: true, isRequired: true, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { imageUploadRequested: "imageUploadRequested" }, viewQueries: [{ propertyName: "menuRef", first: true, predicate: ["menuRef"], descendants: true }], ngImport: i0, template: `
3451
+ <div #menuRef class="slash-commands-menu">
3452
+ @for (command of filteredCommands(); track command.title) {
3453
+ <div
3454
+ class="slash-command-item"
3455
+ [class.selected]="$index === selectedIndex()"
3456
+ (click)="executeCommand(command)"
3457
+ (mouseenter)="selectedIndex.set($index)"
3458
+ >
3459
+ <div class="slash-command-icon">
3460
+ <span class="material-symbols-outlined">{{ command.icon }}</span>
3461
+ </div>
3462
+ <div class="slash-command-content">
3463
+ <div class="slash-command-title">{{ command.title }}</div>
3464
+ <div class="slash-command-description">{{ command.description }}</div>
3465
+ </div>
3466
+ </div>
3467
+ }
3468
+ </div>
3469
+ `, isInline: true, styles: [".slash-commands-menu{background:#fff;border:1px solid #e2e8f0;border-radius:8px;box-shadow:0 4px 12px #00000026;padding:8px;max-height:300px;overflow-y:auto;min-width:280px;outline:none}.slash-command-item{display:flex;align-items:center;gap:12px;padding:8px 12px;border-radius:6px;cursor:pointer;transition:all .2s ease;border:2px solid transparent;outline:none}.slash-command-item:hover{background:#f1f5f9;border-color:#e2e8f0}.slash-command-item.selected{background:#e6f3ff;border-color:#3182ce;box-shadow:0 0 0 1px #3182ce}.slash-command-item:focus{outline:2px solid #3182ce;outline-offset:2px}.slash-command-icon{display:flex;align-items:center;justify-content:center;width:32px;height:32px;background:#f8f9fa;border-radius:6px;color:#3182ce;flex-shrink:0}.slash-command-icon .material-symbols-outlined{font-size:18px}.slash-command-content{flex:1;min-width:0}.slash-command-title{font-weight:600;color:#2d3748;font-size:14px;margin-bottom:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.slash-command-description{color:#718096;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"] }); }
3470
+ }
3471
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapSlashCommandsComponent, decorators: [{
3472
+ type: Component,
3473
+ args: [{ selector: "tiptap-slash-commands", standalone: true, template: `
3474
+ <div #menuRef class="slash-commands-menu">
3475
+ @for (command of filteredCommands(); track command.title) {
3476
+ <div
3477
+ class="slash-command-item"
3478
+ [class.selected]="$index === selectedIndex()"
3479
+ (click)="executeCommand(command)"
3480
+ (mouseenter)="selectedIndex.set($index)"
3481
+ >
3482
+ <div class="slash-command-icon">
3483
+ <span class="material-symbols-outlined">{{ command.icon }}</span>
3484
+ </div>
3485
+ <div class="slash-command-content">
3486
+ <div class="slash-command-title">{{ command.title }}</div>
3487
+ <div class="slash-command-description">{{ command.description }}</div>
3488
+ </div>
3489
+ </div>
3490
+ }
3491
+ </div>
3492
+ `, styles: [".slash-commands-menu{background:#fff;border:1px solid #e2e8f0;border-radius:8px;box-shadow:0 4px 12px #00000026;padding:8px;max-height:300px;overflow-y:auto;min-width:280px;outline:none}.slash-command-item{display:flex;align-items:center;gap:12px;padding:8px 12px;border-radius:6px;cursor:pointer;transition:all .2s ease;border:2px solid transparent;outline:none}.slash-command-item:hover{background:#f1f5f9;border-color:#e2e8f0}.slash-command-item.selected{background:#e6f3ff;border-color:#3182ce;box-shadow:0 0 0 1px #3182ce}.slash-command-item:focus{outline:2px solid #3182ce;outline-offset:2px}.slash-command-icon{display:flex;align-items:center;justify-content:center;width:32px;height:32px;background:#f8f9fa;border-radius:6px;color:#3182ce;flex-shrink:0}.slash-command-icon .material-symbols-outlined{font-size:18px}.slash-command-content{flex:1;min-width:0}.slash-command-title{font-weight:600;color:#2d3748;font-size:14px;margin-bottom:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.slash-command-description{color:#718096;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"] }]
3493
+ }], ctorParameters: () => [], propDecorators: { menuRef: [{
3494
+ type: ViewChild,
3495
+ args: ["menuRef", { static: false }]
3496
+ }] } });
3497
+
3498
+ // Configuration par défaut de la toolbar
3499
+ const DEFAULT_TOOLBAR_CONFIG = {
3500
+ bold: true,
3501
+ italic: true,
3502
+ underline: true,
3503
+ strike: true,
3504
+ code: true,
3505
+ superscript: false,
3506
+ subscript: false,
3507
+ highlight: true,
3508
+ heading1: true,
3509
+ heading2: true,
3510
+ heading3: true,
3511
+ bulletList: true,
3512
+ orderedList: true,
3513
+ blockquote: true,
3514
+ alignLeft: false,
3515
+ alignCenter: false,
3516
+ alignRight: false,
3517
+ alignJustify: false,
3518
+ link: true,
3519
+ image: true,
3520
+ horizontalRule: true,
3521
+ undo: true,
3522
+ redo: true,
3523
+ separator: true,
3524
+ };
3525
+ // Configuration par défaut du bubble menu
3526
+ const DEFAULT_BUBBLE_MENU_CONFIG = {
3527
+ bold: true,
3528
+ italic: true,
3529
+ underline: true,
3530
+ strike: true,
3531
+ code: true,
3532
+ superscript: false,
3533
+ subscript: false,
3534
+ highlight: true,
3535
+ link: true,
3536
+ separator: true,
3537
+ };
3538
+ // Configuration par défaut du bubble menu image
3539
+ const DEFAULT_IMAGE_BUBBLE_MENU_CONFIG = {
3540
+ changeImage: true,
3541
+ resizeSmall: true,
3542
+ resizeMedium: true,
3543
+ resizeLarge: true,
3544
+ resizeOriginal: true,
3545
+ deleteImage: true,
3546
+ separator: true,
3547
+ };
3548
+ class AngularTiptapEditorComponent {
3549
+ constructor(imageService) {
3550
+ this.imageService = imageService;
3551
+ // Nouveaux inputs avec signal
3552
+ this.content = input("");
3553
+ this.placeholder = input("");
3554
+ this.editable = input(true);
3555
+ this.minHeight = input(200);
3556
+ this.height = input(undefined);
3557
+ this.maxHeight = input(undefined);
3558
+ this.showToolbar = input(true);
3559
+ this.showCharacterCount = input(true);
3560
+ this.maxCharacters = input(undefined);
3561
+ this.enableOfficePaste = input(true);
3562
+ this.enableSlashCommands = input(true);
3563
+ this.slashCommandsConfig = input(undefined);
3564
+ this.locale = input(undefined);
3565
+ // Nouveaux inputs pour les bubble menus
3566
+ this.showBubbleMenu = input(true);
3567
+ this.bubbleMenu = input(DEFAULT_BUBBLE_MENU_CONFIG);
3568
+ this.showImageBubbleMenu = input(true);
3569
+ this.imageBubbleMenu = input(DEFAULT_IMAGE_BUBBLE_MENU_CONFIG);
3570
+ // Nouveau input pour la configuration de la toolbar
3571
+ this.toolbar = input({});
3572
+ // Nouveau input pour la configuration de l'upload d'images
3573
+ this.imageUpload = input({});
3574
+ // Nouveaux outputs
3575
+ this.contentChange = output();
3576
+ this.editorCreated = output();
3577
+ this.editorUpdate = output();
3578
+ this.editorFocus = output();
3579
+ this.editorBlur = output();
3580
+ // ViewChild avec signal
3581
+ this.editorElement = viewChild.required("editorElement");
3582
+ // Signals pour l'état interne
3583
+ this.editor = signal(null);
3584
+ this.characterCountData = signal(null);
3585
+ this.isDragOver = signal(false);
3586
+ this.editorFullyInitialized = signal(false);
3587
+ // Computed pour les états de l'éditeur
3588
+ this.isEditorReady = computed(() => this.editor() !== null);
3589
+ // Computed pour la configuration de la toolbar
3590
+ this.toolbarConfig = computed(() => {
3591
+ const userConfig = this.toolbar();
3592
+ // Si aucune configuration n'est fournie, utiliser la configuration par défaut
3593
+ if (Object.keys(userConfig).length === 0) {
3594
+ return DEFAULT_TOOLBAR_CONFIG;
3595
+ }
3596
+ // Sinon, utiliser uniquement la configuration fournie par l'utilisateur
3597
+ return userConfig;
3598
+ });
3599
+ // Computed pour la configuration du bubble menu
3600
+ this.bubbleMenuConfig = computed(() => {
3601
+ const userConfig = this.bubbleMenu();
3602
+ // Si aucune configuration n'est fournie, utiliser la configuration par défaut
3603
+ if (Object.keys(userConfig).length === 0) {
3604
+ return DEFAULT_BUBBLE_MENU_CONFIG;
3605
+ }
3606
+ // Sinon, fusionner avec la configuration par défaut
3607
+ return {
3608
+ ...DEFAULT_BUBBLE_MENU_CONFIG,
3609
+ ...userConfig,
3610
+ };
3611
+ });
3612
+ // Computed pour la configuration du bubble menu image
3613
+ this.imageBubbleMenuConfig = computed(() => {
3614
+ const userConfig = this.imageBubbleMenu();
3615
+ // Si aucune configuration n'est fournie, utiliser la configuration par défaut
3616
+ if (Object.keys(userConfig).length === 0) {
3617
+ return DEFAULT_IMAGE_BUBBLE_MENU_CONFIG;
3618
+ }
3619
+ // Sinon, fusionner avec la configuration par défaut
3620
+ return {
3621
+ ...DEFAULT_IMAGE_BUBBLE_MENU_CONFIG,
3622
+ ...userConfig,
3623
+ };
3624
+ });
3625
+ // Computed pour la configuration de l'upload d'images
3626
+ this.imageUploadConfig = computed(() => {
3627
+ const userConfig = this.imageUpload();
3628
+ return {
3629
+ maxSize: 5,
3630
+ maxWidth: 1920,
3631
+ maxHeight: 1080,
3632
+ allowedTypes: ["image/jpeg", "image/png", "image/gif", "image/webp"],
3633
+ enableDragDrop: true,
3634
+ showPreview: true,
3635
+ multiple: false,
3636
+ compressImages: true,
3637
+ quality: 0.8,
3638
+ ...userConfig,
3639
+ };
3640
+ });
3641
+ // Computed pour la configuration des slash commands
3642
+ this.slashCommandsConfigComputed = computed(() => {
3643
+ const userConfig = this.slashCommandsConfig();
3644
+ if (userConfig) {
3645
+ return userConfig;
3646
+ }
3647
+ // Configuration par défaut si aucune n'est fournie
3648
+ return { commands: undefined }; // Le composant utilisera DEFAULT_SLASH_COMMANDS
3649
+ });
3650
+ // ControlValueAccessor implementation
3651
+ this.onChange = (value) => { };
3652
+ this.onTouched = () => { };
3653
+ this.i18nService = inject(TiptapI18nService);
3654
+ // Effet pour gérer le changement de langue
3655
+ effect(() => {
3656
+ const locale = this.locale();
3657
+ if (locale) {
3658
+ this.i18nService.setLocale(locale);
3659
+ }
3660
+ });
3661
+ // Effet pour mettre à jour le contenu de l'éditeur
3662
+ effect(() => {
3663
+ const editor = this.editor();
3664
+ const content = this.content();
3665
+ if (editor && content !== editor.getHTML()) {
3666
+ editor.commands.setContent(content, false);
3667
+ }
3668
+ });
3669
+ // Effet pour mettre à jour les propriétés de hauteur
3670
+ effect(() => {
3671
+ const minHeight = this.minHeight();
3672
+ const height = this.height();
3673
+ const maxHeight = this.maxHeight();
3674
+ const element = this.editorElement()?.nativeElement;
3675
+ // Calculer automatiquement si le scroll est nécessaire
3676
+ const needsScroll = height !== undefined || maxHeight !== undefined;
3677
+ if (element) {
3678
+ element.style.setProperty("--editor-min-height", `${minHeight}px`);
3679
+ element.style.setProperty("--editor-height", height ? `${height}px` : "auto");
3680
+ element.style.setProperty("--editor-max-height", maxHeight ? `${maxHeight}px` : "none");
3681
+ element.style.setProperty("--editor-overflow", needsScroll ? "auto" : "visible");
3682
+ }
3683
+ });
3684
+ // Effect pour surveiller les changements d'édition
3685
+ effect(() => {
3686
+ const currentEditor = this.editor();
3687
+ const isEditable = this.editable();
3688
+ if (currentEditor) {
3689
+ currentEditor.setEditable(isEditable);
3690
+ }
3691
+ });
3692
+ }
3693
+ ngOnInit() {
3694
+ // L'initialisation se fait maintenant dans ngAfterViewInit
3695
+ }
3696
+ ngAfterViewInit() {
3697
+ // Attendre que la vue soit complètement initialisée avant de créer l'éditeur
3698
+ setTimeout(() => {
3699
+ this.initEditor();
3700
+ }, 0);
3701
+ }
3702
+ ngOnDestroy() {
3703
+ const currentEditor = this.editor();
3704
+ if (currentEditor) {
3705
+ currentEditor.destroy();
3706
+ }
3707
+ this.editorFullyInitialized.set(false);
3708
+ }
3709
+ initEditor() {
3710
+ const extensions = [
3711
+ StarterKit,
3712
+ Placeholder.configure({
3713
+ placeholder: this.placeholder() || this.i18nService.editor().placeholder,
3714
+ }),
3715
+ Underline,
3716
+ Superscript,
3717
+ Subscript,
3718
+ TextAlign.configure({
3719
+ types: ["heading", "paragraph"],
3720
+ }),
3721
+ Link.configure({
3722
+ openOnClick: false,
3723
+ HTMLAttributes: {
3724
+ class: "tiptap-link",
3725
+ },
3726
+ }),
3727
+ Highlight.configure({
3728
+ multicolor: true,
3729
+ HTMLAttributes: {
3730
+ class: "tiptap-highlight",
3731
+ },
3732
+ }),
3733
+ ResizableImage.configure({
3734
+ inline: false,
3735
+ allowBase64: true,
3736
+ HTMLAttributes: {
3737
+ class: "tiptap-image",
3738
+ },
3739
+ }),
3740
+ UploadProgress.configure({
3741
+ isUploading: () => this.imageService.isUploading(),
3742
+ uploadProgress: () => this.imageService.uploadProgress(),
3743
+ uploadMessage: () => this.imageService.uploadMessage(),
3744
+ }),
3745
+ ];
3746
+ // Ajouter l'extension Office Paste si activée
3747
+ if (this.enableOfficePaste()) {
3748
+ extensions.push(OfficePaste.configure({
3749
+ // Configuration par défaut pour une meilleure compatibilité
3750
+ transformPastedHTML: true,
3751
+ transformPastedText: true,
3752
+ }));
3753
+ }
3754
+ if (this.showCharacterCount()) {
3755
+ extensions.push(CharacterCount.configure({
3756
+ limit: this.maxCharacters(),
3757
+ }));
3758
+ }
3759
+ const newEditor = new Editor({
3760
+ element: this.editorElement().nativeElement,
3761
+ extensions,
3762
+ content: this.content(),
3763
+ editable: this.editable(),
3764
+ onUpdate: ({ editor, transaction }) => {
3765
+ const html = editor.getHTML();
3766
+ this.contentChange.emit(html);
3767
+ // Defer the onChange call to prevent ExpressionChangedAfterItHasBeenCheckedError
3768
+ Promise.resolve().then(() => {
3769
+ this.onChange(html);
3770
+ });
3771
+ this.editorUpdate.emit({ editor, transaction });
3772
+ this.updateCharacterCount(editor);
3773
+ },
3774
+ onSelectionUpdate: ({ editor, transaction }) => {
3775
+ // Note: La mise à jour des états des boutons est maintenant gérée par TiptapBubbleMenuComponent
3776
+ },
3777
+ onCreate: ({ editor }) => {
3778
+ this.editor.set(editor);
3779
+ this.editorCreated.emit(editor);
3780
+ this.updateCharacterCount(editor);
3781
+ // Marquer l'éditeur comme complètement initialisé après un court délai
3782
+ // pour s'assurer que tous les plugins et extensions sont prêts
3783
+ setTimeout(() => {
3784
+ this.editorFullyInitialized.set(true);
3785
+ }, 100);
3786
+ },
3787
+ onFocus: ({ editor, event }) => {
3788
+ this.editorFocus.emit({ editor, event });
3789
+ },
3790
+ onBlur: ({ editor, event }) => {
3791
+ this.onTouched();
3792
+ this.editorBlur.emit({ editor, event });
3793
+ },
3794
+ });
3795
+ }
3796
+ updateCharacterCount(editor) {
3797
+ if (this.showCharacterCount() && editor.storage["characterCount"]) {
3798
+ const storage = editor.storage["characterCount"];
3799
+ this.characterCountData.set({
3800
+ characters: storage.characters(),
3801
+ words: storage.words(),
3802
+ });
3803
+ }
3804
+ }
3805
+ // Méthodes pour l'upload d'images
3806
+ onImageUploaded(result) {
3807
+ const currentEditor = this.editor();
3808
+ if (currentEditor) {
3809
+ this.imageService.insertImage(currentEditor, {
3810
+ src: result.src,
3811
+ alt: result.name,
3812
+ title: `${result.name} (${result.width}×${result.height})`,
3813
+ width: result.width,
3814
+ height: result.height,
3815
+ });
3816
+ }
3817
+ }
3818
+ onImageUploadError(error) {
3819
+ // Ici vous pourriez afficher une notification à l'utilisateur
3820
+ }
3821
+ // Gestion de l'upload d'image depuis les slash commands
3822
+ async onSlashCommandImageUpload(file) {
3823
+ const currentEditor = this.editor();
3824
+ if (currentEditor) {
3825
+ try {
3826
+ await this.imageService.uploadAndInsertImage(currentEditor, file);
3827
+ }
3828
+ catch (error) {
3829
+ // Gérer l'erreur silencieusement ou afficher une notification
3830
+ }
3831
+ }
3832
+ }
3833
+ onDragOver(event) {
3834
+ event.preventDefault();
3835
+ event.stopPropagation();
3836
+ this.isDragOver.set(true);
3837
+ }
3838
+ onDrop(event) {
3839
+ event.preventDefault();
3840
+ event.stopPropagation();
3841
+ this.isDragOver.set(false);
3842
+ const files = event.dataTransfer?.files;
3843
+ if (files && files.length > 0) {
3844
+ const file = files[0];
3845
+ if (file.type.startsWith("image/")) {
3846
+ this.insertImageFromFile(file);
3847
+ }
3848
+ }
3849
+ }
3850
+ async insertImageFromFile(file) {
3851
+ const currentEditor = this.editor();
3852
+ if (currentEditor) {
3853
+ try {
3854
+ await this.imageService.uploadAndInsertImage(currentEditor, file);
3855
+ }
3856
+ catch (error) {
3857
+ // Gérer l'erreur silencieusement ou afficher une notification
3858
+ }
3859
+ }
3860
+ }
3861
+ // Public methods
3862
+ getHTML() {
3863
+ return this.editor()?.getHTML() || "";
3864
+ }
3865
+ getJSON() {
3866
+ return this.editor()?.getJSON();
3867
+ }
3868
+ getText() {
3869
+ return this.editor()?.getText() || "";
3870
+ }
3871
+ setContent(content, emitUpdate = true) {
3872
+ this.editor()?.commands.setContent(content, emitUpdate);
3873
+ }
3874
+ focus() {
3875
+ this.editor()?.commands.focus();
3876
+ }
3877
+ blur() {
3878
+ this.editor()?.commands.blur();
3879
+ }
3880
+ clearContent() {
3881
+ this.editor()?.commands.clearContent();
3882
+ }
3883
+ // ControlValueAccessor methods
3884
+ writeValue(value) {
3885
+ const currentEditor = this.editor();
3886
+ if (currentEditor && value !== currentEditor.getHTML()) {
3887
+ currentEditor.commands.setContent(value || "", false);
3888
+ }
3889
+ }
3890
+ registerOnChange(fn) {
3891
+ this.onChange = fn;
3892
+ }
3893
+ registerOnTouched(fn) {
3894
+ this.onTouched = fn;
3895
+ }
3896
+ setDisabledState(isDisabled) {
3897
+ const currentEditor = this.editor();
3898
+ if (currentEditor) {
3899
+ currentEditor.setEditable(!isDisabled);
3900
+ }
3901
+ }
3902
+ onEditorClick(event) {
3903
+ const editor = this.editor();
3904
+ if (!editor)
3905
+ return;
3906
+ // Vérifier si on clique sur l'élément conteneur et non sur le contenu
3907
+ const target = event.target;
3908
+ const editorElement = this.editorElement()?.nativeElement;
3909
+ if (target === editorElement ||
3910
+ target.classList.contains("tiptap-content")) {
3911
+ // On clique dans l'espace vide, positionner le curseur à la fin
3912
+ setTimeout(() => {
3913
+ const { doc } = editor.state;
3914
+ const endPos = doc.content.size;
3915
+ editor.commands.setTextSelection(endPos);
3916
+ editor.commands.focus();
3917
+ }, 0);
3918
+ }
3919
+ }
3920
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: AngularTiptapEditorComponent, deps: [{ token: ImageService }], target: i0.ɵɵFactoryTarget.Component }); }
3921
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.0", type: AngularTiptapEditorComponent, isStandalone: true, selector: "angular-tiptap-editor", inputs: { content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null }, minHeight: { classPropertyName: "minHeight", publicName: "minHeight", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, showToolbar: { classPropertyName: "showToolbar", publicName: "showToolbar", isSignal: true, isRequired: false, transformFunction: null }, showCharacterCount: { classPropertyName: "showCharacterCount", publicName: "showCharacterCount", isSignal: true, isRequired: false, transformFunction: null }, maxCharacters: { classPropertyName: "maxCharacters", publicName: "maxCharacters", isSignal: true, isRequired: false, transformFunction: null }, enableOfficePaste: { classPropertyName: "enableOfficePaste", publicName: "enableOfficePaste", isSignal: true, isRequired: false, transformFunction: null }, enableSlashCommands: { classPropertyName: "enableSlashCommands", publicName: "enableSlashCommands", isSignal: true, isRequired: false, transformFunction: null }, slashCommandsConfig: { classPropertyName: "slashCommandsConfig", publicName: "slashCommandsConfig", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, showBubbleMenu: { classPropertyName: "showBubbleMenu", publicName: "showBubbleMenu", isSignal: true, isRequired: false, transformFunction: null }, bubbleMenu: { classPropertyName: "bubbleMenu", publicName: "bubbleMenu", isSignal: true, isRequired: false, transformFunction: null }, showImageBubbleMenu: { classPropertyName: "showImageBubbleMenu", publicName: "showImageBubbleMenu", isSignal: true, isRequired: false, transformFunction: null }, imageBubbleMenu: { classPropertyName: "imageBubbleMenu", publicName: "imageBubbleMenu", isSignal: true, isRequired: false, transformFunction: null }, toolbar: { classPropertyName: "toolbar", publicName: "toolbar", isSignal: true, isRequired: false, transformFunction: null }, imageUpload: { classPropertyName: "imageUpload", publicName: "imageUpload", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { contentChange: "contentChange", editorCreated: "editorCreated", editorUpdate: "editorUpdate", editorFocus: "editorFocus", editorBlur: "editorBlur" }, providers: [
3922
+ {
3923
+ provide: NG_VALUE_ACCESSOR,
3924
+ useExisting: forwardRef(() => AngularTiptapEditorComponent),
3925
+ multi: true,
3926
+ },
3927
+ ], viewQueries: [{ propertyName: "editorElement", first: true, predicate: ["editorElement"], descendants: true, isSignal: true }], ngImport: i0, template: `
3928
+ <div class="tiptap-editor">
3929
+ <!-- Toolbar -->
3930
+ @if (showToolbar() && editor()) {
3931
+ <tiptap-toolbar [editor]="editor()!" [config]="toolbarConfig()">
3932
+ <div image-upload class="image-upload-container">
3933
+ <tiptap-image-upload
3934
+ [config]="imageUploadConfig()"
3935
+ (imageSelected)="onImageUploaded($event)"
3936
+ (error)="onImageUploadError($event)"
3937
+ />
3938
+ </div>
3939
+ </tiptap-toolbar>
3940
+ }
3941
+
3942
+ <!-- Contenu de l'éditeur -->
3943
+ <div
3944
+ #editorElement
3945
+ class="tiptap-content"
3946
+ [class.drag-over]="isDragOver()"
3947
+ (dragover)="onDragOver($event)"
3948
+ (drop)="onDrop($event)"
3949
+ (click)="onEditorClick($event)"
3950
+ ></div>
3951
+
3952
+ <!-- Bubble Menu pour le texte -->
3953
+ @if (showBubbleMenu() && editor()) {
3954
+ <tiptap-bubble-menu
3955
+ [editor]="editor()!"
3956
+ [config]="bubbleMenuConfig()"
3957
+ [style.display]="editorFullyInitialized() ? 'block' : 'none'"
3958
+ ></tiptap-bubble-menu>
3959
+ }
3960
+
3961
+ <!-- Bubble Menu pour les images -->
3962
+ @if (showImageBubbleMenu() && editor()) {
3963
+ <tiptap-image-bubble-menu
3964
+ [editor]="editor()!"
3965
+ [config]="imageBubbleMenuConfig()"
3966
+ [style.display]="editorFullyInitialized() ? 'block' : 'none'"
3967
+ ></tiptap-image-bubble-menu>
3968
+ }
3969
+
3970
+ <!-- Slash Commands -->
3971
+ @if (enableSlashCommands() && editor()) {
3972
+ <tiptap-slash-commands
3973
+ [editor]="editor()!"
3974
+ [config]="slashCommandsConfigComputed()"
3975
+ [style.display]="editorFullyInitialized() ? 'block' : 'none'"
3976
+ (imageUploadRequested)="onSlashCommandImageUpload($event)"
3977
+ ></tiptap-slash-commands>
3978
+ }
3979
+
3980
+ <!-- Compteur de caractères -->
3981
+ @if (showCharacterCount() && characterCountData()) {
3982
+ <div class="character-count">
3983
+ {{ characterCountData()?.characters }}
3984
+ {{ i18nService.editor().characters }},
3985
+ {{ characterCountData()?.words }} {{ i18nService.editor().words }} @if
3986
+ (maxCharacters()) { /
3987
+ {{ maxCharacters() }}
3988
+ }
3989
+ </div>
3990
+ }
3991
+ </div>
3992
+ `, isInline: true, styles: [".tiptap-editor{border:2px solid #e2e8f0;border-radius:8px;background:#fff;box-shadow:0 2px 4px #0000001a;overflow:hidden;transition:border-color .2s ease}.tiptap-editor:focus-within{border-color:#3182ce;box-shadow:0 0 0 3px #3182ce1a}.tiptap-content{padding:16px;min-height:var(--editor-min-height, 200px);height:var(--editor-height, auto);max-height:var(--editor-max-height, none);overflow-y:var(--editor-overflow, visible);outline:none;position:relative}.tiptap-content.drag-over{background:#f0f8ff;border:2px dashed #3182ce}.character-count{padding:8px 16px;font-size:12px;color:#718096;text-align:right;border-top:1px solid #e2e8f0;background:#f8f9fa}.image-upload-container{position:relative;display:inline-block}:host ::ng-deep .ProseMirror{outline:none;line-height:1.6;color:#2d3748;min-height:100%;height:100%;word-wrap:break-word;overflow-wrap:break-word}:host ::ng-deep .ProseMirror h1{font-size:2em;font-weight:700;margin-top:0;margin-bottom:.5em}:host ::ng-deep .ProseMirror h2{font-size:1.5em;font-weight:700;margin-top:1em;margin-bottom:.5em}:host ::ng-deep .ProseMirror h3{font-size:1.25em;font-weight:700;margin-top:1em;margin-bottom:.5em}:host ::ng-deep .ProseMirror p{margin:.5em 0}:host ::ng-deep .ProseMirror ul,:host ::ng-deep .ProseMirror ol{padding-left:2em;margin:.5em 0}:host ::ng-deep .ProseMirror blockquote{border-left:4px solid #e2e8f0;margin:1em 0;font-style:italic;background:#f8f9fa;padding:.5em 1em;border-radius:0 4px 4px 0}:host ::ng-deep .ProseMirror code{background:#f1f5f9;padding:.2em .4em;border-radius:3px;font-family:Monaco,Consolas,monospace;font-size:.9em}:host ::ng-deep .ProseMirror pre{background:#1a202c;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;margin:1em 0}:host ::ng-deep .ProseMirror pre code{background:none;color:inherit;padding:0}:host ::ng-deep .ProseMirror .placeholder{color:#a0aec0;pointer-events:none;height:0}:host ::ng-deep .ProseMirror .placeholder:before{content:attr(data-placeholder);float:left;height:0;pointer-events:none}:host ::ng-deep .ProseMirror[contenteditable=false]{pointer-events:none}:host ::ng-deep .ProseMirror[contenteditable=false] img{cursor:default;pointer-events:none}:host ::ng-deep .ProseMirror[contenteditable=false] img:hover{transform:none;box-shadow:0 2px 8px #0000001a}:host ::ng-deep .ProseMirror[contenteditable=false] img.ProseMirror-selectednode{outline:none}:host ::ng-deep .ProseMirror img{position:relative;display:inline-block;max-width:100%;height:auto;cursor:pointer;transition:all .2s ease;border:2px solid transparent;border-radius:8px}:host ::ng-deep .ProseMirror img:hover{border-color:#e2e8f0;box-shadow:0 2px 4px #0000001a}:host ::ng-deep .ProseMirror img.selected{border-color:#3182ce;box-shadow:0 0 0 3px #3182ce1a;transition:all .2s ease}:host ::ng-deep .ProseMirror .tiptap-image{max-width:100%;height:auto;border-radius:16px;box-shadow:0 4px 20px #00000014;margin:.5em 0;cursor:pointer;transition:all .3s cubic-bezier(.4,0,.2,1);display:block;filter:brightness(1) contrast(1)}:host ::ng-deep .ProseMirror .tiptap-image:hover{box-shadow:0 8px 30px #0000001f;filter:brightness(1.02) contrast(1.02)}:host ::ng-deep .ProseMirror .tiptap-image.ProseMirror-selectednode{outline:2px solid #6366f1;outline-offset:2px;border-radius:16px;box-shadow:0 0 0 4px #6366f11a}:host ::ng-deep .image-container{margin:.5em 0;text-align:center;border-radius:16px;overflow:hidden;transition:all .3s cubic-bezier(.4,0,.2,1)}:host ::ng-deep .image-container.image-align-left{text-align:left}:host ::ng-deep .image-container.image-align-center{text-align:center}:host ::ng-deep .image-container.image-align-right{text-align:right}:host ::ng-deep .image-container img{display:inline-block;max-width:100%;height:auto;border-radius:16px}:host ::ng-deep .resizable-image-container{position:relative;display:inline-block;margin:.5em 0}:host ::ng-deep .resize-controls{position:absolute;inset:0;pointer-events:none;z-index:1000}:host ::ng-deep .resize-handle{position:absolute;width:12px;height:12px;background:#3b82f6;border:2px solid white;border-radius:50%;pointer-events:all;cursor:pointer;z-index:1001;transition:all .15s ease;box-shadow:0 2px 6px #0003}:host ::ng-deep .resize-handle:hover{background:#2563eb;box-shadow:0 3px 8px #0000004d}:host ::ng-deep .resize-handle:active{background:#1d4ed8}:host ::ng-deep .resize-handle-n:hover,:host ::ng-deep .resize-handle-s:hover{transform:translate(-50%) scale(1.2)}:host ::ng-deep .resize-handle-w:hover,:host ::ng-deep .resize-handle-e:hover{transform:translateY(-50%) scale(1.2)}:host ::ng-deep .resize-handle-n:active,:host ::ng-deep .resize-handle-s:active{transform:translate(-50%) scale(.9)}:host ::ng-deep .resize-handle-w:active,:host ::ng-deep .resize-handle-e:active{transform:translateY(-50%) scale(.9)}:host ::ng-deep .resize-handle-nw:hover,:host ::ng-deep .resize-handle-ne:hover,:host ::ng-deep .resize-handle-sw:hover,:host ::ng-deep .resize-handle-se:hover{transform:scale(1.2)}:host ::ng-deep .resize-handle-nw:active,:host ::ng-deep .resize-handle-ne:active,:host ::ng-deep .resize-handle-sw:active,:host ::ng-deep .resize-handle-se:active{transform:scale(.9)}:host ::ng-deep .resize-handle-nw{top:0;left:-6px;cursor:nw-resize}:host ::ng-deep .resize-handle-n{top:0;left:50%;transform:translate(-50%);cursor:n-resize}:host ::ng-deep .resize-handle-ne{top:0;right:-6px;cursor:ne-resize}:host ::ng-deep .resize-handle-w{top:50%;left:-6px;transform:translateY(-50%);cursor:w-resize}:host ::ng-deep .resize-handle-e{top:50%;right:-6px;transform:translateY(-50%);cursor:e-resize}:host ::ng-deep .resize-handle-sw{bottom:0;left:-6px;cursor:sw-resize}:host ::ng-deep .resize-handle-s{bottom:0;left:50%;transform:translate(-50%);cursor:s-resize}:host ::ng-deep .resize-handle-se{bottom:0;right:-6px;cursor:se-resize}:host ::ng-deep body.resizing{-webkit-user-select:none;user-select:none;cursor:crosshair}:host ::ng-deep body.resizing .ProseMirror{pointer-events:none}:host ::ng-deep body.resizing .ProseMirror .tiptap-image{pointer-events:none}:host ::ng-deep .image-size-info{position:absolute;bottom:-20px;left:50%;transform:translate(-50%);background:#000c;color:#fff;padding:2px 6px;border-radius:3px;font-size:11px;white-space:nowrap;opacity:0;transition:opacity .2s ease}:host ::ng-deep .image-container:hover .image-size-info{opacity:1}\n"], dependencies: [{ kind: "component", type: TiptapToolbarComponent, selector: "tiptap-toolbar", inputs: ["editor", "config"], outputs: ["imageUploaded", "imageError"] }, { kind: "component", type: TiptapImageUploadComponent, selector: "tiptap-image-upload", inputs: ["config"], outputs: ["imageSelected", "error"] }, { kind: "component", type: TiptapBubbleMenuComponent, selector: "tiptap-bubble-menu", inputs: ["editor", "config"] }, { kind: "component", type: TiptapImageBubbleMenuComponent, selector: "tiptap-image-bubble-menu", inputs: ["editor", "config"] }, { kind: "component", type: TiptapSlashCommandsComponent, selector: "tiptap-slash-commands", inputs: ["editor", "config"], outputs: ["imageUploadRequested"] }] }); }
3993
+ }
3994
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: AngularTiptapEditorComponent, decorators: [{
3995
+ type: Component,
3996
+ args: [{ selector: "angular-tiptap-editor", standalone: true, imports: [
3997
+ TiptapToolbarComponent,
3998
+ TiptapImageUploadComponent,
3999
+ TiptapBubbleMenuComponent,
4000
+ TiptapImageBubbleMenuComponent,
4001
+ TiptapSlashCommandsComponent,
4002
+ ], template: `
4003
+ <div class="tiptap-editor">
4004
+ <!-- Toolbar -->
4005
+ @if (showToolbar() && editor()) {
4006
+ <tiptap-toolbar [editor]="editor()!" [config]="toolbarConfig()">
4007
+ <div image-upload class="image-upload-container">
4008
+ <tiptap-image-upload
4009
+ [config]="imageUploadConfig()"
4010
+ (imageSelected)="onImageUploaded($event)"
4011
+ (error)="onImageUploadError($event)"
4012
+ />
4013
+ </div>
4014
+ </tiptap-toolbar>
4015
+ }
4016
+
4017
+ <!-- Contenu de l'éditeur -->
4018
+ <div
4019
+ #editorElement
4020
+ class="tiptap-content"
4021
+ [class.drag-over]="isDragOver()"
4022
+ (dragover)="onDragOver($event)"
4023
+ (drop)="onDrop($event)"
4024
+ (click)="onEditorClick($event)"
4025
+ ></div>
4026
+
4027
+ <!-- Bubble Menu pour le texte -->
4028
+ @if (showBubbleMenu() && editor()) {
4029
+ <tiptap-bubble-menu
4030
+ [editor]="editor()!"
4031
+ [config]="bubbleMenuConfig()"
4032
+ [style.display]="editorFullyInitialized() ? 'block' : 'none'"
4033
+ ></tiptap-bubble-menu>
4034
+ }
4035
+
4036
+ <!-- Bubble Menu pour les images -->
4037
+ @if (showImageBubbleMenu() && editor()) {
4038
+ <tiptap-image-bubble-menu
4039
+ [editor]="editor()!"
4040
+ [config]="imageBubbleMenuConfig()"
4041
+ [style.display]="editorFullyInitialized() ? 'block' : 'none'"
4042
+ ></tiptap-image-bubble-menu>
4043
+ }
4044
+
4045
+ <!-- Slash Commands -->
4046
+ @if (enableSlashCommands() && editor()) {
4047
+ <tiptap-slash-commands
4048
+ [editor]="editor()!"
4049
+ [config]="slashCommandsConfigComputed()"
4050
+ [style.display]="editorFullyInitialized() ? 'block' : 'none'"
4051
+ (imageUploadRequested)="onSlashCommandImageUpload($event)"
4052
+ ></tiptap-slash-commands>
4053
+ }
4054
+
4055
+ <!-- Compteur de caractères -->
4056
+ @if (showCharacterCount() && characterCountData()) {
4057
+ <div class="character-count">
4058
+ {{ characterCountData()?.characters }}
4059
+ {{ i18nService.editor().characters }},
4060
+ {{ characterCountData()?.words }} {{ i18nService.editor().words }} @if
4061
+ (maxCharacters()) { /
4062
+ {{ maxCharacters() }}
4063
+ }
4064
+ </div>
4065
+ }
4066
+ </div>
4067
+ `, providers: [
4068
+ {
4069
+ provide: NG_VALUE_ACCESSOR,
4070
+ useExisting: forwardRef(() => AngularTiptapEditorComponent),
4071
+ multi: true,
4072
+ },
4073
+ ], styles: [".tiptap-editor{border:2px solid #e2e8f0;border-radius:8px;background:#fff;box-shadow:0 2px 4px #0000001a;overflow:hidden;transition:border-color .2s ease}.tiptap-editor:focus-within{border-color:#3182ce;box-shadow:0 0 0 3px #3182ce1a}.tiptap-content{padding:16px;min-height:var(--editor-min-height, 200px);height:var(--editor-height, auto);max-height:var(--editor-max-height, none);overflow-y:var(--editor-overflow, visible);outline:none;position:relative}.tiptap-content.drag-over{background:#f0f8ff;border:2px dashed #3182ce}.character-count{padding:8px 16px;font-size:12px;color:#718096;text-align:right;border-top:1px solid #e2e8f0;background:#f8f9fa}.image-upload-container{position:relative;display:inline-block}:host ::ng-deep .ProseMirror{outline:none;line-height:1.6;color:#2d3748;min-height:100%;height:100%;word-wrap:break-word;overflow-wrap:break-word}:host ::ng-deep .ProseMirror h1{font-size:2em;font-weight:700;margin-top:0;margin-bottom:.5em}:host ::ng-deep .ProseMirror h2{font-size:1.5em;font-weight:700;margin-top:1em;margin-bottom:.5em}:host ::ng-deep .ProseMirror h3{font-size:1.25em;font-weight:700;margin-top:1em;margin-bottom:.5em}:host ::ng-deep .ProseMirror p{margin:.5em 0}:host ::ng-deep .ProseMirror ul,:host ::ng-deep .ProseMirror ol{padding-left:2em;margin:.5em 0}:host ::ng-deep .ProseMirror blockquote{border-left:4px solid #e2e8f0;margin:1em 0;font-style:italic;background:#f8f9fa;padding:.5em 1em;border-radius:0 4px 4px 0}:host ::ng-deep .ProseMirror code{background:#f1f5f9;padding:.2em .4em;border-radius:3px;font-family:Monaco,Consolas,monospace;font-size:.9em}:host ::ng-deep .ProseMirror pre{background:#1a202c;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;margin:1em 0}:host ::ng-deep .ProseMirror pre code{background:none;color:inherit;padding:0}:host ::ng-deep .ProseMirror .placeholder{color:#a0aec0;pointer-events:none;height:0}:host ::ng-deep .ProseMirror .placeholder:before{content:attr(data-placeholder);float:left;height:0;pointer-events:none}:host ::ng-deep .ProseMirror[contenteditable=false]{pointer-events:none}:host ::ng-deep .ProseMirror[contenteditable=false] img{cursor:default;pointer-events:none}:host ::ng-deep .ProseMirror[contenteditable=false] img:hover{transform:none;box-shadow:0 2px 8px #0000001a}:host ::ng-deep .ProseMirror[contenteditable=false] img.ProseMirror-selectednode{outline:none}:host ::ng-deep .ProseMirror img{position:relative;display:inline-block;max-width:100%;height:auto;cursor:pointer;transition:all .2s ease;border:2px solid transparent;border-radius:8px}:host ::ng-deep .ProseMirror img:hover{border-color:#e2e8f0;box-shadow:0 2px 4px #0000001a}:host ::ng-deep .ProseMirror img.selected{border-color:#3182ce;box-shadow:0 0 0 3px #3182ce1a;transition:all .2s ease}:host ::ng-deep .ProseMirror .tiptap-image{max-width:100%;height:auto;border-radius:16px;box-shadow:0 4px 20px #00000014;margin:.5em 0;cursor:pointer;transition:all .3s cubic-bezier(.4,0,.2,1);display:block;filter:brightness(1) contrast(1)}:host ::ng-deep .ProseMirror .tiptap-image:hover{box-shadow:0 8px 30px #0000001f;filter:brightness(1.02) contrast(1.02)}:host ::ng-deep .ProseMirror .tiptap-image.ProseMirror-selectednode{outline:2px solid #6366f1;outline-offset:2px;border-radius:16px;box-shadow:0 0 0 4px #6366f11a}:host ::ng-deep .image-container{margin:.5em 0;text-align:center;border-radius:16px;overflow:hidden;transition:all .3s cubic-bezier(.4,0,.2,1)}:host ::ng-deep .image-container.image-align-left{text-align:left}:host ::ng-deep .image-container.image-align-center{text-align:center}:host ::ng-deep .image-container.image-align-right{text-align:right}:host ::ng-deep .image-container img{display:inline-block;max-width:100%;height:auto;border-radius:16px}:host ::ng-deep .resizable-image-container{position:relative;display:inline-block;margin:.5em 0}:host ::ng-deep .resize-controls{position:absolute;inset:0;pointer-events:none;z-index:1000}:host ::ng-deep .resize-handle{position:absolute;width:12px;height:12px;background:#3b82f6;border:2px solid white;border-radius:50%;pointer-events:all;cursor:pointer;z-index:1001;transition:all .15s ease;box-shadow:0 2px 6px #0003}:host ::ng-deep .resize-handle:hover{background:#2563eb;box-shadow:0 3px 8px #0000004d}:host ::ng-deep .resize-handle:active{background:#1d4ed8}:host ::ng-deep .resize-handle-n:hover,:host ::ng-deep .resize-handle-s:hover{transform:translate(-50%) scale(1.2)}:host ::ng-deep .resize-handle-w:hover,:host ::ng-deep .resize-handle-e:hover{transform:translateY(-50%) scale(1.2)}:host ::ng-deep .resize-handle-n:active,:host ::ng-deep .resize-handle-s:active{transform:translate(-50%) scale(.9)}:host ::ng-deep .resize-handle-w:active,:host ::ng-deep .resize-handle-e:active{transform:translateY(-50%) scale(.9)}:host ::ng-deep .resize-handle-nw:hover,:host ::ng-deep .resize-handle-ne:hover,:host ::ng-deep .resize-handle-sw:hover,:host ::ng-deep .resize-handle-se:hover{transform:scale(1.2)}:host ::ng-deep .resize-handle-nw:active,:host ::ng-deep .resize-handle-ne:active,:host ::ng-deep .resize-handle-sw:active,:host ::ng-deep .resize-handle-se:active{transform:scale(.9)}:host ::ng-deep .resize-handle-nw{top:0;left:-6px;cursor:nw-resize}:host ::ng-deep .resize-handle-n{top:0;left:50%;transform:translate(-50%);cursor:n-resize}:host ::ng-deep .resize-handle-ne{top:0;right:-6px;cursor:ne-resize}:host ::ng-deep .resize-handle-w{top:50%;left:-6px;transform:translateY(-50%);cursor:w-resize}:host ::ng-deep .resize-handle-e{top:50%;right:-6px;transform:translateY(-50%);cursor:e-resize}:host ::ng-deep .resize-handle-sw{bottom:0;left:-6px;cursor:sw-resize}:host ::ng-deep .resize-handle-s{bottom:0;left:50%;transform:translate(-50%);cursor:s-resize}:host ::ng-deep .resize-handle-se{bottom:0;right:-6px;cursor:se-resize}:host ::ng-deep body.resizing{-webkit-user-select:none;user-select:none;cursor:crosshair}:host ::ng-deep body.resizing .ProseMirror{pointer-events:none}:host ::ng-deep body.resizing .ProseMirror .tiptap-image{pointer-events:none}:host ::ng-deep .image-size-info{position:absolute;bottom:-20px;left:50%;transform:translate(-50%);background:#000c;color:#fff;padding:2px 6px;border-radius:3px;font-size:11px;white-space:nowrap;opacity:0;transition:opacity .2s ease}:host ::ng-deep .image-container:hover .image-size-info{opacity:1}\n"] }]
4074
+ }], ctorParameters: () => [{ type: ImageService }] });
4075
+
4076
+ /**
4077
+ * Factory function pour créer les slash commands traduits
4078
+ */
4079
+ function createI18nSlashCommands(i18nService) {
4080
+ const slashCommands = i18nService.slashCommands();
4081
+ return [
4082
+ {
4083
+ title: slashCommands.heading1.title,
4084
+ description: slashCommands.heading1.description,
4085
+ icon: "format_h1",
4086
+ keywords: slashCommands.heading1.keywords,
4087
+ command: (editor) => editor.chain().focus().toggleHeading({ level: 1 }).run(),
4088
+ },
4089
+ {
4090
+ title: slashCommands.heading2.title,
4091
+ description: slashCommands.heading2.description,
4092
+ icon: "format_h2",
4093
+ keywords: slashCommands.heading2.keywords,
4094
+ command: (editor) => editor.chain().focus().toggleHeading({ level: 2 }).run(),
4095
+ },
4096
+ {
4097
+ title: slashCommands.heading3.title,
4098
+ description: slashCommands.heading3.description,
4099
+ icon: "format_h3",
4100
+ keywords: slashCommands.heading3.keywords,
4101
+ command: (editor) => editor.chain().focus().toggleHeading({ level: 3 }).run(),
4102
+ },
4103
+ {
4104
+ title: slashCommands.bulletList.title,
4105
+ description: slashCommands.bulletList.description,
4106
+ icon: "format_list_bulleted",
4107
+ keywords: slashCommands.bulletList.keywords,
4108
+ command: (editor) => editor.chain().focus().toggleBulletList().run(),
4109
+ },
4110
+ {
4111
+ title: slashCommands.orderedList.title,
4112
+ description: slashCommands.orderedList.description,
4113
+ icon: "format_list_numbered",
4114
+ keywords: slashCommands.orderedList.keywords,
4115
+ command: (editor) => editor.chain().focus().toggleOrderedList().run(),
4116
+ },
4117
+ {
4118
+ title: slashCommands.blockquote.title,
4119
+ description: slashCommands.blockquote.description,
4120
+ icon: "format_quote",
4121
+ keywords: slashCommands.blockquote.keywords,
4122
+ command: (editor) => editor.chain().focus().toggleBlockquote().run(),
4123
+ },
4124
+ {
4125
+ title: slashCommands.code.title,
4126
+ description: slashCommands.code.description,
4127
+ icon: "code",
4128
+ keywords: slashCommands.code.keywords,
4129
+ command: (editor) => editor.chain().focus().toggleCodeBlock().run(),
4130
+ },
4131
+ {
4132
+ title: slashCommands.image.title,
4133
+ description: slashCommands.image.description,
4134
+ icon: "image",
4135
+ keywords: slashCommands.image.keywords,
4136
+ command: (editor) => {
4137
+ // Créer un input file temporaire pour sélectionner une image
4138
+ const input = document.createElement("input");
4139
+ input.type = "file";
4140
+ input.accept = "image/*";
4141
+ input.style.display = "none";
4142
+ input.addEventListener("change", async (e) => {
4143
+ const file = e.target.files?.[0];
4144
+ if (file && file.type.startsWith("image/")) {
4145
+ try {
4146
+ // Utiliser la méthode de compression unifiée
4147
+ const canvas = document.createElement("canvas");
4148
+ const ctx = canvas.getContext("2d");
4149
+ const img = new Image();
4150
+ img.onload = () => {
4151
+ // Vérifier les dimensions (max 1920x1080)
4152
+ const maxWidth = 1920;
4153
+ const maxHeight = 1080;
4154
+ let { width, height } = img;
4155
+ // Redimensionner si nécessaire
4156
+ if (width > maxWidth || height > maxHeight) {
4157
+ const ratio = Math.min(maxWidth / width, maxHeight / height);
4158
+ width *= ratio;
4159
+ height *= ratio;
4160
+ }
4161
+ canvas.width = width;
4162
+ canvas.height = height;
4163
+ // Dessiner l'image redimensionnée
4164
+ ctx?.drawImage(img, 0, 0, width, height);
4165
+ // Convertir en base64 avec compression
4166
+ canvas.toBlob((blob) => {
4167
+ if (blob) {
4168
+ const reader = new FileReader();
4169
+ reader.onload = (e) => {
4170
+ const base64 = e.target?.result;
4171
+ if (base64) {
4172
+ // Utiliser setResizableImage avec toutes les propriétés
4173
+ editor
4174
+ .chain()
4175
+ .focus()
4176
+ .setResizableImage({
4177
+ src: base64,
4178
+ alt: file.name,
4179
+ title: `${file.name} (${Math.round(width)}×${Math.round(height)})`,
4180
+ width: Math.round(width),
4181
+ height: Math.round(height),
4182
+ })
4183
+ .run();
4184
+ }
4185
+ };
4186
+ reader.readAsDataURL(blob);
4187
+ }
4188
+ }, file.type, 0.8 // qualité de compression
4189
+ );
4190
+ };
4191
+ img.onerror = () => {
4192
+ console.error(i18nService.editor().imageLoadError);
4193
+ };
4194
+ img.src = URL.createObjectURL(file);
4195
+ }
4196
+ catch (error) {
4197
+ console.error("Error uploading image:", error);
4198
+ }
4199
+ }
4200
+ document.body.removeChild(input);
4201
+ });
4202
+ document.body.appendChild(input);
4203
+ input.click();
4204
+ },
4205
+ },
4206
+ {
4207
+ title: slashCommands.horizontalRule.title,
4208
+ description: slashCommands.horizontalRule.description,
4209
+ icon: "horizontal_rule",
4210
+ keywords: slashCommands.horizontalRule.keywords,
4211
+ command: (editor) => editor.chain().focus().setHorizontalRule().run(),
4212
+ },
4213
+ ];
4214
+ }
4215
+ /**
4216
+ * Mapping des clés de commandes pour la compatibilité
4217
+ */
4218
+ const SLASH_COMMAND_KEYS = {
4219
+ heading1: "heading1",
4220
+ heading2: "heading2",
4221
+ heading3: "heading3",
4222
+ bulletList: "bulletList",
4223
+ orderedList: "orderedList",
4224
+ blockquote: "blockquote",
4225
+ code: "code",
4226
+ image: "image",
4227
+ horizontalRule: "horizontalRule",
4228
+ };
4229
+
4230
+ /*
4231
+ * Public API Surface of tiptap-editor
4232
+ */
4233
+ // Main component - only public component
4234
+
4235
+ /**
4236
+ * Generated bundle index. Do not edit.
4237
+ */
4238
+
4239
+ export { AngularTiptapEditorComponent, DEFAULT_BUBBLE_MENU_CONFIG, DEFAULT_IMAGE_BUBBLE_MENU_CONFIG, DEFAULT_SLASH_COMMANDS, DEFAULT_TOOLBAR_CONFIG, TiptapI18nService, createI18nSlashCommands };
4240
+ //# sourceMappingURL=flogeez-angular-tiptap-editor.mjs.map