@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.
- package/LICENSE +21 -0
- package/README.md +281 -0
- package/fesm2022/flogeez-angular-tiptap-editor.mjs +4240 -0
- package/fesm2022/flogeez-angular-tiptap-editor.mjs.map +1 -0
- package/index.d.ts +564 -0
- package/package.json +58 -0
- package/src/lib/styles/bubble-menu.global.css +32 -0
- package/src/lib/styles/index.css +53 -0
|
@@ -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
|