@flogeez/angular-tiptap-editor 0.3.7 → 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.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, output, Component, computed, effect, ViewChild, signal, Injectable, inject, Directive, viewChild, DestroyRef } from '@angular/core';
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 TiptapBubbleMenuComponent {
596
- // Effect comme propriété de classe pour éviter l'erreur d'injection context
600
+ class ColorPickerService {
597
601
  constructor() {
598
- this.editor = input.required();
599
- this.config = input({
600
- bold: true,
601
- italic: true,
602
- underline: true,
603
- strike: true,
604
- code: true,
605
- superscript: false,
606
- subscript: false,
607
- highlight: true,
608
- link: true,
609
- separator: true,
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
- // Nettoyer les anciens listeners
670
- ed.off("selectionUpdate", this.updateMenu);
671
- ed.off("transaction", this.updateMenu);
672
- ed.off("focus", this.updateMenu);
673
- ed.off("blur", this.handleBlur);
674
- // Ajouter les nouveaux listeners
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 menuElement = this.menuRef.nativeElement;
712
- // S'assurer qu'il n'y a pas déjà une instance
713
- if (this.tippyInstance) {
714
- this.tippyInstance.destroy();
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
- // Créer l'instance Tippy
717
- this.tippyInstance = tippy(document.body, {
718
- content: menuElement,
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
- const range = selection.getRangeAt(0);
759
- return range.getBoundingClientRect();
760
- }
761
- hideOtherMenus() {
762
- // Cette méthode peut être étendue pour fermer d'autres menus si nécessaire
763
- // Pour l'instant, elle sert de placeholder pour une future coordination entre menus
764
- }
765
- showTippy() {
766
- if (!this.tippyInstance)
767
- return;
768
- // Mettre à jour la position
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
- isActive(mark) {
780
- const ed = this.editor();
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: TiptapBubbleMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
822
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.0", type: TiptapBubbleMenuComponent, isStandalone: true, selector: "tiptap-bubble-menu", inputs: { editor: { classPropertyName: "editor", publicName: "editor", isSignal: true, isRequired: true, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "menuRef", first: true, predicate: ["menuRef"], descendants: true }], ngImport: i0, template: `
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: TiptapSeparatorComponent, decorators: [{
990
- type: Component,
991
- args: [{ selector: "tiptap-separator", standalone: true, template: `
992
- <div
993
- class="tiptap-separator"
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
- autoDetectLocale() {
2158
- this.detectBrowserLanguage();
2330
+ ngOnInit() {
2331
+ // Initialiser Tippy de manière synchrone après que le component soit ready
2332
+ this.initTippy();
2159
2333
  }
2160
- getSupportedLocales() {
2161
- return Object.keys(this._translations());
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
- addTranslations(locale, translations) {
2164
- this._translations.update((current) => ({
2165
- ...current,
2166
- [locale]: {
2167
- ...current[locale],
2168
- ...translations,
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
- detectBrowserLanguage() {
2173
- const browserLang = navigator.language.toLowerCase();
2174
- if (browserLang.startsWith("fr")) {
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
- // Méthodes utilitaires pour les composants
2182
- getToolbarTitle(key) {
2183
- return this.translations().toolbar[key];
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
- getBubbleMenuTitle(key) {
2186
- return this.translations().bubbleMenu[key];
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
- getSlashCommand(key) {
2189
- return this.translations().slashCommands[key];
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: TiptapI18nService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2192
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapI18nService, providedIn: "root" }); }
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: TiptapI18nService, decorators: [{
2195
- type: Injectable,
2196
- args: [{
2197
- providedIn: "root",
2198
- }]
2199
- }], ctorParameters: () => [] });
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: [TiptapButtonComponent, TiptapSeparatorComponent], template: `
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
  };
@@ -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,7 +5520,7 @@ 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 }, 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 }, 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: `
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: `
5081
5524
  <div class="tiptap-editor" [class.fill-container]="fillContainer()">
5082
5525
  <!-- Toolbar -->
5083
5526
  @if (showToolbar() && editor()) {