@flogeez/angular-tiptap-editor 0.3.6 → 0.4.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/CHANGELOG.md +122 -0
- package/README.md +53 -19
- package/fesm2022/flogeez-angular-tiptap-editor.mjs +1913 -1468
- package/fesm2022/flogeez-angular-tiptap-editor.mjs.map +1 -1
- package/index.d.ts +11 -2
- package/package.json +1 -1
- package/src/lib/styles/index.css +40 -40
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { input, output, Component,
|
|
2
|
+
import { input, output, Component, Injectable, signal, computed, viewChild, inject, effect, ViewChild, Directive, DestroyRef } from '@angular/core';
|
|
3
3
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
4
4
|
import { Node, nodeInputRule, mergeAttributes, Extension, Editor } from '@tiptap/core';
|
|
5
5
|
import StarterKit from '@tiptap/starter-kit';
|
|
@@ -11,6 +11,8 @@ import Subscript from '@tiptap/extension-subscript';
|
|
|
11
11
|
import TextAlign from '@tiptap/extension-text-align';
|
|
12
12
|
import Link from '@tiptap/extension-link';
|
|
13
13
|
import Highlight from '@tiptap/extension-highlight';
|
|
14
|
+
import TextStyle from '@tiptap/extension-text-style';
|
|
15
|
+
import Color from '@tiptap/extension-color';
|
|
14
16
|
import OfficePaste from '@intevation/tiptap-extension-office-paste';
|
|
15
17
|
import { Plugin, PluginKey } from '@tiptap/pm/state';
|
|
16
18
|
import { DecorationSet, Decoration } from '@tiptap/pm/view';
|
|
@@ -525,6 +527,7 @@ class TiptapButtonComponent {
|
|
|
525
527
|
this.title = input.required();
|
|
526
528
|
this.active = input(false);
|
|
527
529
|
this.disabled = input(false);
|
|
530
|
+
this.color = input();
|
|
528
531
|
this.variant = input("default");
|
|
529
532
|
this.size = input("medium");
|
|
530
533
|
this.iconSize = input("medium");
|
|
@@ -535,7 +538,7 @@ class TiptapButtonComponent {
|
|
|
535
538
|
event.preventDefault();
|
|
536
539
|
}
|
|
537
540
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
538
|
-
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: `
|
|
541
|
+
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 }, color: { classPropertyName: "color", publicName: "color", 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: `
|
|
539
542
|
<button
|
|
540
543
|
class="tiptap-button"
|
|
541
544
|
[class.is-active]="active()"
|
|
@@ -546,6 +549,7 @@ class TiptapButtonComponent {
|
|
|
546
549
|
[class.medium]="size() === 'medium'"
|
|
547
550
|
[class.large]="size() === 'large'"
|
|
548
551
|
[disabled]="disabled()"
|
|
552
|
+
[style.color]="color()"
|
|
549
553
|
[attr.title]="title()"
|
|
550
554
|
(mousedown)="onMouseDown($event)"
|
|
551
555
|
(click)="onClick.emit($event)"
|
|
@@ -575,6 +579,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImpor
|
|
|
575
579
|
[class.medium]="size() === 'medium'"
|
|
576
580
|
[class.large]="size() === 'large'"
|
|
577
581
|
[disabled]="disabled()"
|
|
582
|
+
[style.color]="color()"
|
|
578
583
|
[attr.title]="title()"
|
|
579
584
|
(mousedown)="onMouseDown($event)"
|
|
580
585
|
(click)="onClick.emit($event)"
|
|
@@ -592,1240 +597,151 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImpor
|
|
|
592
597
|
`, 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"] }]
|
|
593
598
|
}] });
|
|
594
599
|
|
|
595
|
-
class
|
|
596
|
-
// Effect comme propriété de classe pour éviter l'erreur d'injection context
|
|
600
|
+
class ColorPickerService {
|
|
597
601
|
constructor() {
|
|
598
|
-
this.
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
});
|
|
611
|
-
this.tippyInstance = null;
|
|
612
|
-
this.updateTimeout = null;
|
|
613
|
-
this.bubbleMenuConfig = computed(() => ({
|
|
614
|
-
bold: true,
|
|
615
|
-
italic: true,
|
|
616
|
-
underline: true,
|
|
617
|
-
strike: true,
|
|
618
|
-
code: true,
|
|
619
|
-
superscript: false,
|
|
620
|
-
subscript: false,
|
|
621
|
-
highlight: true,
|
|
622
|
-
link: true,
|
|
623
|
-
separator: true,
|
|
624
|
-
...this.config(),
|
|
625
|
-
}));
|
|
626
|
-
this.updateMenu = () => {
|
|
627
|
-
// Debounce pour éviter les appels trop fréquents
|
|
628
|
-
if (this.updateTimeout) {
|
|
629
|
-
clearTimeout(this.updateTimeout);
|
|
630
|
-
}
|
|
631
|
-
this.updateTimeout = setTimeout(() => {
|
|
632
|
-
const ed = this.editor();
|
|
633
|
-
if (!ed)
|
|
634
|
-
return;
|
|
635
|
-
const { selection } = ed.state;
|
|
636
|
-
const { from, to } = selection;
|
|
637
|
-
const hasTextSelection = from !== to && !(selection instanceof CellSelection);
|
|
638
|
-
const isImageSelected = ed.isActive("image") || ed.isActive("resizableImage");
|
|
639
|
-
const isTableCellSelected = ed.isActive("tableCell") || ed.isActive("tableHeader");
|
|
640
|
-
const hasCellSelection = selection instanceof CellSelection;
|
|
641
|
-
// Ne montrer le menu texte que si :
|
|
642
|
-
// - Il y a une sélection de texte (pas une sélection de cellules multiples)
|
|
643
|
-
// - Aucune image n'est sélectionnée (priorité aux images)
|
|
644
|
-
// - Ce n'est pas une sélection de cellules multiples (CellSelection)
|
|
645
|
-
// - L'éditeur est éditable
|
|
646
|
-
// Note: Le texte dans une cellule est autorisé (isTableCellSelected peut être true)
|
|
647
|
-
const shouldShow = hasTextSelection &&
|
|
648
|
-
!isImageSelected &&
|
|
649
|
-
!hasCellSelection &&
|
|
650
|
-
ed.isEditable;
|
|
651
|
-
if (shouldShow) {
|
|
652
|
-
this.showTippy();
|
|
653
|
-
}
|
|
654
|
-
else {
|
|
655
|
-
this.hideTippy();
|
|
656
|
-
}
|
|
657
|
-
}, 10);
|
|
658
|
-
};
|
|
659
|
-
this.handleBlur = () => {
|
|
660
|
-
// Masquer le menu quand l'éditeur perd le focus
|
|
661
|
-
setTimeout(() => {
|
|
662
|
-
this.hideTippy();
|
|
663
|
-
}, 100);
|
|
664
|
-
};
|
|
665
|
-
effect(() => {
|
|
666
|
-
const ed = this.editor();
|
|
667
|
-
if (!ed)
|
|
602
|
+
this.storedSelection = null;
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Find the first explicitly applied color within a selection.
|
|
606
|
+
*/
|
|
607
|
+
findFirstAppliedColor(editor, selection) {
|
|
608
|
+
const { from, to } = selection;
|
|
609
|
+
let found = null;
|
|
610
|
+
editor.state.doc.nodesBetween(from, to, (node) => {
|
|
611
|
+
if (found)
|
|
612
|
+
return false;
|
|
613
|
+
if (!node.isText)
|
|
668
614
|
return;
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
ed.on("selectionUpdate", this.updateMenu);
|
|
676
|
-
ed.on("transaction", this.updateMenu);
|
|
677
|
-
ed.on("focus", this.updateMenu);
|
|
678
|
-
ed.on("blur", this.handleBlur);
|
|
679
|
-
// Ne pas appeler updateMenu() ici pour éviter l'affichage prématuré
|
|
680
|
-
// Il sera appelé automatiquement quand l'éditeur sera prêt
|
|
681
|
-
});
|
|
682
|
-
}
|
|
683
|
-
ngOnInit() {
|
|
684
|
-
// Initialiser Tippy de manière synchrone après que le component soit ready
|
|
685
|
-
this.initTippy();
|
|
686
|
-
}
|
|
687
|
-
ngOnDestroy() {
|
|
688
|
-
const ed = this.editor();
|
|
689
|
-
if (ed) {
|
|
690
|
-
ed.off("selectionUpdate", this.updateMenu);
|
|
691
|
-
ed.off("transaction", this.updateMenu);
|
|
692
|
-
ed.off("focus", this.updateMenu);
|
|
693
|
-
ed.off("blur", this.handleBlur);
|
|
694
|
-
}
|
|
695
|
-
// Nettoyer les timeouts
|
|
696
|
-
if (this.updateTimeout) {
|
|
697
|
-
clearTimeout(this.updateTimeout);
|
|
698
|
-
}
|
|
699
|
-
// Nettoyer Tippy
|
|
700
|
-
if (this.tippyInstance) {
|
|
701
|
-
this.tippyInstance.destroy();
|
|
702
|
-
this.tippyInstance = null;
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
initTippy() {
|
|
706
|
-
// Attendre que l'élément soit disponible
|
|
707
|
-
if (!this.menuRef?.nativeElement) {
|
|
708
|
-
setTimeout(() => this.initTippy(), 50);
|
|
615
|
+
const textStyleMark = node.marks.find((m) => m.type.name === "textStyle");
|
|
616
|
+
const color = textStyleMark?.attrs?.color;
|
|
617
|
+
if (color) {
|
|
618
|
+
found = this.normalizeColor(color);
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
709
621
|
return;
|
|
622
|
+
});
|
|
623
|
+
return found;
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Capture current editor selection.
|
|
627
|
+
*/
|
|
628
|
+
captureSelection(editor) {
|
|
629
|
+
const sel = {
|
|
630
|
+
from: editor.state.selection.from,
|
|
631
|
+
to: editor.state.selection.to,
|
|
632
|
+
};
|
|
633
|
+
this.storedSelection = sel;
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Get last captured selection for an editor (if any).
|
|
637
|
+
*/
|
|
638
|
+
getStoredSelection() {
|
|
639
|
+
return this.storedSelection;
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* To be called when color picking is done (picker closes).
|
|
643
|
+
*/
|
|
644
|
+
done() {
|
|
645
|
+
this.storedSelection = null;
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Get the current text color for the selection.
|
|
649
|
+
* If multiple colors are present, returns the first found.
|
|
650
|
+
*/
|
|
651
|
+
getCurrentColor(editor, selection) {
|
|
652
|
+
const sel = selection ??
|
|
653
|
+
{
|
|
654
|
+
from: editor.state.selection.from,
|
|
655
|
+
to: editor.state.selection.to,
|
|
656
|
+
};
|
|
657
|
+
const found = this.findFirstAppliedColor(editor, sel);
|
|
658
|
+
if (found)
|
|
659
|
+
return found;
|
|
660
|
+
const attrs = editor.getAttributes("textStyle") || {};
|
|
661
|
+
return attrs.color ? this.normalizeColor(attrs.color) : "#000000";
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Check if a color is explicitly applied on the selection.
|
|
665
|
+
*/
|
|
666
|
+
hasColorApplied(editor, selection) {
|
|
667
|
+
const sel = selection ??
|
|
668
|
+
{
|
|
669
|
+
from: editor.state.selection.from,
|
|
670
|
+
to: editor.state.selection.to,
|
|
671
|
+
};
|
|
672
|
+
const { from, to } = sel;
|
|
673
|
+
if (from === to) {
|
|
674
|
+
const attrs = editor.getAttributes("textStyle") || {};
|
|
675
|
+
return !!attrs.color;
|
|
710
676
|
}
|
|
711
|
-
const
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
677
|
+
const found = this.findFirstAppliedColor(editor, sel);
|
|
678
|
+
if (found)
|
|
679
|
+
return true;
|
|
680
|
+
const attrs = editor.getAttributes("textStyle") || {};
|
|
681
|
+
return !!attrs.color;
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Normalize color values so they can be used by <input type="color">.
|
|
685
|
+
*/
|
|
686
|
+
normalizeColor(color) {
|
|
687
|
+
if (color.startsWith("#"))
|
|
688
|
+
return color;
|
|
689
|
+
const rgbMatch = color
|
|
690
|
+
.trim()
|
|
691
|
+
.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([0-9.]+))?\s*\)$/i);
|
|
692
|
+
if (!rgbMatch)
|
|
693
|
+
return "#000000";
|
|
694
|
+
const r = Math.max(0, Math.min(255, parseInt(rgbMatch[1], 10)));
|
|
695
|
+
const g = Math.max(0, Math.min(255, parseInt(rgbMatch[2], 10)));
|
|
696
|
+
const b = Math.max(0, Math.min(255, parseInt(rgbMatch[3], 10)));
|
|
697
|
+
return ("#" +
|
|
698
|
+
[r, g, b]
|
|
699
|
+
.map((n) => n.toString(16).padStart(2, "0"))
|
|
700
|
+
.join("")
|
|
701
|
+
.toLowerCase());
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Apply a color to the current selection.
|
|
705
|
+
*/
|
|
706
|
+
applyColor(editor, color, options = {}) {
|
|
707
|
+
const sel = this.getStoredSelection() ?? editor.state.selection;
|
|
708
|
+
const { addToHistory = true } = options;
|
|
709
|
+
let chain = editor.chain().focus();
|
|
710
|
+
if (sel) {
|
|
711
|
+
chain = chain.setTextSelection(sel);
|
|
715
712
|
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
trigger: "manual",
|
|
720
|
-
placement: "top-start",
|
|
721
|
-
appendTo: () => document.body,
|
|
722
|
-
interactive: true,
|
|
723
|
-
arrow: false,
|
|
724
|
-
offset: [0, 8],
|
|
725
|
-
hideOnClick: false,
|
|
726
|
-
onShow: (instance) => {
|
|
727
|
-
// S'assurer que les autres menus sont fermés
|
|
728
|
-
this.hideOtherMenus();
|
|
729
|
-
},
|
|
730
|
-
getReferenceClientRect: () => this.getSelectionRect(),
|
|
731
|
-
// Améliorer le positionnement avec scroll
|
|
732
|
-
popperOptions: {
|
|
733
|
-
modifiers: [
|
|
734
|
-
{
|
|
735
|
-
name: "preventOverflow",
|
|
736
|
-
options: {
|
|
737
|
-
boundary: "viewport",
|
|
738
|
-
padding: 8,
|
|
739
|
-
},
|
|
740
|
-
},
|
|
741
|
-
{
|
|
742
|
-
name: "flip",
|
|
743
|
-
options: {
|
|
744
|
-
fallbackPlacements: ["bottom-start", "top-end", "bottom-end"],
|
|
745
|
-
},
|
|
746
|
-
},
|
|
747
|
-
],
|
|
748
|
-
},
|
|
749
|
-
});
|
|
750
|
-
// Maintenant que Tippy est initialisé, faire un premier check
|
|
751
|
-
this.updateMenu();
|
|
752
|
-
}
|
|
753
|
-
getSelectionRect() {
|
|
754
|
-
const selection = window.getSelection();
|
|
755
|
-
if (!selection || selection.rangeCount === 0) {
|
|
756
|
-
return new DOMRect(0, 0, 0, 0);
|
|
713
|
+
chain.setColor(color);
|
|
714
|
+
if (!addToHistory) {
|
|
715
|
+
chain.setMeta("addToHistory", false);
|
|
757
716
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
this.tippyInstance.setProps({
|
|
770
|
-
getReferenceClientRect: () => this.getSelectionRect(),
|
|
771
|
-
});
|
|
772
|
-
this.tippyInstance.show();
|
|
773
|
-
}
|
|
774
|
-
hideTippy() {
|
|
775
|
-
if (this.tippyInstance) {
|
|
776
|
-
this.tippyInstance.hide();
|
|
717
|
+
chain.run();
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Unset color on the current selection.
|
|
721
|
+
*/
|
|
722
|
+
unsetColor(editor, options = {}) {
|
|
723
|
+
const sel = this.getStoredSelection() ?? editor.state.selection;
|
|
724
|
+
const { addToHistory = true } = options;
|
|
725
|
+
let chain = editor.chain().focus();
|
|
726
|
+
if (sel) {
|
|
727
|
+
chain = chain.setTextSelection(sel);
|
|
777
728
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
return ed?.isActive(mark) || false;
|
|
782
|
-
}
|
|
783
|
-
onCommand(command, event) {
|
|
784
|
-
event.preventDefault();
|
|
785
|
-
const ed = this.editor();
|
|
786
|
-
if (!ed)
|
|
787
|
-
return;
|
|
788
|
-
switch (command) {
|
|
789
|
-
case "bold":
|
|
790
|
-
ed.chain().focus().toggleBold().run();
|
|
791
|
-
break;
|
|
792
|
-
case "italic":
|
|
793
|
-
ed.chain().focus().toggleItalic().run();
|
|
794
|
-
break;
|
|
795
|
-
case "underline":
|
|
796
|
-
ed.chain().focus().toggleUnderline().run();
|
|
797
|
-
break;
|
|
798
|
-
case "strike":
|
|
799
|
-
ed.chain().focus().toggleStrike().run();
|
|
800
|
-
break;
|
|
801
|
-
case "code":
|
|
802
|
-
ed.chain().focus().toggleCode().run();
|
|
803
|
-
break;
|
|
804
|
-
case "superscript":
|
|
805
|
-
ed.chain().focus().toggleSuperscript().run();
|
|
806
|
-
break;
|
|
807
|
-
case "subscript":
|
|
808
|
-
ed.chain().focus().toggleSubscript().run();
|
|
809
|
-
break;
|
|
810
|
-
case "highlight":
|
|
811
|
-
ed.chain().focus().toggleHighlight().run();
|
|
812
|
-
break;
|
|
813
|
-
case "link":
|
|
814
|
-
const href = window.prompt("URL du lien:");
|
|
815
|
-
if (href) {
|
|
816
|
-
ed.chain().focus().toggleLink({ href }).run();
|
|
817
|
-
}
|
|
818
|
-
break;
|
|
729
|
+
chain.unsetColor();
|
|
730
|
+
if (!addToHistory) {
|
|
731
|
+
chain.setMeta("addToHistory", false);
|
|
819
732
|
}
|
|
733
|
+
chain.run();
|
|
820
734
|
}
|
|
821
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type:
|
|
822
|
-
static { this.ɵ
|
|
823
|
-
<div #menuRef class="bubble-menu">
|
|
824
|
-
@if (bubbleMenuConfig().bold) {
|
|
825
|
-
<tiptap-button
|
|
826
|
-
icon="format_bold"
|
|
827
|
-
title="Gras"
|
|
828
|
-
[active]="isActive('bold')"
|
|
829
|
-
(click)="onCommand('bold', $event)"
|
|
830
|
-
></tiptap-button>
|
|
831
|
-
} @if (bubbleMenuConfig().italic) {
|
|
832
|
-
<tiptap-button
|
|
833
|
-
icon="format_italic"
|
|
834
|
-
title="Italique"
|
|
835
|
-
[active]="isActive('italic')"
|
|
836
|
-
(click)="onCommand('italic', $event)"
|
|
837
|
-
></tiptap-button>
|
|
838
|
-
} @if (bubbleMenuConfig().underline) {
|
|
839
|
-
<tiptap-button
|
|
840
|
-
icon="format_underlined"
|
|
841
|
-
title="Souligné"
|
|
842
|
-
[active]="isActive('underline')"
|
|
843
|
-
(click)="onCommand('underline', $event)"
|
|
844
|
-
></tiptap-button>
|
|
845
|
-
} @if (bubbleMenuConfig().strike) {
|
|
846
|
-
<tiptap-button
|
|
847
|
-
icon="strikethrough_s"
|
|
848
|
-
title="Barré"
|
|
849
|
-
[active]="isActive('strike')"
|
|
850
|
-
(click)="onCommand('strike', $event)"
|
|
851
|
-
></tiptap-button>
|
|
852
|
-
} @if (bubbleMenuConfig().superscript) {
|
|
853
|
-
<tiptap-button
|
|
854
|
-
icon="superscript"
|
|
855
|
-
title="Exposant"
|
|
856
|
-
[active]="isActive('superscript')"
|
|
857
|
-
(click)="onCommand('superscript', $event)"
|
|
858
|
-
></tiptap-button>
|
|
859
|
-
} @if (bubbleMenuConfig().subscript) {
|
|
860
|
-
<tiptap-button
|
|
861
|
-
icon="subscript"
|
|
862
|
-
title="Indice"
|
|
863
|
-
[active]="isActive('subscript')"
|
|
864
|
-
(click)="onCommand('subscript', $event)"
|
|
865
|
-
></tiptap-button>
|
|
866
|
-
} @if (bubbleMenuConfig().highlight) {
|
|
867
|
-
<tiptap-button
|
|
868
|
-
icon="highlight"
|
|
869
|
-
title="Surbrillance"
|
|
870
|
-
[active]="isActive('highlight')"
|
|
871
|
-
(click)="onCommand('highlight', $event)"
|
|
872
|
-
></tiptap-button>
|
|
873
|
-
} @if (bubbleMenuConfig().separator && (bubbleMenuConfig().code ||
|
|
874
|
-
bubbleMenuConfig().link)) {
|
|
875
|
-
<div class="tiptap-separator"></div>
|
|
876
|
-
} @if (bubbleMenuConfig().code) {
|
|
877
|
-
<tiptap-button
|
|
878
|
-
icon="code"
|
|
879
|
-
title="Code"
|
|
880
|
-
[active]="isActive('code')"
|
|
881
|
-
(click)="onCommand('code', $event)"
|
|
882
|
-
></tiptap-button>
|
|
883
|
-
} @if (bubbleMenuConfig().link) {
|
|
884
|
-
<tiptap-button
|
|
885
|
-
icon="link"
|
|
886
|
-
title="Lien"
|
|
887
|
-
[active]="isActive('link')"
|
|
888
|
-
(click)="onCommand('link', $event)"
|
|
889
|
-
></tiptap-button>
|
|
890
|
-
}
|
|
891
|
-
</div>
|
|
892
|
-
`, isInline: true, dependencies: [{ kind: "component", type: TiptapButtonComponent, selector: "tiptap-button", inputs: ["icon", "title", "active", "disabled", "variant", "size", "iconSize"], outputs: ["onClick"] }] }); }
|
|
893
|
-
}
|
|
894
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapBubbleMenuComponent, decorators: [{
|
|
895
|
-
type: Component,
|
|
896
|
-
args: [{ selector: "tiptap-bubble-menu", standalone: true, imports: [TiptapButtonComponent], template: `
|
|
897
|
-
<div #menuRef class="bubble-menu">
|
|
898
|
-
@if (bubbleMenuConfig().bold) {
|
|
899
|
-
<tiptap-button
|
|
900
|
-
icon="format_bold"
|
|
901
|
-
title="Gras"
|
|
902
|
-
[active]="isActive('bold')"
|
|
903
|
-
(click)="onCommand('bold', $event)"
|
|
904
|
-
></tiptap-button>
|
|
905
|
-
} @if (bubbleMenuConfig().italic) {
|
|
906
|
-
<tiptap-button
|
|
907
|
-
icon="format_italic"
|
|
908
|
-
title="Italique"
|
|
909
|
-
[active]="isActive('italic')"
|
|
910
|
-
(click)="onCommand('italic', $event)"
|
|
911
|
-
></tiptap-button>
|
|
912
|
-
} @if (bubbleMenuConfig().underline) {
|
|
913
|
-
<tiptap-button
|
|
914
|
-
icon="format_underlined"
|
|
915
|
-
title="Souligné"
|
|
916
|
-
[active]="isActive('underline')"
|
|
917
|
-
(click)="onCommand('underline', $event)"
|
|
918
|
-
></tiptap-button>
|
|
919
|
-
} @if (bubbleMenuConfig().strike) {
|
|
920
|
-
<tiptap-button
|
|
921
|
-
icon="strikethrough_s"
|
|
922
|
-
title="Barré"
|
|
923
|
-
[active]="isActive('strike')"
|
|
924
|
-
(click)="onCommand('strike', $event)"
|
|
925
|
-
></tiptap-button>
|
|
926
|
-
} @if (bubbleMenuConfig().superscript) {
|
|
927
|
-
<tiptap-button
|
|
928
|
-
icon="superscript"
|
|
929
|
-
title="Exposant"
|
|
930
|
-
[active]="isActive('superscript')"
|
|
931
|
-
(click)="onCommand('superscript', $event)"
|
|
932
|
-
></tiptap-button>
|
|
933
|
-
} @if (bubbleMenuConfig().subscript) {
|
|
934
|
-
<tiptap-button
|
|
935
|
-
icon="subscript"
|
|
936
|
-
title="Indice"
|
|
937
|
-
[active]="isActive('subscript')"
|
|
938
|
-
(click)="onCommand('subscript', $event)"
|
|
939
|
-
></tiptap-button>
|
|
940
|
-
} @if (bubbleMenuConfig().highlight) {
|
|
941
|
-
<tiptap-button
|
|
942
|
-
icon="highlight"
|
|
943
|
-
title="Surbrillance"
|
|
944
|
-
[active]="isActive('highlight')"
|
|
945
|
-
(click)="onCommand('highlight', $event)"
|
|
946
|
-
></tiptap-button>
|
|
947
|
-
} @if (bubbleMenuConfig().separator && (bubbleMenuConfig().code ||
|
|
948
|
-
bubbleMenuConfig().link)) {
|
|
949
|
-
<div class="tiptap-separator"></div>
|
|
950
|
-
} @if (bubbleMenuConfig().code) {
|
|
951
|
-
<tiptap-button
|
|
952
|
-
icon="code"
|
|
953
|
-
title="Code"
|
|
954
|
-
[active]="isActive('code')"
|
|
955
|
-
(click)="onCommand('code', $event)"
|
|
956
|
-
></tiptap-button>
|
|
957
|
-
} @if (bubbleMenuConfig().link) {
|
|
958
|
-
<tiptap-button
|
|
959
|
-
icon="link"
|
|
960
|
-
title="Lien"
|
|
961
|
-
[active]="isActive('link')"
|
|
962
|
-
(click)="onCommand('link', $event)"
|
|
963
|
-
></tiptap-button>
|
|
964
|
-
}
|
|
965
|
-
</div>
|
|
966
|
-
` }]
|
|
967
|
-
}], ctorParameters: () => [], propDecorators: { menuRef: [{
|
|
968
|
-
type: ViewChild,
|
|
969
|
-
args: ["menuRef", { static: false }]
|
|
970
|
-
}] } });
|
|
971
|
-
|
|
972
|
-
class TiptapSeparatorComponent {
|
|
973
|
-
constructor() {
|
|
974
|
-
this.orientation = input("vertical");
|
|
975
|
-
this.size = input("medium");
|
|
976
|
-
}
|
|
977
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapSeparatorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
978
|
-
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: `
|
|
979
|
-
<div
|
|
980
|
-
class="tiptap-separator"
|
|
981
|
-
[class.vertical]="orientation() === 'vertical'"
|
|
982
|
-
[class.horizontal]="orientation() === 'horizontal'"
|
|
983
|
-
[class.small]="size() === 'small'"
|
|
984
|
-
[class.medium]="size() === 'medium'"
|
|
985
|
-
[class.large]="size() === 'large'"
|
|
986
|
-
></div>
|
|
987
|
-
`, 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"] }); }
|
|
735
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: ColorPickerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
736
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: ColorPickerService, providedIn: "root" }); }
|
|
988
737
|
}
|
|
989
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type:
|
|
990
|
-
type:
|
|
991
|
-
args: [{
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
[class.vertical]="orientation() === 'vertical'"
|
|
995
|
-
[class.horizontal]="orientation() === 'horizontal'"
|
|
996
|
-
[class.small]="size() === 'small'"
|
|
997
|
-
[class.medium]="size() === 'medium'"
|
|
998
|
-
[class.large]="size() === 'large'"
|
|
999
|
-
></div>
|
|
1000
|
-
`, 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"] }]
|
|
738
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: ColorPickerService, decorators: [{
|
|
739
|
+
type: Injectable,
|
|
740
|
+
args: [{
|
|
741
|
+
providedIn: "root",
|
|
742
|
+
}]
|
|
1001
743
|
}] });
|
|
1002
744
|
|
|
1003
|
-
class ImageService {
|
|
1004
|
-
constructor() {
|
|
1005
|
-
// Signals pour l'état des images
|
|
1006
|
-
this.selectedImage = signal(null);
|
|
1007
|
-
this.isImageSelected = computed(() => this.selectedImage() !== null);
|
|
1008
|
-
this.isResizing = signal(false);
|
|
1009
|
-
// Signaux pour l'upload
|
|
1010
|
-
this.isUploading = signal(false);
|
|
1011
|
-
this.uploadProgress = signal(0);
|
|
1012
|
-
this.uploadMessage = signal("");
|
|
1013
|
-
/**
|
|
1014
|
-
* Custom upload handler for images.
|
|
1015
|
-
* When set, this handler will be called instead of the default base64 conversion.
|
|
1016
|
-
* This allows users to implement their own image storage logic.
|
|
1017
|
-
*
|
|
1018
|
-
* @example
|
|
1019
|
-
* ```typescript
|
|
1020
|
-
* imageService.uploadHandler = async (context) => {
|
|
1021
|
-
* const formData = new FormData();
|
|
1022
|
-
* formData.append('image', context.file);
|
|
1023
|
-
* const response = await fetch('/api/upload', { method: 'POST', body: formData });
|
|
1024
|
-
* const data = await response.json();
|
|
1025
|
-
* return { src: data.url };
|
|
1026
|
-
* };
|
|
1027
|
-
* ```
|
|
1028
|
-
*/
|
|
1029
|
-
this.uploadHandler = null;
|
|
1030
|
-
// Référence à l'éditeur pour les mises à jour
|
|
1031
|
-
this.currentEditor = null;
|
|
1032
|
-
}
|
|
1033
|
-
// Méthodes pour la gestion des images
|
|
1034
|
-
selectImage(editor) {
|
|
1035
|
-
if (editor.isActive("resizableImage")) {
|
|
1036
|
-
const attrs = editor.getAttributes("resizableImage");
|
|
1037
|
-
this.selectedImage.set({
|
|
1038
|
-
src: attrs["src"],
|
|
1039
|
-
alt: attrs["alt"],
|
|
1040
|
-
title: attrs["title"],
|
|
1041
|
-
width: attrs["width"],
|
|
1042
|
-
height: attrs["height"],
|
|
1043
|
-
});
|
|
1044
|
-
}
|
|
1045
|
-
else {
|
|
1046
|
-
this.selectedImage.set(null);
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1049
|
-
clearSelection() {
|
|
1050
|
-
this.selectedImage.set(null);
|
|
1051
|
-
}
|
|
1052
|
-
// Méthodes pour manipuler les images
|
|
1053
|
-
insertImage(editor, imageData) {
|
|
1054
|
-
editor.chain().focus().setResizableImage(imageData).run();
|
|
1055
|
-
}
|
|
1056
|
-
updateImageAttributes(editor, attributes) {
|
|
1057
|
-
if (editor.isActive("resizableImage")) {
|
|
1058
|
-
editor
|
|
1059
|
-
.chain()
|
|
1060
|
-
.focus()
|
|
1061
|
-
.updateAttributes("resizableImage", attributes)
|
|
1062
|
-
.run();
|
|
1063
|
-
this.updateSelectedImage(attributes);
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
// Nouvelles méthodes pour le redimensionnement
|
|
1067
|
-
resizeImage(editor, options) {
|
|
1068
|
-
if (!editor.isActive("resizableImage"))
|
|
1069
|
-
return;
|
|
1070
|
-
const currentAttrs = editor.getAttributes("resizableImage");
|
|
1071
|
-
let newWidth = options.width;
|
|
1072
|
-
let newHeight = options.height;
|
|
1073
|
-
// Maintenir le ratio d'aspect si demandé
|
|
1074
|
-
if (options.maintainAspectRatio !== false &&
|
|
1075
|
-
currentAttrs["width"] &&
|
|
1076
|
-
currentAttrs["height"]) {
|
|
1077
|
-
const aspectRatio = currentAttrs["width"] / currentAttrs["height"];
|
|
1078
|
-
if (newWidth && !newHeight) {
|
|
1079
|
-
newHeight = Math.round(newWidth / aspectRatio);
|
|
1080
|
-
}
|
|
1081
|
-
else if (newHeight && !newWidth) {
|
|
1082
|
-
newWidth = Math.round(newHeight * aspectRatio);
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
// Appliquer des limites minimales
|
|
1086
|
-
if (newWidth)
|
|
1087
|
-
newWidth = Math.max(50, newWidth);
|
|
1088
|
-
if (newHeight)
|
|
1089
|
-
newHeight = Math.max(50, newHeight);
|
|
1090
|
-
this.updateImageAttributes(editor, {
|
|
1091
|
-
width: newWidth,
|
|
1092
|
-
height: newHeight,
|
|
1093
|
-
});
|
|
1094
|
-
}
|
|
1095
|
-
// Méthodes pour redimensionner par pourcentage
|
|
1096
|
-
resizeImageByPercentage(editor, percentage) {
|
|
1097
|
-
if (!editor.isActive("resizableImage"))
|
|
1098
|
-
return;
|
|
1099
|
-
const currentAttrs = editor.getAttributes("resizableImage");
|
|
1100
|
-
if (!currentAttrs["width"] || !currentAttrs["height"])
|
|
1101
|
-
return;
|
|
1102
|
-
const newWidth = Math.round(currentAttrs["width"] * (percentage / 100));
|
|
1103
|
-
const newHeight = Math.round(currentAttrs["height"] * (percentage / 100));
|
|
1104
|
-
this.resizeImage(editor, { width: newWidth, height: newHeight });
|
|
1105
|
-
}
|
|
1106
|
-
// Méthodes pour redimensionner à des tailles prédéfinies
|
|
1107
|
-
resizeImageToSmall(editor) {
|
|
1108
|
-
this.resizeImage(editor, {
|
|
1109
|
-
width: 300,
|
|
1110
|
-
height: 200,
|
|
1111
|
-
maintainAspectRatio: true,
|
|
1112
|
-
});
|
|
1113
|
-
}
|
|
1114
|
-
resizeImageToMedium(editor) {
|
|
1115
|
-
this.resizeImage(editor, {
|
|
1116
|
-
width: 500,
|
|
1117
|
-
height: 350,
|
|
1118
|
-
maintainAspectRatio: true,
|
|
1119
|
-
});
|
|
1120
|
-
}
|
|
1121
|
-
resizeImageToLarge(editor) {
|
|
1122
|
-
this.resizeImage(editor, {
|
|
1123
|
-
width: 800,
|
|
1124
|
-
height: 600,
|
|
1125
|
-
maintainAspectRatio: true,
|
|
1126
|
-
});
|
|
1127
|
-
}
|
|
1128
|
-
resizeImageToOriginal(editor) {
|
|
1129
|
-
if (!editor.isActive("resizableImage"))
|
|
1130
|
-
return;
|
|
1131
|
-
const img = new Image();
|
|
1132
|
-
img.onload = () => {
|
|
1133
|
-
this.resizeImage(editor, {
|
|
1134
|
-
width: img.naturalWidth,
|
|
1135
|
-
height: img.naturalHeight,
|
|
1136
|
-
});
|
|
1137
|
-
};
|
|
1138
|
-
img.src = editor.getAttributes("resizableImage")["src"];
|
|
1139
|
-
}
|
|
1140
|
-
// Méthode pour redimensionner librement (sans maintenir le ratio)
|
|
1141
|
-
resizeImageFreely(editor, width, height) {
|
|
1142
|
-
this.resizeImage(editor, {
|
|
1143
|
-
width,
|
|
1144
|
-
height,
|
|
1145
|
-
maintainAspectRatio: false,
|
|
1146
|
-
});
|
|
1147
|
-
}
|
|
1148
|
-
// Méthode pour obtenir les dimensions actuelles de l'image
|
|
1149
|
-
getImageDimensions(editor) {
|
|
1150
|
-
if (!editor.isActive("resizableImage"))
|
|
1151
|
-
return null;
|
|
1152
|
-
const attrs = editor.getAttributes("resizableImage");
|
|
1153
|
-
return {
|
|
1154
|
-
width: attrs["width"] || 0,
|
|
1155
|
-
height: attrs["height"] || 0,
|
|
1156
|
-
};
|
|
1157
|
-
}
|
|
1158
|
-
// Méthode pour obtenir les dimensions naturelles de l'image
|
|
1159
|
-
getNaturalImageDimensions(src) {
|
|
1160
|
-
return new Promise((resolve, reject) => {
|
|
1161
|
-
const img = new Image();
|
|
1162
|
-
img.onload = () => {
|
|
1163
|
-
resolve({ width: img.naturalWidth, height: img.naturalHeight });
|
|
1164
|
-
};
|
|
1165
|
-
img.onerror = () => {
|
|
1166
|
-
reject(new Error("Impossible de charger l'image"));
|
|
1167
|
-
};
|
|
1168
|
-
img.src = src;
|
|
1169
|
-
});
|
|
1170
|
-
}
|
|
1171
|
-
deleteImage(editor) {
|
|
1172
|
-
if (editor.isActive("resizableImage")) {
|
|
1173
|
-
editor.chain().focus().deleteSelection().run();
|
|
1174
|
-
this.clearSelection();
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
// Méthodes utilitaires
|
|
1178
|
-
updateSelectedImage(attributes) {
|
|
1179
|
-
const current = this.selectedImage();
|
|
1180
|
-
if (current) {
|
|
1181
|
-
this.selectedImage.set({ ...current, ...attributes });
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
// Validation des images
|
|
1185
|
-
validateImage(file, maxSize = 5 * 1024 * 1024) {
|
|
1186
|
-
if (!file.type.startsWith("image/")) {
|
|
1187
|
-
return { valid: false, error: "Le fichier doit être une image" };
|
|
1188
|
-
}
|
|
1189
|
-
if (file.size > maxSize) {
|
|
1190
|
-
return {
|
|
1191
|
-
valid: false,
|
|
1192
|
-
error: `L'image est trop volumineuse (max ${maxSize / 1024 / 1024}MB)`,
|
|
1193
|
-
};
|
|
1194
|
-
}
|
|
1195
|
-
return { valid: true };
|
|
1196
|
-
}
|
|
1197
|
-
// Compression d'image
|
|
1198
|
-
async compressImage(file, quality = 0.8, maxWidth = 1920, maxHeight = 1080) {
|
|
1199
|
-
return new Promise((resolve, reject) => {
|
|
1200
|
-
const canvas = document.createElement("canvas");
|
|
1201
|
-
const ctx = canvas.getContext("2d");
|
|
1202
|
-
const img = new Image();
|
|
1203
|
-
img.onload = () => {
|
|
1204
|
-
// Mise à jour du progrès
|
|
1205
|
-
if (this.isUploading()) {
|
|
1206
|
-
this.uploadProgress.set(40);
|
|
1207
|
-
this.uploadMessage.set("Redimensionnement...");
|
|
1208
|
-
this.forceEditorUpdate();
|
|
1209
|
-
}
|
|
1210
|
-
let { width, height } = img;
|
|
1211
|
-
// Redimensionner si nécessaire
|
|
1212
|
-
if (width > maxWidth || height > maxHeight) {
|
|
1213
|
-
const ratio = Math.min(maxWidth / width, maxHeight / height);
|
|
1214
|
-
width *= ratio;
|
|
1215
|
-
height *= ratio;
|
|
1216
|
-
}
|
|
1217
|
-
canvas.width = width;
|
|
1218
|
-
canvas.height = height;
|
|
1219
|
-
// Dessiner l'image redimensionnée
|
|
1220
|
-
ctx?.drawImage(img, 0, 0, width, height);
|
|
1221
|
-
// Mise à jour du progrès
|
|
1222
|
-
if (this.isUploading()) {
|
|
1223
|
-
this.uploadProgress.set(60);
|
|
1224
|
-
this.uploadMessage.set("Compression...");
|
|
1225
|
-
this.forceEditorUpdate();
|
|
1226
|
-
}
|
|
1227
|
-
// Convertir en base64 avec compression
|
|
1228
|
-
canvas.toBlob((blob) => {
|
|
1229
|
-
if (blob) {
|
|
1230
|
-
const reader = new FileReader();
|
|
1231
|
-
reader.onload = (e) => {
|
|
1232
|
-
const base64 = e.target?.result;
|
|
1233
|
-
if (base64) {
|
|
1234
|
-
const result = {
|
|
1235
|
-
src: base64,
|
|
1236
|
-
name: file.name,
|
|
1237
|
-
size: blob.size,
|
|
1238
|
-
type: file.type,
|
|
1239
|
-
width: Math.round(width),
|
|
1240
|
-
height: Math.round(height),
|
|
1241
|
-
originalSize: file.size,
|
|
1242
|
-
};
|
|
1243
|
-
resolve(result);
|
|
1244
|
-
}
|
|
1245
|
-
else {
|
|
1246
|
-
reject(new Error("Erreur lors de la compression"));
|
|
1247
|
-
}
|
|
1248
|
-
};
|
|
1249
|
-
reader.readAsDataURL(blob);
|
|
1250
|
-
}
|
|
1251
|
-
else {
|
|
1252
|
-
reject(new Error("Erreur lors de la compression"));
|
|
1253
|
-
}
|
|
1254
|
-
}, file.type, quality);
|
|
1255
|
-
};
|
|
1256
|
-
img.onerror = () => reject(new Error("Erreur lors du chargement de l'image"));
|
|
1257
|
-
img.src = URL.createObjectURL(file);
|
|
1258
|
-
});
|
|
1259
|
-
}
|
|
1260
|
-
// Méthode privée générique pour uploader avec progression
|
|
1261
|
-
async uploadImageWithProgress(editor, file, insertionStrategy, actionMessage, options) {
|
|
1262
|
-
try {
|
|
1263
|
-
// Stocker la référence à l'éditeur
|
|
1264
|
-
this.currentEditor = editor;
|
|
1265
|
-
this.isUploading.set(true);
|
|
1266
|
-
this.uploadProgress.set(0);
|
|
1267
|
-
this.uploadMessage.set("Validation du fichier...");
|
|
1268
|
-
this.forceEditorUpdate();
|
|
1269
|
-
// Validation
|
|
1270
|
-
const validation = this.validateImage(file);
|
|
1271
|
-
if (!validation.valid) {
|
|
1272
|
-
throw new Error(validation.error);
|
|
1273
|
-
}
|
|
1274
|
-
this.uploadProgress.set(20);
|
|
1275
|
-
this.uploadMessage.set("Compression en cours...");
|
|
1276
|
-
this.forceEditorUpdate();
|
|
1277
|
-
// Petit délai pour permettre à l'utilisateur de voir la progression
|
|
1278
|
-
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1279
|
-
const result = await this.compressImage(file, options?.quality || 0.8, options?.maxWidth || 1920, options?.maxHeight || 1080);
|
|
1280
|
-
this.uploadProgress.set(80);
|
|
1281
|
-
// Si un handler personnalisé est défini, l'utiliser pour l'upload
|
|
1282
|
-
if (this.uploadHandler) {
|
|
1283
|
-
this.uploadMessage.set("Upload vers le serveur...");
|
|
1284
|
-
this.forceEditorUpdate();
|
|
1285
|
-
try {
|
|
1286
|
-
const handlerResponse = this.uploadHandler({
|
|
1287
|
-
file,
|
|
1288
|
-
width: result.width || 0,
|
|
1289
|
-
height: result.height || 0,
|
|
1290
|
-
type: result.type,
|
|
1291
|
-
base64: result.src,
|
|
1292
|
-
});
|
|
1293
|
-
// Convertir Observable en Promise si nécessaire
|
|
1294
|
-
const handlerResult = isObservable(handlerResponse)
|
|
1295
|
-
? await firstValueFrom(handlerResponse)
|
|
1296
|
-
: await handlerResponse;
|
|
1297
|
-
// Remplacer le src base64 par l'URL retournée par le handler
|
|
1298
|
-
result.src = handlerResult.src;
|
|
1299
|
-
// Appliquer les overrides optionnels du handler
|
|
1300
|
-
if (handlerResult.alt) {
|
|
1301
|
-
result.name = handlerResult.alt;
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
1304
|
-
catch (handlerError) {
|
|
1305
|
-
console.error("Erreur lors de l'upload personnalisé:", handlerError);
|
|
1306
|
-
throw handlerError;
|
|
1307
|
-
}
|
|
1308
|
-
}
|
|
1309
|
-
this.uploadMessage.set(actionMessage);
|
|
1310
|
-
this.forceEditorUpdate();
|
|
1311
|
-
// Petit délai pour l'action
|
|
1312
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1313
|
-
// Exécuter la stratégie d'insertion
|
|
1314
|
-
insertionStrategy(editor, result);
|
|
1315
|
-
// L'action est terminée, maintenant on peut cacher l'indicateur
|
|
1316
|
-
this.isUploading.set(false);
|
|
1317
|
-
this.uploadProgress.set(0);
|
|
1318
|
-
this.uploadMessage.set("");
|
|
1319
|
-
this.forceEditorUpdate();
|
|
1320
|
-
this.currentEditor = null;
|
|
1321
|
-
}
|
|
1322
|
-
catch (error) {
|
|
1323
|
-
this.isUploading.set(false);
|
|
1324
|
-
this.uploadProgress.set(0);
|
|
1325
|
-
this.uploadMessage.set("");
|
|
1326
|
-
this.forceEditorUpdate();
|
|
1327
|
-
this.currentEditor = null;
|
|
1328
|
-
console.error("Erreur lors de l'upload d'image:", error);
|
|
1329
|
-
throw error;
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
// Méthode unifiée pour uploader et insérer une image
|
|
1333
|
-
async uploadAndInsertImage(editor, file, options) {
|
|
1334
|
-
return this.uploadImageWithProgress(editor, file, (editor, result) => {
|
|
1335
|
-
this.insertImage(editor, {
|
|
1336
|
-
src: result.src,
|
|
1337
|
-
alt: result.name,
|
|
1338
|
-
title: `${result.name} (${result.width}×${result.height})`,
|
|
1339
|
-
width: result.width,
|
|
1340
|
-
height: result.height,
|
|
1341
|
-
});
|
|
1342
|
-
}, "Insertion dans l'éditeur...", options);
|
|
1343
|
-
}
|
|
1344
|
-
// Méthode pour forcer la mise à jour de l'éditeur
|
|
1345
|
-
forceEditorUpdate() {
|
|
1346
|
-
if (this.currentEditor) {
|
|
1347
|
-
// Déclencher une transaction vide pour forcer la mise à jour des décorations
|
|
1348
|
-
const { tr } = this.currentEditor.state;
|
|
1349
|
-
this.currentEditor.view.dispatch(tr);
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
// Méthode privée générique pour créer un sélecteur de fichier
|
|
1353
|
-
async selectFileAndProcess(editor, uploadMethod, options) {
|
|
1354
|
-
return new Promise((resolve, reject) => {
|
|
1355
|
-
const input = document.createElement("input");
|
|
1356
|
-
input.type = "file";
|
|
1357
|
-
input.accept = options?.accept || "image/*";
|
|
1358
|
-
input.style.display = "none";
|
|
1359
|
-
input.addEventListener("change", async (e) => {
|
|
1360
|
-
const file = e.target.files?.[0];
|
|
1361
|
-
if (file && file.type.startsWith("image/")) {
|
|
1362
|
-
try {
|
|
1363
|
-
await uploadMethod(editor, file, options);
|
|
1364
|
-
resolve();
|
|
1365
|
-
}
|
|
1366
|
-
catch (error) {
|
|
1367
|
-
reject(error);
|
|
1368
|
-
}
|
|
1369
|
-
}
|
|
1370
|
-
else {
|
|
1371
|
-
reject(new Error("Aucun fichier image sélectionné"));
|
|
1372
|
-
}
|
|
1373
|
-
document.body.removeChild(input);
|
|
1374
|
-
});
|
|
1375
|
-
input.addEventListener("cancel", () => {
|
|
1376
|
-
document.body.removeChild(input);
|
|
1377
|
-
reject(new Error("Sélection annulée"));
|
|
1378
|
-
});
|
|
1379
|
-
document.body.appendChild(input);
|
|
1380
|
-
input.click();
|
|
1381
|
-
});
|
|
1382
|
-
}
|
|
1383
|
-
// Méthode pour créer un sélecteur de fichier et uploader une image
|
|
1384
|
-
async selectAndUploadImage(editor, options) {
|
|
1385
|
-
return this.selectFileAndProcess(editor, this.uploadAndInsertImage.bind(this), options);
|
|
1386
|
-
}
|
|
1387
|
-
// Méthode pour sélectionner et remplacer une image existante
|
|
1388
|
-
async selectAndReplaceImage(editor, options) {
|
|
1389
|
-
return this.selectFileAndProcess(editor, this.uploadAndReplaceImage.bind(this), options);
|
|
1390
|
-
}
|
|
1391
|
-
// Méthode pour remplacer une image existante avec indicateur de progression
|
|
1392
|
-
async uploadAndReplaceImage(editor, file, options) {
|
|
1393
|
-
// Sauvegarder les attributs de l'image actuelle pour restauration en cas d'échec
|
|
1394
|
-
const currentImageAttrs = editor.getAttributes("resizableImage");
|
|
1395
|
-
const backupImage = { ...currentImageAttrs };
|
|
1396
|
-
try {
|
|
1397
|
-
// Supprimer visuellement l'ancienne image immédiatement
|
|
1398
|
-
editor.chain().focus().deleteSelection().run();
|
|
1399
|
-
// Stocker la référence à l'éditeur
|
|
1400
|
-
this.currentEditor = editor;
|
|
1401
|
-
this.isUploading.set(true);
|
|
1402
|
-
this.uploadProgress.set(0);
|
|
1403
|
-
this.uploadMessage.set("Validation du fichier...");
|
|
1404
|
-
this.forceEditorUpdate();
|
|
1405
|
-
// Validation
|
|
1406
|
-
const validation = this.validateImage(file);
|
|
1407
|
-
if (!validation.valid) {
|
|
1408
|
-
throw new Error(validation.error);
|
|
1409
|
-
}
|
|
1410
|
-
this.uploadProgress.set(20);
|
|
1411
|
-
this.uploadMessage.set("Compression en cours...");
|
|
1412
|
-
this.forceEditorUpdate();
|
|
1413
|
-
// Petit délai pour permettre à l'utilisateur de voir la progression
|
|
1414
|
-
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1415
|
-
const result = await this.compressImage(file, options?.quality || 0.8, options?.maxWidth || 1920, options?.maxHeight || 1080);
|
|
1416
|
-
this.uploadProgress.set(80);
|
|
1417
|
-
this.uploadMessage.set("Remplacement de l'image...");
|
|
1418
|
-
this.forceEditorUpdate();
|
|
1419
|
-
// Petit délai pour le remplacement
|
|
1420
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1421
|
-
// Insérer la nouvelle image à la position actuelle
|
|
1422
|
-
this.insertImage(editor, {
|
|
1423
|
-
src: result.src,
|
|
1424
|
-
alt: result.name,
|
|
1425
|
-
title: `${result.name} (${result.width}×${result.height})`,
|
|
1426
|
-
width: result.width,
|
|
1427
|
-
height: result.height,
|
|
1428
|
-
});
|
|
1429
|
-
// L'image est remplacée, maintenant on peut cacher l'indicateur
|
|
1430
|
-
this.isUploading.set(false);
|
|
1431
|
-
this.uploadProgress.set(0);
|
|
1432
|
-
this.uploadMessage.set("");
|
|
1433
|
-
this.forceEditorUpdate();
|
|
1434
|
-
this.currentEditor = null;
|
|
1435
|
-
}
|
|
1436
|
-
catch (error) {
|
|
1437
|
-
// En cas d'erreur, restaurer l'image originale
|
|
1438
|
-
if (backupImage["src"]) {
|
|
1439
|
-
this.insertImage(editor, {
|
|
1440
|
-
src: backupImage["src"],
|
|
1441
|
-
alt: backupImage["alt"],
|
|
1442
|
-
title: backupImage["title"],
|
|
1443
|
-
width: backupImage["width"],
|
|
1444
|
-
height: backupImage["height"],
|
|
1445
|
-
});
|
|
1446
|
-
}
|
|
1447
|
-
this.isUploading.set(false);
|
|
1448
|
-
this.uploadProgress.set(0);
|
|
1449
|
-
this.uploadMessage.set("");
|
|
1450
|
-
this.forceEditorUpdate();
|
|
1451
|
-
this.currentEditor = null;
|
|
1452
|
-
console.error("Erreur lors du remplacement d'image:", error);
|
|
1453
|
-
throw error;
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: ImageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1457
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: ImageService, providedIn: "root" }); }
|
|
1458
|
-
}
|
|
1459
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: ImageService, decorators: [{
|
|
1460
|
-
type: Injectable,
|
|
1461
|
-
args: [{
|
|
1462
|
-
providedIn: "root",
|
|
1463
|
-
}]
|
|
1464
|
-
}] });
|
|
1465
|
-
|
|
1466
|
-
class TiptapImageBubbleMenuComponent {
|
|
1467
|
-
constructor() {
|
|
1468
|
-
this.editor = input.required();
|
|
1469
|
-
this.config = input({
|
|
1470
|
-
changeImage: true,
|
|
1471
|
-
resizeSmall: true,
|
|
1472
|
-
resizeMedium: true,
|
|
1473
|
-
resizeLarge: true,
|
|
1474
|
-
resizeOriginal: true,
|
|
1475
|
-
deleteImage: true,
|
|
1476
|
-
separator: true,
|
|
1477
|
-
});
|
|
1478
|
-
this.tippyInstance = null;
|
|
1479
|
-
this.imageService = inject(ImageService);
|
|
1480
|
-
this.updateTimeout = null;
|
|
1481
|
-
this.imageBubbleMenuConfig = computed(() => ({
|
|
1482
|
-
changeImage: true,
|
|
1483
|
-
resizeSmall: true,
|
|
1484
|
-
resizeMedium: true,
|
|
1485
|
-
resizeLarge: true,
|
|
1486
|
-
resizeOriginal: true,
|
|
1487
|
-
deleteImage: true,
|
|
1488
|
-
separator: true,
|
|
1489
|
-
...this.config(),
|
|
1490
|
-
}));
|
|
1491
|
-
this.hasResizeButtons = computed(() => {
|
|
1492
|
-
const config = this.imageBubbleMenuConfig();
|
|
1493
|
-
return (config.resizeSmall ||
|
|
1494
|
-
config.resizeMedium ||
|
|
1495
|
-
config.resizeLarge ||
|
|
1496
|
-
config.resizeOriginal);
|
|
1497
|
-
});
|
|
1498
|
-
this.updateMenu = () => {
|
|
1499
|
-
// Debounce pour éviter les appels trop fréquents
|
|
1500
|
-
if (this.updateTimeout) {
|
|
1501
|
-
clearTimeout(this.updateTimeout);
|
|
1502
|
-
}
|
|
1503
|
-
this.updateTimeout = setTimeout(() => {
|
|
1504
|
-
const ed = this.editor();
|
|
1505
|
-
if (!ed)
|
|
1506
|
-
return;
|
|
1507
|
-
const isImageSelected = ed.isActive("resizableImage") || ed.isActive("image");
|
|
1508
|
-
const { from, to } = ed.state.selection;
|
|
1509
|
-
const hasTextSelection = from !== to;
|
|
1510
|
-
// Ne montrer le menu image que si :
|
|
1511
|
-
// - Une image est sélectionnée
|
|
1512
|
-
// - L'éditeur est éditable
|
|
1513
|
-
const shouldShow = isImageSelected && ed.isEditable;
|
|
1514
|
-
if (shouldShow) {
|
|
1515
|
-
this.showTippy();
|
|
1516
|
-
}
|
|
1517
|
-
else {
|
|
1518
|
-
this.hideTippy();
|
|
1519
|
-
}
|
|
1520
|
-
}, 10);
|
|
1521
|
-
};
|
|
1522
|
-
this.handleBlur = () => {
|
|
1523
|
-
// Masquer le menu quand l'éditeur perd le focus
|
|
1524
|
-
setTimeout(() => {
|
|
1525
|
-
this.hideTippy();
|
|
1526
|
-
}, 100);
|
|
1527
|
-
};
|
|
1528
|
-
effect(() => {
|
|
1529
|
-
const ed = this.editor();
|
|
1530
|
-
if (!ed)
|
|
1531
|
-
return;
|
|
1532
|
-
// Nettoyer les anciens listeners
|
|
1533
|
-
ed.off("selectionUpdate", this.updateMenu);
|
|
1534
|
-
ed.off("transaction", this.updateMenu);
|
|
1535
|
-
ed.off("focus", this.updateMenu);
|
|
1536
|
-
ed.off("blur", this.handleBlur);
|
|
1537
|
-
// Ajouter les nouveaux listeners
|
|
1538
|
-
ed.on("selectionUpdate", this.updateMenu);
|
|
1539
|
-
ed.on("transaction", this.updateMenu);
|
|
1540
|
-
ed.on("focus", this.updateMenu);
|
|
1541
|
-
ed.on("blur", this.handleBlur);
|
|
1542
|
-
// Ne pas appeler updateMenu() ici pour éviter l'affichage prématuré
|
|
1543
|
-
// Il sera appelé automatiquement quand l'éditeur sera prêt
|
|
1544
|
-
});
|
|
1545
|
-
}
|
|
1546
|
-
ngOnInit() {
|
|
1547
|
-
// Initialiser Tippy de manière synchrone après que le component soit ready
|
|
1548
|
-
this.initTippy();
|
|
1549
|
-
}
|
|
1550
|
-
ngOnDestroy() {
|
|
1551
|
-
const ed = this.editor();
|
|
1552
|
-
if (ed) {
|
|
1553
|
-
ed.off("selectionUpdate", this.updateMenu);
|
|
1554
|
-
ed.off("transaction", this.updateMenu);
|
|
1555
|
-
ed.off("focus", this.updateMenu);
|
|
1556
|
-
ed.off("blur", this.handleBlur);
|
|
1557
|
-
}
|
|
1558
|
-
// Nettoyer les timeouts
|
|
1559
|
-
if (this.updateTimeout) {
|
|
1560
|
-
clearTimeout(this.updateTimeout);
|
|
1561
|
-
}
|
|
1562
|
-
// Nettoyer Tippy
|
|
1563
|
-
if (this.tippyInstance) {
|
|
1564
|
-
this.tippyInstance.destroy();
|
|
1565
|
-
this.tippyInstance = null;
|
|
1566
|
-
}
|
|
1567
|
-
}
|
|
1568
|
-
initTippy() {
|
|
1569
|
-
// Attendre que l'élément soit disponible
|
|
1570
|
-
if (!this.menuRef?.nativeElement) {
|
|
1571
|
-
setTimeout(() => this.initTippy(), 50);
|
|
1572
|
-
return;
|
|
1573
|
-
}
|
|
1574
|
-
const menuElement = this.menuRef.nativeElement;
|
|
1575
|
-
// S'assurer qu'il n'y a pas déjà une instance
|
|
1576
|
-
if (this.tippyInstance) {
|
|
1577
|
-
this.tippyInstance.destroy();
|
|
1578
|
-
}
|
|
1579
|
-
// Créer l'instance Tippy
|
|
1580
|
-
this.tippyInstance = tippy(document.body, {
|
|
1581
|
-
content: menuElement,
|
|
1582
|
-
trigger: "manual",
|
|
1583
|
-
placement: "top-start",
|
|
1584
|
-
appendTo: () => document.body,
|
|
1585
|
-
interactive: true,
|
|
1586
|
-
arrow: false,
|
|
1587
|
-
offset: [0, 8],
|
|
1588
|
-
hideOnClick: false,
|
|
1589
|
-
onShow: (instance) => {
|
|
1590
|
-
// S'assurer que les autres menus sont fermés
|
|
1591
|
-
this.hideOtherMenus();
|
|
1592
|
-
},
|
|
1593
|
-
getReferenceClientRect: () => this.getImageRect(),
|
|
1594
|
-
// Améliorer le positionnement avec scroll
|
|
1595
|
-
popperOptions: {
|
|
1596
|
-
modifiers: [
|
|
1597
|
-
{
|
|
1598
|
-
name: "preventOverflow",
|
|
1599
|
-
options: {
|
|
1600
|
-
boundary: "viewport",
|
|
1601
|
-
padding: 8,
|
|
1602
|
-
},
|
|
1603
|
-
},
|
|
1604
|
-
{
|
|
1605
|
-
name: "flip",
|
|
1606
|
-
options: {
|
|
1607
|
-
fallbackPlacements: ["bottom-start", "top-end", "bottom-end"],
|
|
1608
|
-
},
|
|
1609
|
-
},
|
|
1610
|
-
],
|
|
1611
|
-
},
|
|
1612
|
-
});
|
|
1613
|
-
// Maintenant que Tippy est initialisé, faire un premier check
|
|
1614
|
-
this.updateMenu();
|
|
1615
|
-
}
|
|
1616
|
-
getImageRect() {
|
|
1617
|
-
const ed = this.editor();
|
|
1618
|
-
if (!ed)
|
|
1619
|
-
return new DOMRect(0, 0, 0, 0);
|
|
1620
|
-
// Trouver l'image sélectionnée dans le DOM
|
|
1621
|
-
const { from } = ed.state.selection;
|
|
1622
|
-
// Fonction pour trouver toutes les images dans l'éditeur
|
|
1623
|
-
const getAllImages = () => {
|
|
1624
|
-
const editorElement = ed.view.dom;
|
|
1625
|
-
return Array.from(editorElement.querySelectorAll("img"));
|
|
1626
|
-
};
|
|
1627
|
-
// Fonction pour trouver l'image à la position spécifique
|
|
1628
|
-
const findImageAtPosition = () => {
|
|
1629
|
-
const allImages = getAllImages();
|
|
1630
|
-
for (const img of allImages) {
|
|
1631
|
-
try {
|
|
1632
|
-
// Obtenir la position ProseMirror de cette image
|
|
1633
|
-
const imgPos = ed.view.posAtDOM(img, 0);
|
|
1634
|
-
// Vérifier si cette image correspond à la position sélectionnée
|
|
1635
|
-
if (Math.abs(imgPos - from) <= 1) {
|
|
1636
|
-
return img;
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1639
|
-
catch (error) {
|
|
1640
|
-
// Continuer si on ne peut pas obtenir la position de cette image
|
|
1641
|
-
continue;
|
|
1642
|
-
}
|
|
1643
|
-
}
|
|
1644
|
-
return null;
|
|
1645
|
-
};
|
|
1646
|
-
// Chercher l'image à la position exacte
|
|
1647
|
-
const imageElement = findImageAtPosition();
|
|
1648
|
-
if (imageElement) {
|
|
1649
|
-
return imageElement.getBoundingClientRect();
|
|
1650
|
-
}
|
|
1651
|
-
return new DOMRect(0, 0, 0, 0);
|
|
1652
|
-
}
|
|
1653
|
-
hideOtherMenus() {
|
|
1654
|
-
// Cette méthode peut être étendue pour fermer d'autres menus si nécessaire
|
|
1655
|
-
// Pour l'instant, elle sert de placeholder pour une future coordination entre menus
|
|
1656
|
-
}
|
|
1657
|
-
showTippy() {
|
|
1658
|
-
if (!this.tippyInstance)
|
|
1659
|
-
return;
|
|
1660
|
-
// Mettre à jour la position
|
|
1661
|
-
this.tippyInstance.setProps({
|
|
1662
|
-
getReferenceClientRect: () => this.getImageRect(),
|
|
1663
|
-
});
|
|
1664
|
-
this.tippyInstance.show();
|
|
1665
|
-
}
|
|
1666
|
-
hideTippy() {
|
|
1667
|
-
if (this.tippyInstance) {
|
|
1668
|
-
this.tippyInstance.hide();
|
|
1669
|
-
}
|
|
1670
|
-
}
|
|
1671
|
-
onCommand(command, event) {
|
|
1672
|
-
event.preventDefault();
|
|
1673
|
-
const ed = this.editor();
|
|
1674
|
-
if (!ed)
|
|
1675
|
-
return;
|
|
1676
|
-
switch (command) {
|
|
1677
|
-
case "changeImage":
|
|
1678
|
-
this.changeImage();
|
|
1679
|
-
break;
|
|
1680
|
-
case "resizeSmall":
|
|
1681
|
-
this.imageService.resizeImageToSmall(ed);
|
|
1682
|
-
break;
|
|
1683
|
-
case "resizeMedium":
|
|
1684
|
-
this.imageService.resizeImageToMedium(ed);
|
|
1685
|
-
break;
|
|
1686
|
-
case "resizeLarge":
|
|
1687
|
-
this.imageService.resizeImageToLarge(ed);
|
|
1688
|
-
break;
|
|
1689
|
-
case "resizeOriginal":
|
|
1690
|
-
this.imageService.resizeImageToOriginal(ed);
|
|
1691
|
-
break;
|
|
1692
|
-
case "deleteImage":
|
|
1693
|
-
this.deleteImage();
|
|
1694
|
-
break;
|
|
1695
|
-
}
|
|
1696
|
-
}
|
|
1697
|
-
async changeImage() {
|
|
1698
|
-
const ed = this.editor();
|
|
1699
|
-
if (!ed)
|
|
1700
|
-
return;
|
|
1701
|
-
try {
|
|
1702
|
-
// Utiliser la méthode spécifique pour remplacer une image existante
|
|
1703
|
-
await this.imageService.selectAndReplaceImage(ed, {
|
|
1704
|
-
quality: 0.8,
|
|
1705
|
-
maxWidth: 1920,
|
|
1706
|
-
maxHeight: 1080,
|
|
1707
|
-
accept: "image/*",
|
|
1708
|
-
});
|
|
1709
|
-
}
|
|
1710
|
-
catch (error) {
|
|
1711
|
-
console.error("Erreur lors du changement d'image:", error);
|
|
1712
|
-
}
|
|
1713
|
-
}
|
|
1714
|
-
deleteImage() {
|
|
1715
|
-
const ed = this.editor();
|
|
1716
|
-
if (ed) {
|
|
1717
|
-
ed.chain().focus().deleteSelection().run();
|
|
1718
|
-
}
|
|
1719
|
-
}
|
|
1720
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapImageBubbleMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1721
|
-
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: `
|
|
1722
|
-
<div #menuRef class="bubble-menu">
|
|
1723
|
-
@if (imageBubbleMenuConfig().changeImage) {
|
|
1724
|
-
<tiptap-button
|
|
1725
|
-
icon="drive_file_rename_outline"
|
|
1726
|
-
title="Changer l'image"
|
|
1727
|
-
(click)="onCommand('changeImage', $event)"
|
|
1728
|
-
></tiptap-button>
|
|
1729
|
-
} @if (imageBubbleMenuConfig().separator && hasResizeButtons()) {
|
|
1730
|
-
<tiptap-separator></tiptap-separator>
|
|
1731
|
-
} @if (imageBubbleMenuConfig().resizeSmall) {
|
|
1732
|
-
<tiptap-button
|
|
1733
|
-
icon="crop_square"
|
|
1734
|
-
iconSize="small"
|
|
1735
|
-
title="Petite (300×200)"
|
|
1736
|
-
(click)="onCommand('resizeSmall', $event)"
|
|
1737
|
-
></tiptap-button>
|
|
1738
|
-
} @if (imageBubbleMenuConfig().resizeMedium) {
|
|
1739
|
-
<tiptap-button
|
|
1740
|
-
icon="crop_square"
|
|
1741
|
-
iconSize="medium"
|
|
1742
|
-
title="Moyenne (500×350)"
|
|
1743
|
-
(click)="onCommand('resizeMedium', $event)"
|
|
1744
|
-
></tiptap-button>
|
|
1745
|
-
} @if (imageBubbleMenuConfig().resizeLarge) {
|
|
1746
|
-
<tiptap-button
|
|
1747
|
-
icon="crop_square"
|
|
1748
|
-
iconSize="large"
|
|
1749
|
-
title="Grande (800×600)"
|
|
1750
|
-
(click)="onCommand('resizeLarge', $event)"
|
|
1751
|
-
></tiptap-button>
|
|
1752
|
-
} @if (imageBubbleMenuConfig().resizeOriginal) {
|
|
1753
|
-
<tiptap-button
|
|
1754
|
-
icon="photo_size_select_actual"
|
|
1755
|
-
title="Taille originale"
|
|
1756
|
-
(click)="onCommand('resizeOriginal', $event)"
|
|
1757
|
-
></tiptap-button>
|
|
1758
|
-
} @if (imageBubbleMenuConfig().separator &&
|
|
1759
|
-
imageBubbleMenuConfig().deleteImage) {
|
|
1760
|
-
<tiptap-separator></tiptap-separator>
|
|
1761
|
-
} @if (imageBubbleMenuConfig().deleteImage) {
|
|
1762
|
-
<tiptap-button
|
|
1763
|
-
icon="delete"
|
|
1764
|
-
title="Supprimer l'image"
|
|
1765
|
-
variant="danger"
|
|
1766
|
-
(click)="onCommand('deleteImage', $event)"
|
|
1767
|
-
></tiptap-button>
|
|
1768
|
-
}
|
|
1769
|
-
</div>
|
|
1770
|
-
`, 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"] }] }); }
|
|
1771
|
-
}
|
|
1772
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapImageBubbleMenuComponent, decorators: [{
|
|
1773
|
-
type: Component,
|
|
1774
|
-
args: [{ selector: "tiptap-image-bubble-menu", standalone: true, imports: [TiptapButtonComponent, TiptapSeparatorComponent], template: `
|
|
1775
|
-
<div #menuRef class="bubble-menu">
|
|
1776
|
-
@if (imageBubbleMenuConfig().changeImage) {
|
|
1777
|
-
<tiptap-button
|
|
1778
|
-
icon="drive_file_rename_outline"
|
|
1779
|
-
title="Changer l'image"
|
|
1780
|
-
(click)="onCommand('changeImage', $event)"
|
|
1781
|
-
></tiptap-button>
|
|
1782
|
-
} @if (imageBubbleMenuConfig().separator && hasResizeButtons()) {
|
|
1783
|
-
<tiptap-separator></tiptap-separator>
|
|
1784
|
-
} @if (imageBubbleMenuConfig().resizeSmall) {
|
|
1785
|
-
<tiptap-button
|
|
1786
|
-
icon="crop_square"
|
|
1787
|
-
iconSize="small"
|
|
1788
|
-
title="Petite (300×200)"
|
|
1789
|
-
(click)="onCommand('resizeSmall', $event)"
|
|
1790
|
-
></tiptap-button>
|
|
1791
|
-
} @if (imageBubbleMenuConfig().resizeMedium) {
|
|
1792
|
-
<tiptap-button
|
|
1793
|
-
icon="crop_square"
|
|
1794
|
-
iconSize="medium"
|
|
1795
|
-
title="Moyenne (500×350)"
|
|
1796
|
-
(click)="onCommand('resizeMedium', $event)"
|
|
1797
|
-
></tiptap-button>
|
|
1798
|
-
} @if (imageBubbleMenuConfig().resizeLarge) {
|
|
1799
|
-
<tiptap-button
|
|
1800
|
-
icon="crop_square"
|
|
1801
|
-
iconSize="large"
|
|
1802
|
-
title="Grande (800×600)"
|
|
1803
|
-
(click)="onCommand('resizeLarge', $event)"
|
|
1804
|
-
></tiptap-button>
|
|
1805
|
-
} @if (imageBubbleMenuConfig().resizeOriginal) {
|
|
1806
|
-
<tiptap-button
|
|
1807
|
-
icon="photo_size_select_actual"
|
|
1808
|
-
title="Taille originale"
|
|
1809
|
-
(click)="onCommand('resizeOriginal', $event)"
|
|
1810
|
-
></tiptap-button>
|
|
1811
|
-
} @if (imageBubbleMenuConfig().separator &&
|
|
1812
|
-
imageBubbleMenuConfig().deleteImage) {
|
|
1813
|
-
<tiptap-separator></tiptap-separator>
|
|
1814
|
-
} @if (imageBubbleMenuConfig().deleteImage) {
|
|
1815
|
-
<tiptap-button
|
|
1816
|
-
icon="delete"
|
|
1817
|
-
title="Supprimer l'image"
|
|
1818
|
-
variant="danger"
|
|
1819
|
-
(click)="onCommand('deleteImage', $event)"
|
|
1820
|
-
></tiptap-button>
|
|
1821
|
-
}
|
|
1822
|
-
</div>
|
|
1823
|
-
` }]
|
|
1824
|
-
}], ctorParameters: () => [], propDecorators: { menuRef: [{
|
|
1825
|
-
type: ViewChild,
|
|
1826
|
-
args: ["menuRef", { static: false }]
|
|
1827
|
-
}] } });
|
|
1828
|
-
|
|
1829
745
|
const ENGLISH_TRANSLATIONS = {
|
|
1830
746
|
toolbar: {
|
|
1831
747
|
bold: "Bold",
|
|
@@ -1853,6 +769,7 @@ const ENGLISH_TRANSLATIONS = {
|
|
|
1853
769
|
undo: "Undo",
|
|
1854
770
|
redo: "Redo",
|
|
1855
771
|
clear: "Clear",
|
|
772
|
+
textColor: "Text Color",
|
|
1856
773
|
},
|
|
1857
774
|
bubbleMenu: {
|
|
1858
775
|
bold: "Bold",
|
|
@@ -1863,6 +780,7 @@ const ENGLISH_TRANSLATIONS = {
|
|
|
1863
780
|
superscript: "Superscript",
|
|
1864
781
|
subscript: "Subscript",
|
|
1865
782
|
highlight: "Highlight",
|
|
783
|
+
textColor: "Text Color",
|
|
1866
784
|
link: "Link",
|
|
1867
785
|
addLink: "Add Link",
|
|
1868
786
|
editLink: "Edit Link",
|
|
@@ -2000,6 +918,7 @@ const FRENCH_TRANSLATIONS = {
|
|
|
2000
918
|
undo: "Annuler",
|
|
2001
919
|
redo: "Refaire",
|
|
2002
920
|
clear: "Vider",
|
|
921
|
+
textColor: "Couleur texte",
|
|
2003
922
|
},
|
|
2004
923
|
bubbleMenu: {
|
|
2005
924
|
bold: "Gras",
|
|
@@ -2010,6 +929,7 @@ const FRENCH_TRANSLATIONS = {
|
|
|
2010
929
|
superscript: "Exposant",
|
|
2011
930
|
subscript: "Indice",
|
|
2012
931
|
highlight: "Surligner",
|
|
932
|
+
textColor: "Couleur texte",
|
|
2013
933
|
link: "Lien",
|
|
2014
934
|
addLink: "Ajouter un lien",
|
|
2015
935
|
editLink: "Modifier le lien",
|
|
@@ -2131,72 +1051,1564 @@ const FRENCH_TRANSLATIONS = {
|
|
|
2131
1051
|
};
|
|
2132
1052
|
class TiptapI18nService {
|
|
2133
1053
|
constructor() {
|
|
2134
|
-
this._currentLocale = signal("en");
|
|
2135
|
-
this._translations = signal({
|
|
2136
|
-
en: ENGLISH_TRANSLATIONS,
|
|
2137
|
-
fr: FRENCH_TRANSLATIONS,
|
|
1054
|
+
this._currentLocale = signal("en");
|
|
1055
|
+
this._translations = signal({
|
|
1056
|
+
en: ENGLISH_TRANSLATIONS,
|
|
1057
|
+
fr: FRENCH_TRANSLATIONS,
|
|
1058
|
+
});
|
|
1059
|
+
// Signaux publics
|
|
1060
|
+
this.currentLocale = this._currentLocale.asReadonly();
|
|
1061
|
+
this.translations = computed(() => this._translations()[this._currentLocale()]);
|
|
1062
|
+
// Méthodes de traduction rapides
|
|
1063
|
+
this.t = computed(() => this.translations());
|
|
1064
|
+
this.toolbar = computed(() => this.translations().toolbar);
|
|
1065
|
+
this.bubbleMenu = computed(() => this.translations().bubbleMenu);
|
|
1066
|
+
this.slashCommands = computed(() => this.translations().slashCommands);
|
|
1067
|
+
this.table = computed(() => this.translations().table);
|
|
1068
|
+
this.imageUpload = computed(() => this.translations().imageUpload);
|
|
1069
|
+
this.editor = computed(() => this.translations().editor);
|
|
1070
|
+
this.common = computed(() => this.translations().common);
|
|
1071
|
+
// Détecter automatiquement la langue du navigateur
|
|
1072
|
+
this.detectBrowserLanguage();
|
|
1073
|
+
}
|
|
1074
|
+
setLocale(locale) {
|
|
1075
|
+
this._currentLocale.set(locale);
|
|
1076
|
+
}
|
|
1077
|
+
autoDetectLocale() {
|
|
1078
|
+
this.detectBrowserLanguage();
|
|
1079
|
+
}
|
|
1080
|
+
getSupportedLocales() {
|
|
1081
|
+
return Object.keys(this._translations());
|
|
1082
|
+
}
|
|
1083
|
+
addTranslations(locale, translations) {
|
|
1084
|
+
this._translations.update((current) => ({
|
|
1085
|
+
...current,
|
|
1086
|
+
[locale]: {
|
|
1087
|
+
...current[locale],
|
|
1088
|
+
...translations,
|
|
1089
|
+
},
|
|
1090
|
+
}));
|
|
1091
|
+
}
|
|
1092
|
+
detectBrowserLanguage() {
|
|
1093
|
+
const browserLang = navigator.language.toLowerCase();
|
|
1094
|
+
if (browserLang.startsWith("fr")) {
|
|
1095
|
+
this._currentLocale.set("fr");
|
|
1096
|
+
}
|
|
1097
|
+
else {
|
|
1098
|
+
this._currentLocale.set("en");
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
// Méthodes utilitaires pour les composants
|
|
1102
|
+
getToolbarTitle(key) {
|
|
1103
|
+
return this.translations().toolbar[key];
|
|
1104
|
+
}
|
|
1105
|
+
getBubbleMenuTitle(key) {
|
|
1106
|
+
return this.translations().bubbleMenu[key];
|
|
1107
|
+
}
|
|
1108
|
+
getSlashCommand(key) {
|
|
1109
|
+
return this.translations().slashCommands[key];
|
|
1110
|
+
}
|
|
1111
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapI18nService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1112
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapI18nService, providedIn: "root" }); }
|
|
1113
|
+
}
|
|
1114
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapI18nService, decorators: [{
|
|
1115
|
+
type: Injectable,
|
|
1116
|
+
args: [{
|
|
1117
|
+
providedIn: "root",
|
|
1118
|
+
}]
|
|
1119
|
+
}], ctorParameters: () => [] });
|
|
1120
|
+
|
|
1121
|
+
class TiptapTextColorPickerComponent {
|
|
1122
|
+
constructor() {
|
|
1123
|
+
this.editor = input.required();
|
|
1124
|
+
this.interactionChange = output();
|
|
1125
|
+
this.requestUpdate = output();
|
|
1126
|
+
this.colorInputRef = viewChild("colorInput");
|
|
1127
|
+
this.colorPickerSvc = inject(ColorPickerService);
|
|
1128
|
+
this.i18nService = inject(TiptapI18nService);
|
|
1129
|
+
this.t = this.i18nService.toolbar;
|
|
1130
|
+
this.previewColor = signal(null);
|
|
1131
|
+
this.isPicking = signal(false);
|
|
1132
|
+
this.editorChange = signal(0);
|
|
1133
|
+
this.currentColor = computed(() => {
|
|
1134
|
+
this.editorChange();
|
|
1135
|
+
return (this.previewColor() ?? this.colorPickerSvc.getCurrentColor(this.editor()));
|
|
1136
|
+
});
|
|
1137
|
+
this.hasColorApplied = computed(() => {
|
|
1138
|
+
this.editorChange();
|
|
1139
|
+
return this.previewColor()
|
|
1140
|
+
? true
|
|
1141
|
+
: this.colorPickerSvc.hasColorApplied(this.editor());
|
|
1142
|
+
});
|
|
1143
|
+
this._syncEffect = void effect(() => {
|
|
1144
|
+
const el = this.colorInputRef()?.nativeElement;
|
|
1145
|
+
if (!el)
|
|
1146
|
+
return;
|
|
1147
|
+
el.value = this.currentColor();
|
|
1148
|
+
});
|
|
1149
|
+
effect(() => {
|
|
1150
|
+
const ed = this.editor();
|
|
1151
|
+
if (!ed)
|
|
1152
|
+
return;
|
|
1153
|
+
const update = () => this.notifyEditorChange();
|
|
1154
|
+
ed.on("transaction", update);
|
|
1155
|
+
ed.on("selectionUpdate", update);
|
|
1156
|
+
ed.on("focus", update);
|
|
1157
|
+
return () => {
|
|
1158
|
+
ed.off("transaction", update);
|
|
1159
|
+
ed.off("selectionUpdate", update);
|
|
1160
|
+
ed.off("focus", update);
|
|
1161
|
+
};
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Notify Angular that the editor state should be re-read.
|
|
1166
|
+
*/
|
|
1167
|
+
notifyEditorChange() {
|
|
1168
|
+
this.editorChange.update((v) => v + 1);
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Keep the native <input type="color"> in sync with selection changes.
|
|
1172
|
+
*/
|
|
1173
|
+
syncColorInputValue() {
|
|
1174
|
+
this.previewColor.set(null);
|
|
1175
|
+
this.isPicking.set(false);
|
|
1176
|
+
// Reason: force recomputation from editor selection when bubble menu re-opens.
|
|
1177
|
+
this.notifyEditorChange();
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Programmatically click the hidden color input.
|
|
1181
|
+
*/
|
|
1182
|
+
triggerPicker() {
|
|
1183
|
+
this.colorInputRef()?.nativeElement.click();
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Preserve selection while interacting with native color input.
|
|
1187
|
+
*/
|
|
1188
|
+
onColorMouseDown(event) {
|
|
1189
|
+
event.stopPropagation();
|
|
1190
|
+
this.colorPickerSvc.captureSelection(this.editor());
|
|
1191
|
+
this.isPicking.set(true);
|
|
1192
|
+
this.interactionChange.emit(true);
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Called when the native color picker is closed.
|
|
1196
|
+
*/
|
|
1197
|
+
onColorPickerClose() {
|
|
1198
|
+
this.previewColor.set(null);
|
|
1199
|
+
this.isPicking.set(false);
|
|
1200
|
+
// Commit the final color to history
|
|
1201
|
+
const inputEl = this.colorInputRef()?.nativeElement;
|
|
1202
|
+
if (inputEl) {
|
|
1203
|
+
this.colorPickerSvc.applyColor(this.editor(), inputEl.value, {
|
|
1204
|
+
addToHistory: true,
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
this.colorPickerSvc.done();
|
|
1208
|
+
this.interactionChange.emit(false);
|
|
1209
|
+
this.requestUpdate.emit();
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* Apply selected color.
|
|
1213
|
+
*/
|
|
1214
|
+
onColorInput(event) {
|
|
1215
|
+
const inputEl = event.target;
|
|
1216
|
+
const color = inputEl.value;
|
|
1217
|
+
// Update the UI immediately while the user drags in the native picker.
|
|
1218
|
+
this.previewColor.set(this.colorPickerSvc.normalizeColor(color));
|
|
1219
|
+
// Live preview WITHOUT history pollution
|
|
1220
|
+
this.colorPickerSvc.applyColor(this.editor(), color, {
|
|
1221
|
+
addToHistory: false,
|
|
1222
|
+
});
|
|
1223
|
+
this.requestUpdate.emit();
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* Prevent opening the native picker when clicking the clear badge.
|
|
1227
|
+
*/
|
|
1228
|
+
onClearBadgeMouseDown(event) {
|
|
1229
|
+
event.preventDefault();
|
|
1230
|
+
event.stopPropagation();
|
|
1231
|
+
}
|
|
1232
|
+
/**
|
|
1233
|
+
* Clear color via the badge.
|
|
1234
|
+
*/
|
|
1235
|
+
onClearBadgeClick(event) {
|
|
1236
|
+
event.preventDefault();
|
|
1237
|
+
event.stopPropagation();
|
|
1238
|
+
this.previewColor.set(null);
|
|
1239
|
+
this.isPicking.set(false);
|
|
1240
|
+
this.colorPickerSvc.unsetColor(this.editor());
|
|
1241
|
+
this.requestUpdate.emit();
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Called when the color picker is done interacting.
|
|
1245
|
+
*/
|
|
1246
|
+
done() {
|
|
1247
|
+
this.colorPickerSvc.done();
|
|
1248
|
+
}
|
|
1249
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapTextColorPickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1250
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.0", type: TiptapTextColorPickerComponent, isStandalone: true, selector: "tiptap-text-color-picker", inputs: { editor: { classPropertyName: "editor", publicName: "editor", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { interactionChange: "interactionChange", requestUpdate: "requestUpdate" }, viewQueries: [{ propertyName: "colorInputRef", first: true, predicate: ["colorInput"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
1251
|
+
<div class="text-color-picker-container">
|
|
1252
|
+
<tiptap-button
|
|
1253
|
+
icon="format_color_text"
|
|
1254
|
+
[title]="t().textColor"
|
|
1255
|
+
[color]="hasColorApplied() ? currentColor() : undefined"
|
|
1256
|
+
(onClick)="triggerPicker()"
|
|
1257
|
+
>
|
|
1258
|
+
<input
|
|
1259
|
+
#colorInput
|
|
1260
|
+
type="color"
|
|
1261
|
+
[value]="currentColor()"
|
|
1262
|
+
(mousedown)="onColorMouseDown($event)"
|
|
1263
|
+
(input)="onColorInput($event)"
|
|
1264
|
+
(change)="onColorPickerClose()"
|
|
1265
|
+
(blur)="onColorPickerClose()"
|
|
1266
|
+
/>
|
|
1267
|
+
</tiptap-button>
|
|
1268
|
+
|
|
1269
|
+
@if (hasColorApplied()) {
|
|
1270
|
+
<button
|
|
1271
|
+
class="btn-clear-badge"
|
|
1272
|
+
type="button"
|
|
1273
|
+
[title]="t().clear"
|
|
1274
|
+
(mousedown)="onClearBadgeMouseDown($event)"
|
|
1275
|
+
(click)="onClearBadgeClick($event)"
|
|
1276
|
+
>
|
|
1277
|
+
<span class="material-symbols-outlined">close</span>
|
|
1278
|
+
</button>
|
|
1279
|
+
}
|
|
1280
|
+
</div>
|
|
1281
|
+
`, isInline: true, styles: [".text-color-picker-container{position:relative;display:inline-flex;align-items:center}.text-color-picker-container tiptap-button{position:relative}.btn-clear-badge{position:absolute;top:-4px;right:-4px;width:14px;height:14px;padding:0;border:none;border-radius:999px;background:#0f172abf;color:#fff;display:flex;align-items:center;justify-content:center;z-index:10;opacity:0;pointer-events:none;transition:opacity .12s ease}.text-color-picker-container:hover .btn-clear-badge{opacity:1;pointer-events:auto}.btn-clear-badge .material-symbols-outlined{font-size:10px;line-height:1}input[type=color]{position:absolute;inset:0;opacity:0;width:100%;height:100%;cursor:pointer;z-index:5}\n"], dependencies: [{ kind: "component", type: TiptapButtonComponent, selector: "tiptap-button", inputs: ["icon", "title", "active", "disabled", "color", "variant", "size", "iconSize"], outputs: ["onClick"] }] }); }
|
|
1282
|
+
}
|
|
1283
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapTextColorPickerComponent, decorators: [{
|
|
1284
|
+
type: Component,
|
|
1285
|
+
args: [{ selector: "tiptap-text-color-picker", standalone: true, imports: [TiptapButtonComponent], template: `
|
|
1286
|
+
<div class="text-color-picker-container">
|
|
1287
|
+
<tiptap-button
|
|
1288
|
+
icon="format_color_text"
|
|
1289
|
+
[title]="t().textColor"
|
|
1290
|
+
[color]="hasColorApplied() ? currentColor() : undefined"
|
|
1291
|
+
(onClick)="triggerPicker()"
|
|
1292
|
+
>
|
|
1293
|
+
<input
|
|
1294
|
+
#colorInput
|
|
1295
|
+
type="color"
|
|
1296
|
+
[value]="currentColor()"
|
|
1297
|
+
(mousedown)="onColorMouseDown($event)"
|
|
1298
|
+
(input)="onColorInput($event)"
|
|
1299
|
+
(change)="onColorPickerClose()"
|
|
1300
|
+
(blur)="onColorPickerClose()"
|
|
1301
|
+
/>
|
|
1302
|
+
</tiptap-button>
|
|
1303
|
+
|
|
1304
|
+
@if (hasColorApplied()) {
|
|
1305
|
+
<button
|
|
1306
|
+
class="btn-clear-badge"
|
|
1307
|
+
type="button"
|
|
1308
|
+
[title]="t().clear"
|
|
1309
|
+
(mousedown)="onClearBadgeMouseDown($event)"
|
|
1310
|
+
(click)="onClearBadgeClick($event)"
|
|
1311
|
+
>
|
|
1312
|
+
<span class="material-symbols-outlined">close</span>
|
|
1313
|
+
</button>
|
|
1314
|
+
}
|
|
1315
|
+
</div>
|
|
1316
|
+
`, styles: [".text-color-picker-container{position:relative;display:inline-flex;align-items:center}.text-color-picker-container tiptap-button{position:relative}.btn-clear-badge{position:absolute;top:-4px;right:-4px;width:14px;height:14px;padding:0;border:none;border-radius:999px;background:#0f172abf;color:#fff;display:flex;align-items:center;justify-content:center;z-index:10;opacity:0;pointer-events:none;transition:opacity .12s ease}.text-color-picker-container:hover .btn-clear-badge{opacity:1;pointer-events:auto}.btn-clear-badge .material-symbols-outlined{font-size:10px;line-height:1}input[type=color]{position:absolute;inset:0;opacity:0;width:100%;height:100%;cursor:pointer;z-index:5}\n"] }]
|
|
1317
|
+
}], ctorParameters: () => [] });
|
|
1318
|
+
|
|
1319
|
+
class TiptapBubbleMenuComponent {
|
|
1320
|
+
/**
|
|
1321
|
+
* Keep bubble menu visible while the native color picker steals focus.
|
|
1322
|
+
*/
|
|
1323
|
+
onColorPickerInteractionChange(isInteracting) {
|
|
1324
|
+
this.isColorPickerInteracting = isInteracting;
|
|
1325
|
+
}
|
|
1326
|
+
// Effect comme propriété de classe pour éviter l'erreur d'injection context
|
|
1327
|
+
constructor() {
|
|
1328
|
+
this.editor = input.required();
|
|
1329
|
+
this.config = input({
|
|
1330
|
+
bold: true,
|
|
1331
|
+
italic: true,
|
|
1332
|
+
underline: true,
|
|
1333
|
+
strike: true,
|
|
1334
|
+
code: true,
|
|
1335
|
+
superscript: false,
|
|
1336
|
+
subscript: false,
|
|
1337
|
+
highlight: true,
|
|
1338
|
+
textColor: false,
|
|
1339
|
+
link: true,
|
|
1340
|
+
separator: true,
|
|
1341
|
+
});
|
|
1342
|
+
this.tippyInstance = null;
|
|
1343
|
+
this.updateTimeout = null;
|
|
1344
|
+
this.isColorPickerInteracting = false;
|
|
1345
|
+
this.bubbleMenuConfig = computed(() => ({
|
|
1346
|
+
bold: true,
|
|
1347
|
+
italic: true,
|
|
1348
|
+
underline: true,
|
|
1349
|
+
strike: true,
|
|
1350
|
+
code: true,
|
|
1351
|
+
superscript: false,
|
|
1352
|
+
subscript: false,
|
|
1353
|
+
highlight: true,
|
|
1354
|
+
textColor: false,
|
|
1355
|
+
link: true,
|
|
1356
|
+
separator: true,
|
|
1357
|
+
...this.config(),
|
|
1358
|
+
}));
|
|
1359
|
+
this.i18nService = inject(TiptapI18nService);
|
|
1360
|
+
this.t = this.i18nService.bubbleMenu;
|
|
1361
|
+
this.updateMenu = () => {
|
|
1362
|
+
// Debounce pour éviter les appels trop fréquents
|
|
1363
|
+
if (this.updateTimeout) {
|
|
1364
|
+
clearTimeout(this.updateTimeout);
|
|
1365
|
+
}
|
|
1366
|
+
this.updateTimeout = setTimeout(() => {
|
|
1367
|
+
const ed = this.editor();
|
|
1368
|
+
if (!ed)
|
|
1369
|
+
return;
|
|
1370
|
+
if (!this.isColorPickerInteracting && this.textColorPicker) {
|
|
1371
|
+
this.textColorPicker.done();
|
|
1372
|
+
}
|
|
1373
|
+
const { selection } = ed.state;
|
|
1374
|
+
const { from, to } = selection;
|
|
1375
|
+
const hasTextSelection = from !== to && !(selection instanceof CellSelection);
|
|
1376
|
+
const isImageSelected = ed.isActive("image") || ed.isActive("resizableImage");
|
|
1377
|
+
const isTableCellSelected = ed.isActive("tableCell") || ed.isActive("tableHeader");
|
|
1378
|
+
const hasCellSelection = selection instanceof CellSelection;
|
|
1379
|
+
// Ne montrer le menu texte que si :
|
|
1380
|
+
// - Il y a une sélection de texte (pas une sélection de cellules multiples)
|
|
1381
|
+
// - Aucune image n'est sélectionnée (priorité aux images)
|
|
1382
|
+
// - Ce n'est pas une sélection de cellules multiples (CellSelection)
|
|
1383
|
+
// - L'éditeur est éditable
|
|
1384
|
+
// Note: Le texte dans une cellule est autorisé (isTableCellSelected peut être true)
|
|
1385
|
+
const shouldShow = hasTextSelection &&
|
|
1386
|
+
!isImageSelected &&
|
|
1387
|
+
!hasCellSelection &&
|
|
1388
|
+
ed.isEditable;
|
|
1389
|
+
if (shouldShow) {
|
|
1390
|
+
this.showTippy();
|
|
1391
|
+
}
|
|
1392
|
+
else {
|
|
1393
|
+
if (!this.isColorPickerInteracting) {
|
|
1394
|
+
this.hideTippy();
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
}, 10);
|
|
1398
|
+
};
|
|
1399
|
+
this.handleBlur = () => {
|
|
1400
|
+
// Masquer le menu quand l'éditeur perd le focus
|
|
1401
|
+
setTimeout(() => {
|
|
1402
|
+
if (!this.isColorPickerInteracting && this.textColorPicker) {
|
|
1403
|
+
this.textColorPicker.done();
|
|
1404
|
+
this.hideTippy();
|
|
1405
|
+
}
|
|
1406
|
+
}, 100);
|
|
1407
|
+
};
|
|
1408
|
+
effect(() => {
|
|
1409
|
+
const ed = this.editor();
|
|
1410
|
+
if (!ed)
|
|
1411
|
+
return;
|
|
1412
|
+
// Nettoyer les anciens listeners
|
|
1413
|
+
ed.off("selectionUpdate", this.updateMenu);
|
|
1414
|
+
ed.off("transaction", this.updateMenu);
|
|
1415
|
+
ed.off("focus", this.updateMenu);
|
|
1416
|
+
ed.off("blur", this.handleBlur);
|
|
1417
|
+
// Ajouter les nouveaux listeners
|
|
1418
|
+
ed.on("selectionUpdate", this.updateMenu);
|
|
1419
|
+
ed.on("transaction", this.updateMenu);
|
|
1420
|
+
ed.on("focus", this.updateMenu);
|
|
1421
|
+
ed.on("blur", this.handleBlur);
|
|
1422
|
+
// Ne pas appeler updateMenu() ici pour éviter l'affichage prématuré
|
|
1423
|
+
// Il sera appelé automatiquement quand l'éditeur sera prêt
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
ngOnInit() {
|
|
1427
|
+
// Initialiser Tippy de manière synchrone après que le component soit ready
|
|
1428
|
+
this.initTippy();
|
|
1429
|
+
}
|
|
1430
|
+
ngOnDestroy() {
|
|
1431
|
+
const ed = this.editor();
|
|
1432
|
+
if (ed) {
|
|
1433
|
+
ed.off("selectionUpdate", this.updateMenu);
|
|
1434
|
+
ed.off("transaction", this.updateMenu);
|
|
1435
|
+
ed.off("focus", this.updateMenu);
|
|
1436
|
+
ed.off("blur", this.handleBlur);
|
|
1437
|
+
}
|
|
1438
|
+
// Nettoyer les timeouts
|
|
1439
|
+
if (this.updateTimeout) {
|
|
1440
|
+
clearTimeout(this.updateTimeout);
|
|
1441
|
+
}
|
|
1442
|
+
// Nettoyer Tippy
|
|
1443
|
+
if (this.tippyInstance) {
|
|
1444
|
+
this.tippyInstance.destroy();
|
|
1445
|
+
this.tippyInstance = null;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
initTippy() {
|
|
1449
|
+
// Attendre que l'élément soit disponible
|
|
1450
|
+
if (!this.menuRef?.nativeElement) {
|
|
1451
|
+
setTimeout(() => this.initTippy(), 50);
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
const menuElement = this.menuRef.nativeElement;
|
|
1455
|
+
// S'assurer qu'il n'y a pas déjà une instance
|
|
1456
|
+
if (this.tippyInstance) {
|
|
1457
|
+
this.tippyInstance.destroy();
|
|
1458
|
+
}
|
|
1459
|
+
// Créer l'instance Tippy
|
|
1460
|
+
this.tippyInstance = tippy(document.body, {
|
|
1461
|
+
content: menuElement,
|
|
1462
|
+
trigger: "manual",
|
|
1463
|
+
placement: "top-start",
|
|
1464
|
+
appendTo: () => document.body,
|
|
1465
|
+
interactive: true,
|
|
1466
|
+
arrow: false,
|
|
1467
|
+
offset: [0, 8],
|
|
1468
|
+
hideOnClick: false,
|
|
1469
|
+
onShow: (instance) => {
|
|
1470
|
+
// S'assurer que les autres menus sont fermés
|
|
1471
|
+
this.hideOtherMenus();
|
|
1472
|
+
},
|
|
1473
|
+
getReferenceClientRect: () => this.getSelectionRect(),
|
|
1474
|
+
// Améliorer le positionnement avec scroll
|
|
1475
|
+
popperOptions: {
|
|
1476
|
+
modifiers: [
|
|
1477
|
+
{
|
|
1478
|
+
name: "preventOverflow",
|
|
1479
|
+
options: {
|
|
1480
|
+
boundary: "viewport",
|
|
1481
|
+
padding: 8,
|
|
1482
|
+
},
|
|
1483
|
+
},
|
|
1484
|
+
{
|
|
1485
|
+
name: "flip",
|
|
1486
|
+
options: {
|
|
1487
|
+
fallbackPlacements: ["bottom-start", "top-end", "bottom-end"],
|
|
1488
|
+
},
|
|
1489
|
+
},
|
|
1490
|
+
],
|
|
1491
|
+
},
|
|
1492
|
+
});
|
|
1493
|
+
// Maintenant que Tippy est initialisé, faire un premier check
|
|
1494
|
+
this.updateMenu();
|
|
1495
|
+
}
|
|
1496
|
+
getSelectionRect() {
|
|
1497
|
+
const ed = this.editor();
|
|
1498
|
+
if (!ed)
|
|
1499
|
+
return new DOMRect(0, 0, 0, 0);
|
|
1500
|
+
const { from, to } = ed.state.selection;
|
|
1501
|
+
if (from === to)
|
|
1502
|
+
return new DOMRect(0, 0, 0, 0);
|
|
1503
|
+
// 1. Try native selection for multi-line accuracy
|
|
1504
|
+
const selection = window.getSelection();
|
|
1505
|
+
if (selection && selection.rangeCount > 0) {
|
|
1506
|
+
const range = selection.getRangeAt(0);
|
|
1507
|
+
const rect = range.getBoundingClientRect();
|
|
1508
|
+
// Ensure the rect is valid and belongs to the editor
|
|
1509
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
1510
|
+
return rect;
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
// 2. Fallback to Tiptap coordinates for precision (single line / edge cases)
|
|
1514
|
+
const start = ed.view.coordsAtPos(from);
|
|
1515
|
+
const end = ed.view.coordsAtPos(to);
|
|
1516
|
+
const top = Math.min(start.top, end.top);
|
|
1517
|
+
const bottom = Math.max(start.bottom, end.bottom);
|
|
1518
|
+
const left = Math.min(start.left, end.left);
|
|
1519
|
+
const right = Math.max(start.right, end.right);
|
|
1520
|
+
return new DOMRect(left, top, right - left, bottom - top);
|
|
1521
|
+
}
|
|
1522
|
+
hideOtherMenus() {
|
|
1523
|
+
// Cette méthode peut être étendue pour fermer d'autres menus si nécessaire
|
|
1524
|
+
// Pour l'instant, elle sert de placeholder pour une future coordination entre menus
|
|
1525
|
+
}
|
|
1526
|
+
showTippy() {
|
|
1527
|
+
if (!this.tippyInstance)
|
|
1528
|
+
return;
|
|
1529
|
+
// Mettre à jour la position
|
|
1530
|
+
this.tippyInstance.setProps({
|
|
1531
|
+
getReferenceClientRect: () => this.getSelectionRect(),
|
|
1532
|
+
});
|
|
1533
|
+
this.tippyInstance.show();
|
|
1534
|
+
this.textColorPicker?.syncColorInputValue();
|
|
1535
|
+
}
|
|
1536
|
+
hideTippy() {
|
|
1537
|
+
if (this.tippyInstance) {
|
|
1538
|
+
this.tippyInstance.hide();
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
isActive(mark) {
|
|
1542
|
+
const ed = this.editor();
|
|
1543
|
+
return ed?.isActive(mark) || false;
|
|
1544
|
+
}
|
|
1545
|
+
onCommand(command, event) {
|
|
1546
|
+
event.preventDefault();
|
|
1547
|
+
const ed = this.editor();
|
|
1548
|
+
if (!ed)
|
|
1549
|
+
return;
|
|
1550
|
+
switch (command) {
|
|
1551
|
+
case "bold":
|
|
1552
|
+
ed.chain().focus().toggleBold().run();
|
|
1553
|
+
break;
|
|
1554
|
+
case "italic":
|
|
1555
|
+
ed.chain().focus().toggleItalic().run();
|
|
1556
|
+
break;
|
|
1557
|
+
case "underline":
|
|
1558
|
+
ed.chain().focus().toggleUnderline().run();
|
|
1559
|
+
break;
|
|
1560
|
+
case "strike":
|
|
1561
|
+
ed.chain().focus().toggleStrike().run();
|
|
1562
|
+
break;
|
|
1563
|
+
case "code":
|
|
1564
|
+
ed.chain().focus().toggleCode().run();
|
|
1565
|
+
break;
|
|
1566
|
+
case "superscript":
|
|
1567
|
+
ed.chain().focus().toggleSuperscript().run();
|
|
1568
|
+
break;
|
|
1569
|
+
case "subscript":
|
|
1570
|
+
ed.chain().focus().toggleSubscript().run();
|
|
1571
|
+
break;
|
|
1572
|
+
case "highlight":
|
|
1573
|
+
ed.chain().focus().toggleHighlight().run();
|
|
1574
|
+
break;
|
|
1575
|
+
case "link":
|
|
1576
|
+
const href = window.prompt("URL du lien:");
|
|
1577
|
+
if (href) {
|
|
1578
|
+
ed.chain().focus().toggleLink({ href }).run();
|
|
1579
|
+
}
|
|
1580
|
+
break;
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapBubbleMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1584
|
+
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 }, { propertyName: "textColorPicker", first: true, predicate: ["textColorPicker"], descendants: true }], ngImport: i0, template: `
|
|
1585
|
+
<div #menuRef class="bubble-menu">
|
|
1586
|
+
@if (bubbleMenuConfig().bold) {
|
|
1587
|
+
<tiptap-button
|
|
1588
|
+
icon="format_bold"
|
|
1589
|
+
[title]="t().bold"
|
|
1590
|
+
[active]="isActive('bold')"
|
|
1591
|
+
(click)="onCommand('bold', $event)"
|
|
1592
|
+
></tiptap-button>
|
|
1593
|
+
} @if (bubbleMenuConfig().italic) {
|
|
1594
|
+
<tiptap-button
|
|
1595
|
+
icon="format_italic"
|
|
1596
|
+
[title]="t().italic"
|
|
1597
|
+
[active]="isActive('italic')"
|
|
1598
|
+
(click)="onCommand('italic', $event)"
|
|
1599
|
+
></tiptap-button>
|
|
1600
|
+
} @if (bubbleMenuConfig().underline) {
|
|
1601
|
+
<tiptap-button
|
|
1602
|
+
icon="format_underlined"
|
|
1603
|
+
[title]="t().underline"
|
|
1604
|
+
[active]="isActive('underline')"
|
|
1605
|
+
(click)="onCommand('underline', $event)"
|
|
1606
|
+
></tiptap-button>
|
|
1607
|
+
} @if (bubbleMenuConfig().strike) {
|
|
1608
|
+
<tiptap-button
|
|
1609
|
+
icon="strikethrough_s"
|
|
1610
|
+
[title]="t().strike"
|
|
1611
|
+
[active]="isActive('strike')"
|
|
1612
|
+
(click)="onCommand('strike', $event)"
|
|
1613
|
+
></tiptap-button>
|
|
1614
|
+
} @if (bubbleMenuConfig().superscript) {
|
|
1615
|
+
<tiptap-button
|
|
1616
|
+
icon="superscript"
|
|
1617
|
+
[title]="t().superscript"
|
|
1618
|
+
[active]="isActive('superscript')"
|
|
1619
|
+
(click)="onCommand('superscript', $event)"
|
|
1620
|
+
></tiptap-button>
|
|
1621
|
+
} @if (bubbleMenuConfig().subscript) {
|
|
1622
|
+
<tiptap-button
|
|
1623
|
+
icon="subscript"
|
|
1624
|
+
[title]="t().subscript"
|
|
1625
|
+
[active]="isActive('subscript')"
|
|
1626
|
+
(click)="onCommand('subscript', $event)"
|
|
1627
|
+
></tiptap-button>
|
|
1628
|
+
} @if (bubbleMenuConfig().highlight) {
|
|
1629
|
+
<tiptap-button
|
|
1630
|
+
icon="highlight"
|
|
1631
|
+
[title]="t().highlight"
|
|
1632
|
+
[active]="isActive('highlight')"
|
|
1633
|
+
(click)="onCommand('highlight', $event)"
|
|
1634
|
+
></tiptap-button>
|
|
1635
|
+
} @if (bubbleMenuConfig().textColor) {
|
|
1636
|
+
<tiptap-text-color-picker
|
|
1637
|
+
#textColorPicker
|
|
1638
|
+
[editor]="editor()"
|
|
1639
|
+
(interactionChange)="onColorPickerInteractionChange($event)"
|
|
1640
|
+
(requestUpdate)="updateMenu()"
|
|
1641
|
+
/>
|
|
1642
|
+
} @if (bubbleMenuConfig().separator && (bubbleMenuConfig().code ||
|
|
1643
|
+
bubbleMenuConfig().link)) {
|
|
1644
|
+
<div class="tiptap-separator"></div>
|
|
1645
|
+
} @if (bubbleMenuConfig().code) {
|
|
1646
|
+
<tiptap-button
|
|
1647
|
+
icon="code"
|
|
1648
|
+
[title]="t().code"
|
|
1649
|
+
[active]="isActive('code')"
|
|
1650
|
+
(click)="onCommand('code', $event)"
|
|
1651
|
+
></tiptap-button>
|
|
1652
|
+
} @if (bubbleMenuConfig().link) {
|
|
1653
|
+
<tiptap-button
|
|
1654
|
+
icon="link"
|
|
1655
|
+
[title]="t().link"
|
|
1656
|
+
[active]="isActive('link')"
|
|
1657
|
+
(click)="onCommand('link', $event)"
|
|
1658
|
+
></tiptap-button>
|
|
1659
|
+
}
|
|
1660
|
+
</div>
|
|
1661
|
+
`, isInline: true, dependencies: [{ kind: "component", type: TiptapButtonComponent, selector: "tiptap-button", inputs: ["icon", "title", "active", "disabled", "color", "variant", "size", "iconSize"], outputs: ["onClick"] }, { kind: "component", type: TiptapTextColorPickerComponent, selector: "tiptap-text-color-picker", inputs: ["editor"], outputs: ["interactionChange", "requestUpdate"] }] }); }
|
|
1662
|
+
}
|
|
1663
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapBubbleMenuComponent, decorators: [{
|
|
1664
|
+
type: Component,
|
|
1665
|
+
args: [{
|
|
1666
|
+
selector: "tiptap-bubble-menu",
|
|
1667
|
+
standalone: true,
|
|
1668
|
+
imports: [TiptapButtonComponent, TiptapTextColorPickerComponent],
|
|
1669
|
+
template: `
|
|
1670
|
+
<div #menuRef class="bubble-menu">
|
|
1671
|
+
@if (bubbleMenuConfig().bold) {
|
|
1672
|
+
<tiptap-button
|
|
1673
|
+
icon="format_bold"
|
|
1674
|
+
[title]="t().bold"
|
|
1675
|
+
[active]="isActive('bold')"
|
|
1676
|
+
(click)="onCommand('bold', $event)"
|
|
1677
|
+
></tiptap-button>
|
|
1678
|
+
} @if (bubbleMenuConfig().italic) {
|
|
1679
|
+
<tiptap-button
|
|
1680
|
+
icon="format_italic"
|
|
1681
|
+
[title]="t().italic"
|
|
1682
|
+
[active]="isActive('italic')"
|
|
1683
|
+
(click)="onCommand('italic', $event)"
|
|
1684
|
+
></tiptap-button>
|
|
1685
|
+
} @if (bubbleMenuConfig().underline) {
|
|
1686
|
+
<tiptap-button
|
|
1687
|
+
icon="format_underlined"
|
|
1688
|
+
[title]="t().underline"
|
|
1689
|
+
[active]="isActive('underline')"
|
|
1690
|
+
(click)="onCommand('underline', $event)"
|
|
1691
|
+
></tiptap-button>
|
|
1692
|
+
} @if (bubbleMenuConfig().strike) {
|
|
1693
|
+
<tiptap-button
|
|
1694
|
+
icon="strikethrough_s"
|
|
1695
|
+
[title]="t().strike"
|
|
1696
|
+
[active]="isActive('strike')"
|
|
1697
|
+
(click)="onCommand('strike', $event)"
|
|
1698
|
+
></tiptap-button>
|
|
1699
|
+
} @if (bubbleMenuConfig().superscript) {
|
|
1700
|
+
<tiptap-button
|
|
1701
|
+
icon="superscript"
|
|
1702
|
+
[title]="t().superscript"
|
|
1703
|
+
[active]="isActive('superscript')"
|
|
1704
|
+
(click)="onCommand('superscript', $event)"
|
|
1705
|
+
></tiptap-button>
|
|
1706
|
+
} @if (bubbleMenuConfig().subscript) {
|
|
1707
|
+
<tiptap-button
|
|
1708
|
+
icon="subscript"
|
|
1709
|
+
[title]="t().subscript"
|
|
1710
|
+
[active]="isActive('subscript')"
|
|
1711
|
+
(click)="onCommand('subscript', $event)"
|
|
1712
|
+
></tiptap-button>
|
|
1713
|
+
} @if (bubbleMenuConfig().highlight) {
|
|
1714
|
+
<tiptap-button
|
|
1715
|
+
icon="highlight"
|
|
1716
|
+
[title]="t().highlight"
|
|
1717
|
+
[active]="isActive('highlight')"
|
|
1718
|
+
(click)="onCommand('highlight', $event)"
|
|
1719
|
+
></tiptap-button>
|
|
1720
|
+
} @if (bubbleMenuConfig().textColor) {
|
|
1721
|
+
<tiptap-text-color-picker
|
|
1722
|
+
#textColorPicker
|
|
1723
|
+
[editor]="editor()"
|
|
1724
|
+
(interactionChange)="onColorPickerInteractionChange($event)"
|
|
1725
|
+
(requestUpdate)="updateMenu()"
|
|
1726
|
+
/>
|
|
1727
|
+
} @if (bubbleMenuConfig().separator && (bubbleMenuConfig().code ||
|
|
1728
|
+
bubbleMenuConfig().link)) {
|
|
1729
|
+
<div class="tiptap-separator"></div>
|
|
1730
|
+
} @if (bubbleMenuConfig().code) {
|
|
1731
|
+
<tiptap-button
|
|
1732
|
+
icon="code"
|
|
1733
|
+
[title]="t().code"
|
|
1734
|
+
[active]="isActive('code')"
|
|
1735
|
+
(click)="onCommand('code', $event)"
|
|
1736
|
+
></tiptap-button>
|
|
1737
|
+
} @if (bubbleMenuConfig().link) {
|
|
1738
|
+
<tiptap-button
|
|
1739
|
+
icon="link"
|
|
1740
|
+
[title]="t().link"
|
|
1741
|
+
[active]="isActive('link')"
|
|
1742
|
+
(click)="onCommand('link', $event)"
|
|
1743
|
+
></tiptap-button>
|
|
1744
|
+
}
|
|
1745
|
+
</div>
|
|
1746
|
+
`,
|
|
1747
|
+
}]
|
|
1748
|
+
}], ctorParameters: () => [], propDecorators: { menuRef: [{
|
|
1749
|
+
type: ViewChild,
|
|
1750
|
+
args: ["menuRef", { static: false }]
|
|
1751
|
+
}], textColorPicker: [{
|
|
1752
|
+
type: ViewChild,
|
|
1753
|
+
args: ["textColorPicker", { static: false }]
|
|
1754
|
+
}] } });
|
|
1755
|
+
|
|
1756
|
+
class TiptapSeparatorComponent {
|
|
1757
|
+
constructor() {
|
|
1758
|
+
this.orientation = input("vertical");
|
|
1759
|
+
this.size = input("medium");
|
|
1760
|
+
}
|
|
1761
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapSeparatorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1762
|
+
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: `
|
|
1763
|
+
<div
|
|
1764
|
+
class="tiptap-separator"
|
|
1765
|
+
[class.vertical]="orientation() === 'vertical'"
|
|
1766
|
+
[class.horizontal]="orientation() === 'horizontal'"
|
|
1767
|
+
[class.small]="size() === 'small'"
|
|
1768
|
+
[class.medium]="size() === 'medium'"
|
|
1769
|
+
[class.large]="size() === 'large'"
|
|
1770
|
+
></div>
|
|
1771
|
+
`, 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"] }); }
|
|
1772
|
+
}
|
|
1773
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapSeparatorComponent, decorators: [{
|
|
1774
|
+
type: Component,
|
|
1775
|
+
args: [{ selector: "tiptap-separator", standalone: true, template: `
|
|
1776
|
+
<div
|
|
1777
|
+
class="tiptap-separator"
|
|
1778
|
+
[class.vertical]="orientation() === 'vertical'"
|
|
1779
|
+
[class.horizontal]="orientation() === 'horizontal'"
|
|
1780
|
+
[class.small]="size() === 'small'"
|
|
1781
|
+
[class.medium]="size() === 'medium'"
|
|
1782
|
+
[class.large]="size() === 'large'"
|
|
1783
|
+
></div>
|
|
1784
|
+
`, 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"] }]
|
|
1785
|
+
}] });
|
|
1786
|
+
|
|
1787
|
+
class ImageService {
|
|
1788
|
+
constructor() {
|
|
1789
|
+
// Signals pour l'état des images
|
|
1790
|
+
this.selectedImage = signal(null);
|
|
1791
|
+
this.isImageSelected = computed(() => this.selectedImage() !== null);
|
|
1792
|
+
this.isResizing = signal(false);
|
|
1793
|
+
// Signaux pour l'upload
|
|
1794
|
+
this.isUploading = signal(false);
|
|
1795
|
+
this.uploadProgress = signal(0);
|
|
1796
|
+
this.uploadMessage = signal("");
|
|
1797
|
+
/**
|
|
1798
|
+
* Custom upload handler for images.
|
|
1799
|
+
* When set, this handler will be called instead of the default base64 conversion.
|
|
1800
|
+
* This allows users to implement their own image storage logic.
|
|
1801
|
+
*
|
|
1802
|
+
* @example
|
|
1803
|
+
* ```typescript
|
|
1804
|
+
* imageService.uploadHandler = async (context) => {
|
|
1805
|
+
* const formData = new FormData();
|
|
1806
|
+
* formData.append('image', context.file);
|
|
1807
|
+
* const response = await fetch('/api/upload', { method: 'POST', body: formData });
|
|
1808
|
+
* const data = await response.json();
|
|
1809
|
+
* return { src: data.url };
|
|
1810
|
+
* };
|
|
1811
|
+
* ```
|
|
1812
|
+
*/
|
|
1813
|
+
this.uploadHandler = null;
|
|
1814
|
+
// Référence à l'éditeur pour les mises à jour
|
|
1815
|
+
this.currentEditor = null;
|
|
1816
|
+
}
|
|
1817
|
+
// Méthodes pour la gestion des images
|
|
1818
|
+
selectImage(editor) {
|
|
1819
|
+
if (editor.isActive("resizableImage")) {
|
|
1820
|
+
const attrs = editor.getAttributes("resizableImage");
|
|
1821
|
+
this.selectedImage.set({
|
|
1822
|
+
src: attrs["src"],
|
|
1823
|
+
alt: attrs["alt"],
|
|
1824
|
+
title: attrs["title"],
|
|
1825
|
+
width: attrs["width"],
|
|
1826
|
+
height: attrs["height"],
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
else {
|
|
1830
|
+
this.selectedImage.set(null);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
clearSelection() {
|
|
1834
|
+
this.selectedImage.set(null);
|
|
1835
|
+
}
|
|
1836
|
+
// Méthodes pour manipuler les images
|
|
1837
|
+
insertImage(editor, imageData) {
|
|
1838
|
+
editor.chain().focus().setResizableImage(imageData).run();
|
|
1839
|
+
}
|
|
1840
|
+
updateImageAttributes(editor, attributes) {
|
|
1841
|
+
if (editor.isActive("resizableImage")) {
|
|
1842
|
+
editor
|
|
1843
|
+
.chain()
|
|
1844
|
+
.focus()
|
|
1845
|
+
.updateAttributes("resizableImage", attributes)
|
|
1846
|
+
.run();
|
|
1847
|
+
this.updateSelectedImage(attributes);
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
// Nouvelles méthodes pour le redimensionnement
|
|
1851
|
+
resizeImage(editor, options) {
|
|
1852
|
+
if (!editor.isActive("resizableImage"))
|
|
1853
|
+
return;
|
|
1854
|
+
const currentAttrs = editor.getAttributes("resizableImage");
|
|
1855
|
+
let newWidth = options.width;
|
|
1856
|
+
let newHeight = options.height;
|
|
1857
|
+
// Maintenir le ratio d'aspect si demandé
|
|
1858
|
+
if (options.maintainAspectRatio !== false &&
|
|
1859
|
+
currentAttrs["width"] &&
|
|
1860
|
+
currentAttrs["height"]) {
|
|
1861
|
+
const aspectRatio = currentAttrs["width"] / currentAttrs["height"];
|
|
1862
|
+
if (newWidth && !newHeight) {
|
|
1863
|
+
newHeight = Math.round(newWidth / aspectRatio);
|
|
1864
|
+
}
|
|
1865
|
+
else if (newHeight && !newWidth) {
|
|
1866
|
+
newWidth = Math.round(newHeight * aspectRatio);
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
// Appliquer des limites minimales
|
|
1870
|
+
if (newWidth)
|
|
1871
|
+
newWidth = Math.max(50, newWidth);
|
|
1872
|
+
if (newHeight)
|
|
1873
|
+
newHeight = Math.max(50, newHeight);
|
|
1874
|
+
this.updateImageAttributes(editor, {
|
|
1875
|
+
width: newWidth,
|
|
1876
|
+
height: newHeight,
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
// Méthodes pour redimensionner par pourcentage
|
|
1880
|
+
resizeImageByPercentage(editor, percentage) {
|
|
1881
|
+
if (!editor.isActive("resizableImage"))
|
|
1882
|
+
return;
|
|
1883
|
+
const currentAttrs = editor.getAttributes("resizableImage");
|
|
1884
|
+
if (!currentAttrs["width"] || !currentAttrs["height"])
|
|
1885
|
+
return;
|
|
1886
|
+
const newWidth = Math.round(currentAttrs["width"] * (percentage / 100));
|
|
1887
|
+
const newHeight = Math.round(currentAttrs["height"] * (percentage / 100));
|
|
1888
|
+
this.resizeImage(editor, { width: newWidth, height: newHeight });
|
|
1889
|
+
}
|
|
1890
|
+
// Méthodes pour redimensionner à des tailles prédéfinies
|
|
1891
|
+
resizeImageToSmall(editor) {
|
|
1892
|
+
this.resizeImage(editor, {
|
|
1893
|
+
width: 300,
|
|
1894
|
+
height: 200,
|
|
1895
|
+
maintainAspectRatio: true,
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
resizeImageToMedium(editor) {
|
|
1899
|
+
this.resizeImage(editor, {
|
|
1900
|
+
width: 500,
|
|
1901
|
+
height: 350,
|
|
1902
|
+
maintainAspectRatio: true,
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1905
|
+
resizeImageToLarge(editor) {
|
|
1906
|
+
this.resizeImage(editor, {
|
|
1907
|
+
width: 800,
|
|
1908
|
+
height: 600,
|
|
1909
|
+
maintainAspectRatio: true,
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1912
|
+
resizeImageToOriginal(editor) {
|
|
1913
|
+
if (!editor.isActive("resizableImage"))
|
|
1914
|
+
return;
|
|
1915
|
+
const img = new Image();
|
|
1916
|
+
img.onload = () => {
|
|
1917
|
+
this.resizeImage(editor, {
|
|
1918
|
+
width: img.naturalWidth,
|
|
1919
|
+
height: img.naturalHeight,
|
|
1920
|
+
});
|
|
1921
|
+
};
|
|
1922
|
+
img.src = editor.getAttributes("resizableImage")["src"];
|
|
1923
|
+
}
|
|
1924
|
+
// Méthode pour redimensionner librement (sans maintenir le ratio)
|
|
1925
|
+
resizeImageFreely(editor, width, height) {
|
|
1926
|
+
this.resizeImage(editor, {
|
|
1927
|
+
width,
|
|
1928
|
+
height,
|
|
1929
|
+
maintainAspectRatio: false,
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
// Méthode pour obtenir les dimensions actuelles de l'image
|
|
1933
|
+
getImageDimensions(editor) {
|
|
1934
|
+
if (!editor.isActive("resizableImage"))
|
|
1935
|
+
return null;
|
|
1936
|
+
const attrs = editor.getAttributes("resizableImage");
|
|
1937
|
+
return {
|
|
1938
|
+
width: attrs["width"] || 0,
|
|
1939
|
+
height: attrs["height"] || 0,
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
// Méthode pour obtenir les dimensions naturelles de l'image
|
|
1943
|
+
getNaturalImageDimensions(src) {
|
|
1944
|
+
return new Promise((resolve, reject) => {
|
|
1945
|
+
const img = new Image();
|
|
1946
|
+
img.onload = () => {
|
|
1947
|
+
resolve({ width: img.naturalWidth, height: img.naturalHeight });
|
|
1948
|
+
};
|
|
1949
|
+
img.onerror = () => {
|
|
1950
|
+
reject(new Error("Impossible de charger l'image"));
|
|
1951
|
+
};
|
|
1952
|
+
img.src = src;
|
|
1953
|
+
});
|
|
1954
|
+
}
|
|
1955
|
+
deleteImage(editor) {
|
|
1956
|
+
if (editor.isActive("resizableImage")) {
|
|
1957
|
+
editor.chain().focus().deleteSelection().run();
|
|
1958
|
+
this.clearSelection();
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
// Méthodes utilitaires
|
|
1962
|
+
updateSelectedImage(attributes) {
|
|
1963
|
+
const current = this.selectedImage();
|
|
1964
|
+
if (current) {
|
|
1965
|
+
this.selectedImage.set({ ...current, ...attributes });
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
// Validation des images
|
|
1969
|
+
validateImage(file, maxSize = 5 * 1024 * 1024) {
|
|
1970
|
+
if (!file.type.startsWith("image/")) {
|
|
1971
|
+
return { valid: false, error: "Le fichier doit être une image" };
|
|
1972
|
+
}
|
|
1973
|
+
if (file.size > maxSize) {
|
|
1974
|
+
return {
|
|
1975
|
+
valid: false,
|
|
1976
|
+
error: `L'image est trop volumineuse (max ${maxSize / 1024 / 1024}MB)`,
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
return { valid: true };
|
|
1980
|
+
}
|
|
1981
|
+
// Compression d'image
|
|
1982
|
+
async compressImage(file, quality = 0.8, maxWidth = 1920, maxHeight = 1080) {
|
|
1983
|
+
return new Promise((resolve, reject) => {
|
|
1984
|
+
const canvas = document.createElement("canvas");
|
|
1985
|
+
const ctx = canvas.getContext("2d");
|
|
1986
|
+
const img = new Image();
|
|
1987
|
+
img.onload = () => {
|
|
1988
|
+
// Mise à jour du progrès
|
|
1989
|
+
if (this.isUploading()) {
|
|
1990
|
+
this.uploadProgress.set(40);
|
|
1991
|
+
this.uploadMessage.set("Redimensionnement...");
|
|
1992
|
+
this.forceEditorUpdate();
|
|
1993
|
+
}
|
|
1994
|
+
let { width, height } = img;
|
|
1995
|
+
// Redimensionner si nécessaire
|
|
1996
|
+
if (width > maxWidth || height > maxHeight) {
|
|
1997
|
+
const ratio = Math.min(maxWidth / width, maxHeight / height);
|
|
1998
|
+
width *= ratio;
|
|
1999
|
+
height *= ratio;
|
|
2000
|
+
}
|
|
2001
|
+
canvas.width = width;
|
|
2002
|
+
canvas.height = height;
|
|
2003
|
+
// Dessiner l'image redimensionnée
|
|
2004
|
+
ctx?.drawImage(img, 0, 0, width, height);
|
|
2005
|
+
// Mise à jour du progrès
|
|
2006
|
+
if (this.isUploading()) {
|
|
2007
|
+
this.uploadProgress.set(60);
|
|
2008
|
+
this.uploadMessage.set("Compression...");
|
|
2009
|
+
this.forceEditorUpdate();
|
|
2010
|
+
}
|
|
2011
|
+
// Convertir en base64 avec compression
|
|
2012
|
+
canvas.toBlob((blob) => {
|
|
2013
|
+
if (blob) {
|
|
2014
|
+
const reader = new FileReader();
|
|
2015
|
+
reader.onload = (e) => {
|
|
2016
|
+
const base64 = e.target?.result;
|
|
2017
|
+
if (base64) {
|
|
2018
|
+
const result = {
|
|
2019
|
+
src: base64,
|
|
2020
|
+
name: file.name,
|
|
2021
|
+
size: blob.size,
|
|
2022
|
+
type: file.type,
|
|
2023
|
+
width: Math.round(width),
|
|
2024
|
+
height: Math.round(height),
|
|
2025
|
+
originalSize: file.size,
|
|
2026
|
+
};
|
|
2027
|
+
resolve(result);
|
|
2028
|
+
}
|
|
2029
|
+
else {
|
|
2030
|
+
reject(new Error("Erreur lors de la compression"));
|
|
2031
|
+
}
|
|
2032
|
+
};
|
|
2033
|
+
reader.readAsDataURL(blob);
|
|
2034
|
+
}
|
|
2035
|
+
else {
|
|
2036
|
+
reject(new Error("Erreur lors de la compression"));
|
|
2037
|
+
}
|
|
2038
|
+
}, file.type, quality);
|
|
2039
|
+
};
|
|
2040
|
+
img.onerror = () => reject(new Error("Erreur lors du chargement de l'image"));
|
|
2041
|
+
img.src = URL.createObjectURL(file);
|
|
2042
|
+
});
|
|
2043
|
+
}
|
|
2044
|
+
// Méthode privée générique pour uploader avec progression
|
|
2045
|
+
async uploadImageWithProgress(editor, file, insertionStrategy, actionMessage, options) {
|
|
2046
|
+
try {
|
|
2047
|
+
// Stocker la référence à l'éditeur
|
|
2048
|
+
this.currentEditor = editor;
|
|
2049
|
+
this.isUploading.set(true);
|
|
2050
|
+
this.uploadProgress.set(0);
|
|
2051
|
+
this.uploadMessage.set("Validation du fichier...");
|
|
2052
|
+
this.forceEditorUpdate();
|
|
2053
|
+
// Validation
|
|
2054
|
+
const validation = this.validateImage(file);
|
|
2055
|
+
if (!validation.valid) {
|
|
2056
|
+
throw new Error(validation.error);
|
|
2057
|
+
}
|
|
2058
|
+
this.uploadProgress.set(20);
|
|
2059
|
+
this.uploadMessage.set("Compression en cours...");
|
|
2060
|
+
this.forceEditorUpdate();
|
|
2061
|
+
// Petit délai pour permettre à l'utilisateur de voir la progression
|
|
2062
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
2063
|
+
const result = await this.compressImage(file, options?.quality || 0.8, options?.maxWidth || 1920, options?.maxHeight || 1080);
|
|
2064
|
+
this.uploadProgress.set(80);
|
|
2065
|
+
// Si un handler personnalisé est défini, l'utiliser pour l'upload
|
|
2066
|
+
if (this.uploadHandler) {
|
|
2067
|
+
this.uploadMessage.set("Upload vers le serveur...");
|
|
2068
|
+
this.forceEditorUpdate();
|
|
2069
|
+
try {
|
|
2070
|
+
const handlerResponse = this.uploadHandler({
|
|
2071
|
+
file,
|
|
2072
|
+
width: result.width || 0,
|
|
2073
|
+
height: result.height || 0,
|
|
2074
|
+
type: result.type,
|
|
2075
|
+
base64: result.src,
|
|
2076
|
+
});
|
|
2077
|
+
// Convertir Observable en Promise si nécessaire
|
|
2078
|
+
const handlerResult = isObservable(handlerResponse)
|
|
2079
|
+
? await firstValueFrom(handlerResponse)
|
|
2080
|
+
: await handlerResponse;
|
|
2081
|
+
// Remplacer le src base64 par l'URL retournée par le handler
|
|
2082
|
+
result.src = handlerResult.src;
|
|
2083
|
+
// Appliquer les overrides optionnels du handler
|
|
2084
|
+
if (handlerResult.alt) {
|
|
2085
|
+
result.name = handlerResult.alt;
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
catch (handlerError) {
|
|
2089
|
+
console.error("Erreur lors de l'upload personnalisé:", handlerError);
|
|
2090
|
+
throw handlerError;
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
this.uploadMessage.set(actionMessage);
|
|
2094
|
+
this.forceEditorUpdate();
|
|
2095
|
+
// Petit délai pour l'action
|
|
2096
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2097
|
+
// Exécuter la stratégie d'insertion
|
|
2098
|
+
insertionStrategy(editor, result);
|
|
2099
|
+
// L'action est terminée, maintenant on peut cacher l'indicateur
|
|
2100
|
+
this.isUploading.set(false);
|
|
2101
|
+
this.uploadProgress.set(0);
|
|
2102
|
+
this.uploadMessage.set("");
|
|
2103
|
+
this.forceEditorUpdate();
|
|
2104
|
+
this.currentEditor = null;
|
|
2105
|
+
}
|
|
2106
|
+
catch (error) {
|
|
2107
|
+
this.isUploading.set(false);
|
|
2108
|
+
this.uploadProgress.set(0);
|
|
2109
|
+
this.uploadMessage.set("");
|
|
2110
|
+
this.forceEditorUpdate();
|
|
2111
|
+
this.currentEditor = null;
|
|
2112
|
+
console.error("Erreur lors de l'upload d'image:", error);
|
|
2113
|
+
throw error;
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
// Méthode unifiée pour uploader et insérer une image
|
|
2117
|
+
async uploadAndInsertImage(editor, file, options) {
|
|
2118
|
+
return this.uploadImageWithProgress(editor, file, (editor, result) => {
|
|
2119
|
+
this.insertImage(editor, {
|
|
2120
|
+
src: result.src,
|
|
2121
|
+
alt: result.name,
|
|
2122
|
+
title: `${result.name} (${result.width}×${result.height})`,
|
|
2123
|
+
width: result.width,
|
|
2124
|
+
height: result.height,
|
|
2125
|
+
});
|
|
2126
|
+
}, "Insertion dans l'éditeur...", options);
|
|
2127
|
+
}
|
|
2128
|
+
// Méthode pour forcer la mise à jour de l'éditeur
|
|
2129
|
+
forceEditorUpdate() {
|
|
2130
|
+
if (this.currentEditor) {
|
|
2131
|
+
// Déclencher une transaction vide pour forcer la mise à jour des décorations
|
|
2132
|
+
const { tr } = this.currentEditor.state;
|
|
2133
|
+
this.currentEditor.view.dispatch(tr);
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
// Méthode privée générique pour créer un sélecteur de fichier
|
|
2137
|
+
async selectFileAndProcess(editor, uploadMethod, options) {
|
|
2138
|
+
return new Promise((resolve, reject) => {
|
|
2139
|
+
const input = document.createElement("input");
|
|
2140
|
+
input.type = "file";
|
|
2141
|
+
input.accept = options?.accept || "image/*";
|
|
2142
|
+
input.style.display = "none";
|
|
2143
|
+
input.addEventListener("change", async (e) => {
|
|
2144
|
+
const file = e.target.files?.[0];
|
|
2145
|
+
if (file && file.type.startsWith("image/")) {
|
|
2146
|
+
try {
|
|
2147
|
+
await uploadMethod(editor, file, options);
|
|
2148
|
+
resolve();
|
|
2149
|
+
}
|
|
2150
|
+
catch (error) {
|
|
2151
|
+
reject(error);
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
else {
|
|
2155
|
+
reject(new Error("Aucun fichier image sélectionné"));
|
|
2156
|
+
}
|
|
2157
|
+
document.body.removeChild(input);
|
|
2158
|
+
});
|
|
2159
|
+
input.addEventListener("cancel", () => {
|
|
2160
|
+
document.body.removeChild(input);
|
|
2161
|
+
reject(new Error("Sélection annulée"));
|
|
2162
|
+
});
|
|
2163
|
+
document.body.appendChild(input);
|
|
2164
|
+
input.click();
|
|
2165
|
+
});
|
|
2166
|
+
}
|
|
2167
|
+
// Méthode pour créer un sélecteur de fichier et uploader une image
|
|
2168
|
+
async selectAndUploadImage(editor, options) {
|
|
2169
|
+
return this.selectFileAndProcess(editor, this.uploadAndInsertImage.bind(this), options);
|
|
2170
|
+
}
|
|
2171
|
+
// Méthode pour sélectionner et remplacer une image existante
|
|
2172
|
+
async selectAndReplaceImage(editor, options) {
|
|
2173
|
+
return this.selectFileAndProcess(editor, this.uploadAndReplaceImage.bind(this), options);
|
|
2174
|
+
}
|
|
2175
|
+
// Méthode pour remplacer une image existante avec indicateur de progression
|
|
2176
|
+
async uploadAndReplaceImage(editor, file, options) {
|
|
2177
|
+
// Sauvegarder les attributs de l'image actuelle pour restauration en cas d'échec
|
|
2178
|
+
const currentImageAttrs = editor.getAttributes("resizableImage");
|
|
2179
|
+
const backupImage = { ...currentImageAttrs };
|
|
2180
|
+
try {
|
|
2181
|
+
// Supprimer visuellement l'ancienne image immédiatement
|
|
2182
|
+
editor.chain().focus().deleteSelection().run();
|
|
2183
|
+
// Stocker la référence à l'éditeur
|
|
2184
|
+
this.currentEditor = editor;
|
|
2185
|
+
this.isUploading.set(true);
|
|
2186
|
+
this.uploadProgress.set(0);
|
|
2187
|
+
this.uploadMessage.set("Validation du fichier...");
|
|
2188
|
+
this.forceEditorUpdate();
|
|
2189
|
+
// Validation
|
|
2190
|
+
const validation = this.validateImage(file);
|
|
2191
|
+
if (!validation.valid) {
|
|
2192
|
+
throw new Error(validation.error);
|
|
2193
|
+
}
|
|
2194
|
+
this.uploadProgress.set(20);
|
|
2195
|
+
this.uploadMessage.set("Compression en cours...");
|
|
2196
|
+
this.forceEditorUpdate();
|
|
2197
|
+
// Petit délai pour permettre à l'utilisateur de voir la progression
|
|
2198
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
2199
|
+
const result = await this.compressImage(file, options?.quality || 0.8, options?.maxWidth || 1920, options?.maxHeight || 1080);
|
|
2200
|
+
this.uploadProgress.set(80);
|
|
2201
|
+
this.uploadMessage.set("Remplacement de l'image...");
|
|
2202
|
+
this.forceEditorUpdate();
|
|
2203
|
+
// Petit délai pour le remplacement
|
|
2204
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2205
|
+
// Insérer la nouvelle image à la position actuelle
|
|
2206
|
+
this.insertImage(editor, {
|
|
2207
|
+
src: result.src,
|
|
2208
|
+
alt: result.name,
|
|
2209
|
+
title: `${result.name} (${result.width}×${result.height})`,
|
|
2210
|
+
width: result.width,
|
|
2211
|
+
height: result.height,
|
|
2212
|
+
});
|
|
2213
|
+
// L'image est remplacée, maintenant on peut cacher l'indicateur
|
|
2214
|
+
this.isUploading.set(false);
|
|
2215
|
+
this.uploadProgress.set(0);
|
|
2216
|
+
this.uploadMessage.set("");
|
|
2217
|
+
this.forceEditorUpdate();
|
|
2218
|
+
this.currentEditor = null;
|
|
2219
|
+
}
|
|
2220
|
+
catch (error) {
|
|
2221
|
+
// En cas d'erreur, restaurer l'image originale
|
|
2222
|
+
if (backupImage["src"]) {
|
|
2223
|
+
this.insertImage(editor, {
|
|
2224
|
+
src: backupImage["src"],
|
|
2225
|
+
alt: backupImage["alt"],
|
|
2226
|
+
title: backupImage["title"],
|
|
2227
|
+
width: backupImage["width"],
|
|
2228
|
+
height: backupImage["height"],
|
|
2229
|
+
});
|
|
2230
|
+
}
|
|
2231
|
+
this.isUploading.set(false);
|
|
2232
|
+
this.uploadProgress.set(0);
|
|
2233
|
+
this.uploadMessage.set("");
|
|
2234
|
+
this.forceEditorUpdate();
|
|
2235
|
+
this.currentEditor = null;
|
|
2236
|
+
console.error("Erreur lors du remplacement d'image:", error);
|
|
2237
|
+
throw error;
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: ImageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2241
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: ImageService, providedIn: "root" }); }
|
|
2242
|
+
}
|
|
2243
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: ImageService, decorators: [{
|
|
2244
|
+
type: Injectable,
|
|
2245
|
+
args: [{
|
|
2246
|
+
providedIn: "root",
|
|
2247
|
+
}]
|
|
2248
|
+
}] });
|
|
2249
|
+
|
|
2250
|
+
class TiptapImageBubbleMenuComponent {
|
|
2251
|
+
constructor() {
|
|
2252
|
+
this.editor = input.required();
|
|
2253
|
+
this.config = input({
|
|
2254
|
+
changeImage: true,
|
|
2255
|
+
resizeSmall: true,
|
|
2256
|
+
resizeMedium: true,
|
|
2257
|
+
resizeLarge: true,
|
|
2258
|
+
resizeOriginal: true,
|
|
2259
|
+
deleteImage: true,
|
|
2260
|
+
separator: true,
|
|
2261
|
+
});
|
|
2262
|
+
this.tippyInstance = null;
|
|
2263
|
+
this.imageService = inject(ImageService);
|
|
2264
|
+
this.updateTimeout = null;
|
|
2265
|
+
this.imageBubbleMenuConfig = computed(() => ({
|
|
2266
|
+
changeImage: true,
|
|
2267
|
+
resizeSmall: true,
|
|
2268
|
+
resizeMedium: true,
|
|
2269
|
+
resizeLarge: true,
|
|
2270
|
+
resizeOriginal: true,
|
|
2271
|
+
deleteImage: true,
|
|
2272
|
+
separator: true,
|
|
2273
|
+
...this.config(),
|
|
2274
|
+
}));
|
|
2275
|
+
this.hasResizeButtons = computed(() => {
|
|
2276
|
+
const config = this.imageBubbleMenuConfig();
|
|
2277
|
+
return (config.resizeSmall ||
|
|
2278
|
+
config.resizeMedium ||
|
|
2279
|
+
config.resizeLarge ||
|
|
2280
|
+
config.resizeOriginal);
|
|
2281
|
+
});
|
|
2282
|
+
this.updateMenu = () => {
|
|
2283
|
+
// Debounce pour éviter les appels trop fréquents
|
|
2284
|
+
if (this.updateTimeout) {
|
|
2285
|
+
clearTimeout(this.updateTimeout);
|
|
2286
|
+
}
|
|
2287
|
+
this.updateTimeout = setTimeout(() => {
|
|
2288
|
+
const ed = this.editor();
|
|
2289
|
+
if (!ed)
|
|
2290
|
+
return;
|
|
2291
|
+
const isImageSelected = ed.isActive("resizableImage") || ed.isActive("image");
|
|
2292
|
+
const { from, to } = ed.state.selection;
|
|
2293
|
+
const hasTextSelection = from !== to;
|
|
2294
|
+
// Ne montrer le menu image que si :
|
|
2295
|
+
// - Une image est sélectionnée
|
|
2296
|
+
// - L'éditeur est éditable
|
|
2297
|
+
const shouldShow = isImageSelected && ed.isEditable;
|
|
2298
|
+
if (shouldShow) {
|
|
2299
|
+
this.showTippy();
|
|
2300
|
+
}
|
|
2301
|
+
else {
|
|
2302
|
+
this.hideTippy();
|
|
2303
|
+
}
|
|
2304
|
+
}, 10);
|
|
2305
|
+
};
|
|
2306
|
+
this.handleBlur = () => {
|
|
2307
|
+
// Masquer le menu quand l'éditeur perd le focus
|
|
2308
|
+
setTimeout(() => {
|
|
2309
|
+
this.hideTippy();
|
|
2310
|
+
}, 100);
|
|
2311
|
+
};
|
|
2312
|
+
effect(() => {
|
|
2313
|
+
const ed = this.editor();
|
|
2314
|
+
if (!ed)
|
|
2315
|
+
return;
|
|
2316
|
+
// Nettoyer les anciens listeners
|
|
2317
|
+
ed.off("selectionUpdate", this.updateMenu);
|
|
2318
|
+
ed.off("transaction", this.updateMenu);
|
|
2319
|
+
ed.off("focus", this.updateMenu);
|
|
2320
|
+
ed.off("blur", this.handleBlur);
|
|
2321
|
+
// Ajouter les nouveaux listeners
|
|
2322
|
+
ed.on("selectionUpdate", this.updateMenu);
|
|
2323
|
+
ed.on("transaction", this.updateMenu);
|
|
2324
|
+
ed.on("focus", this.updateMenu);
|
|
2325
|
+
ed.on("blur", this.handleBlur);
|
|
2326
|
+
// Ne pas appeler updateMenu() ici pour éviter l'affichage prématuré
|
|
2327
|
+
// Il sera appelé automatiquement quand l'éditeur sera prêt
|
|
2138
2328
|
});
|
|
2139
|
-
// Signaux publics
|
|
2140
|
-
this.currentLocale = this._currentLocale.asReadonly();
|
|
2141
|
-
this.translations = computed(() => this._translations()[this._currentLocale()]);
|
|
2142
|
-
// Méthodes de traduction rapides
|
|
2143
|
-
this.t = computed(() => this.translations());
|
|
2144
|
-
this.toolbar = computed(() => this.translations().toolbar);
|
|
2145
|
-
this.bubbleMenu = computed(() => this.translations().bubbleMenu);
|
|
2146
|
-
this.slashCommands = computed(() => this.translations().slashCommands);
|
|
2147
|
-
this.table = computed(() => this.translations().table);
|
|
2148
|
-
this.imageUpload = computed(() => this.translations().imageUpload);
|
|
2149
|
-
this.editor = computed(() => this.translations().editor);
|
|
2150
|
-
this.common = computed(() => this.translations().common);
|
|
2151
|
-
// Détecter automatiquement la langue du navigateur
|
|
2152
|
-
this.detectBrowserLanguage();
|
|
2153
|
-
}
|
|
2154
|
-
setLocale(locale) {
|
|
2155
|
-
this._currentLocale.set(locale);
|
|
2156
2329
|
}
|
|
2157
|
-
|
|
2158
|
-
|
|
2330
|
+
ngOnInit() {
|
|
2331
|
+
// Initialiser Tippy de manière synchrone après que le component soit ready
|
|
2332
|
+
this.initTippy();
|
|
2159
2333
|
}
|
|
2160
|
-
|
|
2161
|
-
|
|
2334
|
+
ngOnDestroy() {
|
|
2335
|
+
const ed = this.editor();
|
|
2336
|
+
if (ed) {
|
|
2337
|
+
ed.off("selectionUpdate", this.updateMenu);
|
|
2338
|
+
ed.off("transaction", this.updateMenu);
|
|
2339
|
+
ed.off("focus", this.updateMenu);
|
|
2340
|
+
ed.off("blur", this.handleBlur);
|
|
2341
|
+
}
|
|
2342
|
+
// Nettoyer les timeouts
|
|
2343
|
+
if (this.updateTimeout) {
|
|
2344
|
+
clearTimeout(this.updateTimeout);
|
|
2345
|
+
}
|
|
2346
|
+
// Nettoyer Tippy
|
|
2347
|
+
if (this.tippyInstance) {
|
|
2348
|
+
this.tippyInstance.destroy();
|
|
2349
|
+
this.tippyInstance = null;
|
|
2350
|
+
}
|
|
2162
2351
|
}
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2352
|
+
initTippy() {
|
|
2353
|
+
// Attendre que l'élément soit disponible
|
|
2354
|
+
if (!this.menuRef?.nativeElement) {
|
|
2355
|
+
setTimeout(() => this.initTippy(), 50);
|
|
2356
|
+
return;
|
|
2357
|
+
}
|
|
2358
|
+
const menuElement = this.menuRef.nativeElement;
|
|
2359
|
+
// S'assurer qu'il n'y a pas déjà une instance
|
|
2360
|
+
if (this.tippyInstance) {
|
|
2361
|
+
this.tippyInstance.destroy();
|
|
2362
|
+
}
|
|
2363
|
+
// Créer l'instance Tippy
|
|
2364
|
+
this.tippyInstance = tippy(document.body, {
|
|
2365
|
+
content: menuElement,
|
|
2366
|
+
trigger: "manual",
|
|
2367
|
+
placement: "top-start",
|
|
2368
|
+
appendTo: () => document.body,
|
|
2369
|
+
interactive: true,
|
|
2370
|
+
arrow: false,
|
|
2371
|
+
offset: [0, 8],
|
|
2372
|
+
hideOnClick: false,
|
|
2373
|
+
onShow: (instance) => {
|
|
2374
|
+
// S'assurer que les autres menus sont fermés
|
|
2375
|
+
this.hideOtherMenus();
|
|
2169
2376
|
},
|
|
2170
|
-
|
|
2377
|
+
getReferenceClientRect: () => this.getImageRect(),
|
|
2378
|
+
// Améliorer le positionnement avec scroll
|
|
2379
|
+
popperOptions: {
|
|
2380
|
+
modifiers: [
|
|
2381
|
+
{
|
|
2382
|
+
name: "preventOverflow",
|
|
2383
|
+
options: {
|
|
2384
|
+
boundary: "viewport",
|
|
2385
|
+
padding: 8,
|
|
2386
|
+
},
|
|
2387
|
+
},
|
|
2388
|
+
{
|
|
2389
|
+
name: "flip",
|
|
2390
|
+
options: {
|
|
2391
|
+
fallbackPlacements: ["bottom-start", "top-end", "bottom-end"],
|
|
2392
|
+
},
|
|
2393
|
+
},
|
|
2394
|
+
],
|
|
2395
|
+
},
|
|
2396
|
+
});
|
|
2397
|
+
// Maintenant que Tippy est initialisé, faire un premier check
|
|
2398
|
+
this.updateMenu();
|
|
2399
|
+
}
|
|
2400
|
+
getImageRect() {
|
|
2401
|
+
const ed = this.editor();
|
|
2402
|
+
if (!ed)
|
|
2403
|
+
return new DOMRect(0, 0, 0, 0);
|
|
2404
|
+
// Trouver l'image sélectionnée dans le DOM
|
|
2405
|
+
const { from } = ed.state.selection;
|
|
2406
|
+
// Fonction pour trouver toutes les images dans l'éditeur
|
|
2407
|
+
const getAllImages = () => {
|
|
2408
|
+
const editorElement = ed.view.dom;
|
|
2409
|
+
return Array.from(editorElement.querySelectorAll("img"));
|
|
2410
|
+
};
|
|
2411
|
+
// Fonction pour trouver l'image à la position spécifique
|
|
2412
|
+
const findImageAtPosition = () => {
|
|
2413
|
+
const allImages = getAllImages();
|
|
2414
|
+
for (const img of allImages) {
|
|
2415
|
+
try {
|
|
2416
|
+
// Obtenir la position ProseMirror de cette image
|
|
2417
|
+
const imgPos = ed.view.posAtDOM(img, 0);
|
|
2418
|
+
// Vérifier si cette image correspond à la position sélectionnée
|
|
2419
|
+
if (Math.abs(imgPos - from) <= 1) {
|
|
2420
|
+
return img;
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
catch (error) {
|
|
2424
|
+
// Continuer si on ne peut pas obtenir la position de cette image
|
|
2425
|
+
continue;
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
return null;
|
|
2429
|
+
};
|
|
2430
|
+
// Chercher l'image à la position exacte
|
|
2431
|
+
const imageElement = findImageAtPosition();
|
|
2432
|
+
if (imageElement) {
|
|
2433
|
+
return imageElement.getBoundingClientRect();
|
|
2434
|
+
}
|
|
2435
|
+
return new DOMRect(0, 0, 0, 0);
|
|
2436
|
+
}
|
|
2437
|
+
hideOtherMenus() {
|
|
2438
|
+
// Cette méthode peut être étendue pour fermer d'autres menus si nécessaire
|
|
2439
|
+
// Pour l'instant, elle sert de placeholder pour une future coordination entre menus
|
|
2440
|
+
}
|
|
2441
|
+
showTippy() {
|
|
2442
|
+
if (!this.tippyInstance)
|
|
2443
|
+
return;
|
|
2444
|
+
// Mettre à jour la position
|
|
2445
|
+
this.tippyInstance.setProps({
|
|
2446
|
+
getReferenceClientRect: () => this.getImageRect(),
|
|
2447
|
+
});
|
|
2448
|
+
this.tippyInstance.show();
|
|
2171
2449
|
}
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
this._currentLocale.set("fr");
|
|
2176
|
-
}
|
|
2177
|
-
else {
|
|
2178
|
-
this._currentLocale.set("en");
|
|
2450
|
+
hideTippy() {
|
|
2451
|
+
if (this.tippyInstance) {
|
|
2452
|
+
this.tippyInstance.hide();
|
|
2179
2453
|
}
|
|
2180
2454
|
}
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2455
|
+
onCommand(command, event) {
|
|
2456
|
+
event.preventDefault();
|
|
2457
|
+
const ed = this.editor();
|
|
2458
|
+
if (!ed)
|
|
2459
|
+
return;
|
|
2460
|
+
switch (command) {
|
|
2461
|
+
case "changeImage":
|
|
2462
|
+
this.changeImage();
|
|
2463
|
+
break;
|
|
2464
|
+
case "resizeSmall":
|
|
2465
|
+
this.imageService.resizeImageToSmall(ed);
|
|
2466
|
+
break;
|
|
2467
|
+
case "resizeMedium":
|
|
2468
|
+
this.imageService.resizeImageToMedium(ed);
|
|
2469
|
+
break;
|
|
2470
|
+
case "resizeLarge":
|
|
2471
|
+
this.imageService.resizeImageToLarge(ed);
|
|
2472
|
+
break;
|
|
2473
|
+
case "resizeOriginal":
|
|
2474
|
+
this.imageService.resizeImageToOriginal(ed);
|
|
2475
|
+
break;
|
|
2476
|
+
case "deleteImage":
|
|
2477
|
+
this.deleteImage();
|
|
2478
|
+
break;
|
|
2479
|
+
}
|
|
2184
2480
|
}
|
|
2185
|
-
|
|
2186
|
-
|
|
2481
|
+
async changeImage() {
|
|
2482
|
+
const ed = this.editor();
|
|
2483
|
+
if (!ed)
|
|
2484
|
+
return;
|
|
2485
|
+
try {
|
|
2486
|
+
// Utiliser la méthode spécifique pour remplacer une image existante
|
|
2487
|
+
await this.imageService.selectAndReplaceImage(ed, {
|
|
2488
|
+
quality: 0.8,
|
|
2489
|
+
maxWidth: 1920,
|
|
2490
|
+
maxHeight: 1080,
|
|
2491
|
+
accept: "image/*",
|
|
2492
|
+
});
|
|
2493
|
+
}
|
|
2494
|
+
catch (error) {
|
|
2495
|
+
console.error("Erreur lors du changement d'image:", error);
|
|
2496
|
+
}
|
|
2187
2497
|
}
|
|
2188
|
-
|
|
2189
|
-
|
|
2498
|
+
deleteImage() {
|
|
2499
|
+
const ed = this.editor();
|
|
2500
|
+
if (ed) {
|
|
2501
|
+
ed.chain().focus().deleteSelection().run();
|
|
2502
|
+
}
|
|
2190
2503
|
}
|
|
2191
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type:
|
|
2192
|
-
static { this.ɵ
|
|
2504
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapImageBubbleMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2505
|
+
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: `
|
|
2506
|
+
<div #menuRef class="bubble-menu">
|
|
2507
|
+
@if (imageBubbleMenuConfig().changeImage) {
|
|
2508
|
+
<tiptap-button
|
|
2509
|
+
icon="drive_file_rename_outline"
|
|
2510
|
+
title="Changer l'image"
|
|
2511
|
+
(click)="onCommand('changeImage', $event)"
|
|
2512
|
+
></tiptap-button>
|
|
2513
|
+
} @if (imageBubbleMenuConfig().separator && hasResizeButtons()) {
|
|
2514
|
+
<tiptap-separator></tiptap-separator>
|
|
2515
|
+
} @if (imageBubbleMenuConfig().resizeSmall) {
|
|
2516
|
+
<tiptap-button
|
|
2517
|
+
icon="crop_square"
|
|
2518
|
+
iconSize="small"
|
|
2519
|
+
title="Petite (300×200)"
|
|
2520
|
+
(click)="onCommand('resizeSmall', $event)"
|
|
2521
|
+
></tiptap-button>
|
|
2522
|
+
} @if (imageBubbleMenuConfig().resizeMedium) {
|
|
2523
|
+
<tiptap-button
|
|
2524
|
+
icon="crop_square"
|
|
2525
|
+
iconSize="medium"
|
|
2526
|
+
title="Moyenne (500×350)"
|
|
2527
|
+
(click)="onCommand('resizeMedium', $event)"
|
|
2528
|
+
></tiptap-button>
|
|
2529
|
+
} @if (imageBubbleMenuConfig().resizeLarge) {
|
|
2530
|
+
<tiptap-button
|
|
2531
|
+
icon="crop_square"
|
|
2532
|
+
iconSize="large"
|
|
2533
|
+
title="Grande (800×600)"
|
|
2534
|
+
(click)="onCommand('resizeLarge', $event)"
|
|
2535
|
+
></tiptap-button>
|
|
2536
|
+
} @if (imageBubbleMenuConfig().resizeOriginal) {
|
|
2537
|
+
<tiptap-button
|
|
2538
|
+
icon="photo_size_select_actual"
|
|
2539
|
+
title="Taille originale"
|
|
2540
|
+
(click)="onCommand('resizeOriginal', $event)"
|
|
2541
|
+
></tiptap-button>
|
|
2542
|
+
} @if (imageBubbleMenuConfig().separator &&
|
|
2543
|
+
imageBubbleMenuConfig().deleteImage) {
|
|
2544
|
+
<tiptap-separator></tiptap-separator>
|
|
2545
|
+
} @if (imageBubbleMenuConfig().deleteImage) {
|
|
2546
|
+
<tiptap-button
|
|
2547
|
+
icon="delete"
|
|
2548
|
+
title="Supprimer l'image"
|
|
2549
|
+
variant="danger"
|
|
2550
|
+
(click)="onCommand('deleteImage', $event)"
|
|
2551
|
+
></tiptap-button>
|
|
2552
|
+
}
|
|
2553
|
+
</div>
|
|
2554
|
+
`, isInline: true, dependencies: [{ kind: "component", type: TiptapButtonComponent, selector: "tiptap-button", inputs: ["icon", "title", "active", "disabled", "color", "variant", "size", "iconSize"], outputs: ["onClick"] }, { kind: "component", type: TiptapSeparatorComponent, selector: "tiptap-separator", inputs: ["orientation", "size"] }] }); }
|
|
2193
2555
|
}
|
|
2194
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type:
|
|
2195
|
-
type:
|
|
2196
|
-
args: [{
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2556
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapImageBubbleMenuComponent, decorators: [{
|
|
2557
|
+
type: Component,
|
|
2558
|
+
args: [{ selector: "tiptap-image-bubble-menu", standalone: true, imports: [TiptapButtonComponent, TiptapSeparatorComponent], template: `
|
|
2559
|
+
<div #menuRef class="bubble-menu">
|
|
2560
|
+
@if (imageBubbleMenuConfig().changeImage) {
|
|
2561
|
+
<tiptap-button
|
|
2562
|
+
icon="drive_file_rename_outline"
|
|
2563
|
+
title="Changer l'image"
|
|
2564
|
+
(click)="onCommand('changeImage', $event)"
|
|
2565
|
+
></tiptap-button>
|
|
2566
|
+
} @if (imageBubbleMenuConfig().separator && hasResizeButtons()) {
|
|
2567
|
+
<tiptap-separator></tiptap-separator>
|
|
2568
|
+
} @if (imageBubbleMenuConfig().resizeSmall) {
|
|
2569
|
+
<tiptap-button
|
|
2570
|
+
icon="crop_square"
|
|
2571
|
+
iconSize="small"
|
|
2572
|
+
title="Petite (300×200)"
|
|
2573
|
+
(click)="onCommand('resizeSmall', $event)"
|
|
2574
|
+
></tiptap-button>
|
|
2575
|
+
} @if (imageBubbleMenuConfig().resizeMedium) {
|
|
2576
|
+
<tiptap-button
|
|
2577
|
+
icon="crop_square"
|
|
2578
|
+
iconSize="medium"
|
|
2579
|
+
title="Moyenne (500×350)"
|
|
2580
|
+
(click)="onCommand('resizeMedium', $event)"
|
|
2581
|
+
></tiptap-button>
|
|
2582
|
+
} @if (imageBubbleMenuConfig().resizeLarge) {
|
|
2583
|
+
<tiptap-button
|
|
2584
|
+
icon="crop_square"
|
|
2585
|
+
iconSize="large"
|
|
2586
|
+
title="Grande (800×600)"
|
|
2587
|
+
(click)="onCommand('resizeLarge', $event)"
|
|
2588
|
+
></tiptap-button>
|
|
2589
|
+
} @if (imageBubbleMenuConfig().resizeOriginal) {
|
|
2590
|
+
<tiptap-button
|
|
2591
|
+
icon="photo_size_select_actual"
|
|
2592
|
+
title="Taille originale"
|
|
2593
|
+
(click)="onCommand('resizeOriginal', $event)"
|
|
2594
|
+
></tiptap-button>
|
|
2595
|
+
} @if (imageBubbleMenuConfig().separator &&
|
|
2596
|
+
imageBubbleMenuConfig().deleteImage) {
|
|
2597
|
+
<tiptap-separator></tiptap-separator>
|
|
2598
|
+
} @if (imageBubbleMenuConfig().deleteImage) {
|
|
2599
|
+
<tiptap-button
|
|
2600
|
+
icon="delete"
|
|
2601
|
+
title="Supprimer l'image"
|
|
2602
|
+
variant="danger"
|
|
2603
|
+
(click)="onCommand('deleteImage', $event)"
|
|
2604
|
+
></tiptap-button>
|
|
2605
|
+
}
|
|
2606
|
+
</div>
|
|
2607
|
+
` }]
|
|
2608
|
+
}], ctorParameters: () => [], propDecorators: { menuRef: [{
|
|
2609
|
+
type: ViewChild,
|
|
2610
|
+
args: ["menuRef", { static: false }]
|
|
2611
|
+
}] } });
|
|
2200
2612
|
|
|
2201
2613
|
class EditorCommandsService {
|
|
2202
2614
|
// Méthodes pour vérifier l'état actif
|
|
@@ -2693,7 +3105,7 @@ class TiptapTableBubbleMenuComponent {
|
|
|
2693
3105
|
></tiptap-button>
|
|
2694
3106
|
}
|
|
2695
3107
|
</div>
|
|
2696
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { 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"] }] }); }
|
|
3108
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: TiptapButtonComponent, selector: "tiptap-button", inputs: ["icon", "title", "active", "disabled", "color", "variant", "size", "iconSize"], outputs: ["onClick"] }, { kind: "component", type: TiptapSeparatorComponent, selector: "tiptap-separator", inputs: ["orientation", "size"] }] }); }
|
|
2697
3109
|
}
|
|
2698
3110
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapTableBubbleMenuComponent, decorators: [{
|
|
2699
3111
|
type: Component,
|
|
@@ -2996,7 +3408,7 @@ class TiptapCellBubbleMenuComponent {
|
|
|
2996
3408
|
></tiptap-button>
|
|
2997
3409
|
}
|
|
2998
3410
|
</div>
|
|
2999
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: TiptapButtonComponent, selector: "tiptap-button", inputs: ["icon", "title", "active", "disabled", "variant", "size", "iconSize"], outputs: ["onClick"] }] }); }
|
|
3411
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: TiptapButtonComponent, selector: "tiptap-button", inputs: ["icon", "title", "active", "disabled", "color", "variant", "size", "iconSize"], outputs: ["onClick"] }] }); }
|
|
3000
3412
|
}
|
|
3001
3413
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapCellBubbleMenuComponent, decorators: [{
|
|
3002
3414
|
type: Component,
|
|
@@ -3186,6 +3598,8 @@ class TiptapToolbarComponent {
|
|
|
3186
3598
|
[disabled]="!canExecute('toggleHighlight')"
|
|
3187
3599
|
(onClick)="toggleHighlight()"
|
|
3188
3600
|
/>
|
|
3601
|
+
} @if (config().textColor) {
|
|
3602
|
+
<tiptap-text-color-picker [editor]="editor()" />
|
|
3189
3603
|
} @if (config().separator && (config().heading1 || config().heading2 ||
|
|
3190
3604
|
config().heading3)) {
|
|
3191
3605
|
<tiptap-separator />
|
|
@@ -3323,11 +3737,15 @@ class TiptapToolbarComponent {
|
|
|
3323
3737
|
/>
|
|
3324
3738
|
}
|
|
3325
3739
|
</div>
|
|
3326
|
-
`, 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"] }] }); }
|
|
3740
|
+
`, 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", "color", "variant", "size", "iconSize"], outputs: ["onClick"] }, { kind: "component", type: TiptapSeparatorComponent, selector: "tiptap-separator", inputs: ["orientation", "size"] }, { kind: "component", type: TiptapTextColorPickerComponent, selector: "tiptap-text-color-picker", inputs: ["editor"], outputs: ["interactionChange", "requestUpdate"] }] }); }
|
|
3327
3741
|
}
|
|
3328
3742
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapToolbarComponent, decorators: [{
|
|
3329
3743
|
type: Component,
|
|
3330
|
-
args: [{ selector: "tiptap-toolbar", standalone: true, imports: [
|
|
3744
|
+
args: [{ selector: "tiptap-toolbar", standalone: true, imports: [
|
|
3745
|
+
TiptapButtonComponent,
|
|
3746
|
+
TiptapSeparatorComponent,
|
|
3747
|
+
TiptapTextColorPickerComponent,
|
|
3748
|
+
], template: `
|
|
3331
3749
|
<div class="tiptap-toolbar">
|
|
3332
3750
|
@if (config().bold) {
|
|
3333
3751
|
<tiptap-button
|
|
@@ -3393,6 +3811,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImpor
|
|
|
3393
3811
|
[disabled]="!canExecute('toggleHighlight')"
|
|
3394
3812
|
(onClick)="toggleHighlight()"
|
|
3395
3813
|
/>
|
|
3814
|
+
} @if (config().textColor) {
|
|
3815
|
+
<tiptap-text-color-picker [editor]="editor()" />
|
|
3396
3816
|
} @if (config().separator && (config().heading1 || config().heading2 ||
|
|
3397
3817
|
config().heading3)) {
|
|
3398
3818
|
<tiptap-separator />
|
|
@@ -3982,48 +4402,48 @@ class TiptapSlashCommandsComponent {
|
|
|
3982
4402
|
}));
|
|
3983
4403
|
}
|
|
3984
4404
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapSlashCommandsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3985
|
-
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: `
|
|
3986
|
-
<div #menuRef class="slash-commands-menu">
|
|
3987
|
-
@for (command of filteredCommands(); track command.title) {
|
|
3988
|
-
<div
|
|
3989
|
-
class="slash-command-item"
|
|
3990
|
-
[class.selected]="$index === selectedIndex()"
|
|
3991
|
-
(click)="executeCommand(command)"
|
|
3992
|
-
(mouseenter)="selectedIndex.set($index)"
|
|
3993
|
-
>
|
|
3994
|
-
<div class="slash-command-icon">
|
|
3995
|
-
<span class="material-symbols-outlined">{{ command.icon }}</span>
|
|
3996
|
-
</div>
|
|
3997
|
-
<div class="slash-command-content">
|
|
3998
|
-
<div class="slash-command-title">{{ command.title }}</div>
|
|
3999
|
-
<div class="slash-command-description">{{ command.description }}</div>
|
|
4000
|
-
</div>
|
|
4001
|
-
</div>
|
|
4002
|
-
}
|
|
4003
|
-
</div>
|
|
4405
|
+
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: `
|
|
4406
|
+
<div #menuRef class="slash-commands-menu">
|
|
4407
|
+
@for (command of filteredCommands(); track command.title) {
|
|
4408
|
+
<div
|
|
4409
|
+
class="slash-command-item"
|
|
4410
|
+
[class.selected]="$index === selectedIndex()"
|
|
4411
|
+
(click)="executeCommand(command)"
|
|
4412
|
+
(mouseenter)="selectedIndex.set($index)"
|
|
4413
|
+
>
|
|
4414
|
+
<div class="slash-command-icon">
|
|
4415
|
+
<span class="material-symbols-outlined">{{ command.icon }}</span>
|
|
4416
|
+
</div>
|
|
4417
|
+
<div class="slash-command-content">
|
|
4418
|
+
<div class="slash-command-title">{{ command.title }}</div>
|
|
4419
|
+
<div class="slash-command-description">{{ command.description }}</div>
|
|
4420
|
+
</div>
|
|
4421
|
+
</div>
|
|
4422
|
+
}
|
|
4423
|
+
</div>
|
|
4004
4424
|
`, 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"] }); }
|
|
4005
4425
|
}
|
|
4006
4426
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapSlashCommandsComponent, decorators: [{
|
|
4007
4427
|
type: Component,
|
|
4008
|
-
args: [{ selector: "tiptap-slash-commands", standalone: true, template: `
|
|
4009
|
-
<div #menuRef class="slash-commands-menu">
|
|
4010
|
-
@for (command of filteredCommands(); track command.title) {
|
|
4011
|
-
<div
|
|
4012
|
-
class="slash-command-item"
|
|
4013
|
-
[class.selected]="$index === selectedIndex()"
|
|
4014
|
-
(click)="executeCommand(command)"
|
|
4015
|
-
(mouseenter)="selectedIndex.set($index)"
|
|
4016
|
-
>
|
|
4017
|
-
<div class="slash-command-icon">
|
|
4018
|
-
<span class="material-symbols-outlined">{{ command.icon }}</span>
|
|
4019
|
-
</div>
|
|
4020
|
-
<div class="slash-command-content">
|
|
4021
|
-
<div class="slash-command-title">{{ command.title }}</div>
|
|
4022
|
-
<div class="slash-command-description">{{ command.description }}</div>
|
|
4023
|
-
</div>
|
|
4024
|
-
</div>
|
|
4025
|
-
}
|
|
4026
|
-
</div>
|
|
4428
|
+
args: [{ selector: "tiptap-slash-commands", standalone: true, template: `
|
|
4429
|
+
<div #menuRef class="slash-commands-menu">
|
|
4430
|
+
@for (command of filteredCommands(); track command.title) {
|
|
4431
|
+
<div
|
|
4432
|
+
class="slash-command-item"
|
|
4433
|
+
[class.selected]="$index === selectedIndex()"
|
|
4434
|
+
(click)="executeCommand(command)"
|
|
4435
|
+
(mouseenter)="selectedIndex.set($index)"
|
|
4436
|
+
>
|
|
4437
|
+
<div class="slash-command-icon">
|
|
4438
|
+
<span class="material-symbols-outlined">{{ command.icon }}</span>
|
|
4439
|
+
</div>
|
|
4440
|
+
<div class="slash-command-content">
|
|
4441
|
+
<div class="slash-command-title">{{ command.title }}</div>
|
|
4442
|
+
<div class="slash-command-description">{{ command.description }}</div>
|
|
4443
|
+
</div>
|
|
4444
|
+
</div>
|
|
4445
|
+
}
|
|
4446
|
+
</div>
|
|
4027
4447
|
`, 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"] }]
|
|
4028
4448
|
}], ctorParameters: () => [], propDecorators: { menuRef: [{
|
|
4029
4449
|
type: ViewChild,
|
|
@@ -4229,150 +4649,150 @@ class TiptapImageUploadComponent {
|
|
|
4229
4649
|
this.previewInfo.set("");
|
|
4230
4650
|
}
|
|
4231
4651
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapImageUploadComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
4232
|
-
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: `
|
|
4233
|
-
<div class="image-upload-container">
|
|
4234
|
-
<!-- Bouton d'upload -->
|
|
4235
|
-
<tiptap-button
|
|
4236
|
-
icon="image"
|
|
4237
|
-
title="Ajouter une image"
|
|
4238
|
-
[disabled]="isUploading()"
|
|
4239
|
-
(onClick)="triggerFileInput()"
|
|
4240
|
-
/>
|
|
4241
|
-
|
|
4242
|
-
<!-- Input file caché -->
|
|
4243
|
-
<input
|
|
4244
|
-
#fileInput
|
|
4245
|
-
type="file"
|
|
4246
|
-
[accept]="acceptedTypes()"
|
|
4247
|
-
[multiple]="config().multiple"
|
|
4248
|
-
(change)="onFileSelected($event)"
|
|
4249
|
-
style="display: none;"
|
|
4250
|
-
/>
|
|
4251
|
-
|
|
4252
|
-
<!-- Zone de drag & drop -->
|
|
4253
|
-
@if (config().enableDragDrop && isDragOver()) {
|
|
4254
|
-
<div
|
|
4255
|
-
class="drag-overlay"
|
|
4256
|
-
(dragover)="onDragOver($event)"
|
|
4257
|
-
(drop)="onDrop($event)"
|
|
4258
|
-
(dragleave)="onDragLeave($event)"
|
|
4259
|
-
>
|
|
4260
|
-
<div class="drag-content">
|
|
4261
|
-
<span class="material-symbols-outlined">cloud_upload</span>
|
|
4262
|
-
<p>Déposez votre image ici</p>
|
|
4263
|
-
</div>
|
|
4264
|
-
</div>
|
|
4265
|
-
}
|
|
4266
|
-
|
|
4267
|
-
<!-- Barre de progression -->
|
|
4268
|
-
@if (isUploading() && uploadProgress() > 0) {
|
|
4269
|
-
<div class="upload-progress">
|
|
4270
|
-
<div class="progress-bar">
|
|
4271
|
-
<div class="progress-fill" [style.width.%]="uploadProgress()"></div>
|
|
4272
|
-
</div>
|
|
4273
|
-
<div class="progress-text">{{ uploadProgress() }}%</div>
|
|
4274
|
-
</div>
|
|
4275
|
-
}
|
|
4276
|
-
|
|
4277
|
-
<!-- Prévisualisation -->
|
|
4278
|
-
@if (config().showPreview && previewImage()) {
|
|
4279
|
-
<div class="image-preview">
|
|
4280
|
-
<img [src]="previewImage()" alt="Prévisualisation" />
|
|
4281
|
-
<div class="preview-info">
|
|
4282
|
-
<span>{{ previewInfo() }}</span>
|
|
4283
|
-
</div>
|
|
4284
|
-
<button
|
|
4285
|
-
class="preview-close"
|
|
4286
|
-
(click)="clearPreview()"
|
|
4287
|
-
title="Fermer la prévisualisation"
|
|
4288
|
-
>
|
|
4289
|
-
<span class="material-symbols-outlined">close</span>
|
|
4290
|
-
</button>
|
|
4291
|
-
</div>
|
|
4292
|
-
}
|
|
4293
|
-
|
|
4294
|
-
<!-- Messages d'erreur -->
|
|
4295
|
-
@if (errorMessage()) {
|
|
4296
|
-
<div class="error-message">
|
|
4297
|
-
<span class="material-symbols-outlined">error</span>
|
|
4298
|
-
{{ errorMessage() }}
|
|
4299
|
-
</div>
|
|
4300
|
-
}
|
|
4301
|
-
</div>
|
|
4302
|
-
`, 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"] }] }); }
|
|
4652
|
+
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: `
|
|
4653
|
+
<div class="image-upload-container">
|
|
4654
|
+
<!-- Bouton d'upload -->
|
|
4655
|
+
<tiptap-button
|
|
4656
|
+
icon="image"
|
|
4657
|
+
title="Ajouter une image"
|
|
4658
|
+
[disabled]="isUploading()"
|
|
4659
|
+
(onClick)="triggerFileInput()"
|
|
4660
|
+
/>
|
|
4661
|
+
|
|
4662
|
+
<!-- Input file caché -->
|
|
4663
|
+
<input
|
|
4664
|
+
#fileInput
|
|
4665
|
+
type="file"
|
|
4666
|
+
[accept]="acceptedTypes()"
|
|
4667
|
+
[multiple]="config().multiple"
|
|
4668
|
+
(change)="onFileSelected($event)"
|
|
4669
|
+
style="display: none;"
|
|
4670
|
+
/>
|
|
4671
|
+
|
|
4672
|
+
<!-- Zone de drag & drop -->
|
|
4673
|
+
@if (config().enableDragDrop && isDragOver()) {
|
|
4674
|
+
<div
|
|
4675
|
+
class="drag-overlay"
|
|
4676
|
+
(dragover)="onDragOver($event)"
|
|
4677
|
+
(drop)="onDrop($event)"
|
|
4678
|
+
(dragleave)="onDragLeave($event)"
|
|
4679
|
+
>
|
|
4680
|
+
<div class="drag-content">
|
|
4681
|
+
<span class="material-symbols-outlined">cloud_upload</span>
|
|
4682
|
+
<p>Déposez votre image ici</p>
|
|
4683
|
+
</div>
|
|
4684
|
+
</div>
|
|
4685
|
+
}
|
|
4686
|
+
|
|
4687
|
+
<!-- Barre de progression -->
|
|
4688
|
+
@if (isUploading() && uploadProgress() > 0) {
|
|
4689
|
+
<div class="upload-progress">
|
|
4690
|
+
<div class="progress-bar">
|
|
4691
|
+
<div class="progress-fill" [style.width.%]="uploadProgress()"></div>
|
|
4692
|
+
</div>
|
|
4693
|
+
<div class="progress-text">{{ uploadProgress() }}%</div>
|
|
4694
|
+
</div>
|
|
4695
|
+
}
|
|
4696
|
+
|
|
4697
|
+
<!-- Prévisualisation -->
|
|
4698
|
+
@if (config().showPreview && previewImage()) {
|
|
4699
|
+
<div class="image-preview">
|
|
4700
|
+
<img [src]="previewImage()" alt="Prévisualisation" />
|
|
4701
|
+
<div class="preview-info">
|
|
4702
|
+
<span>{{ previewInfo() }}</span>
|
|
4703
|
+
</div>
|
|
4704
|
+
<button
|
|
4705
|
+
class="preview-close"
|
|
4706
|
+
(click)="clearPreview()"
|
|
4707
|
+
title="Fermer la prévisualisation"
|
|
4708
|
+
>
|
|
4709
|
+
<span class="material-symbols-outlined">close</span>
|
|
4710
|
+
</button>
|
|
4711
|
+
</div>
|
|
4712
|
+
}
|
|
4713
|
+
|
|
4714
|
+
<!-- Messages d'erreur -->
|
|
4715
|
+
@if (errorMessage()) {
|
|
4716
|
+
<div class="error-message">
|
|
4717
|
+
<span class="material-symbols-outlined">error</span>
|
|
4718
|
+
{{ errorMessage() }}
|
|
4719
|
+
</div>
|
|
4720
|
+
}
|
|
4721
|
+
</div>
|
|
4722
|
+
`, 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", "color", "variant", "size", "iconSize"], outputs: ["onClick"] }] }); }
|
|
4303
4723
|
}
|
|
4304
4724
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapImageUploadComponent, decorators: [{
|
|
4305
4725
|
type: Component,
|
|
4306
|
-
args: [{ selector: "tiptap-image-upload", standalone: true, imports: [TiptapButtonComponent], template: `
|
|
4307
|
-
<div class="image-upload-container">
|
|
4308
|
-
<!-- Bouton d'upload -->
|
|
4309
|
-
<tiptap-button
|
|
4310
|
-
icon="image"
|
|
4311
|
-
title="Ajouter une image"
|
|
4312
|
-
[disabled]="isUploading()"
|
|
4313
|
-
(onClick)="triggerFileInput()"
|
|
4314
|
-
/>
|
|
4315
|
-
|
|
4316
|
-
<!-- Input file caché -->
|
|
4317
|
-
<input
|
|
4318
|
-
#fileInput
|
|
4319
|
-
type="file"
|
|
4320
|
-
[accept]="acceptedTypes()"
|
|
4321
|
-
[multiple]="config().multiple"
|
|
4322
|
-
(change)="onFileSelected($event)"
|
|
4323
|
-
style="display: none;"
|
|
4324
|
-
/>
|
|
4325
|
-
|
|
4326
|
-
<!-- Zone de drag & drop -->
|
|
4327
|
-
@if (config().enableDragDrop && isDragOver()) {
|
|
4328
|
-
<div
|
|
4329
|
-
class="drag-overlay"
|
|
4330
|
-
(dragover)="onDragOver($event)"
|
|
4331
|
-
(drop)="onDrop($event)"
|
|
4332
|
-
(dragleave)="onDragLeave($event)"
|
|
4333
|
-
>
|
|
4334
|
-
<div class="drag-content">
|
|
4335
|
-
<span class="material-symbols-outlined">cloud_upload</span>
|
|
4336
|
-
<p>Déposez votre image ici</p>
|
|
4337
|
-
</div>
|
|
4338
|
-
</div>
|
|
4339
|
-
}
|
|
4340
|
-
|
|
4341
|
-
<!-- Barre de progression -->
|
|
4342
|
-
@if (isUploading() && uploadProgress() > 0) {
|
|
4343
|
-
<div class="upload-progress">
|
|
4344
|
-
<div class="progress-bar">
|
|
4345
|
-
<div class="progress-fill" [style.width.%]="uploadProgress()"></div>
|
|
4346
|
-
</div>
|
|
4347
|
-
<div class="progress-text">{{ uploadProgress() }}%</div>
|
|
4348
|
-
</div>
|
|
4349
|
-
}
|
|
4350
|
-
|
|
4351
|
-
<!-- Prévisualisation -->
|
|
4352
|
-
@if (config().showPreview && previewImage()) {
|
|
4353
|
-
<div class="image-preview">
|
|
4354
|
-
<img [src]="previewImage()" alt="Prévisualisation" />
|
|
4355
|
-
<div class="preview-info">
|
|
4356
|
-
<span>{{ previewInfo() }}</span>
|
|
4357
|
-
</div>
|
|
4358
|
-
<button
|
|
4359
|
-
class="preview-close"
|
|
4360
|
-
(click)="clearPreview()"
|
|
4361
|
-
title="Fermer la prévisualisation"
|
|
4362
|
-
>
|
|
4363
|
-
<span class="material-symbols-outlined">close</span>
|
|
4364
|
-
</button>
|
|
4365
|
-
</div>
|
|
4366
|
-
}
|
|
4367
|
-
|
|
4368
|
-
<!-- Messages d'erreur -->
|
|
4369
|
-
@if (errorMessage()) {
|
|
4370
|
-
<div class="error-message">
|
|
4371
|
-
<span class="material-symbols-outlined">error</span>
|
|
4372
|
-
{{ errorMessage() }}
|
|
4373
|
-
</div>
|
|
4374
|
-
}
|
|
4375
|
-
</div>
|
|
4726
|
+
args: [{ selector: "tiptap-image-upload", standalone: true, imports: [TiptapButtonComponent], template: `
|
|
4727
|
+
<div class="image-upload-container">
|
|
4728
|
+
<!-- Bouton d'upload -->
|
|
4729
|
+
<tiptap-button
|
|
4730
|
+
icon="image"
|
|
4731
|
+
title="Ajouter une image"
|
|
4732
|
+
[disabled]="isUploading()"
|
|
4733
|
+
(onClick)="triggerFileInput()"
|
|
4734
|
+
/>
|
|
4735
|
+
|
|
4736
|
+
<!-- Input file caché -->
|
|
4737
|
+
<input
|
|
4738
|
+
#fileInput
|
|
4739
|
+
type="file"
|
|
4740
|
+
[accept]="acceptedTypes()"
|
|
4741
|
+
[multiple]="config().multiple"
|
|
4742
|
+
(change)="onFileSelected($event)"
|
|
4743
|
+
style="display: none;"
|
|
4744
|
+
/>
|
|
4745
|
+
|
|
4746
|
+
<!-- Zone de drag & drop -->
|
|
4747
|
+
@if (config().enableDragDrop && isDragOver()) {
|
|
4748
|
+
<div
|
|
4749
|
+
class="drag-overlay"
|
|
4750
|
+
(dragover)="onDragOver($event)"
|
|
4751
|
+
(drop)="onDrop($event)"
|
|
4752
|
+
(dragleave)="onDragLeave($event)"
|
|
4753
|
+
>
|
|
4754
|
+
<div class="drag-content">
|
|
4755
|
+
<span class="material-symbols-outlined">cloud_upload</span>
|
|
4756
|
+
<p>Déposez votre image ici</p>
|
|
4757
|
+
</div>
|
|
4758
|
+
</div>
|
|
4759
|
+
}
|
|
4760
|
+
|
|
4761
|
+
<!-- Barre de progression -->
|
|
4762
|
+
@if (isUploading() && uploadProgress() > 0) {
|
|
4763
|
+
<div class="upload-progress">
|
|
4764
|
+
<div class="progress-bar">
|
|
4765
|
+
<div class="progress-fill" [style.width.%]="uploadProgress()"></div>
|
|
4766
|
+
</div>
|
|
4767
|
+
<div class="progress-text">{{ uploadProgress() }}%</div>
|
|
4768
|
+
</div>
|
|
4769
|
+
}
|
|
4770
|
+
|
|
4771
|
+
<!-- Prévisualisation -->
|
|
4772
|
+
@if (config().showPreview && previewImage()) {
|
|
4773
|
+
<div class="image-preview">
|
|
4774
|
+
<img [src]="previewImage()" alt="Prévisualisation" />
|
|
4775
|
+
<div class="preview-info">
|
|
4776
|
+
<span>{{ previewInfo() }}</span>
|
|
4777
|
+
</div>
|
|
4778
|
+
<button
|
|
4779
|
+
class="preview-close"
|
|
4780
|
+
(click)="clearPreview()"
|
|
4781
|
+
title="Fermer la prévisualisation"
|
|
4782
|
+
>
|
|
4783
|
+
<span class="material-symbols-outlined">close</span>
|
|
4784
|
+
</button>
|
|
4785
|
+
</div>
|
|
4786
|
+
}
|
|
4787
|
+
|
|
4788
|
+
<!-- Messages d'erreur -->
|
|
4789
|
+
@if (errorMessage()) {
|
|
4790
|
+
<div class="error-message">
|
|
4791
|
+
<span class="material-symbols-outlined">error</span>
|
|
4792
|
+
{{ errorMessage() }}
|
|
4793
|
+
</div>
|
|
4794
|
+
}
|
|
4795
|
+
</div>
|
|
4376
4796
|
`, 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"] }]
|
|
4377
4797
|
}] });
|
|
4378
4798
|
|
|
@@ -4602,6 +5022,7 @@ const DEFAULT_TOOLBAR_CONFIG = {
|
|
|
4602
5022
|
undo: true,
|
|
4603
5023
|
redo: true,
|
|
4604
5024
|
clear: false, // Désactivé par défaut (opt-in)
|
|
5025
|
+
textColor: true,
|
|
4605
5026
|
separator: true,
|
|
4606
5027
|
};
|
|
4607
5028
|
// Configuration par défaut du bubble menu
|
|
@@ -4614,6 +5035,7 @@ const DEFAULT_BUBBLE_MENU_CONFIG = {
|
|
|
4614
5035
|
superscript: false,
|
|
4615
5036
|
subscript: false,
|
|
4616
5037
|
highlight: true,
|
|
5038
|
+
textColor: true,
|
|
4617
5039
|
link: true,
|
|
4618
5040
|
separator: true,
|
|
4619
5041
|
};
|
|
@@ -4649,13 +5071,13 @@ const DEFAULT_SLASH_COMMANDS = {
|
|
|
4649
5071
|
};
|
|
4650
5072
|
class AngularTiptapEditorComponent {
|
|
4651
5073
|
constructor() {
|
|
4652
|
-
// Nouveaux inputs avec signal
|
|
4653
5074
|
this.content = input("");
|
|
4654
5075
|
this.placeholder = input("");
|
|
4655
5076
|
this.editable = input(true);
|
|
4656
5077
|
this.minHeight = input(200);
|
|
4657
5078
|
this.height = input(undefined);
|
|
4658
5079
|
this.maxHeight = input(undefined);
|
|
5080
|
+
this.fillContainer = input(false);
|
|
4659
5081
|
this.showToolbar = input(true);
|
|
4660
5082
|
this.showCharacterCount = input(true);
|
|
4661
5083
|
this.maxCharacters = input(undefined);
|
|
@@ -4664,6 +5086,8 @@ class AngularTiptapEditorComponent {
|
|
|
4664
5086
|
this.slashCommandsConfig = input(undefined);
|
|
4665
5087
|
this.locale = input(undefined);
|
|
4666
5088
|
this.autofocus = input(false);
|
|
5089
|
+
this.tiptapExtensions = input([]);
|
|
5090
|
+
this.tiptapOptions = input({});
|
|
4667
5091
|
// Nouveaux inputs pour les bubble menus
|
|
4668
5092
|
this.showBubbleMenu = input(true);
|
|
4669
5093
|
this.bubbleMenu = input(DEFAULT_BUBBLE_MENU_CONFIG);
|
|
@@ -4845,6 +5269,10 @@ class AngularTiptapEditorComponent {
|
|
|
4845
5269
|
initEditor() {
|
|
4846
5270
|
const extensions = [
|
|
4847
5271
|
StarterKit,
|
|
5272
|
+
TextStyle,
|
|
5273
|
+
Color.configure({
|
|
5274
|
+
types: ["textStyle"],
|
|
5275
|
+
}),
|
|
4848
5276
|
Placeholder.configure({
|
|
4849
5277
|
placeholder: this.placeholder() || this.i18nService.editor().placeholder,
|
|
4850
5278
|
}),
|
|
@@ -4893,7 +5321,22 @@ class AngularTiptapEditorComponent {
|
|
|
4893
5321
|
limit: this.maxCharacters(),
|
|
4894
5322
|
}));
|
|
4895
5323
|
}
|
|
5324
|
+
// Allow addition of custom extensions, but avoid duplicates by filtering by name
|
|
5325
|
+
const customExtensions = this.tiptapExtensions();
|
|
5326
|
+
if (customExtensions.length > 0) {
|
|
5327
|
+
const existingNames = new Set(extensions
|
|
5328
|
+
.map((ext) => ext?.name)
|
|
5329
|
+
.filter((name) => !!name));
|
|
5330
|
+
const filteredCustom = customExtensions.filter((ext) => {
|
|
5331
|
+
const name = ext?.name;
|
|
5332
|
+
return !name || !existingNames.has(name);
|
|
5333
|
+
});
|
|
5334
|
+
extensions.push(...filteredCustom);
|
|
5335
|
+
}
|
|
5336
|
+
// Also allow any tiptap user options
|
|
5337
|
+
const userOptions = this.tiptapOptions();
|
|
4896
5338
|
const newEditor = new Editor({
|
|
5339
|
+
...userOptions,
|
|
4897
5340
|
element: this.editorElement().nativeElement,
|
|
4898
5341
|
extensions,
|
|
4899
5342
|
content: this.content(),
|
|
@@ -5077,8 +5520,8 @@ class AngularTiptapEditorComponent {
|
|
|
5077
5520
|
}
|
|
5078
5521
|
}
|
|
5079
5522
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: AngularTiptapEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
5080
|
-
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 }, autofocus: { classPropertyName: "autofocus", publicName: "autofocus", 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 }, imageUploadHandler: { classPropertyName: "imageUploadHandler", publicName: "imageUploadHandler", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { contentChange: "contentChange", editorCreated: "editorCreated", editorUpdate: "editorUpdate", editorFocus: "editorFocus", editorBlur: "editorBlur" }, viewQueries: [{ propertyName: "editorElement", first: true, predicate: ["editorElement"], descendants: true, isSignal: true }], hostDirectives: [{ directive: NoopValueAccessorDirective }], ngImport: i0, template: `
|
|
5081
|
-
<div class="tiptap-editor">
|
|
5523
|
+
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 }, fillContainer: { classPropertyName: "fillContainer", publicName: "fillContainer", 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 }, autofocus: { classPropertyName: "autofocus", publicName: "autofocus", isSignal: true, isRequired: false, transformFunction: null }, tiptapExtensions: { classPropertyName: "tiptapExtensions", publicName: "tiptapExtensions", isSignal: true, isRequired: false, transformFunction: null }, tiptapOptions: { classPropertyName: "tiptapOptions", publicName: "tiptapOptions", 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 }, imageUploadHandler: { classPropertyName: "imageUploadHandler", publicName: "imageUploadHandler", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { contentChange: "contentChange", editorCreated: "editorCreated", editorUpdate: "editorUpdate", editorFocus: "editorFocus", editorBlur: "editorBlur" }, host: { properties: { "class.fill-container": "fillContainer()" } }, viewQueries: [{ propertyName: "editorElement", first: true, predicate: ["editorElement"], descendants: true, isSignal: true }], hostDirectives: [{ directive: NoopValueAccessorDirective }], ngImport: i0, template: `
|
|
5524
|
+
<div class="tiptap-editor" [class.fill-container]="fillContainer()">
|
|
5082
5525
|
<!-- Toolbar -->
|
|
5083
5526
|
@if (showToolbar() && editor()) {
|
|
5084
5527
|
<tiptap-toolbar [editor]="editor()!" [config]="toolbarConfig()">
|
|
@@ -5163,11 +5606,13 @@ class AngularTiptapEditorComponent {
|
|
|
5163
5606
|
</div>
|
|
5164
5607
|
}
|
|
5165
5608
|
</div>
|
|
5166
|
-
`, isInline: true, styles: [".tiptap-editor{border:2px solid #e2e8f0;border-radius:8px;background:#fff;overflow:hidden;transition:border-color .2s ease}.tiptap-editor:focus-within{border-color:#3182ce}.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 p.is-editor-empty:first-child:before{content:attr(data-placeholder);color:#a0aec0;pointer-events:none;float:left;height:0}: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.ProseMirror-selectednode{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}:host ::ng-deep .ProseMirror table{border-collapse:separate;border-spacing:0;margin:0;table-layout:fixed;width:100%;border-radius:8px;overflow:hidden}:host ::ng-deep .ProseMirror table td,:host ::ng-deep .ProseMirror table th{border:none;border-right:1px solid #e2e8f0;border-bottom:1px solid #e2e8f0;box-sizing:border-box;min-width:1em;padding:8px 12px;position:relative;vertical-align:top;background:#fff}:host ::ng-deep .ProseMirror table td:first-child,:host ::ng-deep .ProseMirror table th:first-child{border-left:1px solid #e2e8f0}:host ::ng-deep .ProseMirror table tr:first-child td,:host ::ng-deep .ProseMirror table tr:first-child th{border-top:1px solid #e2e8f0}:host ::ng-deep .ProseMirror table tr:first-child th:first-child{border-top-left-radius:8px}:host ::ng-deep .ProseMirror table tr:first-child th:last-child{border-top-right-radius:8px}:host ::ng-deep .ProseMirror table tr:last-child td:first-child{border-bottom-left-radius:8px}:host ::ng-deep .ProseMirror table tr:last-child td:last-child{border-bottom-right-radius:8px}:host ::ng-deep .ProseMirror table th{background:#f8f9fa;font-weight:600;color:#374151}:host ::ng-deep .ProseMirror table .selectedCell:after{background:#c8c8ff66;content:\"\";inset:0;pointer-events:none;position:absolute;z-index:2}:host ::ng-deep .ProseMirror table .column-resize-handle{position:absolute;right:-2px;top:0;bottom:0;width:4px;background-color:#6366f1;opacity:0;transition:opacity .2s ease}:host ::ng-deep .ProseMirror table:hover .column-resize-handle{opacity:1}:host ::ng-deep .ProseMirror table .column-resize-handle:hover{background-color:#4f46e5}:host ::ng-deep .ProseMirror .tableWrapper{overflow-x:auto;margin:1em 0;border-radius:8px}:host ::ng-deep .ProseMirror .tableWrapper table{margin:0;border-radius:8px;min-width:600px;overflow:hidden}:host ::ng-deep .ProseMirror table p{margin:0}:host ::ng-deep .ProseMirror table .selectedCell{background-color:#6366f11a}:host ::ng-deep .ProseMirror table th{background-color:#f8f9fa;font-weight:600;color:#374151;text-align:left}:host ::ng-deep .ProseMirror table tbody tr:nth-child(2n){background-color:#fafbfc}:host ::ng-deep .ProseMirror table tbody tr:hover{background-color:#f1f5f9}\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: TiptapTableBubbleMenuComponent, selector: "tiptap-table-bubble-menu", inputs: ["editor", "config"] }, { kind: "component", type: TiptapCellBubbleMenuComponent, selector: "tiptap-cell-bubble-menu", inputs: ["editor", "config"] }, { kind: "component", type: TiptapSlashCommandsComponent, selector: "tiptap-slash-commands", inputs: ["editor", "config"], outputs: ["imageUploadRequested"] }] }); }
|
|
5609
|
+
`, isInline: true, styles: [":host(.fill-container){display:block;height:100%}.tiptap-editor{border:2px solid #e2e8f0;border-radius:8px;background:#fff;overflow:hidden;transition:border-color .2s ease}.tiptap-editor.fill-container{display:flex;flex-direction:column;height:100%}.tiptap-editor.fill-container .tiptap-content{flex:1;min-height:0;overflow-y:auto}.tiptap-editor:focus-within{border-color:#3182ce}.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 p.is-editor-empty:first-child:before{content:attr(data-placeholder);color:#a0aec0;pointer-events:none;float:left;height:0}: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.ProseMirror-selectednode{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}:host ::ng-deep .ProseMirror table{border-collapse:separate;border-spacing:0;margin:0;table-layout:fixed;width:100%;border-radius:8px;overflow:hidden}:host ::ng-deep .ProseMirror table td,:host ::ng-deep .ProseMirror table th{border:none;border-right:1px solid #e2e8f0;border-bottom:1px solid #e2e8f0;box-sizing:border-box;min-width:1em;padding:8px 12px;position:relative;vertical-align:top;background:#fff}:host ::ng-deep .ProseMirror table td:first-child,:host ::ng-deep .ProseMirror table th:first-child{border-left:1px solid #e2e8f0}:host ::ng-deep .ProseMirror table tr:first-child td,:host ::ng-deep .ProseMirror table tr:first-child th{border-top:1px solid #e2e8f0}:host ::ng-deep .ProseMirror table tr:first-child th:first-child{border-top-left-radius:8px}:host ::ng-deep .ProseMirror table tr:first-child th:last-child{border-top-right-radius:8px}:host ::ng-deep .ProseMirror table tr:last-child td:first-child{border-bottom-left-radius:8px}:host ::ng-deep .ProseMirror table tr:last-child td:last-child{border-bottom-right-radius:8px}:host ::ng-deep .ProseMirror table th{background:#f8f9fa;font-weight:600;color:#374151}:host ::ng-deep .ProseMirror table .selectedCell:after{background:#c8c8ff66;content:\"\";inset:0;pointer-events:none;position:absolute;z-index:2}:host ::ng-deep .ProseMirror table .column-resize-handle{position:absolute;right:-2px;top:0;bottom:0;width:4px;background-color:#6366f1;opacity:0;transition:opacity .2s ease}:host ::ng-deep .ProseMirror table:hover .column-resize-handle{opacity:1}:host ::ng-deep .ProseMirror table .column-resize-handle:hover{background-color:#4f46e5}:host ::ng-deep .ProseMirror .tableWrapper{overflow-x:auto;margin:1em 0;border-radius:8px}:host ::ng-deep .ProseMirror .tableWrapper table{margin:0;border-radius:8px;min-width:600px;overflow:hidden}:host ::ng-deep .ProseMirror table p{margin:0}:host ::ng-deep .ProseMirror table .selectedCell{background-color:#6366f11a}:host ::ng-deep .ProseMirror table th{background-color:#f8f9fa;font-weight:600;color:#374151;text-align:left}:host ::ng-deep .ProseMirror table tbody tr:nth-child(2n){background-color:#fafbfc}:host ::ng-deep .ProseMirror table tbody tr:hover{background-color:#f1f5f9}\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: TiptapTableBubbleMenuComponent, selector: "tiptap-table-bubble-menu", inputs: ["editor", "config"] }, { kind: "component", type: TiptapCellBubbleMenuComponent, selector: "tiptap-cell-bubble-menu", inputs: ["editor", "config"] }, { kind: "component", type: TiptapSlashCommandsComponent, selector: "tiptap-slash-commands", inputs: ["editor", "config"], outputs: ["imageUploadRequested"] }] }); }
|
|
5167
5610
|
}
|
|
5168
5611
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: AngularTiptapEditorComponent, decorators: [{
|
|
5169
5612
|
type: Component,
|
|
5170
|
-
args: [{ selector: "angular-tiptap-editor", standalone: true, hostDirectives: [NoopValueAccessorDirective],
|
|
5613
|
+
args: [{ selector: "angular-tiptap-editor", standalone: true, hostDirectives: [NoopValueAccessorDirective], host: {
|
|
5614
|
+
'[class.fill-container]': 'fillContainer()',
|
|
5615
|
+
}, imports: [
|
|
5171
5616
|
TiptapToolbarComponent,
|
|
5172
5617
|
TiptapImageUploadComponent,
|
|
5173
5618
|
TiptapBubbleMenuComponent,
|
|
@@ -5176,7 +5621,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImpor
|
|
|
5176
5621
|
TiptapCellBubbleMenuComponent,
|
|
5177
5622
|
TiptapSlashCommandsComponent,
|
|
5178
5623
|
], template: `
|
|
5179
|
-
<div class="tiptap-editor">
|
|
5624
|
+
<div class="tiptap-editor" [class.fill-container]="fillContainer()">
|
|
5180
5625
|
<!-- Toolbar -->
|
|
5181
5626
|
@if (showToolbar() && editor()) {
|
|
5182
5627
|
<tiptap-toolbar [editor]="editor()!" [config]="toolbarConfig()">
|
|
@@ -5261,7 +5706,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImpor
|
|
|
5261
5706
|
</div>
|
|
5262
5707
|
}
|
|
5263
5708
|
</div>
|
|
5264
|
-
`, styles: [".tiptap-editor{border:2px solid #e2e8f0;border-radius:8px;background:#fff;overflow:hidden;transition:border-color .2s ease}.tiptap-editor:focus-within{border-color:#3182ce}.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 p.is-editor-empty:first-child:before{content:attr(data-placeholder);color:#a0aec0;pointer-events:none;float:left;height:0}: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.ProseMirror-selectednode{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}:host ::ng-deep .ProseMirror table{border-collapse:separate;border-spacing:0;margin:0;table-layout:fixed;width:100%;border-radius:8px;overflow:hidden}:host ::ng-deep .ProseMirror table td,:host ::ng-deep .ProseMirror table th{border:none;border-right:1px solid #e2e8f0;border-bottom:1px solid #e2e8f0;box-sizing:border-box;min-width:1em;padding:8px 12px;position:relative;vertical-align:top;background:#fff}:host ::ng-deep .ProseMirror table td:first-child,:host ::ng-deep .ProseMirror table th:first-child{border-left:1px solid #e2e8f0}:host ::ng-deep .ProseMirror table tr:first-child td,:host ::ng-deep .ProseMirror table tr:first-child th{border-top:1px solid #e2e8f0}:host ::ng-deep .ProseMirror table tr:first-child th:first-child{border-top-left-radius:8px}:host ::ng-deep .ProseMirror table tr:first-child th:last-child{border-top-right-radius:8px}:host ::ng-deep .ProseMirror table tr:last-child td:first-child{border-bottom-left-radius:8px}:host ::ng-deep .ProseMirror table tr:last-child td:last-child{border-bottom-right-radius:8px}:host ::ng-deep .ProseMirror table th{background:#f8f9fa;font-weight:600;color:#374151}:host ::ng-deep .ProseMirror table .selectedCell:after{background:#c8c8ff66;content:\"\";inset:0;pointer-events:none;position:absolute;z-index:2}:host ::ng-deep .ProseMirror table .column-resize-handle{position:absolute;right:-2px;top:0;bottom:0;width:4px;background-color:#6366f1;opacity:0;transition:opacity .2s ease}:host ::ng-deep .ProseMirror table:hover .column-resize-handle{opacity:1}:host ::ng-deep .ProseMirror table .column-resize-handle:hover{background-color:#4f46e5}:host ::ng-deep .ProseMirror .tableWrapper{overflow-x:auto;margin:1em 0;border-radius:8px}:host ::ng-deep .ProseMirror .tableWrapper table{margin:0;border-radius:8px;min-width:600px;overflow:hidden}:host ::ng-deep .ProseMirror table p{margin:0}:host ::ng-deep .ProseMirror table .selectedCell{background-color:#6366f11a}:host ::ng-deep .ProseMirror table th{background-color:#f8f9fa;font-weight:600;color:#374151;text-align:left}:host ::ng-deep .ProseMirror table tbody tr:nth-child(2n){background-color:#fafbfc}:host ::ng-deep .ProseMirror table tbody tr:hover{background-color:#f1f5f9}\n"] }]
|
|
5709
|
+
`, styles: [":host(.fill-container){display:block;height:100%}.tiptap-editor{border:2px solid #e2e8f0;border-radius:8px;background:#fff;overflow:hidden;transition:border-color .2s ease}.tiptap-editor.fill-container{display:flex;flex-direction:column;height:100%}.tiptap-editor.fill-container .tiptap-content{flex:1;min-height:0;overflow-y:auto}.tiptap-editor:focus-within{border-color:#3182ce}.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 p.is-editor-empty:first-child:before{content:attr(data-placeholder);color:#a0aec0;pointer-events:none;float:left;height:0}: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.ProseMirror-selectednode{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}:host ::ng-deep .ProseMirror table{border-collapse:separate;border-spacing:0;margin:0;table-layout:fixed;width:100%;border-radius:8px;overflow:hidden}:host ::ng-deep .ProseMirror table td,:host ::ng-deep .ProseMirror table th{border:none;border-right:1px solid #e2e8f0;border-bottom:1px solid #e2e8f0;box-sizing:border-box;min-width:1em;padding:8px 12px;position:relative;vertical-align:top;background:#fff}:host ::ng-deep .ProseMirror table td:first-child,:host ::ng-deep .ProseMirror table th:first-child{border-left:1px solid #e2e8f0}:host ::ng-deep .ProseMirror table tr:first-child td,:host ::ng-deep .ProseMirror table tr:first-child th{border-top:1px solid #e2e8f0}:host ::ng-deep .ProseMirror table tr:first-child th:first-child{border-top-left-radius:8px}:host ::ng-deep .ProseMirror table tr:first-child th:last-child{border-top-right-radius:8px}:host ::ng-deep .ProseMirror table tr:last-child td:first-child{border-bottom-left-radius:8px}:host ::ng-deep .ProseMirror table tr:last-child td:last-child{border-bottom-right-radius:8px}:host ::ng-deep .ProseMirror table th{background:#f8f9fa;font-weight:600;color:#374151}:host ::ng-deep .ProseMirror table .selectedCell:after{background:#c8c8ff66;content:\"\";inset:0;pointer-events:none;position:absolute;z-index:2}:host ::ng-deep .ProseMirror table .column-resize-handle{position:absolute;right:-2px;top:0;bottom:0;width:4px;background-color:#6366f1;opacity:0;transition:opacity .2s ease}:host ::ng-deep .ProseMirror table:hover .column-resize-handle{opacity:1}:host ::ng-deep .ProseMirror table .column-resize-handle:hover{background-color:#4f46e5}:host ::ng-deep .ProseMirror .tableWrapper{overflow-x:auto;margin:1em 0;border-radius:8px}:host ::ng-deep .ProseMirror .tableWrapper table{margin:0;border-radius:8px;min-width:600px;overflow:hidden}:host ::ng-deep .ProseMirror table p{margin:0}:host ::ng-deep .ProseMirror table .selectedCell{background-color:#6366f11a}:host ::ng-deep .ProseMirror table th{background-color:#f8f9fa;font-weight:600;color:#374151;text-align:left}:host ::ng-deep .ProseMirror table tbody tr:nth-child(2n){background-color:#fafbfc}:host ::ng-deep .ProseMirror table tbody tr:hover{background-color:#f1f5f9}\n"] }]
|
|
5265
5710
|
}], ctorParameters: () => [] });
|
|
5266
5711
|
|
|
5267
5712
|
/*
|