@flogeez/angular-tiptap-editor 0.2.7 → 0.3.2

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, signal, computed, Injectable, inject, effect, ViewChild, Directive, viewChild, DestroyRef } from '@angular/core';
2
+ import { input, output, Component, computed, effect, ViewChild, signal, Injectable, inject, Directive, viewChild, 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';
@@ -14,7 +14,13 @@ import Highlight from '@tiptap/extension-highlight';
14
14
  import OfficePaste from '@intevation/tiptap-extension-office-paste';
15
15
  import { Plugin, PluginKey } from '@tiptap/pm/state';
16
16
  import { DecorationSet, Decoration } from '@tiptap/pm/view';
17
+ import Table from '@tiptap/extension-table';
18
+ import TableRow from '@tiptap/extension-table-row';
19
+ import TableCell from '@tiptap/extension-table-cell';
20
+ import TableHeader from '@tiptap/extension-table-header';
17
21
  import tippy from 'tippy.js';
22
+ import { CellSelection } from '@tiptap/pm/tables';
23
+ import { CommonModule } from '@angular/common';
18
24
  import { Plugin as Plugin$1, PluginKey as PluginKey$1 } from 'prosemirror-state';
19
25
  import { NG_VALUE_ACCESSOR, NgControl } from '@angular/forms';
20
26
  import { concat, defer, of, tap } from 'rxjs';
@@ -488,6 +494,30 @@ const UploadProgress = Extension.create({
488
494
  },
489
495
  });
490
496
 
497
+ const TableExtension = Extension.create({
498
+ name: "tableExtension",
499
+ addExtensions() {
500
+ return [
501
+ Table.configure({
502
+ resizable: true,
503
+ handleWidth: 5,
504
+ cellMinWidth: 100,
505
+ }),
506
+ TableRow,
507
+ TableHeader.configure({
508
+ HTMLAttributes: {
509
+ class: "table-header",
510
+ },
511
+ }),
512
+ TableCell.configure({
513
+ HTMLAttributes: {
514
+ class: "table-cell",
515
+ },
516
+ }),
517
+ ];
518
+ },
519
+ });
520
+
491
521
  class TiptapButtonComponent {
492
522
  constructor() {
493
523
  // Inputs
@@ -562,6 +592,393 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImpor
562
592
  `, 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"] }]
563
593
  }] });
564
594
 
595
+ class TiptapBubbleMenuComponent {
596
+ // Effect comme propriété de classe pour éviter l'erreur d'injection context
597
+ 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
+ console.log("TextBubbleMenu - updateMenu:", {
642
+ hasTextSelection,
643
+ isImageSelected,
644
+ isTableCellSelected,
645
+ hasCellSelection,
646
+ selectionType: selection.constructor.name,
647
+ selectionEmpty: selection.empty,
648
+ from,
649
+ to,
650
+ });
651
+ // Ne montrer le menu texte que si :
652
+ // - Il y a une sélection de texte (pas une sélection de cellules multiples)
653
+ // - Aucune image n'est sélectionnée (priorité aux images)
654
+ // - Ce n'est pas une sélection de cellules multiples (CellSelection)
655
+ // - L'éditeur est éditable
656
+ // Note: Le texte dans une cellule est autorisé (isTableCellSelected peut être true)
657
+ const shouldShow = hasTextSelection &&
658
+ !isImageSelected &&
659
+ !hasCellSelection &&
660
+ ed.isEditable;
661
+ if (shouldShow) {
662
+ this.showTippy();
663
+ }
664
+ else {
665
+ this.hideTippy();
666
+ }
667
+ }, 10);
668
+ };
669
+ this.handleBlur = () => {
670
+ // Masquer le menu quand l'éditeur perd le focus
671
+ setTimeout(() => {
672
+ this.hideTippy();
673
+ }, 100);
674
+ };
675
+ effect(() => {
676
+ const ed = this.editor();
677
+ if (!ed)
678
+ return;
679
+ // Nettoyer les anciens listeners
680
+ ed.off("selectionUpdate", this.updateMenu);
681
+ ed.off("transaction", this.updateMenu);
682
+ ed.off("focus", this.updateMenu);
683
+ ed.off("blur", this.handleBlur);
684
+ // Ajouter les nouveaux listeners
685
+ ed.on("selectionUpdate", this.updateMenu);
686
+ ed.on("transaction", this.updateMenu);
687
+ ed.on("focus", this.updateMenu);
688
+ ed.on("blur", this.handleBlur);
689
+ // Ne pas appeler updateMenu() ici pour éviter l'affichage prématuré
690
+ // Il sera appelé automatiquement quand l'éditeur sera prêt
691
+ });
692
+ }
693
+ ngOnInit() {
694
+ // Initialiser Tippy de manière synchrone après que le component soit ready
695
+ this.initTippy();
696
+ }
697
+ ngOnDestroy() {
698
+ const ed = this.editor();
699
+ if (ed) {
700
+ ed.off("selectionUpdate", this.updateMenu);
701
+ ed.off("transaction", this.updateMenu);
702
+ ed.off("focus", this.updateMenu);
703
+ ed.off("blur", this.handleBlur);
704
+ }
705
+ // Nettoyer les timeouts
706
+ if (this.updateTimeout) {
707
+ clearTimeout(this.updateTimeout);
708
+ }
709
+ // Nettoyer Tippy
710
+ if (this.tippyInstance) {
711
+ this.tippyInstance.destroy();
712
+ this.tippyInstance = null;
713
+ }
714
+ }
715
+ initTippy() {
716
+ // Attendre que l'élément soit disponible
717
+ if (!this.menuRef?.nativeElement) {
718
+ setTimeout(() => this.initTippy(), 50);
719
+ return;
720
+ }
721
+ const menuElement = this.menuRef.nativeElement;
722
+ // S'assurer qu'il n'y a pas déjà une instance
723
+ if (this.tippyInstance) {
724
+ this.tippyInstance.destroy();
725
+ }
726
+ // Créer l'instance Tippy
727
+ this.tippyInstance = tippy(document.body, {
728
+ content: menuElement,
729
+ trigger: "manual",
730
+ placement: "top-start",
731
+ appendTo: () => document.body,
732
+ interactive: true,
733
+ arrow: false,
734
+ offset: [0, 8],
735
+ hideOnClick: false,
736
+ onShow: (instance) => {
737
+ // S'assurer que les autres menus sont fermés
738
+ this.hideOtherMenus();
739
+ },
740
+ getReferenceClientRect: () => this.getSelectionRect(),
741
+ // Améliorer le positionnement avec scroll
742
+ popperOptions: {
743
+ modifiers: [
744
+ {
745
+ name: "preventOverflow",
746
+ options: {
747
+ boundary: "viewport",
748
+ padding: 8,
749
+ },
750
+ },
751
+ {
752
+ name: "flip",
753
+ options: {
754
+ fallbackPlacements: ["bottom-start", "top-end", "bottom-end"],
755
+ },
756
+ },
757
+ ],
758
+ },
759
+ });
760
+ // Maintenant que Tippy est initialisé, faire un premier check
761
+ this.updateMenu();
762
+ }
763
+ getSelectionRect() {
764
+ const selection = window.getSelection();
765
+ if (!selection || selection.rangeCount === 0) {
766
+ return new DOMRect(0, 0, 0, 0);
767
+ }
768
+ const range = selection.getRangeAt(0);
769
+ return range.getBoundingClientRect();
770
+ }
771
+ hideOtherMenus() {
772
+ // Cette méthode peut être étendue pour fermer d'autres menus si nécessaire
773
+ // Pour l'instant, elle sert de placeholder pour une future coordination entre menus
774
+ }
775
+ showTippy() {
776
+ if (!this.tippyInstance)
777
+ return;
778
+ // Mettre à jour la position
779
+ this.tippyInstance.setProps({
780
+ getReferenceClientRect: () => this.getSelectionRect(),
781
+ });
782
+ this.tippyInstance.show();
783
+ }
784
+ hideTippy() {
785
+ if (this.tippyInstance) {
786
+ this.tippyInstance.hide();
787
+ }
788
+ }
789
+ isActive(mark) {
790
+ const ed = this.editor();
791
+ return ed?.isActive(mark) || false;
792
+ }
793
+ onCommand(command, event) {
794
+ event.preventDefault();
795
+ const ed = this.editor();
796
+ if (!ed)
797
+ return;
798
+ switch (command) {
799
+ case "bold":
800
+ ed.chain().focus().toggleBold().run();
801
+ break;
802
+ case "italic":
803
+ ed.chain().focus().toggleItalic().run();
804
+ break;
805
+ case "underline":
806
+ ed.chain().focus().toggleUnderline().run();
807
+ break;
808
+ case "strike":
809
+ ed.chain().focus().toggleStrike().run();
810
+ break;
811
+ case "code":
812
+ ed.chain().focus().toggleCode().run();
813
+ break;
814
+ case "superscript":
815
+ ed.chain().focus().toggleSuperscript().run();
816
+ break;
817
+ case "subscript":
818
+ ed.chain().focus().toggleSubscript().run();
819
+ break;
820
+ case "highlight":
821
+ ed.chain().focus().toggleHighlight().run();
822
+ break;
823
+ case "link":
824
+ const href = window.prompt("URL du lien:");
825
+ if (href) {
826
+ ed.chain().focus().toggleLink({ href }).run();
827
+ }
828
+ break;
829
+ }
830
+ }
831
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapBubbleMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
832
+ 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: `
833
+ <div #menuRef class="bubble-menu">
834
+ @if (bubbleMenuConfig().bold) {
835
+ <tiptap-button
836
+ icon="format_bold"
837
+ title="Gras"
838
+ [active]="isActive('bold')"
839
+ (click)="onCommand('bold', $event)"
840
+ ></tiptap-button>
841
+ } @if (bubbleMenuConfig().italic) {
842
+ <tiptap-button
843
+ icon="format_italic"
844
+ title="Italique"
845
+ [active]="isActive('italic')"
846
+ (click)="onCommand('italic', $event)"
847
+ ></tiptap-button>
848
+ } @if (bubbleMenuConfig().underline) {
849
+ <tiptap-button
850
+ icon="format_underlined"
851
+ title="Souligné"
852
+ [active]="isActive('underline')"
853
+ (click)="onCommand('underline', $event)"
854
+ ></tiptap-button>
855
+ } @if (bubbleMenuConfig().strike) {
856
+ <tiptap-button
857
+ icon="strikethrough_s"
858
+ title="Barré"
859
+ [active]="isActive('strike')"
860
+ (click)="onCommand('strike', $event)"
861
+ ></tiptap-button>
862
+ } @if (bubbleMenuConfig().superscript) {
863
+ <tiptap-button
864
+ icon="superscript"
865
+ title="Exposant"
866
+ [active]="isActive('superscript')"
867
+ (click)="onCommand('superscript', $event)"
868
+ ></tiptap-button>
869
+ } @if (bubbleMenuConfig().subscript) {
870
+ <tiptap-button
871
+ icon="subscript"
872
+ title="Indice"
873
+ [active]="isActive('subscript')"
874
+ (click)="onCommand('subscript', $event)"
875
+ ></tiptap-button>
876
+ } @if (bubbleMenuConfig().highlight) {
877
+ <tiptap-button
878
+ icon="highlight"
879
+ title="Surbrillance"
880
+ [active]="isActive('highlight')"
881
+ (click)="onCommand('highlight', $event)"
882
+ ></tiptap-button>
883
+ } @if (bubbleMenuConfig().separator && (bubbleMenuConfig().code ||
884
+ bubbleMenuConfig().link)) {
885
+ <div class="tiptap-separator"></div>
886
+ } @if (bubbleMenuConfig().code) {
887
+ <tiptap-button
888
+ icon="code"
889
+ title="Code"
890
+ [active]="isActive('code')"
891
+ (click)="onCommand('code', $event)"
892
+ ></tiptap-button>
893
+ } @if (bubbleMenuConfig().link) {
894
+ <tiptap-button
895
+ icon="link"
896
+ title="Lien"
897
+ [active]="isActive('link')"
898
+ (click)="onCommand('link', $event)"
899
+ ></tiptap-button>
900
+ }
901
+ </div>
902
+ `, isInline: true, dependencies: [{ kind: "component", type: TiptapButtonComponent, selector: "tiptap-button", inputs: ["icon", "title", "active", "disabled", "variant", "size", "iconSize"], outputs: ["onClick"] }] }); }
903
+ }
904
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapBubbleMenuComponent, decorators: [{
905
+ type: Component,
906
+ args: [{ selector: "tiptap-bubble-menu", standalone: true, imports: [TiptapButtonComponent], template: `
907
+ <div #menuRef class="bubble-menu">
908
+ @if (bubbleMenuConfig().bold) {
909
+ <tiptap-button
910
+ icon="format_bold"
911
+ title="Gras"
912
+ [active]="isActive('bold')"
913
+ (click)="onCommand('bold', $event)"
914
+ ></tiptap-button>
915
+ } @if (bubbleMenuConfig().italic) {
916
+ <tiptap-button
917
+ icon="format_italic"
918
+ title="Italique"
919
+ [active]="isActive('italic')"
920
+ (click)="onCommand('italic', $event)"
921
+ ></tiptap-button>
922
+ } @if (bubbleMenuConfig().underline) {
923
+ <tiptap-button
924
+ icon="format_underlined"
925
+ title="Souligné"
926
+ [active]="isActive('underline')"
927
+ (click)="onCommand('underline', $event)"
928
+ ></tiptap-button>
929
+ } @if (bubbleMenuConfig().strike) {
930
+ <tiptap-button
931
+ icon="strikethrough_s"
932
+ title="Barré"
933
+ [active]="isActive('strike')"
934
+ (click)="onCommand('strike', $event)"
935
+ ></tiptap-button>
936
+ } @if (bubbleMenuConfig().superscript) {
937
+ <tiptap-button
938
+ icon="superscript"
939
+ title="Exposant"
940
+ [active]="isActive('superscript')"
941
+ (click)="onCommand('superscript', $event)"
942
+ ></tiptap-button>
943
+ } @if (bubbleMenuConfig().subscript) {
944
+ <tiptap-button
945
+ icon="subscript"
946
+ title="Indice"
947
+ [active]="isActive('subscript')"
948
+ (click)="onCommand('subscript', $event)"
949
+ ></tiptap-button>
950
+ } @if (bubbleMenuConfig().highlight) {
951
+ <tiptap-button
952
+ icon="highlight"
953
+ title="Surbrillance"
954
+ [active]="isActive('highlight')"
955
+ (click)="onCommand('highlight', $event)"
956
+ ></tiptap-button>
957
+ } @if (bubbleMenuConfig().separator && (bubbleMenuConfig().code ||
958
+ bubbleMenuConfig().link)) {
959
+ <div class="tiptap-separator"></div>
960
+ } @if (bubbleMenuConfig().code) {
961
+ <tiptap-button
962
+ icon="code"
963
+ title="Code"
964
+ [active]="isActive('code')"
965
+ (click)="onCommand('code', $event)"
966
+ ></tiptap-button>
967
+ } @if (bubbleMenuConfig().link) {
968
+ <tiptap-button
969
+ icon="link"
970
+ title="Lien"
971
+ [active]="isActive('link')"
972
+ (click)="onCommand('link', $event)"
973
+ ></tiptap-button>
974
+ }
975
+ </div>
976
+ ` }]
977
+ }], ctorParameters: () => [], propDecorators: { menuRef: [{
978
+ type: ViewChild,
979
+ args: ["menuRef", { static: false }]
980
+ }] } });
981
+
565
982
  class TiptapSeparatorComponent {
566
983
  constructor() {
567
984
  this.orientation = input("vertical");
@@ -1011,31 +1428,396 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImpor
1011
1428
  }]
1012
1429
  }] });
1013
1430
 
1014
- const ENGLISH_TRANSLATIONS = {
1015
- toolbar: {
1016
- bold: "Bold",
1017
- italic: "Italic",
1018
- underline: "Underline",
1019
- strike: "Strikethrough",
1020
- code: "Code",
1021
- superscript: "Superscript",
1022
- subscript: "Subscript",
1023
- highlight: "Highlight",
1024
- heading1: "Heading 1",
1025
- heading2: "Heading 2",
1026
- heading3: "Heading 3",
1027
- bulletList: "Bullet List",
1028
- orderedList: "Ordered List",
1029
- blockquote: "Blockquote",
1030
- alignLeft: "Align Left",
1031
- alignCenter: "Align Center",
1431
+ class TiptapImageBubbleMenuComponent {
1432
+ constructor() {
1433
+ this.editor = input.required();
1434
+ this.config = input({
1435
+ changeImage: true,
1436
+ resizeSmall: true,
1437
+ resizeMedium: true,
1438
+ resizeLarge: true,
1439
+ resizeOriginal: true,
1440
+ deleteImage: true,
1441
+ separator: true,
1442
+ });
1443
+ this.tippyInstance = null;
1444
+ this.imageService = inject(ImageService);
1445
+ this.updateTimeout = null;
1446
+ this.imageBubbleMenuConfig = computed(() => ({
1447
+ changeImage: true,
1448
+ resizeSmall: true,
1449
+ resizeMedium: true,
1450
+ resizeLarge: true,
1451
+ resizeOriginal: true,
1452
+ deleteImage: true,
1453
+ separator: true,
1454
+ ...this.config(),
1455
+ }));
1456
+ this.hasResizeButtons = computed(() => {
1457
+ const config = this.imageBubbleMenuConfig();
1458
+ return (config.resizeSmall ||
1459
+ config.resizeMedium ||
1460
+ config.resizeLarge ||
1461
+ config.resizeOriginal);
1462
+ });
1463
+ this.updateMenu = () => {
1464
+ // Debounce pour éviter les appels trop fréquents
1465
+ if (this.updateTimeout) {
1466
+ clearTimeout(this.updateTimeout);
1467
+ }
1468
+ this.updateTimeout = setTimeout(() => {
1469
+ const ed = this.editor();
1470
+ if (!ed)
1471
+ return;
1472
+ const isImageSelected = ed.isActive("resizableImage") || ed.isActive("image");
1473
+ const { from, to } = ed.state.selection;
1474
+ const hasTextSelection = from !== to;
1475
+ // Ne montrer le menu image que si :
1476
+ // - Une image est sélectionnée
1477
+ // - L'éditeur est éditable
1478
+ const shouldShow = isImageSelected && ed.isEditable;
1479
+ if (shouldShow) {
1480
+ this.showTippy();
1481
+ }
1482
+ else {
1483
+ this.hideTippy();
1484
+ }
1485
+ }, 10);
1486
+ };
1487
+ this.handleBlur = () => {
1488
+ // Masquer le menu quand l'éditeur perd le focus
1489
+ setTimeout(() => {
1490
+ this.hideTippy();
1491
+ }, 100);
1492
+ };
1493
+ effect(() => {
1494
+ const ed = this.editor();
1495
+ if (!ed)
1496
+ return;
1497
+ // Nettoyer les anciens listeners
1498
+ ed.off("selectionUpdate", this.updateMenu);
1499
+ ed.off("transaction", this.updateMenu);
1500
+ ed.off("focus", this.updateMenu);
1501
+ ed.off("blur", this.handleBlur);
1502
+ // Ajouter les nouveaux listeners
1503
+ ed.on("selectionUpdate", this.updateMenu);
1504
+ ed.on("transaction", this.updateMenu);
1505
+ ed.on("focus", this.updateMenu);
1506
+ ed.on("blur", this.handleBlur);
1507
+ // Ne pas appeler updateMenu() ici pour éviter l'affichage prématuré
1508
+ // Il sera appelé automatiquement quand l'éditeur sera prêt
1509
+ });
1510
+ }
1511
+ ngOnInit() {
1512
+ // Initialiser Tippy de manière synchrone après que le component soit ready
1513
+ this.initTippy();
1514
+ }
1515
+ ngOnDestroy() {
1516
+ const ed = this.editor();
1517
+ if (ed) {
1518
+ ed.off("selectionUpdate", this.updateMenu);
1519
+ ed.off("transaction", this.updateMenu);
1520
+ ed.off("focus", this.updateMenu);
1521
+ ed.off("blur", this.handleBlur);
1522
+ }
1523
+ // Nettoyer les timeouts
1524
+ if (this.updateTimeout) {
1525
+ clearTimeout(this.updateTimeout);
1526
+ }
1527
+ // Nettoyer Tippy
1528
+ if (this.tippyInstance) {
1529
+ this.tippyInstance.destroy();
1530
+ this.tippyInstance = null;
1531
+ }
1532
+ }
1533
+ initTippy() {
1534
+ // Attendre que l'élément soit disponible
1535
+ if (!this.menuRef?.nativeElement) {
1536
+ setTimeout(() => this.initTippy(), 50);
1537
+ return;
1538
+ }
1539
+ const menuElement = this.menuRef.nativeElement;
1540
+ // S'assurer qu'il n'y a pas déjà une instance
1541
+ if (this.tippyInstance) {
1542
+ this.tippyInstance.destroy();
1543
+ }
1544
+ // Créer l'instance Tippy
1545
+ this.tippyInstance = tippy(document.body, {
1546
+ content: menuElement,
1547
+ trigger: "manual",
1548
+ placement: "top-start",
1549
+ appendTo: () => document.body,
1550
+ interactive: true,
1551
+ arrow: false,
1552
+ offset: [0, 8],
1553
+ hideOnClick: false,
1554
+ onShow: (instance) => {
1555
+ // S'assurer que les autres menus sont fermés
1556
+ this.hideOtherMenus();
1557
+ },
1558
+ getReferenceClientRect: () => this.getImageRect(),
1559
+ // Améliorer le positionnement avec scroll
1560
+ popperOptions: {
1561
+ modifiers: [
1562
+ {
1563
+ name: "preventOverflow",
1564
+ options: {
1565
+ boundary: "viewport",
1566
+ padding: 8,
1567
+ },
1568
+ },
1569
+ {
1570
+ name: "flip",
1571
+ options: {
1572
+ fallbackPlacements: ["bottom-start", "top-end", "bottom-end"],
1573
+ },
1574
+ },
1575
+ ],
1576
+ },
1577
+ });
1578
+ // Maintenant que Tippy est initialisé, faire un premier check
1579
+ this.updateMenu();
1580
+ }
1581
+ getImageRect() {
1582
+ const ed = this.editor();
1583
+ if (!ed)
1584
+ return new DOMRect(0, 0, 0, 0);
1585
+ // Trouver l'image sélectionnée dans le DOM
1586
+ const { from } = ed.state.selection;
1587
+ // Fonction pour trouver toutes les images dans l'éditeur
1588
+ const getAllImages = () => {
1589
+ const editorElement = ed.view.dom;
1590
+ return Array.from(editorElement.querySelectorAll("img"));
1591
+ };
1592
+ // Fonction pour trouver l'image à la position spécifique
1593
+ const findImageAtPosition = () => {
1594
+ const allImages = getAllImages();
1595
+ for (const img of allImages) {
1596
+ try {
1597
+ // Obtenir la position ProseMirror de cette image
1598
+ const imgPos = ed.view.posAtDOM(img, 0);
1599
+ // Vérifier si cette image correspond à la position sélectionnée
1600
+ if (Math.abs(imgPos - from) <= 1) {
1601
+ return img;
1602
+ }
1603
+ }
1604
+ catch (error) {
1605
+ // Continuer si on ne peut pas obtenir la position de cette image
1606
+ continue;
1607
+ }
1608
+ }
1609
+ return null;
1610
+ };
1611
+ // Chercher l'image à la position exacte
1612
+ const imageElement = findImageAtPosition();
1613
+ if (imageElement) {
1614
+ return imageElement.getBoundingClientRect();
1615
+ }
1616
+ return new DOMRect(0, 0, 0, 0);
1617
+ }
1618
+ hideOtherMenus() {
1619
+ // Cette méthode peut être étendue pour fermer d'autres menus si nécessaire
1620
+ // Pour l'instant, elle sert de placeholder pour une future coordination entre menus
1621
+ }
1622
+ showTippy() {
1623
+ if (!this.tippyInstance)
1624
+ return;
1625
+ // Mettre à jour la position
1626
+ this.tippyInstance.setProps({
1627
+ getReferenceClientRect: () => this.getImageRect(),
1628
+ });
1629
+ this.tippyInstance.show();
1630
+ }
1631
+ hideTippy() {
1632
+ if (this.tippyInstance) {
1633
+ this.tippyInstance.hide();
1634
+ }
1635
+ }
1636
+ onCommand(command, event) {
1637
+ event.preventDefault();
1638
+ const ed = this.editor();
1639
+ if (!ed)
1640
+ return;
1641
+ switch (command) {
1642
+ case "changeImage":
1643
+ this.changeImage();
1644
+ break;
1645
+ case "resizeSmall":
1646
+ this.imageService.resizeImageToSmall(ed);
1647
+ break;
1648
+ case "resizeMedium":
1649
+ this.imageService.resizeImageToMedium(ed);
1650
+ break;
1651
+ case "resizeLarge":
1652
+ this.imageService.resizeImageToLarge(ed);
1653
+ break;
1654
+ case "resizeOriginal":
1655
+ this.imageService.resizeImageToOriginal(ed);
1656
+ break;
1657
+ case "deleteImage":
1658
+ this.deleteImage();
1659
+ break;
1660
+ }
1661
+ }
1662
+ async changeImage() {
1663
+ const ed = this.editor();
1664
+ if (!ed)
1665
+ return;
1666
+ try {
1667
+ // Utiliser la méthode spécifique pour remplacer une image existante
1668
+ await this.imageService.selectAndReplaceImage(ed, {
1669
+ quality: 0.8,
1670
+ maxWidth: 1920,
1671
+ maxHeight: 1080,
1672
+ accept: "image/*",
1673
+ });
1674
+ }
1675
+ catch (error) {
1676
+ console.error("Erreur lors du changement d'image:", error);
1677
+ }
1678
+ }
1679
+ deleteImage() {
1680
+ const ed = this.editor();
1681
+ if (ed) {
1682
+ ed.chain().focus().deleteSelection().run();
1683
+ }
1684
+ }
1685
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapImageBubbleMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1686
+ 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: `
1687
+ <div #menuRef class="bubble-menu">
1688
+ @if (imageBubbleMenuConfig().changeImage) {
1689
+ <tiptap-button
1690
+ icon="drive_file_rename_outline"
1691
+ title="Changer l'image"
1692
+ (click)="onCommand('changeImage', $event)"
1693
+ ></tiptap-button>
1694
+ } @if (imageBubbleMenuConfig().separator && hasResizeButtons()) {
1695
+ <tiptap-separator></tiptap-separator>
1696
+ } @if (imageBubbleMenuConfig().resizeSmall) {
1697
+ <tiptap-button
1698
+ icon="crop_square"
1699
+ iconSize="small"
1700
+ title="Petite (300×200)"
1701
+ (click)="onCommand('resizeSmall', $event)"
1702
+ ></tiptap-button>
1703
+ } @if (imageBubbleMenuConfig().resizeMedium) {
1704
+ <tiptap-button
1705
+ icon="crop_square"
1706
+ iconSize="medium"
1707
+ title="Moyenne (500×350)"
1708
+ (click)="onCommand('resizeMedium', $event)"
1709
+ ></tiptap-button>
1710
+ } @if (imageBubbleMenuConfig().resizeLarge) {
1711
+ <tiptap-button
1712
+ icon="crop_square"
1713
+ iconSize="large"
1714
+ title="Grande (800×600)"
1715
+ (click)="onCommand('resizeLarge', $event)"
1716
+ ></tiptap-button>
1717
+ } @if (imageBubbleMenuConfig().resizeOriginal) {
1718
+ <tiptap-button
1719
+ icon="photo_size_select_actual"
1720
+ title="Taille originale"
1721
+ (click)="onCommand('resizeOriginal', $event)"
1722
+ ></tiptap-button>
1723
+ } @if (imageBubbleMenuConfig().separator &&
1724
+ imageBubbleMenuConfig().deleteImage) {
1725
+ <tiptap-separator></tiptap-separator>
1726
+ } @if (imageBubbleMenuConfig().deleteImage) {
1727
+ <tiptap-button
1728
+ icon="delete"
1729
+ title="Supprimer l'image"
1730
+ variant="danger"
1731
+ (click)="onCommand('deleteImage', $event)"
1732
+ ></tiptap-button>
1733
+ }
1734
+ </div>
1735
+ `, 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"] }] }); }
1736
+ }
1737
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapImageBubbleMenuComponent, decorators: [{
1738
+ type: Component,
1739
+ args: [{ selector: "tiptap-image-bubble-menu", standalone: true, imports: [TiptapButtonComponent, TiptapSeparatorComponent], template: `
1740
+ <div #menuRef class="bubble-menu">
1741
+ @if (imageBubbleMenuConfig().changeImage) {
1742
+ <tiptap-button
1743
+ icon="drive_file_rename_outline"
1744
+ title="Changer l'image"
1745
+ (click)="onCommand('changeImage', $event)"
1746
+ ></tiptap-button>
1747
+ } @if (imageBubbleMenuConfig().separator && hasResizeButtons()) {
1748
+ <tiptap-separator></tiptap-separator>
1749
+ } @if (imageBubbleMenuConfig().resizeSmall) {
1750
+ <tiptap-button
1751
+ icon="crop_square"
1752
+ iconSize="small"
1753
+ title="Petite (300×200)"
1754
+ (click)="onCommand('resizeSmall', $event)"
1755
+ ></tiptap-button>
1756
+ } @if (imageBubbleMenuConfig().resizeMedium) {
1757
+ <tiptap-button
1758
+ icon="crop_square"
1759
+ iconSize="medium"
1760
+ title="Moyenne (500×350)"
1761
+ (click)="onCommand('resizeMedium', $event)"
1762
+ ></tiptap-button>
1763
+ } @if (imageBubbleMenuConfig().resizeLarge) {
1764
+ <tiptap-button
1765
+ icon="crop_square"
1766
+ iconSize="large"
1767
+ title="Grande (800×600)"
1768
+ (click)="onCommand('resizeLarge', $event)"
1769
+ ></tiptap-button>
1770
+ } @if (imageBubbleMenuConfig().resizeOriginal) {
1771
+ <tiptap-button
1772
+ icon="photo_size_select_actual"
1773
+ title="Taille originale"
1774
+ (click)="onCommand('resizeOriginal', $event)"
1775
+ ></tiptap-button>
1776
+ } @if (imageBubbleMenuConfig().separator &&
1777
+ imageBubbleMenuConfig().deleteImage) {
1778
+ <tiptap-separator></tiptap-separator>
1779
+ } @if (imageBubbleMenuConfig().deleteImage) {
1780
+ <tiptap-button
1781
+ icon="delete"
1782
+ title="Supprimer l'image"
1783
+ variant="danger"
1784
+ (click)="onCommand('deleteImage', $event)"
1785
+ ></tiptap-button>
1786
+ }
1787
+ </div>
1788
+ ` }]
1789
+ }], ctorParameters: () => [], propDecorators: { menuRef: [{
1790
+ type: ViewChild,
1791
+ args: ["menuRef", { static: false }]
1792
+ }] } });
1793
+
1794
+ const ENGLISH_TRANSLATIONS = {
1795
+ toolbar: {
1796
+ bold: "Bold",
1797
+ italic: "Italic",
1798
+ underline: "Underline",
1799
+ strike: "Strikethrough",
1800
+ code: "Code",
1801
+ superscript: "Superscript",
1802
+ subscript: "Subscript",
1803
+ highlight: "Highlight",
1804
+ heading1: "Heading 1",
1805
+ heading2: "Heading 2",
1806
+ heading3: "Heading 3",
1807
+ bulletList: "Bullet List",
1808
+ orderedList: "Ordered List",
1809
+ blockquote: "Blockquote",
1810
+ alignLeft: "Align Left",
1811
+ alignCenter: "Align Center",
1032
1812
  alignRight: "Align Right",
1033
1813
  alignJustify: "Align Justify",
1034
1814
  link: "Add Link",
1035
1815
  image: "Add Image",
1036
1816
  horizontalRule: "Horizontal Rule",
1817
+ table: "Insert Table",
1037
1818
  undo: "Undo",
1038
1819
  redo: "Redo",
1820
+ clear: "Clear",
1039
1821
  },
1040
1822
  bubbleMenu: {
1041
1823
  bold: "Bold",
@@ -1100,6 +1882,24 @@ const ENGLISH_TRANSLATIONS = {
1100
1882
  description: "Add a horizontal line",
1101
1883
  keywords: ["hr", "horizontal", "rule", "line", "separator"],
1102
1884
  },
1885
+ table: {
1886
+ title: "Table",
1887
+ description: "Insert a table",
1888
+ keywords: ["table", "grid", "data", "rows", "columns"],
1889
+ },
1890
+ },
1891
+ table: {
1892
+ addRowBefore: "Add row before",
1893
+ addRowAfter: "Add row after",
1894
+ deleteRow: "Delete row",
1895
+ addColumnBefore: "Add column before",
1896
+ addColumnAfter: "Add column after",
1897
+ deleteColumn: "Delete column",
1898
+ deleteTable: "Delete table",
1899
+ toggleHeaderRow: "Toggle header row",
1900
+ toggleHeaderColumn: "Toggle header column",
1901
+ mergeCells: "Merge cells",
1902
+ splitCell: "Split cell",
1103
1903
  },
1104
1904
  imageUpload: {
1105
1905
  selectImage: "Select Image",
@@ -1119,8 +1919,8 @@ const ENGLISH_TRANSLATIONS = {
1119
1919
  },
1120
1920
  editor: {
1121
1921
  placeholder: "Start typing...",
1122
- characters: "characters",
1123
- words: "words",
1922
+ character: "character",
1923
+ word: "word",
1124
1924
  imageLoadError: "Error loading image",
1125
1925
  linkPrompt: "Enter link URL",
1126
1926
  linkUrlPrompt: "Enter URL",
@@ -1161,8 +1961,10 @@ const FRENCH_TRANSLATIONS = {
1161
1961
  link: "Ajouter un lien",
1162
1962
  image: "Ajouter une image",
1163
1963
  horizontalRule: "Ligne horizontale",
1964
+ table: "Insérer un tableau",
1164
1965
  undo: "Annuler",
1165
1966
  redo: "Refaire",
1967
+ clear: "Vider",
1166
1968
  },
1167
1969
  bubbleMenu: {
1168
1970
  bold: "Gras",
@@ -1227,6 +2029,33 @@ const FRENCH_TRANSLATIONS = {
1227
2029
  description: "Ajouter une ligne de séparation",
1228
2030
  keywords: ["hr", "horizontal", "rule", "ligne", "séparation"],
1229
2031
  },
2032
+ table: {
2033
+ title: "Tableau",
2034
+ description: "Insérer un tableau",
2035
+ keywords: [
2036
+ "table",
2037
+ "tableau",
2038
+ "grid",
2039
+ "grille",
2040
+ "data",
2041
+ "données",
2042
+ "rows",
2043
+ "colonnes",
2044
+ ],
2045
+ },
2046
+ },
2047
+ table: {
2048
+ addRowBefore: "Ajouter une ligne avant",
2049
+ addRowAfter: "Ajouter une ligne après",
2050
+ deleteRow: "Supprimer la ligne",
2051
+ addColumnBefore: "Ajouter une colonne avant",
2052
+ addColumnAfter: "Ajouter une colonne après",
2053
+ deleteColumn: "Supprimer la colonne",
2054
+ deleteTable: "Supprimer le tableau",
2055
+ toggleHeaderRow: "Basculer ligne d'en-tête",
2056
+ toggleHeaderColumn: "Basculer colonne d'en-tête",
2057
+ mergeCells: "Fusionner les cellules",
2058
+ splitCell: "Diviser la cellule",
1230
2059
  },
1231
2060
  imageUpload: {
1232
2061
  selectImage: "Sélectionner une image",
@@ -1246,8 +2075,8 @@ const FRENCH_TRANSLATIONS = {
1246
2075
  },
1247
2076
  editor: {
1248
2077
  placeholder: "Commencez à écrire...",
1249
- characters: "caractères",
1250
- words: "mots",
2078
+ character: "caractère",
2079
+ word: "mot",
1251
2080
  imageLoadError: "Erreur de chargement de l'image",
1252
2081
  linkPrompt: "Entrez l'URL du lien",
1253
2082
  linkUrlPrompt: "Entrez l'URL",
@@ -1280,6 +2109,7 @@ class TiptapI18nService {
1280
2109
  this.toolbar = computed(() => this.translations().toolbar);
1281
2110
  this.bubbleMenu = computed(() => this.translations().bubbleMenu);
1282
2111
  this.slashCommands = computed(() => this.translations().slashCommands);
2112
+ this.table = computed(() => this.translations().table);
1283
2113
  this.imageUpload = computed(() => this.translations().imageUpload);
1284
2114
  this.editor = computed(() => this.translations().editor);
1285
2115
  this.common = computed(() => this.translations().common);
@@ -1301,1004 +2131,251 @@ class TiptapI18nService {
1301
2131
  [locale]: {
1302
2132
  ...current[locale],
1303
2133
  ...translations,
1304
- },
1305
- }));
1306
- }
1307
- detectBrowserLanguage() {
1308
- const browserLang = navigator.language.toLowerCase();
1309
- if (browserLang.startsWith("fr")) {
1310
- this._currentLocale.set("fr");
1311
- }
1312
- else {
1313
- this._currentLocale.set("en");
1314
- }
1315
- }
1316
- // Méthodes utilitaires pour les composants
1317
- getToolbarTitle(key) {
1318
- return this.translations().toolbar[key];
1319
- }
1320
- getBubbleMenuTitle(key) {
1321
- return this.translations().bubbleMenu[key];
1322
- }
1323
- getSlashCommand(key) {
1324
- return this.translations().slashCommands[key];
1325
- }
1326
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapI18nService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1327
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapI18nService, providedIn: "root" }); }
1328
- }
1329
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapI18nService, decorators: [{
1330
- type: Injectable,
1331
- args: [{
1332
- providedIn: "root",
1333
- }]
1334
- }], ctorParameters: () => [] });
1335
-
1336
- class EditorCommandsService {
1337
- // Méthodes pour vérifier l'état actif
1338
- isActive(editor, name, attributes) {
1339
- return editor.isActive(name, attributes);
1340
- }
1341
- // Méthodes pour vérifier si une commande peut être exécutée
1342
- canExecute(editor, command) {
1343
- if (!editor)
1344
- return false;
1345
- switch (command) {
1346
- case "toggleBold":
1347
- return editor.can().chain().focus().toggleBold().run();
1348
- case "toggleItalic":
1349
- return editor.can().chain().focus().toggleItalic().run();
1350
- case "toggleStrike":
1351
- return editor.can().chain().focus().toggleStrike().run();
1352
- case "toggleCode":
1353
- return editor.can().chain().focus().toggleCode().run();
1354
- case "toggleUnderline":
1355
- return editor.can().chain().focus().toggleUnderline().run();
1356
- case "toggleSuperscript":
1357
- return editor.can().chain().focus().toggleSuperscript().run();
1358
- case "toggleSubscript":
1359
- return editor.can().chain().focus().toggleSubscript().run();
1360
- case "setTextAlign":
1361
- return editor.can().chain().focus().setTextAlign("left").run();
1362
- case "toggleLink":
1363
- return editor.can().chain().focus().toggleLink({ href: "" }).run();
1364
- case "insertHorizontalRule":
1365
- return editor.can().chain().focus().setHorizontalRule().run();
1366
- case "toggleHighlight":
1367
- return editor.can().chain().focus().toggleHighlight().run();
1368
- case "undo":
1369
- return editor.can().chain().focus().undo().run();
1370
- case "redo":
1371
- return editor.can().chain().focus().redo().run();
1372
- default:
1373
- return false;
1374
- }
1375
- }
1376
- // Méthodes pour exécuter les commandes
1377
- toggleBold(editor) {
1378
- editor.chain().focus().toggleBold().run();
1379
- }
1380
- toggleItalic(editor) {
1381
- editor.chain().focus().toggleItalic().run();
1382
- }
1383
- toggleStrike(editor) {
1384
- editor.chain().focus().toggleStrike().run();
1385
- }
1386
- toggleCode(editor) {
1387
- editor.chain().focus().toggleCode().run();
1388
- }
1389
- toggleHeading(editor, level) {
1390
- editor.chain().focus().toggleHeading({ level }).run();
1391
- }
1392
- toggleBulletList(editor) {
1393
- editor.chain().focus().toggleBulletList().run();
1394
- }
1395
- toggleOrderedList(editor) {
1396
- editor.chain().focus().toggleOrderedList().run();
1397
- }
1398
- toggleBlockquote(editor) {
1399
- editor.chain().focus().toggleBlockquote().run();
1400
- }
1401
- undo(editor) {
1402
- editor.chain().focus().undo().run();
1403
- }
1404
- redo(editor) {
1405
- editor.chain().focus().redo().run();
1406
- }
1407
- // Nouvelles méthodes pour les formatages supplémentaires
1408
- toggleUnderline(editor) {
1409
- editor.chain().focus().toggleUnderline().run();
1410
- }
1411
- toggleSuperscript(editor) {
1412
- editor.chain().focus().toggleSuperscript().run();
1413
- }
1414
- toggleSubscript(editor) {
1415
- editor.chain().focus().toggleSubscript().run();
1416
- }
1417
- setTextAlign(editor, alignment) {
1418
- editor.chain().focus().setTextAlign(alignment).run();
1419
- }
1420
- toggleLink(editor, url) {
1421
- if (url) {
1422
- editor.chain().focus().toggleLink({ href: url }).run();
1423
- }
1424
- else {
1425
- // Si pas d'URL fournie, on demande à l'utilisateur
1426
- const href = window.prompt("URL du lien:");
1427
- if (href) {
1428
- editor.chain().focus().toggleLink({ href }).run();
1429
- }
1430
- }
1431
- }
1432
- insertHorizontalRule(editor) {
1433
- editor.chain().focus().setHorizontalRule().run();
1434
- }
1435
- toggleHighlight(editor, color) {
1436
- if (color) {
1437
- editor.chain().focus().toggleHighlight({ color }).run();
1438
- }
1439
- else {
1440
- editor.chain().focus().toggleHighlight().run();
1441
- }
1442
- }
1443
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: EditorCommandsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1444
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: EditorCommandsService, providedIn: "root" }); }
1445
- }
1446
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: EditorCommandsService, decorators: [{
1447
- type: Injectable,
1448
- args: [{
1449
- providedIn: "root",
1450
- }]
1451
- }] });
1452
-
1453
- class TiptapToolbarComponent {
1454
- constructor(editorCommands) {
1455
- this.editorCommands = editorCommands;
1456
- this.editor = input.required();
1457
- this.config = input.required();
1458
- // Outputs pour les événements d'image
1459
- this.imageUploaded = output();
1460
- this.imageError = output();
1461
- this.imageService = inject(ImageService);
1462
- this.i18nService = inject(TiptapI18nService);
1463
- // Computed values pour les traductions
1464
- this.t = this.i18nService.toolbar;
1465
- }
1466
- isActive(name, attributes) {
1467
- return this.editorCommands.isActive(this.editor(), name, attributes);
1468
- }
1469
- canExecute(command) {
1470
- return this.editorCommands.canExecute(this.editor(), command);
1471
- }
1472
- toggleBold() {
1473
- this.editorCommands.toggleBold(this.editor());
1474
- }
1475
- toggleItalic() {
1476
- this.editorCommands.toggleItalic(this.editor());
1477
- }
1478
- toggleStrike() {
1479
- this.editorCommands.toggleStrike(this.editor());
1480
- }
1481
- toggleCode() {
1482
- this.editorCommands.toggleCode(this.editor());
1483
- }
1484
- toggleHeading(level) {
1485
- this.editorCommands.toggleHeading(this.editor(), level);
1486
- }
1487
- toggleBulletList() {
1488
- this.editorCommands.toggleBulletList(this.editor());
2134
+ },
2135
+ }));
1489
2136
  }
1490
- toggleOrderedList() {
1491
- this.editorCommands.toggleOrderedList(this.editor());
2137
+ detectBrowserLanguage() {
2138
+ const browserLang = navigator.language.toLowerCase();
2139
+ if (browserLang.startsWith("fr")) {
2140
+ this._currentLocale.set("fr");
2141
+ }
2142
+ else {
2143
+ this._currentLocale.set("en");
2144
+ }
1492
2145
  }
1493
- toggleBlockquote() {
1494
- this.editorCommands.toggleBlockquote(this.editor());
2146
+ // Méthodes utilitaires pour les composants
2147
+ getToolbarTitle(key) {
2148
+ return this.translations().toolbar[key];
1495
2149
  }
1496
- undo() {
1497
- this.editorCommands.undo(this.editor());
2150
+ getBubbleMenuTitle(key) {
2151
+ return this.translations().bubbleMenu[key];
1498
2152
  }
1499
- redo() {
1500
- this.editorCommands.redo(this.editor());
2153
+ getSlashCommand(key) {
2154
+ return this.translations().slashCommands[key];
1501
2155
  }
1502
- // Nouvelles méthodes pour les formatages supplémentaires
1503
- toggleUnderline() {
1504
- this.editorCommands.toggleUnderline(this.editor());
2156
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapI18nService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2157
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapI18nService, providedIn: "root" }); }
2158
+ }
2159
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapI18nService, decorators: [{
2160
+ type: Injectable,
2161
+ args: [{
2162
+ providedIn: "root",
2163
+ }]
2164
+ }], ctorParameters: () => [] });
2165
+
2166
+ class EditorCommandsService {
2167
+ // Méthodes pour vérifier l'état actif
2168
+ isActive(editor, name, attributes) {
2169
+ return editor.isActive(name, attributes);
1505
2170
  }
1506
- toggleSuperscript() {
1507
- this.editorCommands.toggleSuperscript(this.editor());
2171
+ // Méthodes pour vérifier si une commande peut être exécutée
2172
+ canExecute(editor, command) {
2173
+ if (!editor)
2174
+ return false;
2175
+ switch (command) {
2176
+ case "toggleBold":
2177
+ return editor.can().chain().focus().toggleBold().run();
2178
+ case "toggleItalic":
2179
+ return editor.can().chain().focus().toggleItalic().run();
2180
+ case "toggleStrike":
2181
+ return editor.can().chain().focus().toggleStrike().run();
2182
+ case "toggleCode":
2183
+ return editor.can().chain().focus().toggleCode().run();
2184
+ case "toggleUnderline":
2185
+ return editor.can().chain().focus().toggleUnderline().run();
2186
+ case "toggleSuperscript":
2187
+ return editor.can().chain().focus().toggleSuperscript().run();
2188
+ case "toggleSubscript":
2189
+ return editor.can().chain().focus().toggleSubscript().run();
2190
+ case "setTextAlign":
2191
+ return editor.can().chain().focus().setTextAlign("left").run();
2192
+ case "toggleLink":
2193
+ return editor.can().chain().focus().toggleLink({ href: "" }).run();
2194
+ case "insertHorizontalRule":
2195
+ return editor.can().chain().focus().setHorizontalRule().run();
2196
+ case "toggleHighlight":
2197
+ return editor.can().chain().focus().toggleHighlight().run();
2198
+ case "undo":
2199
+ return editor.can().chain().focus().undo().run();
2200
+ case "redo":
2201
+ return editor.can().chain().focus().redo().run();
2202
+ case "insertTable":
2203
+ return editor.can().chain().focus().insertTable().run();
2204
+ case "addColumnBefore":
2205
+ return editor.can().chain().focus().addColumnBefore().run();
2206
+ case "addColumnAfter":
2207
+ return editor.can().chain().focus().addColumnAfter().run();
2208
+ case "deleteColumn":
2209
+ return editor.can().chain().focus().deleteColumn().run();
2210
+ case "addRowBefore":
2211
+ return editor.can().chain().focus().addRowBefore().run();
2212
+ case "addRowAfter":
2213
+ return editor.can().chain().focus().addRowAfter().run();
2214
+ case "deleteRow":
2215
+ return editor.can().chain().focus().deleteRow().run();
2216
+ case "deleteTable":
2217
+ return editor.can().chain().focus().deleteTable().run();
2218
+ case "mergeCells":
2219
+ return editor.can().chain().focus().mergeCells().run();
2220
+ case "splitCell":
2221
+ return editor.can().chain().focus().splitCell().run();
2222
+ case "toggleHeaderColumn":
2223
+ return editor.can().chain().focus().toggleHeaderColumn().run();
2224
+ case "toggleHeaderRow":
2225
+ return editor.can().chain().focus().toggleHeaderRow().run();
2226
+ case "toggleHeaderCell":
2227
+ return editor.can().chain().focus().toggleHeaderCell().run();
2228
+ default:
2229
+ return false;
2230
+ }
1508
2231
  }
1509
- toggleSubscript() {
1510
- this.editorCommands.toggleSubscript(this.editor());
2232
+ // Méthodes pour exécuter les commandes
2233
+ toggleBold(editor) {
2234
+ editor.chain().focus().toggleBold().run();
1511
2235
  }
1512
- setTextAlign(alignment) {
1513
- this.editorCommands.setTextAlign(this.editor(), alignment);
2236
+ toggleItalic(editor) {
2237
+ editor.chain().focus().toggleItalic().run();
1514
2238
  }
1515
- toggleLink() {
1516
- this.editorCommands.toggleLink(this.editor());
2239
+ toggleStrike(editor) {
2240
+ editor.chain().focus().toggleStrike().run();
1517
2241
  }
1518
- insertHorizontalRule() {
1519
- this.editorCommands.insertHorizontalRule(this.editor());
2242
+ toggleCode(editor) {
2243
+ editor.chain().focus().toggleCode().run();
1520
2244
  }
1521
- toggleHighlight() {
1522
- this.editorCommands.toggleHighlight(this.editor());
2245
+ toggleHeading(editor, level) {
2246
+ editor.chain().focus().toggleHeading({ level }).run();
1523
2247
  }
1524
- // Méthode pour insérer une image
1525
- async insertImage() {
1526
- try {
1527
- await this.imageService.selectAndUploadImage(this.editor());
1528
- }
1529
- catch (error) {
1530
- console.error("Erreur lors de l'upload d'image:", error);
1531
- this.imageError.emit("Erreur lors de l'upload d'image");
1532
- }
2248
+ toggleBulletList(editor) {
2249
+ editor.chain().focus().toggleBulletList().run();
1533
2250
  }
1534
- // Méthodes pour les événements d'image (conservées pour compatibilité)
1535
- onImageSelected(result) {
1536
- this.imageUploaded.emit(result);
2251
+ toggleOrderedList(editor) {
2252
+ editor.chain().focus().toggleOrderedList().run();
1537
2253
  }
1538
- onImageError(error) {
1539
- this.imageError.emit(error);
2254
+ toggleBlockquote(editor) {
2255
+ editor.chain().focus().toggleBlockquote().run();
1540
2256
  }
1541
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapToolbarComponent, deps: [{ token: EditorCommandsService }], target: i0.ɵɵFactoryTarget.Component }); }
1542
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.0", type: TiptapToolbarComponent, isStandalone: true, selector: "tiptap-toolbar", inputs: { editor: { classPropertyName: "editor", publicName: "editor", isSignal: true, isRequired: true, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { imageUploaded: "imageUploaded", imageError: "imageError" }, ngImport: i0, template: `
1543
- <div class="tiptap-toolbar">
1544
- @if (config().bold) {
1545
- <tiptap-button
1546
- icon="format_bold"
1547
- [title]="t().bold"
1548
- [active]="isActive('bold')"
1549
- [disabled]="!canExecute('toggleBold')"
1550
- (onClick)="toggleBold()"
1551
- />
1552
- } @if (config().italic) {
1553
- <tiptap-button
1554
- icon="format_italic"
1555
- [title]="t().italic"
1556
- [active]="isActive('italic')"
1557
- [disabled]="!canExecute('toggleItalic')"
1558
- (onClick)="toggleItalic()"
1559
- />
1560
- } @if (config().underline) {
1561
- <tiptap-button
1562
- icon="format_underlined"
1563
- [title]="t().underline"
1564
- [active]="isActive('underline')"
1565
- [disabled]="!canExecute('toggleUnderline')"
1566
- (onClick)="toggleUnderline()"
1567
- />
1568
- } @if (config().strike) {
1569
- <tiptap-button
1570
- icon="strikethrough_s"
1571
- [title]="t().strike"
1572
- [active]="isActive('strike')"
1573
- [disabled]="!canExecute('toggleStrike')"
1574
- (onClick)="toggleStrike()"
1575
- />
1576
- } @if (config().code) {
1577
- <tiptap-button
1578
- icon="code"
1579
- [title]="t().code"
1580
- [active]="isActive('code')"
1581
- [disabled]="!canExecute('toggleCode')"
1582
- (onClick)="toggleCode()"
1583
- />
1584
- } @if (config().superscript) {
1585
- <tiptap-button
1586
- icon="superscript"
1587
- [title]="t().superscript"
1588
- [active]="isActive('superscript')"
1589
- [disabled]="!canExecute('toggleSuperscript')"
1590
- (onClick)="toggleSuperscript()"
1591
- />
1592
- } @if (config().subscript) {
1593
- <tiptap-button
1594
- icon="subscript"
1595
- [title]="t().subscript"
1596
- [active]="isActive('subscript')"
1597
- [disabled]="!canExecute('toggleSubscript')"
1598
- (onClick)="toggleSubscript()"
1599
- />
1600
- } @if (config().highlight) {
1601
- <tiptap-button
1602
- icon="highlight"
1603
- [title]="t().highlight"
1604
- [active]="isActive('highlight')"
1605
- [disabled]="!canExecute('toggleHighlight')"
1606
- (onClick)="toggleHighlight()"
1607
- />
1608
- } @if (config().separator && (config().heading1 || config().heading2 ||
1609
- config().heading3)) {
1610
- <tiptap-separator />
1611
- } @if (config().heading1) {
1612
- <tiptap-button
1613
- icon="format_h1"
1614
- [title]="t().heading1"
1615
- variant="text"
1616
- [active]="isActive('heading', { level: 1 })"
1617
- (onClick)="toggleHeading(1)"
1618
- />
1619
- } @if (config().heading2) {
1620
- <tiptap-button
1621
- icon="format_h2"
1622
- [title]="t().heading2"
1623
- variant="text"
1624
- [active]="isActive('heading', { level: 2 })"
1625
- (onClick)="toggleHeading(2)"
1626
- />
1627
- } @if (config().heading3) {
1628
- <tiptap-button
1629
- icon="format_h3"
1630
- [title]="t().heading3"
1631
- variant="text"
1632
- [active]="isActive('heading', { level: 3 })"
1633
- (onClick)="toggleHeading(3)"
1634
- />
1635
- } @if (config().separator && (config().bulletList || config().orderedList
1636
- || config().blockquote)) {
1637
- <tiptap-separator />
1638
- } @if (config().bulletList) {
1639
- <tiptap-button
1640
- icon="format_list_bulleted"
1641
- [title]="t().bulletList"
1642
- [active]="isActive('bulletList')"
1643
- (onClick)="toggleBulletList()"
1644
- />
1645
- } @if (config().orderedList) {
1646
- <tiptap-button
1647
- icon="format_list_numbered"
1648
- [title]="t().orderedList"
1649
- [active]="isActive('orderedList')"
1650
- (onClick)="toggleOrderedList()"
1651
- />
1652
- } @if (config().blockquote) {
1653
- <tiptap-button
1654
- icon="format_quote"
1655
- [title]="t().blockquote"
1656
- [active]="isActive('blockquote')"
1657
- (onClick)="toggleBlockquote()"
1658
- />
1659
- } @if (config().separator && (config().alignLeft || config().alignCenter
1660
- || config().alignRight || config().alignJustify)) {
1661
- <tiptap-separator />
1662
- } @if (config().alignLeft) {
1663
- <tiptap-button
1664
- icon="format_align_left"
1665
- [title]="t().alignLeft"
1666
- [active]="isActive('textAlign', { textAlign: 'left' })"
1667
- (onClick)="setTextAlign('left')"
1668
- />
1669
- } @if (config().alignCenter) {
1670
- <tiptap-button
1671
- icon="format_align_center"
1672
- [title]="t().alignCenter"
1673
- [active]="isActive('textAlign', { textAlign: 'center' })"
1674
- (onClick)="setTextAlign('center')"
1675
- />
1676
- } @if (config().alignRight) {
1677
- <tiptap-button
1678
- icon="format_align_right"
1679
- [title]="t().alignRight"
1680
- [active]="isActive('textAlign', { textAlign: 'right' })"
1681
- (onClick)="setTextAlign('right')"
1682
- />
1683
- } @if (config().alignJustify) {
1684
- <tiptap-button
1685
- icon="format_align_justify"
1686
- [title]="t().alignJustify"
1687
- [active]="isActive('textAlign', { textAlign: 'justify' })"
1688
- (onClick)="setTextAlign('justify')"
1689
- />
1690
- } @if (config().separator && (config().link || config().horizontalRule)) {
1691
- <tiptap-separator />
1692
- } @if (config().link) {
1693
- <tiptap-button
1694
- icon="link"
1695
- [title]="t().link"
1696
- [active]="isActive('link')"
1697
- (onClick)="toggleLink()"
1698
- />
1699
- } @if (config().horizontalRule) {
1700
- <tiptap-button
1701
- icon="horizontal_rule"
1702
- [title]="t().horizontalRule"
1703
- (onClick)="insertHorizontalRule()"
1704
- />
1705
- } @if (config().separator && config().image) {
1706
- <tiptap-separator />
1707
- } @if (config().image) {
1708
- <tiptap-button
1709
- icon="image"
1710
- [title]="t().image"
1711
- (onClick)="insertImage()"
1712
- />
1713
- } @if (config().separator && (config().undo || config().redo)) {
1714
- <tiptap-separator />
1715
- } @if (config().undo) {
1716
- <tiptap-button
1717
- icon="undo"
1718
- [title]="t().undo"
1719
- [disabled]="!canExecute('undo')"
1720
- (onClick)="undo()"
1721
- />
1722
- } @if (config().redo) {
1723
- <tiptap-button
1724
- icon="redo"
1725
- [title]="t().redo"
1726
- [disabled]="!canExecute('redo')"
1727
- (onClick)="redo()"
1728
- />
1729
- }
1730
- </div>
1731
- `, 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"] }] }); }
1732
- }
1733
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapToolbarComponent, decorators: [{
1734
- type: Component,
1735
- args: [{ selector: "tiptap-toolbar", standalone: true, imports: [TiptapButtonComponent, TiptapSeparatorComponent], template: `
1736
- <div class="tiptap-toolbar">
1737
- @if (config().bold) {
1738
- <tiptap-button
1739
- icon="format_bold"
1740
- [title]="t().bold"
1741
- [active]="isActive('bold')"
1742
- [disabled]="!canExecute('toggleBold')"
1743
- (onClick)="toggleBold()"
1744
- />
1745
- } @if (config().italic) {
1746
- <tiptap-button
1747
- icon="format_italic"
1748
- [title]="t().italic"
1749
- [active]="isActive('italic')"
1750
- [disabled]="!canExecute('toggleItalic')"
1751
- (onClick)="toggleItalic()"
1752
- />
1753
- } @if (config().underline) {
1754
- <tiptap-button
1755
- icon="format_underlined"
1756
- [title]="t().underline"
1757
- [active]="isActive('underline')"
1758
- [disabled]="!canExecute('toggleUnderline')"
1759
- (onClick)="toggleUnderline()"
1760
- />
1761
- } @if (config().strike) {
1762
- <tiptap-button
1763
- icon="strikethrough_s"
1764
- [title]="t().strike"
1765
- [active]="isActive('strike')"
1766
- [disabled]="!canExecute('toggleStrike')"
1767
- (onClick)="toggleStrike()"
1768
- />
1769
- } @if (config().code) {
1770
- <tiptap-button
1771
- icon="code"
1772
- [title]="t().code"
1773
- [active]="isActive('code')"
1774
- [disabled]="!canExecute('toggleCode')"
1775
- (onClick)="toggleCode()"
1776
- />
1777
- } @if (config().superscript) {
1778
- <tiptap-button
1779
- icon="superscript"
1780
- [title]="t().superscript"
1781
- [active]="isActive('superscript')"
1782
- [disabled]="!canExecute('toggleSuperscript')"
1783
- (onClick)="toggleSuperscript()"
1784
- />
1785
- } @if (config().subscript) {
1786
- <tiptap-button
1787
- icon="subscript"
1788
- [title]="t().subscript"
1789
- [active]="isActive('subscript')"
1790
- [disabled]="!canExecute('toggleSubscript')"
1791
- (onClick)="toggleSubscript()"
1792
- />
1793
- } @if (config().highlight) {
1794
- <tiptap-button
1795
- icon="highlight"
1796
- [title]="t().highlight"
1797
- [active]="isActive('highlight')"
1798
- [disabled]="!canExecute('toggleHighlight')"
1799
- (onClick)="toggleHighlight()"
1800
- />
1801
- } @if (config().separator && (config().heading1 || config().heading2 ||
1802
- config().heading3)) {
1803
- <tiptap-separator />
1804
- } @if (config().heading1) {
1805
- <tiptap-button
1806
- icon="format_h1"
1807
- [title]="t().heading1"
1808
- variant="text"
1809
- [active]="isActive('heading', { level: 1 })"
1810
- (onClick)="toggleHeading(1)"
1811
- />
1812
- } @if (config().heading2) {
1813
- <tiptap-button
1814
- icon="format_h2"
1815
- [title]="t().heading2"
1816
- variant="text"
1817
- [active]="isActive('heading', { level: 2 })"
1818
- (onClick)="toggleHeading(2)"
1819
- />
1820
- } @if (config().heading3) {
1821
- <tiptap-button
1822
- icon="format_h3"
1823
- [title]="t().heading3"
1824
- variant="text"
1825
- [active]="isActive('heading', { level: 3 })"
1826
- (onClick)="toggleHeading(3)"
1827
- />
1828
- } @if (config().separator && (config().bulletList || config().orderedList
1829
- || config().blockquote)) {
1830
- <tiptap-separator />
1831
- } @if (config().bulletList) {
1832
- <tiptap-button
1833
- icon="format_list_bulleted"
1834
- [title]="t().bulletList"
1835
- [active]="isActive('bulletList')"
1836
- (onClick)="toggleBulletList()"
1837
- />
1838
- } @if (config().orderedList) {
1839
- <tiptap-button
1840
- icon="format_list_numbered"
1841
- [title]="t().orderedList"
1842
- [active]="isActive('orderedList')"
1843
- (onClick)="toggleOrderedList()"
1844
- />
1845
- } @if (config().blockquote) {
1846
- <tiptap-button
1847
- icon="format_quote"
1848
- [title]="t().blockquote"
1849
- [active]="isActive('blockquote')"
1850
- (onClick)="toggleBlockquote()"
1851
- />
1852
- } @if (config().separator && (config().alignLeft || config().alignCenter
1853
- || config().alignRight || config().alignJustify)) {
1854
- <tiptap-separator />
1855
- } @if (config().alignLeft) {
1856
- <tiptap-button
1857
- icon="format_align_left"
1858
- [title]="t().alignLeft"
1859
- [active]="isActive('textAlign', { textAlign: 'left' })"
1860
- (onClick)="setTextAlign('left')"
1861
- />
1862
- } @if (config().alignCenter) {
1863
- <tiptap-button
1864
- icon="format_align_center"
1865
- [title]="t().alignCenter"
1866
- [active]="isActive('textAlign', { textAlign: 'center' })"
1867
- (onClick)="setTextAlign('center')"
1868
- />
1869
- } @if (config().alignRight) {
1870
- <tiptap-button
1871
- icon="format_align_right"
1872
- [title]="t().alignRight"
1873
- [active]="isActive('textAlign', { textAlign: 'right' })"
1874
- (onClick)="setTextAlign('right')"
1875
- />
1876
- } @if (config().alignJustify) {
1877
- <tiptap-button
1878
- icon="format_align_justify"
1879
- [title]="t().alignJustify"
1880
- [active]="isActive('textAlign', { textAlign: 'justify' })"
1881
- (onClick)="setTextAlign('justify')"
1882
- />
1883
- } @if (config().separator && (config().link || config().horizontalRule)) {
1884
- <tiptap-separator />
1885
- } @if (config().link) {
1886
- <tiptap-button
1887
- icon="link"
1888
- [title]="t().link"
1889
- [active]="isActive('link')"
1890
- (onClick)="toggleLink()"
1891
- />
1892
- } @if (config().horizontalRule) {
1893
- <tiptap-button
1894
- icon="horizontal_rule"
1895
- [title]="t().horizontalRule"
1896
- (onClick)="insertHorizontalRule()"
1897
- />
1898
- } @if (config().separator && config().image) {
1899
- <tiptap-separator />
1900
- } @if (config().image) {
1901
- <tiptap-button
1902
- icon="image"
1903
- [title]="t().image"
1904
- (onClick)="insertImage()"
1905
- />
1906
- } @if (config().separator && (config().undo || config().redo)) {
1907
- <tiptap-separator />
1908
- } @if (config().undo) {
1909
- <tiptap-button
1910
- icon="undo"
1911
- [title]="t().undo"
1912
- [disabled]="!canExecute('undo')"
1913
- (onClick)="undo()"
1914
- />
1915
- } @if (config().redo) {
1916
- <tiptap-button
1917
- icon="redo"
1918
- [title]="t().redo"
1919
- [disabled]="!canExecute('redo')"
1920
- (onClick)="redo()"
1921
- />
1922
- }
1923
- </div>
1924
- `, 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"] }]
1925
- }], ctorParameters: () => [{ type: EditorCommandsService }] });
1926
-
1927
- class TiptapImageUploadComponent {
1928
- constructor() {
1929
- // Inputs
1930
- this.config = input({
1931
- maxSize: 5, // 5MB par défaut
1932
- maxWidth: 1920, // largeur max par défaut
1933
- maxHeight: 1080, // hauteur max par défaut
1934
- allowedTypes: ["image/jpeg", "image/png", "image/gif", "image/webp"],
1935
- enableDragDrop: true,
1936
- showPreview: true,
1937
- multiple: false,
1938
- compressImages: true,
1939
- quality: 0.8,
1940
- });
1941
- // Outputs
1942
- this.imageSelected = output();
1943
- this.error = output();
1944
- // Signals internes
1945
- this.isDragOver = signal(false);
1946
- this.isUploading = signal(false);
1947
- this.uploadProgress = signal(0);
1948
- this.previewImage = signal(null);
1949
- this.previewInfo = signal("");
1950
- this.errorMessage = signal(null);
1951
- // Computed
1952
- this.acceptedTypes = computed(() => {
1953
- const types = this.config().allowedTypes || ["image/*"];
1954
- return types.join(",");
1955
- });
2257
+ undo(editor) {
2258
+ editor.chain().focus().undo().run();
1956
2259
  }
1957
- triggerFileInput() {
1958
- const input = document.querySelector('input[type="file"]');
1959
- if (input) {
1960
- input.click();
1961
- }
2260
+ redo(editor) {
2261
+ editor.chain().focus().redo().run();
1962
2262
  }
1963
- onFileSelected(event) {
1964
- const input = event.target;
1965
- const files = input.files;
1966
- if (files && files.length > 0) {
1967
- this.processFiles(Array.from(files));
1968
- }
1969
- // Reset input
1970
- input.value = "";
2263
+ // Nouvelles méthodes pour les formatages supplémentaires
2264
+ toggleUnderline(editor) {
2265
+ editor.chain().focus().toggleUnderline().run();
1971
2266
  }
1972
- onDragOver(event) {
1973
- event.preventDefault();
1974
- event.stopPropagation();
1975
- this.isDragOver.set(true);
2267
+ toggleSuperscript(editor) {
2268
+ editor.chain().focus().toggleSuperscript().run();
1976
2269
  }
1977
- onDrop(event) {
1978
- event.preventDefault();
1979
- event.stopPropagation();
1980
- this.isDragOver.set(false);
1981
- const files = event.dataTransfer?.files;
1982
- if (files && files.length > 0) {
1983
- this.processFiles(Array.from(files));
1984
- }
2270
+ toggleSubscript(editor) {
2271
+ editor.chain().focus().toggleSubscript().run();
1985
2272
  }
1986
- onDragLeave(event) {
1987
- event.preventDefault();
1988
- event.stopPropagation();
1989
- this.isDragOver.set(false);
2273
+ setTextAlign(editor, alignment) {
2274
+ editor.chain().focus().setTextAlign(alignment).run();
1990
2275
  }
1991
- processFiles(files) {
1992
- const config = this.config();
1993
- const maxSize = (config.maxSize || 5) * 1024 * 1024; // Convertir en bytes
1994
- const allowedTypes = config.allowedTypes || ["image/*"];
1995
- // Vérifier le nombre de fichiers
1996
- if (!config.multiple && files.length > 1) {
1997
- this.showError("Veuillez sélectionner une seule image");
1998
- return;
2276
+ toggleLink(editor, url) {
2277
+ if (url) {
2278
+ editor.chain().focus().toggleLink({ href: url }).run();
1999
2279
  }
2000
- // Traiter chaque fichier
2001
- files.forEach((file) => {
2002
- // Vérifier le type
2003
- if (!this.isValidFileType(file, allowedTypes)) {
2004
- this.showError(`Type de fichier non supporté: ${file.name}`);
2005
- return;
2006
- }
2007
- // Vérifier la taille
2008
- if (file.size > maxSize) {
2009
- this.showError(`Fichier trop volumineux: ${file.name} (max ${config.maxSize}MB)`);
2010
- return;
2280
+ else {
2281
+ // Si pas d'URL fournie, on demande à l'utilisateur
2282
+ const href = window.prompt("URL du lien:");
2283
+ if (href) {
2284
+ editor.chain().focus().toggleLink({ href }).run();
2011
2285
  }
2012
- // Traiter l'image avec compression si nécessaire
2013
- this.processImage(file);
2014
- });
2286
+ }
2015
2287
  }
2016
- isValidFileType(file, allowedTypes) {
2017
- if (allowedTypes.includes("image/*")) {
2018
- return file.type.startsWith("image/");
2288
+ insertHorizontalRule(editor) {
2289
+ editor.chain().focus().setHorizontalRule().run();
2290
+ }
2291
+ toggleHighlight(editor, color) {
2292
+ if (color) {
2293
+ editor.chain().focus().toggleHighlight({ color }).run();
2294
+ }
2295
+ else {
2296
+ editor.chain().focus().toggleHighlight().run();
2019
2297
  }
2020
- return allowedTypes.includes(file.type);
2021
2298
  }
2022
- processImage(file) {
2023
- this.isUploading.set(true);
2024
- this.uploadProgress.set(10);
2025
- const config = this.config();
2026
- const originalSize = file.size;
2027
- // Créer un canvas pour la compression
2028
- const canvas = document.createElement("canvas");
2029
- const ctx = canvas.getContext("2d");
2030
- const img = new Image();
2031
- img.onload = () => {
2032
- this.uploadProgress.set(30);
2033
- // Vérifier les dimensions
2034
- const maxWidth = config.maxWidth || 1920;
2035
- const maxHeight = config.maxHeight || 1080;
2036
- let { width, height } = img;
2037
- // Redimensionner si nécessaire
2038
- if (width > maxWidth || height > maxHeight) {
2039
- const ratio = Math.min(maxWidth / width, maxHeight / height);
2040
- width *= ratio;
2041
- height *= ratio;
2042
- }
2043
- canvas.width = width;
2044
- canvas.height = height;
2045
- // Dessiner l'image redimensionnée
2046
- ctx?.drawImage(img, 0, 0, width, height);
2047
- this.uploadProgress.set(70);
2048
- // Convertir en base64 avec compression
2049
- const quality = config.quality || 0.8;
2050
- const mimeType = file.type;
2051
- canvas.toBlob((blob) => {
2052
- this.uploadProgress.set(90);
2053
- if (blob) {
2054
- const reader = new FileReader();
2055
- reader.onload = (e) => {
2056
- const base64 = e.target?.result;
2057
- if (base64) {
2058
- const result = {
2059
- src: base64,
2060
- name: file.name,
2061
- size: blob.size,
2062
- type: file.type,
2063
- width: Math.round(width),
2064
- height: Math.round(height),
2065
- originalSize: originalSize,
2066
- };
2067
- // Afficher la prévisualisation si activée
2068
- if (config.showPreview) {
2069
- this.previewImage.set(base64);
2070
- this.previewInfo.set(`${result.width}×${result.height} • ${this.formatFileSize(blob.size)}`);
2071
- }
2072
- // Émettre l'événement
2073
- this.imageSelected.emit(result);
2074
- this.clearError();
2075
- }
2076
- this.uploadProgress.set(100);
2077
- setTimeout(() => {
2078
- this.isUploading.set(false);
2079
- this.uploadProgress.set(0);
2080
- }, 500);
2081
- };
2082
- reader.readAsDataURL(blob);
2083
- }
2084
- else {
2085
- this.showError("Erreur lors de la compression de l'image");
2086
- this.isUploading.set(false);
2087
- this.uploadProgress.set(0);
2088
- }
2089
- }, mimeType, quality);
2090
- };
2091
- img.onerror = () => {
2092
- this.showError("Erreur lors du chargement de l'image");
2093
- this.isUploading.set(false);
2094
- this.uploadProgress.set(0);
2095
- };
2096
- img.src = URL.createObjectURL(file);
2299
+ // Table commands
2300
+ insertTable(editor, rows = 3, cols = 3) {
2301
+ editor.chain().focus().insertTable({ rows, cols }).run();
2097
2302
  }
2098
- formatFileSize(bytes) {
2099
- if (bytes === 0)
2100
- return "0 B";
2101
- const k = 1024;
2102
- const sizes = ["B", "KB", "MB", "GB"];
2103
- const i = Math.floor(Math.log(bytes) / Math.log(k));
2104
- return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
2303
+ addColumnBefore(editor) {
2304
+ editor.chain().focus().addColumnBefore().run();
2105
2305
  }
2106
- showError(message) {
2107
- this.errorMessage.set(message);
2108
- this.error.emit(message);
2109
- this.isUploading.set(false);
2110
- this.uploadProgress.set(0);
2111
- // Auto-clear après 5 secondes
2112
- setTimeout(() => {
2113
- this.clearError();
2114
- }, 5000);
2306
+ addColumnAfter(editor) {
2307
+ editor.chain().focus().addColumnAfter().run();
2115
2308
  }
2116
- clearError() {
2117
- this.errorMessage.set(null);
2309
+ deleteColumn(editor) {
2310
+ editor.chain().focus().deleteColumn().run();
2118
2311
  }
2119
- clearPreview() {
2120
- this.previewImage.set(null);
2121
- this.previewInfo.set("");
2312
+ addRowBefore(editor) {
2313
+ editor.chain().focus().addRowBefore().run();
2122
2314
  }
2123
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapImageUploadComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2124
- 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: `
2125
- <div class="image-upload-container">
2126
- <!-- Bouton d'upload -->
2127
- <tiptap-button
2128
- icon="image"
2129
- title="Ajouter une image"
2130
- [disabled]="isUploading()"
2131
- (onClick)="triggerFileInput()"
2132
- />
2133
-
2134
- <!-- Input file caché -->
2135
- <input
2136
- #fileInput
2137
- type="file"
2138
- [accept]="acceptedTypes()"
2139
- [multiple]="config().multiple"
2140
- (change)="onFileSelected($event)"
2141
- style="display: none;"
2142
- />
2143
-
2144
- <!-- Zone de drag & drop -->
2145
- @if (config().enableDragDrop && isDragOver()) {
2146
- <div
2147
- class="drag-overlay"
2148
- (dragover)="onDragOver($event)"
2149
- (drop)="onDrop($event)"
2150
- (dragleave)="onDragLeave($event)"
2151
- >
2152
- <div class="drag-content">
2153
- <span class="material-symbols-outlined">cloud_upload</span>
2154
- <p>Déposez votre image ici</p>
2155
- </div>
2156
- </div>
2157
- }
2158
-
2159
- <!-- Barre de progression -->
2160
- @if (isUploading() && uploadProgress() > 0) {
2161
- <div class="upload-progress">
2162
- <div class="progress-bar">
2163
- <div class="progress-fill" [style.width.%]="uploadProgress()"></div>
2164
- </div>
2165
- <div class="progress-text">{{ uploadProgress() }}%</div>
2166
- </div>
2167
- }
2168
-
2169
- <!-- Prévisualisation -->
2170
- @if (config().showPreview && previewImage()) {
2171
- <div class="image-preview">
2172
- <img [src]="previewImage()" alt="Prévisualisation" />
2173
- <div class="preview-info">
2174
- <span>{{ previewInfo() }}</span>
2175
- </div>
2176
- <button
2177
- class="preview-close"
2178
- (click)="clearPreview()"
2179
- title="Fermer la prévisualisation"
2180
- >
2181
- <span class="material-symbols-outlined">close</span>
2182
- </button>
2183
- </div>
2184
- }
2185
-
2186
- <!-- Messages d'erreur -->
2187
- @if (errorMessage()) {
2188
- <div class="error-message">
2189
- <span class="material-symbols-outlined">error</span>
2190
- {{ errorMessage() }}
2191
- </div>
2192
- }
2193
- </div>
2194
- `, 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"] }] }); }
2315
+ addRowAfter(editor) {
2316
+ editor.chain().focus().addRowAfter().run();
2317
+ }
2318
+ deleteRow(editor) {
2319
+ editor.chain().focus().deleteRow().run();
2320
+ }
2321
+ deleteTable(editor) {
2322
+ editor.chain().focus().deleteTable().run();
2323
+ }
2324
+ mergeCells(editor) {
2325
+ editor.chain().focus().mergeCells().run();
2326
+ }
2327
+ splitCell(editor) {
2328
+ editor.chain().focus().splitCell().run();
2329
+ }
2330
+ toggleHeaderColumn(editor) {
2331
+ editor.chain().focus().toggleHeaderColumn().run();
2332
+ }
2333
+ toggleHeaderRow(editor) {
2334
+ editor.chain().focus().toggleHeaderRow().run();
2335
+ }
2336
+ toggleHeaderCell(editor) {
2337
+ editor.chain().focus().toggleHeaderCell().run();
2338
+ }
2339
+ // Méthode pour vider le contenu
2340
+ clearContent(editor) {
2341
+ editor.chain().focus().setContent("", true).run(); // ✅ Forcer l'émission de l'événement
2342
+ }
2343
+ // Méthodes de base de l'éditeur
2344
+ focus(editor) {
2345
+ editor.chain().focus().run();
2346
+ }
2347
+ blur(editor) {
2348
+ editor.chain().blur().run();
2349
+ }
2350
+ setContent(editor, content, emitUpdate = true) {
2351
+ editor.chain().focus().setContent(content, emitUpdate).run();
2352
+ }
2353
+ setEditable(editor, editable) {
2354
+ editor.setEditable(editable);
2355
+ }
2356
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: EditorCommandsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2357
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: EditorCommandsService, providedIn: "root" }); }
2195
2358
  }
2196
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapImageUploadComponent, decorators: [{
2197
- type: Component,
2198
- args: [{ selector: "tiptap-image-upload", standalone: true, imports: [TiptapButtonComponent], template: `
2199
- <div class="image-upload-container">
2200
- <!-- Bouton d'upload -->
2201
- <tiptap-button
2202
- icon="image"
2203
- title="Ajouter une image"
2204
- [disabled]="isUploading()"
2205
- (onClick)="triggerFileInput()"
2206
- />
2207
-
2208
- <!-- Input file caché -->
2209
- <input
2210
- #fileInput
2211
- type="file"
2212
- [accept]="acceptedTypes()"
2213
- [multiple]="config().multiple"
2214
- (change)="onFileSelected($event)"
2215
- style="display: none;"
2216
- />
2217
-
2218
- <!-- Zone de drag & drop -->
2219
- @if (config().enableDragDrop && isDragOver()) {
2220
- <div
2221
- class="drag-overlay"
2222
- (dragover)="onDragOver($event)"
2223
- (drop)="onDrop($event)"
2224
- (dragleave)="onDragLeave($event)"
2225
- >
2226
- <div class="drag-content">
2227
- <span class="material-symbols-outlined">cloud_upload</span>
2228
- <p>Déposez votre image ici</p>
2229
- </div>
2230
- </div>
2231
- }
2232
-
2233
- <!-- Barre de progression -->
2234
- @if (isUploading() && uploadProgress() > 0) {
2235
- <div class="upload-progress">
2236
- <div class="progress-bar">
2237
- <div class="progress-fill" [style.width.%]="uploadProgress()"></div>
2238
- </div>
2239
- <div class="progress-text">{{ uploadProgress() }}%</div>
2240
- </div>
2241
- }
2242
-
2243
- <!-- Prévisualisation -->
2244
- @if (config().showPreview && previewImage()) {
2245
- <div class="image-preview">
2246
- <img [src]="previewImage()" alt="Prévisualisation" />
2247
- <div class="preview-info">
2248
- <span>{{ previewInfo() }}</span>
2249
- </div>
2250
- <button
2251
- class="preview-close"
2252
- (click)="clearPreview()"
2253
- title="Fermer la prévisualisation"
2254
- >
2255
- <span class="material-symbols-outlined">close</span>
2256
- </button>
2257
- </div>
2258
- }
2259
-
2260
- <!-- Messages d'erreur -->
2261
- @if (errorMessage()) {
2262
- <div class="error-message">
2263
- <span class="material-symbols-outlined">error</span>
2264
- {{ errorMessage() }}
2265
- </div>
2266
- }
2267
- </div>
2268
- `, 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"] }]
2359
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: EditorCommandsService, decorators: [{
2360
+ type: Injectable,
2361
+ args: [{
2362
+ providedIn: "root",
2363
+ }]
2269
2364
  }] });
2270
2365
 
2271
- class TiptapBubbleMenuComponent {
2272
- // Effect comme propriété de classe pour éviter l'erreur d'injection context
2366
+ class TiptapTableBubbleMenuComponent {
2273
2367
  constructor() {
2368
+ // Inputs
2274
2369
  this.editor = input.required();
2275
- this.config = input({
2276
- bold: true,
2277
- italic: true,
2278
- underline: true,
2279
- strike: true,
2280
- code: true,
2281
- superscript: false,
2282
- subscript: false,
2283
- highlight: true,
2284
- link: true,
2285
- separator: true,
2286
- });
2370
+ this.config = input({});
2371
+ // Services
2372
+ this.i18nService = inject(TiptapI18nService);
2373
+ this.commandsService = inject(EditorCommandsService);
2374
+ // Tippy instance
2287
2375
  this.tippyInstance = null;
2288
2376
  this.updateTimeout = null;
2289
- this.bubbleMenuConfig = computed(() => ({
2290
- bold: true,
2291
- italic: true,
2292
- underline: true,
2293
- strike: true,
2294
- code: true,
2295
- superscript: false,
2296
- subscript: false,
2297
- highlight: true,
2298
- link: true,
2299
- separator: true,
2300
- ...this.config(),
2301
- }));
2377
+ // Signaux
2378
+ this.i18n = this.i18nService;
2302
2379
  this.updateMenu = () => {
2303
2380
  // Debounce pour éviter les appels trop fréquents
2304
2381
  if (this.updateTimeout) {
@@ -2308,14 +2385,25 @@ class TiptapBubbleMenuComponent {
2308
2385
  const ed = this.editor();
2309
2386
  if (!ed)
2310
2387
  return;
2388
+ const isTableSelected = ed.isActive("table") ||
2389
+ ed.isActive("tableCell") ||
2390
+ ed.isActive("tableHeader");
2391
+ // Vérifier s'il y a une sélection de cellules (priorité au menu de cellules)
2311
2392
  const { from, to } = ed.state.selection;
2312
- const hasTextSelection = from !== to;
2313
- const isImageSelected = ed.isActive("image") || ed.isActive("resizableImage");
2314
- // Ne montrer le menu texte que si :
2315
- // - Il y a une sélection de texte
2316
- // - Aucune image n'est sélectionnée (priorité aux images)
2317
- // - L'éditeur est éditable
2318
- const shouldShow = hasTextSelection && !isImageSelected && ed.isEditable;
2393
+ const hasCellSelection = from !== to;
2394
+ const isTableCell = ed.isActive("tableCell") || ed.isActive("tableHeader");
2395
+ // Vérifier si la sélection traverse plusieurs cellules
2396
+ const selectionSize = to - from;
2397
+ const hasMultiCellSelection = hasCellSelection && selectionSize > 1;
2398
+ // Ne montrer le menu de table que si :
2399
+ // 1. Une table est sélectionnée
2400
+ // 2. L'éditeur est éditable
2401
+ // 3. Il n'y a PAS de sélection de cellules (priorité au menu de cellules)
2402
+ // 4. Il n'y a PAS de sélection multi-cellules
2403
+ const shouldShow = isTableSelected &&
2404
+ ed.isEditable &&
2405
+ !(hasCellSelection && isTableCell) &&
2406
+ !hasMultiCellSelection;
2319
2407
  if (shouldShow) {
2320
2408
  this.showTippy();
2321
2409
  }
@@ -2330,57 +2418,41 @@ class TiptapBubbleMenuComponent {
2330
2418
  this.hideTippy();
2331
2419
  }, 100);
2332
2420
  };
2421
+ // Effet pour mettre à jour le menu quand l'éditeur change
2333
2422
  effect(() => {
2334
- const ed = this.editor();
2335
- if (!ed)
2336
- return;
2337
- // Nettoyer les anciens listeners
2338
- ed.off("selectionUpdate", this.updateMenu);
2339
- ed.off("transaction", this.updateMenu);
2340
- ed.off("focus", this.updateMenu);
2341
- ed.off("blur", this.handleBlur);
2342
- // Ajouter les nouveaux listeners
2343
- ed.on("selectionUpdate", this.updateMenu);
2344
- ed.on("transaction", this.updateMenu);
2345
- ed.on("focus", this.updateMenu);
2346
- ed.on("blur", this.handleBlur);
2347
- // Ne pas appeler updateMenu() ici pour éviter l'affichage prématuré
2348
- // Il sera appelé automatiquement quand l'éditeur sera prêt
2423
+ const editor = this.editor();
2424
+ if (editor) {
2425
+ // Nettoyer les anciens listeners
2426
+ editor.off("selectionUpdate", this.updateMenu);
2427
+ editor.off("focus", this.updateMenu);
2428
+ editor.off("blur", this.handleBlur);
2429
+ // Ajouter les nouveaux listeners
2430
+ editor.on("selectionUpdate", this.updateMenu);
2431
+ editor.on("focus", this.updateMenu);
2432
+ editor.on("blur", this.handleBlur);
2433
+ }
2349
2434
  });
2350
2435
  }
2351
2436
  ngOnInit() {
2352
- // Initialiser Tippy de manière synchrone après que le component soit ready
2353
2437
  this.initTippy();
2354
2438
  }
2355
2439
  ngOnDestroy() {
2356
- const ed = this.editor();
2357
- if (ed) {
2358
- ed.off("selectionUpdate", this.updateMenu);
2359
- ed.off("transaction", this.updateMenu);
2360
- ed.off("focus", this.updateMenu);
2361
- ed.off("blur", this.handleBlur);
2362
- }
2363
- // Nettoyer les timeouts
2364
- if (this.updateTimeout) {
2365
- clearTimeout(this.updateTimeout);
2440
+ const editor = this.editor();
2441
+ if (editor) {
2442
+ // Nettoyer les événements
2443
+ editor.off("selectionUpdate", this.updateMenu);
2444
+ editor.off("focus", this.updateMenu);
2445
+ editor.off("blur", this.handleBlur);
2366
2446
  }
2367
- // Nettoyer Tippy
2368
2447
  if (this.tippyInstance) {
2369
2448
  this.tippyInstance.destroy();
2370
- this.tippyInstance = null;
2449
+ }
2450
+ if (this.updateTimeout) {
2451
+ clearTimeout(this.updateTimeout);
2371
2452
  }
2372
2453
  }
2373
2454
  initTippy() {
2374
- // Attendre que l'élément soit disponible
2375
- if (!this.menuRef?.nativeElement) {
2376
- setTimeout(() => this.initTippy(), 50);
2377
- return;
2378
- }
2379
- const menuElement = this.menuRef.nativeElement;
2380
- // S'assurer qu'il n'y a pas déjà une instance
2381
- if (this.tippyInstance) {
2382
- this.tippyInstance.destroy();
2383
- }
2455
+ const menuElement = this.menuElement.nativeElement;
2384
2456
  // Créer l'instance Tippy
2385
2457
  this.tippyInstance = tippy(document.body, {
2386
2458
  content: menuElement,
@@ -2390,13 +2462,13 @@ class TiptapBubbleMenuComponent {
2390
2462
  interactive: true,
2391
2463
  arrow: false,
2392
2464
  offset: [0, 8],
2465
+ maxWidth: "none",
2393
2466
  hideOnClick: false,
2394
2467
  onShow: (instance) => {
2395
2468
  // S'assurer que les autres menus sont fermés
2396
2469
  this.hideOtherMenus();
2397
2470
  },
2398
- getReferenceClientRect: () => this.getSelectionRect(),
2399
- // Améliorer le positionnement avec scroll
2471
+ getReferenceClientRect: () => this.getTableRect(),
2400
2472
  popperOptions: {
2401
2473
  modifiers: [
2402
2474
  {
@@ -2418,257 +2490,275 @@ class TiptapBubbleMenuComponent {
2418
2490
  // Maintenant que Tippy est initialisé, faire un premier check
2419
2491
  this.updateMenu();
2420
2492
  }
2421
- getSelectionRect() {
2422
- const selection = window.getSelection();
2423
- if (!selection || selection.rangeCount === 0) {
2493
+ getTableRect() {
2494
+ const ed = this.editor();
2495
+ if (!ed)
2424
2496
  return new DOMRect(0, 0, 0, 0);
2497
+ // Méthode 1: Utiliser coordsAtPos (méthode native ProseMirror)
2498
+ const { from } = ed.state.selection;
2499
+ const coords = ed.view.coordsAtPos(from);
2500
+ // Trouver la table qui contient cette position
2501
+ const editorElement = ed.view.dom;
2502
+ const tables = Array.from(editorElement.querySelectorAll("table"));
2503
+ for (let i = 0; i < tables.length; i++) {
2504
+ const table = tables[i];
2505
+ try {
2506
+ const tableRect = table.getBoundingClientRect();
2507
+ // Vérifier si la position ProseMirror est dans cette table
2508
+ const isInside = coords.left >= tableRect.left &&
2509
+ coords.left <= tableRect.right &&
2510
+ coords.top >= tableRect.top &&
2511
+ coords.top <= tableRect.bottom;
2512
+ if (isInside) {
2513
+ return tableRect;
2514
+ }
2515
+ }
2516
+ catch (error) {
2517
+ continue;
2518
+ }
2425
2519
  }
2426
- const range = selection.getRangeAt(0);
2427
- return range.getBoundingClientRect();
2520
+ // Fallback : utiliser la méthode DOM si ProseMirror échoue
2521
+ const selection = window.getSelection();
2522
+ if (selection && selection.rangeCount > 0) {
2523
+ const range = selection.getRangeAt(0);
2524
+ const rect = range.getBoundingClientRect();
2525
+ if (rect.width > 0 && rect.height > 0) {
2526
+ return rect;
2527
+ }
2528
+ }
2529
+ // Dernier fallback : première table
2530
+ if (tables.length > 0) {
2531
+ return tables[0].getBoundingClientRect();
2532
+ }
2533
+ return new DOMRect(0, 0, 0, 0);
2428
2534
  }
2429
2535
  hideOtherMenus() {
2430
2536
  // Cette méthode peut être étendue pour fermer d'autres menus si nécessaire
2431
- // Pour l'instant, elle sert de placeholder pour une future coordination entre menus
2432
2537
  }
2433
2538
  showTippy() {
2434
2539
  if (!this.tippyInstance)
2435
- return;
2436
- // Mettre à jour la position
2437
- this.tippyInstance.setProps({
2438
- getReferenceClientRect: () => this.getSelectionRect(),
2439
- });
2440
- this.tippyInstance.show();
2441
- }
2442
- hideTippy() {
2443
- if (this.tippyInstance) {
2444
- this.tippyInstance.hide();
2445
- }
2446
- }
2447
- isActive(mark) {
2448
- const ed = this.editor();
2449
- return ed?.isActive(mark) || false;
2450
- }
2451
- onCommand(command, event) {
2452
- event.preventDefault();
2453
- const ed = this.editor();
2454
- if (!ed)
2455
- return;
2456
- switch (command) {
2457
- case "bold":
2458
- ed.chain().focus().toggleBold().run();
2459
- break;
2460
- case "italic":
2461
- ed.chain().focus().toggleItalic().run();
2462
- break;
2463
- case "underline":
2464
- ed.chain().focus().toggleUnderline().run();
2465
- break;
2466
- case "strike":
2467
- ed.chain().focus().toggleStrike().run();
2468
- break;
2469
- case "code":
2470
- ed.chain().focus().toggleCode().run();
2471
- break;
2472
- case "superscript":
2473
- ed.chain().focus().toggleSuperscript().run();
2474
- break;
2475
- case "subscript":
2476
- ed.chain().focus().toggleSubscript().run();
2477
- break;
2478
- case "highlight":
2479
- ed.chain().focus().toggleHighlight().run();
2480
- break;
2481
- case "link":
2482
- const href = window.prompt("URL du lien:");
2483
- if (href) {
2484
- ed.chain().focus().toggleLink({ href }).run();
2485
- }
2486
- break;
2487
- }
2488
- }
2489
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapBubbleMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2490
- 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: `
2491
- <div #menuRef class="bubble-menu">
2492
- @if (bubbleMenuConfig().bold) {
2493
- <tiptap-button
2494
- icon="format_bold"
2495
- title="Gras"
2496
- [active]="isActive('bold')"
2497
- (click)="onCommand('bold', $event)"
2498
- ></tiptap-button>
2499
- } @if (bubbleMenuConfig().italic) {
2500
- <tiptap-button
2501
- icon="format_italic"
2502
- title="Italique"
2503
- [active]="isActive('italic')"
2504
- (click)="onCommand('italic', $event)"
2505
- ></tiptap-button>
2506
- } @if (bubbleMenuConfig().underline) {
2507
- <tiptap-button
2508
- icon="format_underlined"
2509
- title="Souligné"
2510
- [active]="isActive('underline')"
2511
- (click)="onCommand('underline', $event)"
2512
- ></tiptap-button>
2513
- } @if (bubbleMenuConfig().strike) {
2514
- <tiptap-button
2515
- icon="strikethrough_s"
2516
- title="Barré"
2517
- [active]="isActive('strike')"
2518
- (click)="onCommand('strike', $event)"
2519
- ></tiptap-button>
2520
- } @if (bubbleMenuConfig().superscript) {
2521
- <tiptap-button
2522
- icon="superscript"
2523
- title="Exposant"
2524
- [active]="isActive('superscript')"
2525
- (click)="onCommand('superscript', $event)"
2526
- ></tiptap-button>
2527
- } @if (bubbleMenuConfig().subscript) {
2528
- <tiptap-button
2529
- icon="subscript"
2530
- title="Indice"
2531
- [active]="isActive('subscript')"
2532
- (click)="onCommand('subscript', $event)"
2533
- ></tiptap-button>
2534
- } @if (bubbleMenuConfig().highlight) {
2535
- <tiptap-button
2536
- icon="highlight"
2537
- title="Surbrillance"
2538
- [active]="isActive('highlight')"
2539
- (click)="onCommand('highlight', $event)"
2540
- ></tiptap-button>
2541
- } @if (bubbleMenuConfig().separator && (bubbleMenuConfig().code ||
2542
- bubbleMenuConfig().link)) {
2543
- <div class="tiptap-separator"></div>
2544
- } @if (bubbleMenuConfig().code) {
2545
- <tiptap-button
2546
- icon="code"
2547
- title="Code"
2548
- [active]="isActive('code')"
2549
- (click)="onCommand('code', $event)"
2550
- ></tiptap-button>
2551
- } @if (bubbleMenuConfig().link) {
2552
- <tiptap-button
2553
- icon="link"
2554
- title="Lien"
2555
- [active]="isActive('link')"
2556
- (click)="onCommand('link', $event)"
2557
- ></tiptap-button>
2558
- }
2559
- </div>
2560
- `, isInline: true, dependencies: [{ kind: "component", type: TiptapButtonComponent, selector: "tiptap-button", inputs: ["icon", "title", "active", "disabled", "variant", "size", "iconSize"], outputs: ["onClick"] }] }); }
2540
+ return;
2541
+ // Mettre à jour la position
2542
+ this.tippyInstance.setProps({
2543
+ getReferenceClientRect: () => this.getTableRect(),
2544
+ });
2545
+ this.tippyInstance.show();
2546
+ }
2547
+ hideTippy() {
2548
+ if (!this.tippyInstance)
2549
+ return;
2550
+ this.tippyInstance.hide();
2551
+ }
2552
+ // Actions de lignes
2553
+ addRowBefore() {
2554
+ this.commandsService.addRowBefore(this.editor());
2555
+ }
2556
+ addRowAfter() {
2557
+ this.commandsService.addRowAfter(this.editor());
2558
+ }
2559
+ deleteRow() {
2560
+ this.commandsService.deleteRow(this.editor());
2561
+ }
2562
+ // Actions de colonnes
2563
+ addColumnBefore() {
2564
+ this.commandsService.addColumnBefore(this.editor());
2565
+ }
2566
+ addColumnAfter() {
2567
+ this.commandsService.addColumnAfter(this.editor());
2568
+ }
2569
+ deleteColumn() {
2570
+ this.commandsService.deleteColumn(this.editor());
2571
+ }
2572
+ // Actions de headers
2573
+ toggleHeaderRow() {
2574
+ this.commandsService.toggleHeaderRow(this.editor());
2575
+ }
2576
+ toggleHeaderColumn() {
2577
+ this.commandsService.toggleHeaderColumn(this.editor());
2578
+ }
2579
+ // Actions de table
2580
+ deleteTable() {
2581
+ this.commandsService.deleteTable(this.editor());
2582
+ }
2583
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapTableBubbleMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2584
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.0", type: TiptapTableBubbleMenuComponent, isStandalone: true, selector: "tiptap-table-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: "menuElement", first: true, predicate: ["menuElement"], descendants: true, static: true }], ngImport: i0, template: `
2585
+ <div #menuElement class="bubble-menu">
2586
+ <!-- Actions de lignes -->
2587
+ @if (config().addRowBefore !== false) {
2588
+ <tiptap-button
2589
+ icon="add_row_above"
2590
+ title="{{ i18n.table().addRowBefore }}"
2591
+ (click)="addRowBefore()"
2592
+ ></tiptap-button>
2593
+ } @if (config().addRowAfter !== false) {
2594
+ <tiptap-button
2595
+ icon="add_row_below"
2596
+ title="{{ i18n.table().addRowAfter }}"
2597
+ (click)="addRowAfter()"
2598
+ ></tiptap-button>
2599
+ } @if (config().deleteRow !== false) {
2600
+ <tiptap-button
2601
+ icon="delete"
2602
+ title="{{ i18n.table().deleteRow }}"
2603
+ variant="danger"
2604
+ (click)="deleteRow()"
2605
+ ></tiptap-button>
2606
+ } @if (config().separator !== false) {
2607
+ <tiptap-separator></tiptap-separator>
2608
+ }
2609
+
2610
+ <!-- Actions de colonnes -->
2611
+ @if (config().addColumnBefore !== false) {
2612
+ <tiptap-button
2613
+ icon="add_column_left"
2614
+ title="{{ i18n.table().addColumnBefore }}"
2615
+ (click)="addColumnBefore()"
2616
+ ></tiptap-button>
2617
+ } @if (config().addColumnAfter !== false) {
2618
+ <tiptap-button
2619
+ icon="add_column_right"
2620
+ title="{{ i18n.table().addColumnAfter }}"
2621
+ (click)="addColumnAfter()"
2622
+ ></tiptap-button>
2623
+ } @if (config().deleteColumn !== false) {
2624
+ <tiptap-button
2625
+ icon="delete"
2626
+ title="{{ i18n.table().deleteColumn }}"
2627
+ variant="danger"
2628
+ (click)="deleteColumn()"
2629
+ ></tiptap-button>
2630
+ } @if (config().separator !== false) {
2631
+ <tiptap-separator></tiptap-separator>
2632
+ }
2633
+
2634
+ <!-- Actions de cellules -->
2635
+ @if (config().toggleHeaderRow !== false) {
2636
+ <tiptap-button
2637
+ icon="toolbar"
2638
+ title="{{ i18n.table().toggleHeaderRow }}"
2639
+ (click)="toggleHeaderRow()"
2640
+ ></tiptap-button>
2641
+ } @if (config().toggleHeaderColumn !== false) {
2642
+ <tiptap-button
2643
+ icon="dock_to_right"
2644
+ title="{{ i18n.table().toggleHeaderColumn }}"
2645
+ (click)="toggleHeaderColumn()"
2646
+ ></tiptap-button>
2647
+ } @if (config().separator !== false && config().deleteTable !== false) {
2648
+ <tiptap-separator></tiptap-separator>
2649
+ }
2650
+
2651
+ <!-- Actions de table -->
2652
+ @if (config().deleteTable !== false) {
2653
+ <tiptap-button
2654
+ icon="delete_forever"
2655
+ title="{{ i18n.table().deleteTable }}"
2656
+ variant="danger"
2657
+ (click)="deleteTable()"
2658
+ ></tiptap-button>
2659
+ }
2660
+ </div>
2661
+ `, 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"] }] }); }
2561
2662
  }
2562
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapBubbleMenuComponent, decorators: [{
2663
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapTableBubbleMenuComponent, decorators: [{
2563
2664
  type: Component,
2564
- args: [{ selector: "tiptap-bubble-menu", standalone: true, imports: [TiptapButtonComponent], template: `
2565
- <div #menuRef class="bubble-menu">
2566
- @if (bubbleMenuConfig().bold) {
2567
- <tiptap-button
2568
- icon="format_bold"
2569
- title="Gras"
2570
- [active]="isActive('bold')"
2571
- (click)="onCommand('bold', $event)"
2572
- ></tiptap-button>
2573
- } @if (bubbleMenuConfig().italic) {
2574
- <tiptap-button
2575
- icon="format_italic"
2576
- title="Italique"
2577
- [active]="isActive('italic')"
2578
- (click)="onCommand('italic', $event)"
2579
- ></tiptap-button>
2580
- } @if (bubbleMenuConfig().underline) {
2581
- <tiptap-button
2582
- icon="format_underlined"
2583
- title="Souligné"
2584
- [active]="isActive('underline')"
2585
- (click)="onCommand('underline', $event)"
2586
- ></tiptap-button>
2587
- } @if (bubbleMenuConfig().strike) {
2588
- <tiptap-button
2589
- icon="strikethrough_s"
2590
- title="Barré"
2591
- [active]="isActive('strike')"
2592
- (click)="onCommand('strike', $event)"
2593
- ></tiptap-button>
2594
- } @if (bubbleMenuConfig().superscript) {
2595
- <tiptap-button
2596
- icon="superscript"
2597
- title="Exposant"
2598
- [active]="isActive('superscript')"
2599
- (click)="onCommand('superscript', $event)"
2600
- ></tiptap-button>
2601
- } @if (bubbleMenuConfig().subscript) {
2602
- <tiptap-button
2603
- icon="subscript"
2604
- title="Indice"
2605
- [active]="isActive('subscript')"
2606
- (click)="onCommand('subscript', $event)"
2607
- ></tiptap-button>
2608
- } @if (bubbleMenuConfig().highlight) {
2609
- <tiptap-button
2610
- icon="highlight"
2611
- title="Surbrillance"
2612
- [active]="isActive('highlight')"
2613
- (click)="onCommand('highlight', $event)"
2614
- ></tiptap-button>
2615
- } @if (bubbleMenuConfig().separator && (bubbleMenuConfig().code ||
2616
- bubbleMenuConfig().link)) {
2617
- <div class="tiptap-separator"></div>
2618
- } @if (bubbleMenuConfig().code) {
2619
- <tiptap-button
2620
- icon="code"
2621
- title="Code"
2622
- [active]="isActive('code')"
2623
- (click)="onCommand('code', $event)"
2624
- ></tiptap-button>
2625
- } @if (bubbleMenuConfig().link) {
2626
- <tiptap-button
2627
- icon="link"
2628
- title="Lien"
2629
- [active]="isActive('link')"
2630
- (click)="onCommand('link', $event)"
2631
- ></tiptap-button>
2632
- }
2633
- </div>
2665
+ args: [{ selector: "tiptap-table-bubble-menu", standalone: true, imports: [CommonModule, TiptapButtonComponent, TiptapSeparatorComponent], template: `
2666
+ <div #menuElement class="bubble-menu">
2667
+ <!-- Actions de lignes -->
2668
+ @if (config().addRowBefore !== false) {
2669
+ <tiptap-button
2670
+ icon="add_row_above"
2671
+ title="{{ i18n.table().addRowBefore }}"
2672
+ (click)="addRowBefore()"
2673
+ ></tiptap-button>
2674
+ } @if (config().addRowAfter !== false) {
2675
+ <tiptap-button
2676
+ icon="add_row_below"
2677
+ title="{{ i18n.table().addRowAfter }}"
2678
+ (click)="addRowAfter()"
2679
+ ></tiptap-button>
2680
+ } @if (config().deleteRow !== false) {
2681
+ <tiptap-button
2682
+ icon="delete"
2683
+ title="{{ i18n.table().deleteRow }}"
2684
+ variant="danger"
2685
+ (click)="deleteRow()"
2686
+ ></tiptap-button>
2687
+ } @if (config().separator !== false) {
2688
+ <tiptap-separator></tiptap-separator>
2689
+ }
2690
+
2691
+ <!-- Actions de colonnes -->
2692
+ @if (config().addColumnBefore !== false) {
2693
+ <tiptap-button
2694
+ icon="add_column_left"
2695
+ title="{{ i18n.table().addColumnBefore }}"
2696
+ (click)="addColumnBefore()"
2697
+ ></tiptap-button>
2698
+ } @if (config().addColumnAfter !== false) {
2699
+ <tiptap-button
2700
+ icon="add_column_right"
2701
+ title="{{ i18n.table().addColumnAfter }}"
2702
+ (click)="addColumnAfter()"
2703
+ ></tiptap-button>
2704
+ } @if (config().deleteColumn !== false) {
2705
+ <tiptap-button
2706
+ icon="delete"
2707
+ title="{{ i18n.table().deleteColumn }}"
2708
+ variant="danger"
2709
+ (click)="deleteColumn()"
2710
+ ></tiptap-button>
2711
+ } @if (config().separator !== false) {
2712
+ <tiptap-separator></tiptap-separator>
2713
+ }
2714
+
2715
+ <!-- Actions de cellules -->
2716
+ @if (config().toggleHeaderRow !== false) {
2717
+ <tiptap-button
2718
+ icon="toolbar"
2719
+ title="{{ i18n.table().toggleHeaderRow }}"
2720
+ (click)="toggleHeaderRow()"
2721
+ ></tiptap-button>
2722
+ } @if (config().toggleHeaderColumn !== false) {
2723
+ <tiptap-button
2724
+ icon="dock_to_right"
2725
+ title="{{ i18n.table().toggleHeaderColumn }}"
2726
+ (click)="toggleHeaderColumn()"
2727
+ ></tiptap-button>
2728
+ } @if (config().separator !== false && config().deleteTable !== false) {
2729
+ <tiptap-separator></tiptap-separator>
2730
+ }
2731
+
2732
+ <!-- Actions de table -->
2733
+ @if (config().deleteTable !== false) {
2734
+ <tiptap-button
2735
+ icon="delete_forever"
2736
+ title="{{ i18n.table().deleteTable }}"
2737
+ variant="danger"
2738
+ (click)="deleteTable()"
2739
+ ></tiptap-button>
2740
+ }
2741
+ </div>
2634
2742
  ` }]
2635
- }], ctorParameters: () => [], propDecorators: { menuRef: [{
2743
+ }], ctorParameters: () => [], propDecorators: { menuElement: [{
2636
2744
  type: ViewChild,
2637
- args: ["menuRef", { static: false }]
2745
+ args: ["menuElement", { static: true }]
2638
2746
  }] } });
2639
2747
 
2640
- class TiptapImageBubbleMenuComponent {
2748
+ class TiptapCellBubbleMenuComponent {
2641
2749
  constructor() {
2750
+ // Inputs
2642
2751
  this.editor = input.required();
2643
- this.config = input({
2644
- changeImage: true,
2645
- resizeSmall: true,
2646
- resizeMedium: true,
2647
- resizeLarge: true,
2648
- resizeOriginal: true,
2649
- deleteImage: true,
2650
- separator: true,
2651
- });
2752
+ this.config = input({});
2753
+ // Services
2754
+ this.i18nService = inject(TiptapI18nService);
2755
+ this.commandsService = inject(EditorCommandsService);
2756
+ // Tippy instance
2652
2757
  this.tippyInstance = null;
2653
- this.imageService = inject(ImageService);
2654
2758
  this.updateTimeout = null;
2655
- this.imageBubbleMenuConfig = computed(() => ({
2656
- changeImage: true,
2657
- resizeSmall: true,
2658
- resizeMedium: true,
2659
- resizeLarge: true,
2660
- resizeOriginal: true,
2661
- deleteImage: true,
2662
- separator: true,
2663
- ...this.config(),
2664
- }));
2665
- this.hasResizeButtons = computed(() => {
2666
- const config = this.imageBubbleMenuConfig();
2667
- return (config.resizeSmall ||
2668
- config.resizeMedium ||
2669
- config.resizeLarge ||
2670
- config.resizeOriginal);
2671
- });
2759
+ // Signaux
2760
+ this.i18n = this.i18nService;
2761
+ this.isSingleCellSelected = false;
2672
2762
  this.updateMenu = () => {
2673
2763
  // Debounce pour éviter les appels trop fréquents
2674
2764
  if (this.updateTimeout) {
@@ -2678,14 +2768,33 @@ class TiptapImageBubbleMenuComponent {
2678
2768
  const ed = this.editor();
2679
2769
  if (!ed)
2680
2770
  return;
2681
- const isImageSelected = ed.isActive("resizableImage") || ed.isActive("image");
2682
- const { from, to } = ed.state.selection;
2683
- const hasTextSelection = from !== to;
2684
- // Ne montrer le menu image que si :
2685
- // - Une image est sélectionnée
2686
- // - L'éditeur est éditable
2687
- const shouldShow = isImageSelected && ed.isEditable;
2771
+ const { selection } = ed.state;
2772
+ const { from, to } = selection;
2773
+ // Détecter spécifiquement la sélection de CELLULES (pas de texte)
2774
+ const hasCellSelection = selection instanceof CellSelection;
2775
+ // Une seule cellule si ancre et tête pointent vers la même cellule
2776
+ this.isSingleCellSelected =
2777
+ hasCellSelection &&
2778
+ selection.$anchorCell.pos ===
2779
+ selection.$headCell.pos;
2780
+ const hasTextSelection = !selection.empty && !(selection instanceof CellSelection);
2781
+ const isTableCell = ed.isActive("tableCell") || ed.isActive("tableHeader");
2782
+ console.log("CellBubbleMenu - updateMenu:", {
2783
+ hasCellSelection,
2784
+ isSingleCellSelected: this.isSingleCellSelected,
2785
+ hasTextSelection,
2786
+ isTableCell,
2787
+ selectionEmpty: selection.empty,
2788
+ selectionType: selection.constructor.name,
2789
+ from,
2790
+ to,
2791
+ isEditable: ed.isEditable,
2792
+ });
2793
+ // Le menu de cellule ne s'affiche QUE pour les sélections de cellules multiples
2794
+ // (pas pour la sélection de texte dans une cellule)
2795
+ const shouldShow = hasCellSelection && isTableCell && ed.isEditable;
2688
2796
  if (shouldShow) {
2797
+ console.log("CellBubbleMenu - Affichage du menu de cellules");
2689
2798
  this.showTippy();
2690
2799
  }
2691
2800
  else {
@@ -2699,57 +2808,41 @@ class TiptapImageBubbleMenuComponent {
2699
2808
  this.hideTippy();
2700
2809
  }, 100);
2701
2810
  };
2811
+ // Effet pour mettre à jour le menu quand l'éditeur change
2702
2812
  effect(() => {
2703
- const ed = this.editor();
2704
- if (!ed)
2705
- return;
2706
- // Nettoyer les anciens listeners
2707
- ed.off("selectionUpdate", this.updateMenu);
2708
- ed.off("transaction", this.updateMenu);
2709
- ed.off("focus", this.updateMenu);
2710
- ed.off("blur", this.handleBlur);
2711
- // Ajouter les nouveaux listeners
2712
- ed.on("selectionUpdate", this.updateMenu);
2713
- ed.on("transaction", this.updateMenu);
2714
- ed.on("focus", this.updateMenu);
2715
- ed.on("blur", this.handleBlur);
2716
- // Ne pas appeler updateMenu() ici pour éviter l'affichage prématuré
2717
- // Il sera appelé automatiquement quand l'éditeur sera prêt
2813
+ const editor = this.editor();
2814
+ if (editor) {
2815
+ // Nettoyer les anciens listeners
2816
+ editor.off("selectionUpdate", this.updateMenu);
2817
+ editor.off("focus", this.updateMenu);
2818
+ editor.off("blur", this.handleBlur);
2819
+ // Ajouter les nouveaux listeners
2820
+ editor.on("selectionUpdate", this.updateMenu);
2821
+ editor.on("focus", this.updateMenu);
2822
+ editor.on("blur", this.handleBlur);
2823
+ }
2718
2824
  });
2719
2825
  }
2720
2826
  ngOnInit() {
2721
- // Initialiser Tippy de manière synchrone après que le component soit ready
2722
2827
  this.initTippy();
2723
2828
  }
2724
2829
  ngOnDestroy() {
2725
- const ed = this.editor();
2726
- if (ed) {
2727
- ed.off("selectionUpdate", this.updateMenu);
2728
- ed.off("transaction", this.updateMenu);
2729
- ed.off("focus", this.updateMenu);
2730
- ed.off("blur", this.handleBlur);
2731
- }
2732
- // Nettoyer les timeouts
2733
- if (this.updateTimeout) {
2734
- clearTimeout(this.updateTimeout);
2830
+ const editor = this.editor();
2831
+ if (editor) {
2832
+ // Nettoyer les événements
2833
+ editor.off("selectionUpdate", this.updateMenu);
2834
+ editor.off("focus", this.updateMenu);
2835
+ editor.off("blur", this.handleBlur);
2735
2836
  }
2736
- // Nettoyer Tippy
2737
2837
  if (this.tippyInstance) {
2738
2838
  this.tippyInstance.destroy();
2739
- this.tippyInstance = null;
2839
+ }
2840
+ if (this.updateTimeout) {
2841
+ clearTimeout(this.updateTimeout);
2740
2842
  }
2741
2843
  }
2742
2844
  initTippy() {
2743
- // Attendre que l'élément soit disponible
2744
- if (!this.menuRef?.nativeElement) {
2745
- setTimeout(() => this.initTippy(), 50);
2746
- return;
2747
- }
2748
- const menuElement = this.menuRef.nativeElement;
2749
- // S'assurer qu'il n'y a pas déjà une instance
2750
- if (this.tippyInstance) {
2751
- this.tippyInstance.destroy();
2752
- }
2845
+ const menuElement = this.menuElement.nativeElement;
2753
2846
  // Créer l'instance Tippy
2754
2847
  this.tippyInstance = tippy(document.body, {
2755
2848
  content: menuElement,
@@ -2764,8 +2857,7 @@ class TiptapImageBubbleMenuComponent {
2764
2857
  // S'assurer que les autres menus sont fermés
2765
2858
  this.hideOtherMenus();
2766
2859
  },
2767
- getReferenceClientRect: () => this.getImageRect(),
2768
- // Améliorer le positionnement avec scroll
2860
+ getReferenceClientRect: () => this.getCellRect(),
2769
2861
  popperOptions: {
2770
2862
  modifiers: [
2771
2863
  {
@@ -2787,218 +2879,636 @@ class TiptapImageBubbleMenuComponent {
2787
2879
  // Maintenant que Tippy est initialisé, faire un premier check
2788
2880
  this.updateMenu();
2789
2881
  }
2790
- getImageRect() {
2882
+ getCellRect() {
2791
2883
  const ed = this.editor();
2792
2884
  if (!ed)
2793
2885
  return new DOMRect(0, 0, 0, 0);
2794
- // Trouver l'image sélectionnée dans le DOM
2795
- const { from } = ed.state.selection;
2796
- // Fonction pour trouver toutes les images dans l'éditeur
2797
- const getAllImages = () => {
2798
- const editorElement = ed.view.dom;
2799
- return Array.from(editorElement.querySelectorAll("img"));
2800
- };
2801
- // Fonction pour trouver l'image à la position spécifique
2802
- const findImageAtPosition = () => {
2803
- const allImages = getAllImages();
2804
- for (const img of allImages) {
2805
- try {
2806
- // Obtenir la position ProseMirror de cette image
2807
- const imgPos = ed.view.posAtDOM(img, 0);
2808
- // Vérifier si cette image correspond à la position sélectionnée
2809
- if (Math.abs(imgPos - from) <= 1) {
2810
- return img;
2811
- }
2812
- }
2813
- catch (error) {
2814
- // Continuer si on ne peut pas obtenir la position de cette image
2815
- continue;
2816
- }
2817
- }
2818
- return null;
2819
- };
2820
- // Chercher l'image à la position exacte
2821
- const imageElement = findImageAtPosition();
2822
- if (imageElement) {
2823
- return imageElement.getBoundingClientRect();
2886
+ // Détecter la sélection de cellules
2887
+ const { from, to } = ed.state.selection;
2888
+ const hasCellSelection = from !== to;
2889
+ if (!hasCellSelection) {
2890
+ return new DOMRect(0, 0, 0, 0);
2824
2891
  }
2825
- return new DOMRect(0, 0, 0, 0);
2892
+ // Obtenir les coordonnées de la sélection
2893
+ const coords = ed.view.coordsAtPos(from);
2894
+ const endCoords = ed.view.coordsAtPos(to);
2895
+ // Créer un rectangle englobant la sélection
2896
+ const rect = new DOMRect(Math.min(coords.left, endCoords.left), Math.min(coords.top, endCoords.top), Math.abs(endCoords.left - coords.left), Math.abs(endCoords.top - coords.top));
2897
+ return rect;
2826
2898
  }
2827
2899
  hideOtherMenus() {
2828
- // Cette méthode peut être étendue pour fermer d'autres menus si nécessaire
2829
- // Pour l'instant, elle sert de placeholder pour une future coordination entre menus
2900
+ // Masquer tous les autres menus quand le menu de cellules est actif
2901
+ this.hideTableMenu();
2902
+ this.hideTextBubbleMenu();
2830
2903
  }
2831
2904
  showTippy() {
2832
2905
  if (!this.tippyInstance)
2833
2906
  return;
2907
+ // Masquer les autres menus avant d'afficher le menu de cellules
2908
+ this.hideTableMenu();
2909
+ this.hideTextBubbleMenu();
2834
2910
  // Mettre à jour la position
2835
2911
  this.tippyInstance.setProps({
2836
- getReferenceClientRect: () => this.getImageRect(),
2912
+ getReferenceClientRect: () => this.getCellRect(),
2837
2913
  });
2838
2914
  this.tippyInstance.show();
2839
2915
  }
2840
- hideTippy() {
2841
- if (this.tippyInstance) {
2842
- this.tippyInstance.hide();
2916
+ hideTableMenu() {
2917
+ // Masquer le menu de table quand le menu de cellules est actif
2918
+ const tableMenu = document.querySelector("tiptap-table-bubble-menu");
2919
+ if (tableMenu) {
2920
+ const tippyInstance = tableMenu._tippy;
2921
+ if (tippyInstance) {
2922
+ tippyInstance.hide();
2923
+ }
2924
+ }
2925
+ // Alternative : masquer via l'élément Angular
2926
+ const tableMenuComponent = document.querySelector("tiptap-table-bubble-menu");
2927
+ if (tableMenuComponent && tableMenuComponent.hideTippy) {
2928
+ tableMenuComponent.hideTippy();
2843
2929
  }
2844
2930
  }
2845
- onCommand(command, event) {
2846
- event.preventDefault();
2847
- const ed = this.editor();
2848
- if (!ed)
2849
- return;
2850
- switch (command) {
2851
- case "changeImage":
2852
- this.changeImage();
2853
- break;
2854
- case "resizeSmall":
2855
- this.imageService.resizeImageToSmall(ed);
2856
- break;
2857
- case "resizeMedium":
2858
- this.imageService.resizeImageToMedium(ed);
2859
- break;
2860
- case "resizeLarge":
2861
- this.imageService.resizeImageToLarge(ed);
2862
- break;
2863
- case "resizeOriginal":
2864
- this.imageService.resizeImageToOriginal(ed);
2865
- break;
2866
- case "deleteImage":
2867
- this.deleteImage();
2868
- break;
2931
+ hideTextBubbleMenu() {
2932
+ // Masquer le menu de texte (bubble menu général) quand le menu de cellules est actif
2933
+ const textMenu = document.querySelector("tiptap-bubble-menu");
2934
+ if (textMenu) {
2935
+ const tippyInstance = textMenu._tippy;
2936
+ if (tippyInstance) {
2937
+ tippyInstance.hide();
2938
+ }
2939
+ }
2940
+ // Alternative : masquer via l'élément Angular
2941
+ const textMenuComponent = document.querySelector("tiptap-bubble-menu");
2942
+ if (textMenuComponent && textMenuComponent.hideTippy) {
2943
+ textMenuComponent.hideTippy();
2869
2944
  }
2870
2945
  }
2871
- async changeImage() {
2872
- const ed = this.editor();
2873
- if (!ed)
2946
+ hideTippy() {
2947
+ if (!this.tippyInstance)
2874
2948
  return;
2949
+ this.tippyInstance.hide();
2950
+ }
2951
+ // Actions spécifiques aux cellules
2952
+ mergeCells() {
2953
+ this.commandsService.mergeCells(this.editor());
2954
+ }
2955
+ splitCell() {
2956
+ this.commandsService.splitCell(this.editor());
2957
+ }
2958
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapCellBubbleMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2959
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.0", type: TiptapCellBubbleMenuComponent, isStandalone: true, selector: "tiptap-cell-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: "menuElement", first: true, predicate: ["menuElement"], descendants: true, static: true }], ngImport: i0, template: `
2960
+ <div #menuElement class="bubble-menu">
2961
+ <!-- Actions spécifiques aux cellules -->
2962
+ @if (config().mergeCells !== false && !isSingleCellSelected) {
2963
+ <tiptap-button
2964
+ icon="cell_merge"
2965
+ title="{{ i18n.table().mergeCells }}"
2966
+ (click)="mergeCells()"
2967
+ ></tiptap-button>
2968
+ } @if (config().splitCell !== false && isSingleCellSelected) {
2969
+ <tiptap-button
2970
+ icon="split_scene"
2971
+ title="{{ i18n.table().splitCell }}"
2972
+ (click)="splitCell()"
2973
+ ></tiptap-button>
2974
+ }
2975
+ </div>
2976
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: TiptapButtonComponent, selector: "tiptap-button", inputs: ["icon", "title", "active", "disabled", "variant", "size", "iconSize"], outputs: ["onClick"] }] }); }
2977
+ }
2978
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapCellBubbleMenuComponent, decorators: [{
2979
+ type: Component,
2980
+ args: [{ selector: "tiptap-cell-bubble-menu", standalone: true, imports: [CommonModule, TiptapButtonComponent], template: `
2981
+ <div #menuElement class="bubble-menu">
2982
+ <!-- Actions spécifiques aux cellules -->
2983
+ @if (config().mergeCells !== false && !isSingleCellSelected) {
2984
+ <tiptap-button
2985
+ icon="cell_merge"
2986
+ title="{{ i18n.table().mergeCells }}"
2987
+ (click)="mergeCells()"
2988
+ ></tiptap-button>
2989
+ } @if (config().splitCell !== false && isSingleCellSelected) {
2990
+ <tiptap-button
2991
+ icon="split_scene"
2992
+ title="{{ i18n.table().splitCell }}"
2993
+ (click)="splitCell()"
2994
+ ></tiptap-button>
2995
+ }
2996
+ </div>
2997
+ ` }]
2998
+ }], ctorParameters: () => [], propDecorators: { menuElement: [{
2999
+ type: ViewChild,
3000
+ args: ["menuElement", { static: true }]
3001
+ }] } });
3002
+
3003
+ class TiptapToolbarComponent {
3004
+ constructor(editorCommands) {
3005
+ this.editorCommands = editorCommands;
3006
+ this.editor = input.required();
3007
+ this.config = input.required();
3008
+ // Outputs pour les événements d'image
3009
+ this.imageUploaded = output();
3010
+ this.imageError = output();
3011
+ this.imageService = inject(ImageService);
3012
+ this.i18nService = inject(TiptapI18nService);
3013
+ // Computed values pour les traductions
3014
+ this.t = this.i18nService.toolbar;
3015
+ }
3016
+ isActive(name, attributes) {
3017
+ return this.editorCommands.isActive(this.editor(), name, attributes);
3018
+ }
3019
+ canExecute(command) {
3020
+ return this.editorCommands.canExecute(this.editor(), command);
3021
+ }
3022
+ toggleBold() {
3023
+ this.editorCommands.toggleBold(this.editor());
3024
+ }
3025
+ toggleItalic() {
3026
+ this.editorCommands.toggleItalic(this.editor());
3027
+ }
3028
+ toggleStrike() {
3029
+ this.editorCommands.toggleStrike(this.editor());
3030
+ }
3031
+ toggleCode() {
3032
+ this.editorCommands.toggleCode(this.editor());
3033
+ }
3034
+ toggleHeading(level) {
3035
+ this.editorCommands.toggleHeading(this.editor(), level);
3036
+ }
3037
+ toggleBulletList() {
3038
+ this.editorCommands.toggleBulletList(this.editor());
3039
+ }
3040
+ toggleOrderedList() {
3041
+ this.editorCommands.toggleOrderedList(this.editor());
3042
+ }
3043
+ toggleBlockquote() {
3044
+ this.editorCommands.toggleBlockquote(this.editor());
3045
+ }
3046
+ undo() {
3047
+ this.editorCommands.undo(this.editor());
3048
+ }
3049
+ redo() {
3050
+ this.editorCommands.redo(this.editor());
3051
+ }
3052
+ // Nouvelles méthodes pour les formatages supplémentaires
3053
+ toggleUnderline() {
3054
+ this.editorCommands.toggleUnderline(this.editor());
3055
+ }
3056
+ toggleSuperscript() {
3057
+ this.editorCommands.toggleSuperscript(this.editor());
3058
+ }
3059
+ toggleSubscript() {
3060
+ this.editorCommands.toggleSubscript(this.editor());
3061
+ }
3062
+ setTextAlign(alignment) {
3063
+ this.editorCommands.setTextAlign(this.editor(), alignment);
3064
+ }
3065
+ toggleLink() {
3066
+ this.editorCommands.toggleLink(this.editor());
3067
+ }
3068
+ insertHorizontalRule() {
3069
+ this.editorCommands.insertHorizontalRule(this.editor());
3070
+ }
3071
+ toggleHighlight() {
3072
+ this.editorCommands.toggleHighlight(this.editor());
3073
+ }
3074
+ // Méthode pour insérer un tableau
3075
+ insertTable() {
3076
+ this.editorCommands.insertTable(this.editor());
3077
+ }
3078
+ // Méthode pour insérer une image
3079
+ async insertImage() {
2875
3080
  try {
2876
- // Utiliser la méthode spécifique pour remplacer une image existante
2877
- await this.imageService.selectAndReplaceImage(ed, {
2878
- quality: 0.8,
2879
- maxWidth: 1920,
2880
- maxHeight: 1080,
2881
- accept: "image/*",
2882
- });
3081
+ await this.imageService.selectAndUploadImage(this.editor());
2883
3082
  }
2884
3083
  catch (error) {
2885
- console.error("Erreur lors du changement d'image:", error);
3084
+ console.error("Erreur lors de l'upload d'image:", error);
3085
+ this.imageError.emit("Erreur lors de l'upload d'image");
2886
3086
  }
2887
3087
  }
2888
- deleteImage() {
2889
- const ed = this.editor();
2890
- if (ed) {
2891
- ed.chain().focus().deleteSelection().run();
2892
- }
3088
+ // Méthode pour vider le contenu
3089
+ clearContent() {
3090
+ this.editorCommands.clearContent(this.editor());
2893
3091
  }
2894
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapImageBubbleMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2895
- 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: `
2896
- <div #menuRef class="bubble-menu">
2897
- @if (imageBubbleMenuConfig().changeImage) {
2898
- <tiptap-button
2899
- icon="drive_file_rename_outline"
2900
- title="Changer l'image"
2901
- (click)="onCommand('changeImage', $event)"
2902
- ></tiptap-button>
2903
- } @if (imageBubbleMenuConfig().separator && hasResizeButtons()) {
2904
- <tiptap-separator></tiptap-separator>
2905
- } @if (imageBubbleMenuConfig().resizeSmall) {
2906
- <tiptap-button
2907
- icon="crop_square"
2908
- iconSize="small"
2909
- title="Petite (300×200)"
2910
- (click)="onCommand('resizeSmall', $event)"
2911
- ></tiptap-button>
2912
- } @if (imageBubbleMenuConfig().resizeMedium) {
2913
- <tiptap-button
2914
- icon="crop_square"
2915
- iconSize="medium"
2916
- title="Moyenne (500×350)"
2917
- (click)="onCommand('resizeMedium', $event)"
2918
- ></tiptap-button>
2919
- } @if (imageBubbleMenuConfig().resizeLarge) {
2920
- <tiptap-button
2921
- icon="crop_square"
2922
- iconSize="large"
2923
- title="Grande (800×600)"
2924
- (click)="onCommand('resizeLarge', $event)"
2925
- ></tiptap-button>
2926
- } @if (imageBubbleMenuConfig().resizeOriginal) {
2927
- <tiptap-button
2928
- icon="photo_size_select_actual"
2929
- title="Taille originale"
2930
- (click)="onCommand('resizeOriginal', $event)"
2931
- ></tiptap-button>
2932
- } @if (imageBubbleMenuConfig().separator &&
2933
- imageBubbleMenuConfig().deleteImage) {
2934
- <tiptap-separator></tiptap-separator>
2935
- } @if (imageBubbleMenuConfig().deleteImage) {
2936
- <tiptap-button
2937
- icon="delete"
2938
- title="Supprimer l'image"
2939
- variant="danger"
2940
- (click)="onCommand('deleteImage', $event)"
2941
- ></tiptap-button>
2942
- }
2943
- </div>
2944
- `, 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"] }] }); }
3092
+ // Méthodes pour les événements d'image (conservées pour compatibilité)
3093
+ onImageSelected(result) {
3094
+ this.imageUploaded.emit(result);
3095
+ }
3096
+ onImageError(error) {
3097
+ this.imageError.emit(error);
3098
+ }
3099
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapToolbarComponent, deps: [{ token: EditorCommandsService }], target: i0.ɵɵFactoryTarget.Component }); }
3100
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.0", type: TiptapToolbarComponent, isStandalone: true, selector: "tiptap-toolbar", inputs: { editor: { classPropertyName: "editor", publicName: "editor", isSignal: true, isRequired: true, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { imageUploaded: "imageUploaded", imageError: "imageError" }, ngImport: i0, template: `
3101
+ <div class="tiptap-toolbar">
3102
+ @if (config().bold) {
3103
+ <tiptap-button
3104
+ icon="format_bold"
3105
+ [title]="t().bold"
3106
+ [active]="isActive('bold')"
3107
+ [disabled]="!canExecute('toggleBold')"
3108
+ (onClick)="toggleBold()"
3109
+ />
3110
+ } @if (config().italic) {
3111
+ <tiptap-button
3112
+ icon="format_italic"
3113
+ [title]="t().italic"
3114
+ [active]="isActive('italic')"
3115
+ [disabled]="!canExecute('toggleItalic')"
3116
+ (onClick)="toggleItalic()"
3117
+ />
3118
+ } @if (config().underline) {
3119
+ <tiptap-button
3120
+ icon="format_underlined"
3121
+ [title]="t().underline"
3122
+ [active]="isActive('underline')"
3123
+ [disabled]="!canExecute('toggleUnderline')"
3124
+ (onClick)="toggleUnderline()"
3125
+ />
3126
+ } @if (config().strike) {
3127
+ <tiptap-button
3128
+ icon="strikethrough_s"
3129
+ [title]="t().strike"
3130
+ [active]="isActive('strike')"
3131
+ [disabled]="!canExecute('toggleStrike')"
3132
+ (onClick)="toggleStrike()"
3133
+ />
3134
+ } @if (config().code) {
3135
+ <tiptap-button
3136
+ icon="code"
3137
+ [title]="t().code"
3138
+ [active]="isActive('code')"
3139
+ [disabled]="!canExecute('toggleCode')"
3140
+ (onClick)="toggleCode()"
3141
+ />
3142
+ } @if (config().superscript) {
3143
+ <tiptap-button
3144
+ icon="superscript"
3145
+ [title]="t().superscript"
3146
+ [active]="isActive('superscript')"
3147
+ [disabled]="!canExecute('toggleSuperscript')"
3148
+ (onClick)="toggleSuperscript()"
3149
+ />
3150
+ } @if (config().subscript) {
3151
+ <tiptap-button
3152
+ icon="subscript"
3153
+ [title]="t().subscript"
3154
+ [active]="isActive('subscript')"
3155
+ [disabled]="!canExecute('toggleSubscript')"
3156
+ (onClick)="toggleSubscript()"
3157
+ />
3158
+ } @if (config().highlight) {
3159
+ <tiptap-button
3160
+ icon="highlight"
3161
+ [title]="t().highlight"
3162
+ [active]="isActive('highlight')"
3163
+ [disabled]="!canExecute('toggleHighlight')"
3164
+ (onClick)="toggleHighlight()"
3165
+ />
3166
+ } @if (config().separator && (config().heading1 || config().heading2 ||
3167
+ config().heading3)) {
3168
+ <tiptap-separator />
3169
+ } @if (config().heading1) {
3170
+ <tiptap-button
3171
+ icon="format_h1"
3172
+ [title]="t().heading1"
3173
+ variant="text"
3174
+ [active]="isActive('heading', { level: 1 })"
3175
+ (onClick)="toggleHeading(1)"
3176
+ />
3177
+ } @if (config().heading2) {
3178
+ <tiptap-button
3179
+ icon="format_h2"
3180
+ [title]="t().heading2"
3181
+ variant="text"
3182
+ [active]="isActive('heading', { level: 2 })"
3183
+ (onClick)="toggleHeading(2)"
3184
+ />
3185
+ } @if (config().heading3) {
3186
+ <tiptap-button
3187
+ icon="format_h3"
3188
+ [title]="t().heading3"
3189
+ variant="text"
3190
+ [active]="isActive('heading', { level: 3 })"
3191
+ (onClick)="toggleHeading(3)"
3192
+ />
3193
+ } @if (config().separator && (config().bulletList || config().orderedList
3194
+ || config().blockquote)) {
3195
+ <tiptap-separator />
3196
+ } @if (config().bulletList) {
3197
+ <tiptap-button
3198
+ icon="format_list_bulleted"
3199
+ [title]="t().bulletList"
3200
+ [active]="isActive('bulletList')"
3201
+ (onClick)="toggleBulletList()"
3202
+ />
3203
+ } @if (config().orderedList) {
3204
+ <tiptap-button
3205
+ icon="format_list_numbered"
3206
+ [title]="t().orderedList"
3207
+ [active]="isActive('orderedList')"
3208
+ (onClick)="toggleOrderedList()"
3209
+ />
3210
+ } @if (config().blockquote) {
3211
+ <tiptap-button
3212
+ icon="format_quote"
3213
+ [title]="t().blockquote"
3214
+ [active]="isActive('blockquote')"
3215
+ (onClick)="toggleBlockquote()"
3216
+ />
3217
+ } @if (config().separator && (config().alignLeft || config().alignCenter
3218
+ || config().alignRight || config().alignJustify)) {
3219
+ <tiptap-separator />
3220
+ } @if (config().alignLeft) {
3221
+ <tiptap-button
3222
+ icon="format_align_left"
3223
+ [title]="t().alignLeft"
3224
+ [active]="isActive('textAlign', { textAlign: 'left' })"
3225
+ (onClick)="setTextAlign('left')"
3226
+ />
3227
+ } @if (config().alignCenter) {
3228
+ <tiptap-button
3229
+ icon="format_align_center"
3230
+ [title]="t().alignCenter"
3231
+ [active]="isActive('textAlign', { textAlign: 'center' })"
3232
+ (onClick)="setTextAlign('center')"
3233
+ />
3234
+ } @if (config().alignRight) {
3235
+ <tiptap-button
3236
+ icon="format_align_right"
3237
+ [title]="t().alignRight"
3238
+ [active]="isActive('textAlign', { textAlign: 'right' })"
3239
+ (onClick)="setTextAlign('right')"
3240
+ />
3241
+ } @if (config().alignJustify) {
3242
+ <tiptap-button
3243
+ icon="format_align_justify"
3244
+ [title]="t().alignJustify"
3245
+ [active]="isActive('textAlign', { textAlign: 'justify' })"
3246
+ (onClick)="setTextAlign('justify')"
3247
+ />
3248
+ } @if (config().separator && (config().link || config().horizontalRule)) {
3249
+ <tiptap-separator />
3250
+ } @if (config().link) {
3251
+ <tiptap-button
3252
+ icon="link"
3253
+ [title]="t().link"
3254
+ [active]="isActive('link')"
3255
+ (onClick)="toggleLink()"
3256
+ />
3257
+ } @if (config().horizontalRule) {
3258
+ <tiptap-button
3259
+ icon="horizontal_rule"
3260
+ [title]="t().horizontalRule"
3261
+ (onClick)="insertHorizontalRule()"
3262
+ />
3263
+ } @if (config().table) {
3264
+ <tiptap-button
3265
+ icon="table_view"
3266
+ [title]="t().table"
3267
+ (onClick)="insertTable()"
3268
+ />
3269
+ } @if (config().separator && config().image) {
3270
+ <tiptap-separator />
3271
+ } @if (config().image) {
3272
+ <tiptap-button
3273
+ icon="image"
3274
+ [title]="t().image"
3275
+ (onClick)="insertImage()"
3276
+ />
3277
+ } @if (config().separator && (config().undo || config().redo)) {
3278
+ <tiptap-separator />
3279
+ } @if (config().undo) {
3280
+ <tiptap-button
3281
+ icon="undo"
3282
+ [title]="t().undo"
3283
+ [disabled]="!canExecute('undo')"
3284
+ (onClick)="undo()"
3285
+ />
3286
+ } @if (config().redo) {
3287
+ <tiptap-button
3288
+ icon="redo"
3289
+ [title]="t().redo"
3290
+ [disabled]="!canExecute('redo')"
3291
+ (onClick)="redo()"
3292
+ />
3293
+ } @if (config().separator && config().clear) {
3294
+ <tiptap-separator />
3295
+ } @if (config().clear) {
3296
+ <tiptap-button
3297
+ icon="delete"
3298
+ [title]="t().clear"
3299
+ (onClick)="clearContent()"
3300
+ />
3301
+ }
3302
+ </div>
3303
+ `, 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"] }] }); }
2945
3304
  }
2946
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapImageBubbleMenuComponent, decorators: [{
3305
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapToolbarComponent, decorators: [{
2947
3306
  type: Component,
2948
- args: [{ selector: "tiptap-image-bubble-menu", standalone: true, imports: [TiptapButtonComponent, TiptapSeparatorComponent], template: `
2949
- <div #menuRef class="bubble-menu">
2950
- @if (imageBubbleMenuConfig().changeImage) {
2951
- <tiptap-button
2952
- icon="drive_file_rename_outline"
2953
- title="Changer l'image"
2954
- (click)="onCommand('changeImage', $event)"
2955
- ></tiptap-button>
2956
- } @if (imageBubbleMenuConfig().separator && hasResizeButtons()) {
2957
- <tiptap-separator></tiptap-separator>
2958
- } @if (imageBubbleMenuConfig().resizeSmall) {
2959
- <tiptap-button
2960
- icon="crop_square"
2961
- iconSize="small"
2962
- title="Petite (300×200)"
2963
- (click)="onCommand('resizeSmall', $event)"
2964
- ></tiptap-button>
2965
- } @if (imageBubbleMenuConfig().resizeMedium) {
2966
- <tiptap-button
2967
- icon="crop_square"
2968
- iconSize="medium"
2969
- title="Moyenne (500×350)"
2970
- (click)="onCommand('resizeMedium', $event)"
2971
- ></tiptap-button>
2972
- } @if (imageBubbleMenuConfig().resizeLarge) {
2973
- <tiptap-button
2974
- icon="crop_square"
2975
- iconSize="large"
2976
- title="Grande (800×600)"
2977
- (click)="onCommand('resizeLarge', $event)"
2978
- ></tiptap-button>
2979
- } @if (imageBubbleMenuConfig().resizeOriginal) {
2980
- <tiptap-button
2981
- icon="photo_size_select_actual"
2982
- title="Taille originale"
2983
- (click)="onCommand('resizeOriginal', $event)"
2984
- ></tiptap-button>
2985
- } @if (imageBubbleMenuConfig().separator &&
2986
- imageBubbleMenuConfig().deleteImage) {
2987
- <tiptap-separator></tiptap-separator>
2988
- } @if (imageBubbleMenuConfig().deleteImage) {
2989
- <tiptap-button
2990
- icon="delete"
2991
- title="Supprimer l'image"
2992
- variant="danger"
2993
- (click)="onCommand('deleteImage', $event)"
2994
- ></tiptap-button>
2995
- }
2996
- </div>
2997
- ` }]
2998
- }], ctorParameters: () => [], propDecorators: { menuRef: [{
2999
- type: ViewChild,
3000
- args: ["menuRef", { static: false }]
3001
- }] } });
3307
+ args: [{ selector: "tiptap-toolbar", standalone: true, imports: [TiptapButtonComponent, TiptapSeparatorComponent], template: `
3308
+ <div class="tiptap-toolbar">
3309
+ @if (config().bold) {
3310
+ <tiptap-button
3311
+ icon="format_bold"
3312
+ [title]="t().bold"
3313
+ [active]="isActive('bold')"
3314
+ [disabled]="!canExecute('toggleBold')"
3315
+ (onClick)="toggleBold()"
3316
+ />
3317
+ } @if (config().italic) {
3318
+ <tiptap-button
3319
+ icon="format_italic"
3320
+ [title]="t().italic"
3321
+ [active]="isActive('italic')"
3322
+ [disabled]="!canExecute('toggleItalic')"
3323
+ (onClick)="toggleItalic()"
3324
+ />
3325
+ } @if (config().underline) {
3326
+ <tiptap-button
3327
+ icon="format_underlined"
3328
+ [title]="t().underline"
3329
+ [active]="isActive('underline')"
3330
+ [disabled]="!canExecute('toggleUnderline')"
3331
+ (onClick)="toggleUnderline()"
3332
+ />
3333
+ } @if (config().strike) {
3334
+ <tiptap-button
3335
+ icon="strikethrough_s"
3336
+ [title]="t().strike"
3337
+ [active]="isActive('strike')"
3338
+ [disabled]="!canExecute('toggleStrike')"
3339
+ (onClick)="toggleStrike()"
3340
+ />
3341
+ } @if (config().code) {
3342
+ <tiptap-button
3343
+ icon="code"
3344
+ [title]="t().code"
3345
+ [active]="isActive('code')"
3346
+ [disabled]="!canExecute('toggleCode')"
3347
+ (onClick)="toggleCode()"
3348
+ />
3349
+ } @if (config().superscript) {
3350
+ <tiptap-button
3351
+ icon="superscript"
3352
+ [title]="t().superscript"
3353
+ [active]="isActive('superscript')"
3354
+ [disabled]="!canExecute('toggleSuperscript')"
3355
+ (onClick)="toggleSuperscript()"
3356
+ />
3357
+ } @if (config().subscript) {
3358
+ <tiptap-button
3359
+ icon="subscript"
3360
+ [title]="t().subscript"
3361
+ [active]="isActive('subscript')"
3362
+ [disabled]="!canExecute('toggleSubscript')"
3363
+ (onClick)="toggleSubscript()"
3364
+ />
3365
+ } @if (config().highlight) {
3366
+ <tiptap-button
3367
+ icon="highlight"
3368
+ [title]="t().highlight"
3369
+ [active]="isActive('highlight')"
3370
+ [disabled]="!canExecute('toggleHighlight')"
3371
+ (onClick)="toggleHighlight()"
3372
+ />
3373
+ } @if (config().separator && (config().heading1 || config().heading2 ||
3374
+ config().heading3)) {
3375
+ <tiptap-separator />
3376
+ } @if (config().heading1) {
3377
+ <tiptap-button
3378
+ icon="format_h1"
3379
+ [title]="t().heading1"
3380
+ variant="text"
3381
+ [active]="isActive('heading', { level: 1 })"
3382
+ (onClick)="toggleHeading(1)"
3383
+ />
3384
+ } @if (config().heading2) {
3385
+ <tiptap-button
3386
+ icon="format_h2"
3387
+ [title]="t().heading2"
3388
+ variant="text"
3389
+ [active]="isActive('heading', { level: 2 })"
3390
+ (onClick)="toggleHeading(2)"
3391
+ />
3392
+ } @if (config().heading3) {
3393
+ <tiptap-button
3394
+ icon="format_h3"
3395
+ [title]="t().heading3"
3396
+ variant="text"
3397
+ [active]="isActive('heading', { level: 3 })"
3398
+ (onClick)="toggleHeading(3)"
3399
+ />
3400
+ } @if (config().separator && (config().bulletList || config().orderedList
3401
+ || config().blockquote)) {
3402
+ <tiptap-separator />
3403
+ } @if (config().bulletList) {
3404
+ <tiptap-button
3405
+ icon="format_list_bulleted"
3406
+ [title]="t().bulletList"
3407
+ [active]="isActive('bulletList')"
3408
+ (onClick)="toggleBulletList()"
3409
+ />
3410
+ } @if (config().orderedList) {
3411
+ <tiptap-button
3412
+ icon="format_list_numbered"
3413
+ [title]="t().orderedList"
3414
+ [active]="isActive('orderedList')"
3415
+ (onClick)="toggleOrderedList()"
3416
+ />
3417
+ } @if (config().blockquote) {
3418
+ <tiptap-button
3419
+ icon="format_quote"
3420
+ [title]="t().blockquote"
3421
+ [active]="isActive('blockquote')"
3422
+ (onClick)="toggleBlockquote()"
3423
+ />
3424
+ } @if (config().separator && (config().alignLeft || config().alignCenter
3425
+ || config().alignRight || config().alignJustify)) {
3426
+ <tiptap-separator />
3427
+ } @if (config().alignLeft) {
3428
+ <tiptap-button
3429
+ icon="format_align_left"
3430
+ [title]="t().alignLeft"
3431
+ [active]="isActive('textAlign', { textAlign: 'left' })"
3432
+ (onClick)="setTextAlign('left')"
3433
+ />
3434
+ } @if (config().alignCenter) {
3435
+ <tiptap-button
3436
+ icon="format_align_center"
3437
+ [title]="t().alignCenter"
3438
+ [active]="isActive('textAlign', { textAlign: 'center' })"
3439
+ (onClick)="setTextAlign('center')"
3440
+ />
3441
+ } @if (config().alignRight) {
3442
+ <tiptap-button
3443
+ icon="format_align_right"
3444
+ [title]="t().alignRight"
3445
+ [active]="isActive('textAlign', { textAlign: 'right' })"
3446
+ (onClick)="setTextAlign('right')"
3447
+ />
3448
+ } @if (config().alignJustify) {
3449
+ <tiptap-button
3450
+ icon="format_align_justify"
3451
+ [title]="t().alignJustify"
3452
+ [active]="isActive('textAlign', { textAlign: 'justify' })"
3453
+ (onClick)="setTextAlign('justify')"
3454
+ />
3455
+ } @if (config().separator && (config().link || config().horizontalRule)) {
3456
+ <tiptap-separator />
3457
+ } @if (config().link) {
3458
+ <tiptap-button
3459
+ icon="link"
3460
+ [title]="t().link"
3461
+ [active]="isActive('link')"
3462
+ (onClick)="toggleLink()"
3463
+ />
3464
+ } @if (config().horizontalRule) {
3465
+ <tiptap-button
3466
+ icon="horizontal_rule"
3467
+ [title]="t().horizontalRule"
3468
+ (onClick)="insertHorizontalRule()"
3469
+ />
3470
+ } @if (config().table) {
3471
+ <tiptap-button
3472
+ icon="table_view"
3473
+ [title]="t().table"
3474
+ (onClick)="insertTable()"
3475
+ />
3476
+ } @if (config().separator && config().image) {
3477
+ <tiptap-separator />
3478
+ } @if (config().image) {
3479
+ <tiptap-button
3480
+ icon="image"
3481
+ [title]="t().image"
3482
+ (onClick)="insertImage()"
3483
+ />
3484
+ } @if (config().separator && (config().undo || config().redo)) {
3485
+ <tiptap-separator />
3486
+ } @if (config().undo) {
3487
+ <tiptap-button
3488
+ icon="undo"
3489
+ [title]="t().undo"
3490
+ [disabled]="!canExecute('undo')"
3491
+ (onClick)="undo()"
3492
+ />
3493
+ } @if (config().redo) {
3494
+ <tiptap-button
3495
+ icon="redo"
3496
+ [title]="t().redo"
3497
+ [disabled]="!canExecute('redo')"
3498
+ (onClick)="redo()"
3499
+ />
3500
+ } @if (config().separator && config().clear) {
3501
+ <tiptap-separator />
3502
+ } @if (config().clear) {
3503
+ <tiptap-button
3504
+ icon="delete"
3505
+ [title]="t().clear"
3506
+ (onClick)="clearContent()"
3507
+ />
3508
+ }
3509
+ </div>
3510
+ `, 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"] }]
3511
+ }], ctorParameters: () => [{ type: EditorCommandsService }] });
3002
3512
 
3003
3513
  const DEFAULT_SLASH_COMMANDS = [
3004
3514
  {
@@ -3448,54 +3958,400 @@ class TiptapSlashCommandsComponent {
3448
3958
  plugins: [keyboardPlugin, ...ed.view.state.plugins],
3449
3959
  }));
3450
3960
  }
3451
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapSlashCommandsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3452
- 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: `
3453
- <div #menuRef class="slash-commands-menu">
3454
- @for (command of filteredCommands(); track command.title) {
3961
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapSlashCommandsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3962
+ 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: `
3963
+ <div #menuRef class="slash-commands-menu">
3964
+ @for (command of filteredCommands(); track command.title) {
3965
+ <div
3966
+ class="slash-command-item"
3967
+ [class.selected]="$index === selectedIndex()"
3968
+ (click)="executeCommand(command)"
3969
+ (mouseenter)="selectedIndex.set($index)"
3970
+ >
3971
+ <div class="slash-command-icon">
3972
+ <span class="material-symbols-outlined">{{ command.icon }}</span>
3973
+ </div>
3974
+ <div class="slash-command-content">
3975
+ <div class="slash-command-title">{{ command.title }}</div>
3976
+ <div class="slash-command-description">{{ command.description }}</div>
3977
+ </div>
3978
+ </div>
3979
+ }
3980
+ </div>
3981
+ `, 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"] }); }
3982
+ }
3983
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapSlashCommandsComponent, decorators: [{
3984
+ type: Component,
3985
+ args: [{ selector: "tiptap-slash-commands", standalone: true, 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>
4004
+ `, 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
+ }], ctorParameters: () => [], propDecorators: { menuRef: [{
4006
+ type: ViewChild,
4007
+ args: ["menuRef", { static: false }]
4008
+ }] } });
4009
+
4010
+ // Main component
4011
+
4012
+ class TiptapImageUploadComponent {
4013
+ constructor() {
4014
+ // Inputs
4015
+ this.config = input({
4016
+ maxSize: 5, // 5MB par défaut
4017
+ maxWidth: 1920, // largeur max par défaut
4018
+ maxHeight: 1080, // hauteur max par défaut
4019
+ allowedTypes: ["image/jpeg", "image/png", "image/gif", "image/webp"],
4020
+ enableDragDrop: true,
4021
+ showPreview: true,
4022
+ multiple: false,
4023
+ compressImages: true,
4024
+ quality: 0.8,
4025
+ });
4026
+ // Outputs
4027
+ this.imageSelected = output();
4028
+ this.error = output();
4029
+ // Signals internes
4030
+ this.isDragOver = signal(false);
4031
+ this.isUploading = signal(false);
4032
+ this.uploadProgress = signal(0);
4033
+ this.previewImage = signal(null);
4034
+ this.previewInfo = signal("");
4035
+ this.errorMessage = signal(null);
4036
+ // Computed
4037
+ this.acceptedTypes = computed(() => {
4038
+ const types = this.config().allowedTypes || ["image/*"];
4039
+ return types.join(",");
4040
+ });
4041
+ }
4042
+ triggerFileInput() {
4043
+ const input = document.querySelector('input[type="file"]');
4044
+ if (input) {
4045
+ input.click();
4046
+ }
4047
+ }
4048
+ onFileSelected(event) {
4049
+ const input = event.target;
4050
+ const files = input.files;
4051
+ if (files && files.length > 0) {
4052
+ this.processFiles(Array.from(files));
4053
+ }
4054
+ // Reset input
4055
+ input.value = "";
4056
+ }
4057
+ onDragOver(event) {
4058
+ event.preventDefault();
4059
+ event.stopPropagation();
4060
+ this.isDragOver.set(true);
4061
+ }
4062
+ onDrop(event) {
4063
+ event.preventDefault();
4064
+ event.stopPropagation();
4065
+ this.isDragOver.set(false);
4066
+ const files = event.dataTransfer?.files;
4067
+ if (files && files.length > 0) {
4068
+ this.processFiles(Array.from(files));
4069
+ }
4070
+ }
4071
+ onDragLeave(event) {
4072
+ event.preventDefault();
4073
+ event.stopPropagation();
4074
+ this.isDragOver.set(false);
4075
+ }
4076
+ processFiles(files) {
4077
+ const config = this.config();
4078
+ const maxSize = (config.maxSize || 5) * 1024 * 1024; // Convertir en bytes
4079
+ const allowedTypes = config.allowedTypes || ["image/*"];
4080
+ // Vérifier le nombre de fichiers
4081
+ if (!config.multiple && files.length > 1) {
4082
+ this.showError("Veuillez sélectionner une seule image");
4083
+ return;
4084
+ }
4085
+ // Traiter chaque fichier
4086
+ files.forEach((file) => {
4087
+ // Vérifier le type
4088
+ if (!this.isValidFileType(file, allowedTypes)) {
4089
+ this.showError(`Type de fichier non supporté: ${file.name}`);
4090
+ return;
4091
+ }
4092
+ // Vérifier la taille
4093
+ if (file.size > maxSize) {
4094
+ this.showError(`Fichier trop volumineux: ${file.name} (max ${config.maxSize}MB)`);
4095
+ return;
4096
+ }
4097
+ // Traiter l'image avec compression si nécessaire
4098
+ this.processImage(file);
4099
+ });
4100
+ }
4101
+ isValidFileType(file, allowedTypes) {
4102
+ if (allowedTypes.includes("image/*")) {
4103
+ return file.type.startsWith("image/");
4104
+ }
4105
+ return allowedTypes.includes(file.type);
4106
+ }
4107
+ processImage(file) {
4108
+ this.isUploading.set(true);
4109
+ this.uploadProgress.set(10);
4110
+ const config = this.config();
4111
+ const originalSize = file.size;
4112
+ // Créer un canvas pour la compression
4113
+ const canvas = document.createElement("canvas");
4114
+ const ctx = canvas.getContext("2d");
4115
+ const img = new Image();
4116
+ img.onload = () => {
4117
+ this.uploadProgress.set(30);
4118
+ // Vérifier les dimensions
4119
+ const maxWidth = config.maxWidth || 1920;
4120
+ const maxHeight = config.maxHeight || 1080;
4121
+ let { width, height } = img;
4122
+ // Redimensionner si nécessaire
4123
+ if (width > maxWidth || height > maxHeight) {
4124
+ const ratio = Math.min(maxWidth / width, maxHeight / height);
4125
+ width *= ratio;
4126
+ height *= ratio;
4127
+ }
4128
+ canvas.width = width;
4129
+ canvas.height = height;
4130
+ // Dessiner l'image redimensionnée
4131
+ ctx?.drawImage(img, 0, 0, width, height);
4132
+ this.uploadProgress.set(70);
4133
+ // Convertir en base64 avec compression
4134
+ const quality = config.quality || 0.8;
4135
+ const mimeType = file.type;
4136
+ canvas.toBlob((blob) => {
4137
+ this.uploadProgress.set(90);
4138
+ if (blob) {
4139
+ const reader = new FileReader();
4140
+ reader.onload = (e) => {
4141
+ const base64 = e.target?.result;
4142
+ if (base64) {
4143
+ const result = {
4144
+ src: base64,
4145
+ name: file.name,
4146
+ size: blob.size,
4147
+ type: file.type,
4148
+ width: Math.round(width),
4149
+ height: Math.round(height),
4150
+ originalSize: originalSize,
4151
+ };
4152
+ // Afficher la prévisualisation si activée
4153
+ if (config.showPreview) {
4154
+ this.previewImage.set(base64);
4155
+ this.previewInfo.set(`${result.width}×${result.height} • ${this.formatFileSize(blob.size)}`);
4156
+ }
4157
+ // Émettre l'événement
4158
+ this.imageSelected.emit(result);
4159
+ this.clearError();
4160
+ }
4161
+ this.uploadProgress.set(100);
4162
+ setTimeout(() => {
4163
+ this.isUploading.set(false);
4164
+ this.uploadProgress.set(0);
4165
+ }, 500);
4166
+ };
4167
+ reader.readAsDataURL(blob);
4168
+ }
4169
+ else {
4170
+ this.showError("Erreur lors de la compression de l'image");
4171
+ this.isUploading.set(false);
4172
+ this.uploadProgress.set(0);
4173
+ }
4174
+ }, mimeType, quality);
4175
+ };
4176
+ img.onerror = () => {
4177
+ this.showError("Erreur lors du chargement de l'image");
4178
+ this.isUploading.set(false);
4179
+ this.uploadProgress.set(0);
4180
+ };
4181
+ img.src = URL.createObjectURL(file);
4182
+ }
4183
+ formatFileSize(bytes) {
4184
+ if (bytes === 0)
4185
+ return "0 B";
4186
+ const k = 1024;
4187
+ const sizes = ["B", "KB", "MB", "GB"];
4188
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
4189
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
4190
+ }
4191
+ showError(message) {
4192
+ this.errorMessage.set(message);
4193
+ this.error.emit(message);
4194
+ this.isUploading.set(false);
4195
+ this.uploadProgress.set(0);
4196
+ // Auto-clear après 5 secondes
4197
+ setTimeout(() => {
4198
+ this.clearError();
4199
+ }, 5000);
4200
+ }
4201
+ clearError() {
4202
+ this.errorMessage.set(null);
4203
+ }
4204
+ clearPreview() {
4205
+ this.previewImage.set(null);
4206
+ this.previewInfo.set("");
4207
+ }
4208
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapImageUploadComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
4209
+ 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: `
4210
+ <div class="image-upload-container">
4211
+ <!-- Bouton d'upload -->
4212
+ <tiptap-button
4213
+ icon="image"
4214
+ title="Ajouter une image"
4215
+ [disabled]="isUploading()"
4216
+ (onClick)="triggerFileInput()"
4217
+ />
4218
+
4219
+ <!-- Input file caché -->
4220
+ <input
4221
+ #fileInput
4222
+ type="file"
4223
+ [accept]="acceptedTypes()"
4224
+ [multiple]="config().multiple"
4225
+ (change)="onFileSelected($event)"
4226
+ style="display: none;"
4227
+ />
4228
+
4229
+ <!-- Zone de drag & drop -->
4230
+ @if (config().enableDragDrop && isDragOver()) {
3455
4231
  <div
3456
- class="slash-command-item"
3457
- [class.selected]="$index === selectedIndex()"
3458
- (click)="executeCommand(command)"
3459
- (mouseenter)="selectedIndex.set($index)"
4232
+ class="drag-overlay"
4233
+ (dragover)="onDragOver($event)"
4234
+ (drop)="onDrop($event)"
4235
+ (dragleave)="onDragLeave($event)"
3460
4236
  >
3461
- <div class="slash-command-icon">
3462
- <span class="material-symbols-outlined">{{ command.icon }}</span>
4237
+ <div class="drag-content">
4238
+ <span class="material-symbols-outlined">cloud_upload</span>
4239
+ <p>Déposez votre image ici</p>
3463
4240
  </div>
3464
- <div class="slash-command-content">
3465
- <div class="slash-command-title">{{ command.title }}</div>
3466
- <div class="slash-command-description">{{ command.description }}</div>
4241
+ </div>
4242
+ }
4243
+
4244
+ <!-- Barre de progression -->
4245
+ @if (isUploading() && uploadProgress() > 0) {
4246
+ <div class="upload-progress">
4247
+ <div class="progress-bar">
4248
+ <div class="progress-fill" [style.width.%]="uploadProgress()"></div>
4249
+ </div>
4250
+ <div class="progress-text">{{ uploadProgress() }}%</div>
4251
+ </div>
4252
+ }
4253
+
4254
+ <!-- Prévisualisation -->
4255
+ @if (config().showPreview && previewImage()) {
4256
+ <div class="image-preview">
4257
+ <img [src]="previewImage()" alt="Prévisualisation" />
4258
+ <div class="preview-info">
4259
+ <span>{{ previewInfo() }}</span>
3467
4260
  </div>
4261
+ <button
4262
+ class="preview-close"
4263
+ (click)="clearPreview()"
4264
+ title="Fermer la prévisualisation"
4265
+ >
4266
+ <span class="material-symbols-outlined">close</span>
4267
+ </button>
4268
+ </div>
4269
+ }
4270
+
4271
+ <!-- Messages d'erreur -->
4272
+ @if (errorMessage()) {
4273
+ <div class="error-message">
4274
+ <span class="material-symbols-outlined">error</span>
4275
+ {{ errorMessage() }}
3468
4276
  </div>
3469
4277
  }
3470
4278
  </div>
3471
- `, 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"] }); }
4279
+ `, 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"] }] }); }
3472
4280
  }
3473
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapSlashCommandsComponent, decorators: [{
4281
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TiptapImageUploadComponent, decorators: [{
3474
4282
  type: Component,
3475
- args: [{ selector: "tiptap-slash-commands", standalone: true, template: `
3476
- <div #menuRef class="slash-commands-menu">
3477
- @for (command of filteredCommands(); track command.title) {
4283
+ args: [{ selector: "tiptap-image-upload", standalone: true, imports: [TiptapButtonComponent], template: `
4284
+ <div class="image-upload-container">
4285
+ <!-- Bouton d'upload -->
4286
+ <tiptap-button
4287
+ icon="image"
4288
+ title="Ajouter une image"
4289
+ [disabled]="isUploading()"
4290
+ (onClick)="triggerFileInput()"
4291
+ />
4292
+
4293
+ <!-- Input file caché -->
4294
+ <input
4295
+ #fileInput
4296
+ type="file"
4297
+ [accept]="acceptedTypes()"
4298
+ [multiple]="config().multiple"
4299
+ (change)="onFileSelected($event)"
4300
+ style="display: none;"
4301
+ />
4302
+
4303
+ <!-- Zone de drag & drop -->
4304
+ @if (config().enableDragDrop && isDragOver()) {
3478
4305
  <div
3479
- class="slash-command-item"
3480
- [class.selected]="$index === selectedIndex()"
3481
- (click)="executeCommand(command)"
3482
- (mouseenter)="selectedIndex.set($index)"
4306
+ class="drag-overlay"
4307
+ (dragover)="onDragOver($event)"
4308
+ (drop)="onDrop($event)"
4309
+ (dragleave)="onDragLeave($event)"
3483
4310
  >
3484
- <div class="slash-command-icon">
3485
- <span class="material-symbols-outlined">{{ command.icon }}</span>
4311
+ <div class="drag-content">
4312
+ <span class="material-symbols-outlined">cloud_upload</span>
4313
+ <p>Déposez votre image ici</p>
3486
4314
  </div>
3487
- <div class="slash-command-content">
3488
- <div class="slash-command-title">{{ command.title }}</div>
3489
- <div class="slash-command-description">{{ command.description }}</div>
4315
+ </div>
4316
+ }
4317
+
4318
+ <!-- Barre de progression -->
4319
+ @if (isUploading() && uploadProgress() > 0) {
4320
+ <div class="upload-progress">
4321
+ <div class="progress-bar">
4322
+ <div class="progress-fill" [style.width.%]="uploadProgress()"></div>
3490
4323
  </div>
4324
+ <div class="progress-text">{{ uploadProgress() }}%</div>
4325
+ </div>
4326
+ }
4327
+
4328
+ <!-- Prévisualisation -->
4329
+ @if (config().showPreview && previewImage()) {
4330
+ <div class="image-preview">
4331
+ <img [src]="previewImage()" alt="Prévisualisation" />
4332
+ <div class="preview-info">
4333
+ <span>{{ previewInfo() }}</span>
4334
+ </div>
4335
+ <button
4336
+ class="preview-close"
4337
+ (click)="clearPreview()"
4338
+ title="Fermer la prévisualisation"
4339
+ >
4340
+ <span class="material-symbols-outlined">close</span>
4341
+ </button>
4342
+ </div>
4343
+ }
4344
+
4345
+ <!-- Messages d'erreur -->
4346
+ @if (errorMessage()) {
4347
+ <div class="error-message">
4348
+ <span class="material-symbols-outlined">error</span>
4349
+ {{ errorMessage() }}
3491
4350
  </div>
3492
4351
  }
3493
4352
  </div>
3494
- `, 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"] }]
3495
- }], ctorParameters: () => [], propDecorators: { menuRef: [{
3496
- type: ViewChild,
3497
- args: ["menuRef", { static: false }]
3498
- }] } });
4353
+ `, 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"] }]
4354
+ }] });
3499
4355
 
3500
4356
  class NoopValueAccessorDirective {
3501
4357
  writeValue(obj) { }
@@ -3547,8 +4403,10 @@ const DEFAULT_TOOLBAR_CONFIG = {
3547
4403
  link: true,
3548
4404
  image: true,
3549
4405
  horizontalRule: true,
4406
+ table: true,
3550
4407
  undo: true,
3551
4408
  redo: true,
4409
+ clear: false, // Désactivé par défaut (opt-in)
3552
4410
  separator: true,
3553
4411
  };
3554
4412
  // Configuration par défaut du bubble menu
@@ -3574,6 +4432,23 @@ const DEFAULT_IMAGE_BUBBLE_MENU_CONFIG = {
3574
4432
  deleteImage: true,
3575
4433
  separator: true,
3576
4434
  };
4435
+ // Configuration par défaut du menu de table
4436
+ const DEFAULT_TABLE_MENU_CONFIG = {
4437
+ addRowBefore: true,
4438
+ addRowAfter: true,
4439
+ deleteRow: true,
4440
+ addColumnBefore: true,
4441
+ addColumnAfter: true,
4442
+ deleteColumn: true,
4443
+ toggleHeaderRow: true,
4444
+ toggleHeaderColumn: true,
4445
+ deleteTable: true,
4446
+ separator: true,
4447
+ };
4448
+ const DEFAULT_CELL_MENU_CONFIG = {
4449
+ mergeCells: true,
4450
+ splitCell: true,
4451
+ };
3577
4452
  class AngularTiptapEditorComponent {
3578
4453
  constructor() {
3579
4454
  // Nouveaux inputs avec signal
@@ -3607,11 +4482,18 @@ class AngularTiptapEditorComponent {
3607
4482
  this.editorBlur = output();
3608
4483
  // ViewChild avec signal
3609
4484
  this.editorElement = viewChild.required("editorElement");
3610
- // Signals pour l'état interne
3611
- this.editor = signal(null);
3612
- this.characterCountData = signal(null);
3613
- this.isDragOver = signal(false);
3614
- this.editorFullyInitialized = signal(false);
4485
+ // Signals privés pour l'état interne
4486
+ this._editor = signal(null);
4487
+ this._characterCount = signal(0);
4488
+ this._wordCount = signal(0);
4489
+ this._isDragOver = signal(false);
4490
+ this._editorFullyInitialized = signal(false);
4491
+ // Accès en lecture seule aux signaux
4492
+ this.editor = this._editor.asReadonly();
4493
+ this.characterCount = this._characterCount.asReadonly();
4494
+ this.wordCount = this._wordCount.asReadonly();
4495
+ this.isDragOver = this._isDragOver.asReadonly();
4496
+ this.editorFullyInitialized = this._editorFullyInitialized.asReadonly();
3615
4497
  // Computed pour les états de l'éditeur
3616
4498
  this.isEditorReady = computed(() => this.editor() !== null);
3617
4499
  // Computed pour la configuration de la toolbar
@@ -3626,6 +4508,23 @@ class AngularTiptapEditorComponent {
3626
4508
  this.imageBubbleMenuConfig = computed(() => Object.keys(this.imageBubbleMenu()).length === 0
3627
4509
  ? DEFAULT_IMAGE_BUBBLE_MENU_CONFIG
3628
4510
  : { ...DEFAULT_IMAGE_BUBBLE_MENU_CONFIG, ...this.imageBubbleMenu() });
4511
+ // Computed pour la configuration du bubble menu table
4512
+ this.tableBubbleMenuConfig = computed(() => ({
4513
+ addRowBefore: true,
4514
+ addRowAfter: true,
4515
+ deleteRow: true,
4516
+ addColumnBefore: true,
4517
+ addColumnAfter: true,
4518
+ deleteColumn: true,
4519
+ deleteTable: true,
4520
+ toggleHeaderRow: true,
4521
+ toggleHeaderColumn: true,
4522
+ }));
4523
+ // Computed pour la configuration du menu de cellules
4524
+ this.cellBubbleMenuConfig = computed(() => ({
4525
+ mergeCells: true,
4526
+ splitCell: true,
4527
+ }));
3629
4528
  // Computed pour la configuration de l'upload d'images
3630
4529
  this.imageUploadConfig = computed(() => ({
3631
4530
  maxSize: 5,
@@ -3646,6 +4545,7 @@ class AngularTiptapEditorComponent {
3646
4545
  this.ngControl = inject(NgControl, { self: true, optional: true });
3647
4546
  this.i18nService = inject(TiptapI18nService);
3648
4547
  this.imageService = inject(ImageService);
4548
+ this.editorCommandsService = inject(EditorCommandsService);
3649
4549
  // Effet pour gérer le changement de langue
3650
4550
  effect(() => {
3651
4551
  const locale = this.locale();
@@ -3686,9 +4586,16 @@ class AngularTiptapEditorComponent {
3686
4586
  const currentEditor = this.editor();
3687
4587
  const isEditable = this.editable();
3688
4588
  if (currentEditor) {
3689
- currentEditor.setEditable(isEditable);
4589
+ this.editorCommandsService.setEditable(currentEditor, isEditable);
3690
4590
  }
3691
4591
  });
4592
+ // Effect pour la détection du survol des tables
4593
+ effect(() => {
4594
+ const currentEditor = this.editor();
4595
+ if (!currentEditor)
4596
+ return;
4597
+ // Table hover detection supprimée car remplacée par le menu bubble
4598
+ });
3692
4599
  }
3693
4600
  ngAfterViewInit() {
3694
4601
  // La vue est déjà complètement initialisée dans ngAfterViewInit
@@ -3701,7 +4608,7 @@ class AngularTiptapEditorComponent {
3701
4608
  if (currentEditor) {
3702
4609
  currentEditor.destroy();
3703
4610
  }
3704
- this.editorFullyInitialized.set(false);
4611
+ this._editorFullyInitialized.set(false);
3705
4612
  }
3706
4613
  initEditor() {
3707
4614
  const extensions = [
@@ -3739,6 +4646,7 @@ class AngularTiptapEditorComponent {
3739
4646
  uploadProgress: () => this.imageService.uploadProgress(),
3740
4647
  uploadMessage: () => this.imageService.uploadMessage(),
3741
4648
  }),
4649
+ TableExtension,
3742
4650
  ];
3743
4651
  // Ajouter l'extension Office Paste si activée
3744
4652
  if (this.enableOfficePaste()) {
@@ -3778,7 +4686,7 @@ class AngularTiptapEditorComponent {
3778
4686
  // Marquer l'éditeur comme complètement initialisé après un court délai
3779
4687
  // pour s'assurer que tous les plugins et extensions sont prêts
3780
4688
  setTimeout(() => {
3781
- this.editorFullyInitialized.set(true);
4689
+ this._editorFullyInitialized.set(true);
3782
4690
  }, 100);
3783
4691
  },
3784
4692
  onFocus: ({ editor, event }) => {
@@ -3793,15 +4701,13 @@ class AngularTiptapEditorComponent {
3793
4701
  },
3794
4702
  });
3795
4703
  // Stocker la référence de l'éditeur immédiatement
3796
- this.editor.set(newEditor);
4704
+ this._editor.set(newEditor);
3797
4705
  }
3798
4706
  updateCharacterCount(editor) {
3799
4707
  if (this.showCharacterCount() && editor.storage["characterCount"]) {
3800
4708
  const storage = editor.storage["characterCount"];
3801
- this.characterCountData.set({
3802
- characters: storage.characters(),
3803
- words: storage.words(),
3804
- });
4709
+ this._characterCount.set(storage.characters());
4710
+ this._wordCount.set(storage.words());
3805
4711
  }
3806
4712
  }
3807
4713
  // Méthodes pour l'upload d'images
@@ -3835,12 +4741,12 @@ class AngularTiptapEditorComponent {
3835
4741
  onDragOver(event) {
3836
4742
  event.preventDefault();
3837
4743
  event.stopPropagation();
3838
- this.isDragOver.set(true);
4744
+ this._isDragOver.set(true);
3839
4745
  }
3840
4746
  onDrop(event) {
3841
4747
  event.preventDefault();
3842
4748
  event.stopPropagation();
3843
- this.isDragOver.set(false);
4749
+ this._isDragOver.set(false);
3844
4750
  const files = event.dataTransfer?.files;
3845
4751
  if (files && files.length > 0) {
3846
4752
  const file = files[0];
@@ -3871,16 +4777,32 @@ class AngularTiptapEditorComponent {
3871
4777
  return this.editor()?.getText() || "";
3872
4778
  }
3873
4779
  setContent(content, emitUpdate = true) {
3874
- this.editor()?.commands.setContent(content, emitUpdate);
4780
+ const editor = this.editor();
4781
+ if (editor) {
4782
+ this.editorCommandsService.setContent(editor, content, emitUpdate);
4783
+ }
3875
4784
  }
3876
4785
  focus() {
3877
- this.editor()?.commands.focus();
4786
+ const editor = this.editor();
4787
+ if (editor) {
4788
+ this.editorCommandsService.focus(editor);
4789
+ }
3878
4790
  }
3879
4791
  blur() {
3880
- this.editor()?.commands.blur();
4792
+ const editor = this.editor();
4793
+ if (editor) {
4794
+ this.editorCommandsService.blur(editor);
4795
+ }
3881
4796
  }
3882
4797
  clearContent() {
3883
- this.editor()?.commands.clearContent();
4798
+ const editor = this.editor();
4799
+ if (editor) {
4800
+ this.editorCommandsService.clearContent(editor);
4801
+ }
4802
+ }
4803
+ // Méthode publique pour obtenir l'éditeur
4804
+ getEditor() {
4805
+ return this.editor();
3884
4806
  }
3885
4807
  setupFormControlSubscription() {
3886
4808
  const control = this.ngControl?.control;
@@ -3900,7 +4822,7 @@ class AngularTiptapEditorComponent {
3900
4822
  setDisabledState(isDisabled) {
3901
4823
  const currentEditor = this.editor();
3902
4824
  if (currentEditor) {
3903
- currentEditor.setEditable(!isDisabled);
4825
+ this.editorCommandsService.setEditable(currentEditor, !isDisabled);
3904
4826
  }
3905
4827
  }
3906
4828
  onEditorClick(event) {
@@ -3975,19 +4897,40 @@ class AngularTiptapEditorComponent {
3975
4897
  ></tiptap-slash-commands>
3976
4898
  }
3977
4899
 
4900
+ <!-- Table Menu -->
4901
+ @if (editor()) {
4902
+ <tiptap-table-bubble-menu
4903
+ [editor]="editor()!"
4904
+ [config]="tableBubbleMenuConfig()"
4905
+ [style.display]="editorFullyInitialized() ? 'block' : 'none'"
4906
+ ></tiptap-table-bubble-menu>
4907
+ }
4908
+
4909
+ <!-- Cell Menu -->
4910
+ @if (editor()) {
4911
+ <tiptap-cell-bubble-menu
4912
+ [editor]="editor()!"
4913
+ [config]="cellBubbleMenuConfig()"
4914
+ [style.display]="editorFullyInitialized() ? 'block' : 'none'"
4915
+ ></tiptap-cell-bubble-menu>
4916
+ }
4917
+
4918
+ <!-- Table Edit Button - Supprimé car remplacé par le menu bubble -->
4919
+
3978
4920
  <!-- Compteur de caractères -->
3979
- @if (showCharacterCount() && characterCountData()) {
4921
+ @if (showCharacterCount()) {
3980
4922
  <div class="character-count">
3981
- {{ characterCountData()?.characters }}
3982
- {{ i18nService.editor().characters }},
3983
- {{ characterCountData()?.words }} {{ i18nService.editor().words }} @if
3984
- (maxCharacters()) { /
4923
+ {{ characterCount() }}
4924
+ {{ i18nService.editor().character
4925
+ }}{{ characterCount() > 1 ? "s" : "" }}, {{ wordCount() }}
4926
+ {{ i18nService.editor().word }}{{ wordCount() > 1 ? "s" : "" }}
4927
+ @if (maxCharacters()) { /
3985
4928
  {{ maxCharacters() }}
3986
4929
  }
3987
4930
  </div>
3988
4931
  }
3989
4932
  </div>
3990
- `, isInline: true, styles: [".tiptap-editor{border:2px solid #e2e8f0;border-radius:8px;background:#fff;box-shadow:0 2px 4px #0000001a;overflow:hidden;transition:border-color .2s ease}.tiptap-editor:focus-within{border-color:#3182ce;box-shadow:0 0 0 3px #3182ce1a}.tiptap-content{padding:16px;min-height:var(--editor-min-height, 200px);height:var(--editor-height, auto);max-height:var(--editor-max-height, none);overflow-y:var(--editor-overflow, visible);outline:none;position:relative}.tiptap-content.drag-over{background:#f0f8ff;border:2px dashed #3182ce}.character-count{padding:8px 16px;font-size:12px;color:#718096;text-align:right;border-top:1px solid #e2e8f0;background:#f8f9fa}.image-upload-container{position:relative;display:inline-block}:host ::ng-deep .ProseMirror{outline:none;line-height:1.6;color:#2d3748;min-height:100%;height:100%;word-wrap:break-word;overflow-wrap:break-word}:host ::ng-deep .ProseMirror h1{font-size:2em;font-weight:700;margin-top:0;margin-bottom:.5em}:host ::ng-deep .ProseMirror h2{font-size:1.5em;font-weight:700;margin-top:1em;margin-bottom:.5em}:host ::ng-deep .ProseMirror h3{font-size:1.25em;font-weight:700;margin-top:1em;margin-bottom:.5em}:host ::ng-deep .ProseMirror p{margin:.5em 0}:host ::ng-deep .ProseMirror ul,:host ::ng-deep .ProseMirror ol{padding-left:2em;margin:.5em 0}:host ::ng-deep .ProseMirror blockquote{border-left:4px solid #e2e8f0;margin:1em 0;font-style:italic;background:#f8f9fa;padding:.5em 1em;border-radius:0 4px 4px 0}:host ::ng-deep .ProseMirror code{background:#f1f5f9;padding:.2em .4em;border-radius:3px;font-family:Monaco,Consolas,monospace;font-size:.9em}:host ::ng-deep .ProseMirror pre{background:#1a202c;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;margin:1em 0}:host ::ng-deep .ProseMirror pre code{background:none;color:inherit;padding:0}:host ::ng-deep .ProseMirror p.is-editor-empty:first-child:before{content:attr(data-placeholder);color:#a0aec0;pointer-events:none;float:left;height:0}:host ::ng-deep .ProseMirror[contenteditable=false]{pointer-events:none}:host ::ng-deep .ProseMirror[contenteditable=false] img{cursor:default;pointer-events:none}:host ::ng-deep .ProseMirror[contenteditable=false] img:hover{transform:none;box-shadow:0 2px 8px #0000001a}:host ::ng-deep .ProseMirror[contenteditable=false] img.ProseMirror-selectednode{outline:none}:host ::ng-deep .ProseMirror img{position:relative;display:inline-block;max-width:100%;height:auto;cursor:pointer;transition:all .2s ease;border:2px solid transparent;border-radius:8px}:host ::ng-deep .ProseMirror img:hover{border-color:#e2e8f0;box-shadow:0 2px 4px #0000001a}:host ::ng-deep .ProseMirror img.ProseMirror-selectednode{border-color:#3182ce;box-shadow:0 0 0 3px #3182ce1a;transition:all .2s ease}:host ::ng-deep .ProseMirror .tiptap-image{max-width:100%;height:auto;border-radius:16px;box-shadow:0 4px 20px #00000014;margin:.5em 0;cursor:pointer;transition:all .3s cubic-bezier(.4,0,.2,1);display:block;filter:brightness(1) contrast(1)}:host ::ng-deep .ProseMirror .tiptap-image:hover{box-shadow:0 8px 30px #0000001f;filter:brightness(1.02) contrast(1.02)}:host ::ng-deep .ProseMirror .tiptap-image.ProseMirror-selectednode{outline:2px solid #6366f1;outline-offset:2px;border-radius:16px;box-shadow:0 0 0 4px #6366f11a}:host ::ng-deep .image-container{margin:.5em 0;text-align:center;border-radius:16px;overflow:hidden;transition:all .3s cubic-bezier(.4,0,.2,1)}:host ::ng-deep .image-container.image-align-left{text-align:left}:host ::ng-deep .image-container.image-align-center{text-align:center}:host ::ng-deep .image-container.image-align-right{text-align:right}:host ::ng-deep .image-container img{display:inline-block;max-width:100%;height:auto;border-radius:16px}:host ::ng-deep .resizable-image-container{position:relative;display:inline-block;margin:.5em 0}:host ::ng-deep .resize-controls{position:absolute;inset:0;pointer-events:none;z-index:1000}:host ::ng-deep .resize-handle{position:absolute;width:12px;height:12px;background:#3b82f6;border:2px solid white;border-radius:50%;pointer-events:all;cursor:pointer;z-index:1001;transition:all .15s ease;box-shadow:0 2px 6px #0003}:host ::ng-deep .resize-handle:hover{background:#2563eb;box-shadow:0 3px 8px #0000004d}:host ::ng-deep .resize-handle:active{background:#1d4ed8}:host ::ng-deep .resize-handle-n:hover,:host ::ng-deep .resize-handle-s:hover{transform:translate(-50%) scale(1.2)}:host ::ng-deep .resize-handle-w:hover,:host ::ng-deep .resize-handle-e:hover{transform:translateY(-50%) scale(1.2)}:host ::ng-deep .resize-handle-n:active,:host ::ng-deep .resize-handle-s:active{transform:translate(-50%) scale(.9)}:host ::ng-deep .resize-handle-w:active,:host ::ng-deep .resize-handle-e:active{transform:translateY(-50%) scale(.9)}:host ::ng-deep .resize-handle-nw:hover,:host ::ng-deep .resize-handle-ne:hover,:host ::ng-deep .resize-handle-sw:hover,:host ::ng-deep .resize-handle-se:hover{transform:scale(1.2)}:host ::ng-deep .resize-handle-nw:active,:host ::ng-deep .resize-handle-ne:active,:host ::ng-deep .resize-handle-sw:active,:host ::ng-deep .resize-handle-se:active{transform:scale(.9)}:host ::ng-deep .resize-handle-nw{top:0;left:-6px;cursor:nw-resize}:host ::ng-deep .resize-handle-n{top:0;left:50%;transform:translate(-50%);cursor:n-resize}:host ::ng-deep .resize-handle-ne{top:0;right:-6px;cursor:ne-resize}:host ::ng-deep .resize-handle-w{top:50%;left:-6px;transform:translateY(-50%);cursor:w-resize}:host ::ng-deep .resize-handle-e{top:50%;right:-6px;transform:translateY(-50%);cursor:e-resize}:host ::ng-deep .resize-handle-sw{bottom:0;left:-6px;cursor:sw-resize}:host ::ng-deep .resize-handle-s{bottom:0;left:50%;transform:translate(-50%);cursor:s-resize}:host ::ng-deep .resize-handle-se{bottom:0;right:-6px;cursor:se-resize}:host ::ng-deep body.resizing{-webkit-user-select:none;user-select:none;cursor:crosshair}:host ::ng-deep body.resizing .ProseMirror{pointer-events:none}:host ::ng-deep body.resizing .ProseMirror .tiptap-image{pointer-events:none}:host ::ng-deep .image-size-info{position:absolute;bottom:-20px;left:50%;transform:translate(-50%);background:#000c;color:#fff;padding:2px 6px;border-radius:3px;font-size:11px;white-space:nowrap;opacity:0;transition:opacity .2s ease}:host ::ng-deep .image-container:hover .image-size-info{opacity:1}\n"], dependencies: [{ kind: "component", type: TiptapToolbarComponent, selector: "tiptap-toolbar", inputs: ["editor", "config"], outputs: ["imageUploaded", "imageError"] }, { kind: "component", type: TiptapImageUploadComponent, selector: "tiptap-image-upload", inputs: ["config"], outputs: ["imageSelected", "error"] }, { kind: "component", type: TiptapBubbleMenuComponent, selector: "tiptap-bubble-menu", inputs: ["editor", "config"] }, { kind: "component", type: TiptapImageBubbleMenuComponent, selector: "tiptap-image-bubble-menu", inputs: ["editor", "config"] }, { kind: "component", type: TiptapSlashCommandsComponent, selector: "tiptap-slash-commands", inputs: ["editor", "config"], outputs: ["imageUploadRequested"] }] }); }
4933
+ `, isInline: true, styles: [".tiptap-editor{border:2px solid #e2e8f0;border-radius:8px;background:#fff;overflow:hidden;transition:border-color .2s ease}.tiptap-editor:focus-within{border-color:#3182ce}.tiptap-content{padding:16px;min-height:var(--editor-min-height, 200px);height:var(--editor-height, auto);max-height:var(--editor-max-height, none);overflow-y:var(--editor-overflow, visible);outline:none;position:relative}.tiptap-content.drag-over{background:#f0f8ff;border:2px dashed #3182ce}.character-count{padding:8px 16px;font-size:12px;color:#718096;text-align:right;border-top:1px solid #e2e8f0;background:#f8f9fa}.image-upload-container{position:relative;display:inline-block}:host ::ng-deep .ProseMirror{outline:none;line-height:1.6;color:#2d3748;min-height:100%;height:100%;word-wrap:break-word;overflow-wrap:break-word}:host ::ng-deep .ProseMirror h1{font-size:2em;font-weight:700;margin-top:0;margin-bottom:.5em}:host ::ng-deep .ProseMirror h2{font-size:1.5em;font-weight:700;margin-top:1em;margin-bottom:.5em}:host ::ng-deep .ProseMirror h3{font-size:1.25em;font-weight:700;margin-top:1em;margin-bottom:.5em}:host ::ng-deep .ProseMirror p{margin:.5em 0}:host ::ng-deep .ProseMirror ul,:host ::ng-deep .ProseMirror ol{padding-left:2em;margin:.5em 0}:host ::ng-deep .ProseMirror blockquote{border-left:4px solid #e2e8f0;margin:1em 0;font-style:italic;background:#f8f9fa;padding:.5em 1em;border-radius:0 4px 4px 0}:host ::ng-deep .ProseMirror code{background:#f1f5f9;padding:.2em .4em;border-radius:3px;font-family:Monaco,Consolas,monospace;font-size:.9em}:host ::ng-deep .ProseMirror pre{background:#1a202c;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;margin:1em 0}:host ::ng-deep .ProseMirror pre code{background:none;color:inherit;padding:0}:host ::ng-deep .ProseMirror p.is-editor-empty:first-child:before{content:attr(data-placeholder);color:#a0aec0;pointer-events:none;float:left;height:0}:host ::ng-deep .ProseMirror[contenteditable=false]{pointer-events:none}:host ::ng-deep .ProseMirror[contenteditable=false] img{cursor:default;pointer-events:none}:host ::ng-deep .ProseMirror[contenteditable=false] img:hover{transform:none;box-shadow:0 2px 8px #0000001a}:host ::ng-deep .ProseMirror[contenteditable=false] img.ProseMirror-selectednode{outline:none}:host ::ng-deep .ProseMirror img{position:relative;display:inline-block;max-width:100%;height:auto;cursor:pointer;transition:all .2s ease;border:2px solid transparent;border-radius:8px}:host ::ng-deep .ProseMirror img:hover{border-color:#e2e8f0;box-shadow:0 2px 4px #0000001a}:host ::ng-deep .ProseMirror img.ProseMirror-selectednode{border-color:#3182ce;box-shadow:0 0 0 3px #3182ce1a;transition:all .2s ease}:host ::ng-deep .ProseMirror .tiptap-image{max-width:100%;height:auto;border-radius:16px;box-shadow:0 4px 20px #00000014;margin:.5em 0;cursor:pointer;transition:all .3s cubic-bezier(.4,0,.2,1);display:block;filter:brightness(1) contrast(1)}:host ::ng-deep .ProseMirror .tiptap-image:hover{box-shadow:0 8px 30px #0000001f;filter:brightness(1.02) contrast(1.02)}:host ::ng-deep .ProseMirror .tiptap-image.ProseMirror-selectednode{outline:2px solid #6366f1;outline-offset:2px;border-radius:16px;box-shadow:0 0 0 4px #6366f11a}:host ::ng-deep .image-container{margin:.5em 0;text-align:center;border-radius:16px;overflow:hidden;transition:all .3s cubic-bezier(.4,0,.2,1)}:host ::ng-deep .image-container.image-align-left{text-align:left}:host ::ng-deep .image-container.image-align-center{text-align:center}:host ::ng-deep .image-container.image-align-right{text-align:right}:host ::ng-deep .image-container img{display:inline-block;max-width:100%;height:auto;border-radius:16px}:host ::ng-deep .resizable-image-container{position:relative;display:inline-block;margin:.5em 0}:host ::ng-deep .resize-controls{position:absolute;inset:0;pointer-events:none;z-index:1000}:host ::ng-deep .resize-handle{position:absolute;width:12px;height:12px;background:#3b82f6;border:2px solid white;border-radius:50%;pointer-events:all;cursor:pointer;z-index:1001;transition:all .15s ease;box-shadow:0 2px 6px #0003}:host ::ng-deep .resize-handle:hover{background:#2563eb;box-shadow:0 3px 8px #0000004d}:host ::ng-deep .resize-handle:active{background:#1d4ed8}:host ::ng-deep .resize-handle-n:hover,:host ::ng-deep .resize-handle-s:hover{transform:translate(-50%) scale(1.2)}:host ::ng-deep .resize-handle-w:hover,:host ::ng-deep .resize-handle-e:hover{transform:translateY(-50%) scale(1.2)}:host ::ng-deep .resize-handle-n:active,:host ::ng-deep .resize-handle-s:active{transform:translate(-50%) scale(.9)}:host ::ng-deep .resize-handle-w:active,:host ::ng-deep .resize-handle-e:active{transform:translateY(-50%) scale(.9)}:host ::ng-deep .resize-handle-nw:hover,:host ::ng-deep .resize-handle-ne:hover,:host ::ng-deep .resize-handle-sw:hover,:host ::ng-deep .resize-handle-se:hover{transform:scale(1.2)}:host ::ng-deep .resize-handle-nw:active,:host ::ng-deep .resize-handle-ne:active,:host ::ng-deep .resize-handle-sw:active,:host ::ng-deep .resize-handle-se:active{transform:scale(.9)}:host ::ng-deep .resize-handle-nw{top:0;left:-6px;cursor:nw-resize}:host ::ng-deep .resize-handle-n{top:0;left:50%;transform:translate(-50%);cursor:n-resize}:host ::ng-deep .resize-handle-ne{top:0;right:-6px;cursor:ne-resize}:host ::ng-deep .resize-handle-w{top:50%;left:-6px;transform:translateY(-50%);cursor:w-resize}:host ::ng-deep .resize-handle-e{top:50%;right:-6px;transform:translateY(-50%);cursor:e-resize}:host ::ng-deep .resize-handle-sw{bottom:0;left:-6px;cursor:sw-resize}:host ::ng-deep .resize-handle-s{bottom:0;left:50%;transform:translate(-50%);cursor:s-resize}:host ::ng-deep .resize-handle-se{bottom:0;right:-6px;cursor:se-resize}:host ::ng-deep body.resizing{-webkit-user-select:none;user-select:none;cursor:crosshair}:host ::ng-deep body.resizing .ProseMirror{pointer-events:none}:host ::ng-deep body.resizing .ProseMirror .tiptap-image{pointer-events:none}:host ::ng-deep .image-size-info{position:absolute;bottom:-20px;left:50%;transform:translate(-50%);background:#000c;color:#fff;padding:2px 6px;border-radius:3px;font-size:11px;white-space:nowrap;opacity:0;transition:opacity .2s ease}:host ::ng-deep .image-container:hover .image-size-info{opacity:1}:host ::ng-deep .ProseMirror table{border-collapse:separate;border-spacing:0;margin:0;table-layout:fixed;width:100%;border-radius:8px;overflow:hidden}:host ::ng-deep .ProseMirror table td,:host ::ng-deep .ProseMirror table th{border:none;border-right:1px solid #e2e8f0;border-bottom:1px solid #e2e8f0;box-sizing:border-box;min-width:1em;padding:8px 12px;position:relative;vertical-align:top;background:#fff}:host ::ng-deep .ProseMirror table td:first-child,:host ::ng-deep .ProseMirror table th:first-child{border-left:1px solid #e2e8f0}:host ::ng-deep .ProseMirror table tr:first-child td,:host ::ng-deep .ProseMirror table tr:first-child th{border-top:1px solid #e2e8f0}:host ::ng-deep .ProseMirror table tr:first-child th:first-child{border-top-left-radius:8px}:host ::ng-deep .ProseMirror table tr:first-child th:last-child{border-top-right-radius:8px}:host ::ng-deep .ProseMirror table tr:last-child td:first-child{border-bottom-left-radius:8px}:host ::ng-deep .ProseMirror table tr:last-child td:last-child{border-bottom-right-radius:8px}:host ::ng-deep .ProseMirror table th{background:#f8f9fa;font-weight:600;color:#374151}:host ::ng-deep .ProseMirror table .selectedCell:after{background:#c8c8ff66;content:\"\";inset:0;pointer-events:none;position:absolute;z-index:2}:host ::ng-deep .ProseMirror table .column-resize-handle{position:absolute;right:-2px;top:0;bottom:0;width:4px;background-color:#6366f1;opacity:0;transition:opacity .2s ease}:host ::ng-deep .ProseMirror table:hover .column-resize-handle{opacity:1}:host ::ng-deep .ProseMirror table .column-resize-handle:hover{background-color:#4f46e5}:host ::ng-deep .ProseMirror .tableWrapper{overflow-x:auto;margin:1em 0;border-radius:8px}:host ::ng-deep .ProseMirror .tableWrapper table{margin:0;border-radius:8px;min-width:600px;overflow:hidden}:host ::ng-deep .ProseMirror table p{margin:0}:host ::ng-deep .ProseMirror table .selectedCell{background-color:#6366f11a}:host ::ng-deep .ProseMirror table th{background-color:#f8f9fa;font-weight:600;color:#374151;text-align:left}:host ::ng-deep .ProseMirror table tbody tr:nth-child(2n){background-color:#fafbfc}:host ::ng-deep .ProseMirror table tbody tr:hover{background-color:#f1f5f9}\n"], dependencies: [{ kind: "component", type: TiptapToolbarComponent, selector: "tiptap-toolbar", inputs: ["editor", "config"], outputs: ["imageUploaded", "imageError"] }, { kind: "component", type: TiptapImageUploadComponent, selector: "tiptap-image-upload", inputs: ["config"], outputs: ["imageSelected", "error"] }, { kind: "component", type: TiptapBubbleMenuComponent, selector: "tiptap-bubble-menu", inputs: ["editor", "config"] }, { kind: "component", type: TiptapImageBubbleMenuComponent, selector: "tiptap-image-bubble-menu", inputs: ["editor", "config"] }, { kind: "component", type: TiptapTableBubbleMenuComponent, selector: "tiptap-table-bubble-menu", inputs: ["editor", "config"] }, { kind: "component", type: TiptapCellBubbleMenuComponent, selector: "tiptap-cell-bubble-menu", inputs: ["editor", "config"] }, { kind: "component", type: TiptapSlashCommandsComponent, selector: "tiptap-slash-commands", inputs: ["editor", "config"], outputs: ["imageUploadRequested"] }] }); }
3991
4934
  }
3992
4935
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: AngularTiptapEditorComponent, decorators: [{
3993
4936
  type: Component,
@@ -3996,6 +4939,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImpor
3996
4939
  TiptapImageUploadComponent,
3997
4940
  TiptapBubbleMenuComponent,
3998
4941
  TiptapImageBubbleMenuComponent,
4942
+ TiptapTableBubbleMenuComponent,
4943
+ TiptapCellBubbleMenuComponent,
3999
4944
  TiptapSlashCommandsComponent,
4000
4945
  ], template: `
4001
4946
  <div class="tiptap-editor">
@@ -4050,21 +4995,57 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImpor
4050
4995
  ></tiptap-slash-commands>
4051
4996
  }
4052
4997
 
4998
+ <!-- Table Menu -->
4999
+ @if (editor()) {
5000
+ <tiptap-table-bubble-menu
5001
+ [editor]="editor()!"
5002
+ [config]="tableBubbleMenuConfig()"
5003
+ [style.display]="editorFullyInitialized() ? 'block' : 'none'"
5004
+ ></tiptap-table-bubble-menu>
5005
+ }
5006
+
5007
+ <!-- Cell Menu -->
5008
+ @if (editor()) {
5009
+ <tiptap-cell-bubble-menu
5010
+ [editor]="editor()!"
5011
+ [config]="cellBubbleMenuConfig()"
5012
+ [style.display]="editorFullyInitialized() ? 'block' : 'none'"
5013
+ ></tiptap-cell-bubble-menu>
5014
+ }
5015
+
5016
+ <!-- Table Edit Button - Supprimé car remplacé par le menu bubble -->
5017
+
4053
5018
  <!-- Compteur de caractères -->
4054
- @if (showCharacterCount() && characterCountData()) {
5019
+ @if (showCharacterCount()) {
4055
5020
  <div class="character-count">
4056
- {{ characterCountData()?.characters }}
4057
- {{ i18nService.editor().characters }},
4058
- {{ characterCountData()?.words }} {{ i18nService.editor().words }} @if
4059
- (maxCharacters()) { /
5021
+ {{ characterCount() }}
5022
+ {{ i18nService.editor().character
5023
+ }}{{ characterCount() > 1 ? "s" : "" }}, {{ wordCount() }}
5024
+ {{ i18nService.editor().word }}{{ wordCount() > 1 ? "s" : "" }}
5025
+ @if (maxCharacters()) { /
4060
5026
  {{ maxCharacters() }}
4061
5027
  }
4062
5028
  </div>
4063
5029
  }
4064
5030
  </div>
4065
- `, styles: [".tiptap-editor{border:2px solid #e2e8f0;border-radius:8px;background:#fff;box-shadow:0 2px 4px #0000001a;overflow:hidden;transition:border-color .2s ease}.tiptap-editor:focus-within{border-color:#3182ce;box-shadow:0 0 0 3px #3182ce1a}.tiptap-content{padding:16px;min-height:var(--editor-min-height, 200px);height:var(--editor-height, auto);max-height:var(--editor-max-height, none);overflow-y:var(--editor-overflow, visible);outline:none;position:relative}.tiptap-content.drag-over{background:#f0f8ff;border:2px dashed #3182ce}.character-count{padding:8px 16px;font-size:12px;color:#718096;text-align:right;border-top:1px solid #e2e8f0;background:#f8f9fa}.image-upload-container{position:relative;display:inline-block}:host ::ng-deep .ProseMirror{outline:none;line-height:1.6;color:#2d3748;min-height:100%;height:100%;word-wrap:break-word;overflow-wrap:break-word}:host ::ng-deep .ProseMirror h1{font-size:2em;font-weight:700;margin-top:0;margin-bottom:.5em}:host ::ng-deep .ProseMirror h2{font-size:1.5em;font-weight:700;margin-top:1em;margin-bottom:.5em}:host ::ng-deep .ProseMirror h3{font-size:1.25em;font-weight:700;margin-top:1em;margin-bottom:.5em}:host ::ng-deep .ProseMirror p{margin:.5em 0}:host ::ng-deep .ProseMirror ul,:host ::ng-deep .ProseMirror ol{padding-left:2em;margin:.5em 0}:host ::ng-deep .ProseMirror blockquote{border-left:4px solid #e2e8f0;margin:1em 0;font-style:italic;background:#f8f9fa;padding:.5em 1em;border-radius:0 4px 4px 0}:host ::ng-deep .ProseMirror code{background:#f1f5f9;padding:.2em .4em;border-radius:3px;font-family:Monaco,Consolas,monospace;font-size:.9em}:host ::ng-deep .ProseMirror pre{background:#1a202c;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;margin:1em 0}:host ::ng-deep .ProseMirror pre code{background:none;color:inherit;padding:0}:host ::ng-deep .ProseMirror p.is-editor-empty:first-child:before{content:attr(data-placeholder);color:#a0aec0;pointer-events:none;float:left;height:0}:host ::ng-deep .ProseMirror[contenteditable=false]{pointer-events:none}:host ::ng-deep .ProseMirror[contenteditable=false] img{cursor:default;pointer-events:none}:host ::ng-deep .ProseMirror[contenteditable=false] img:hover{transform:none;box-shadow:0 2px 8px #0000001a}:host ::ng-deep .ProseMirror[contenteditable=false] img.ProseMirror-selectednode{outline:none}:host ::ng-deep .ProseMirror img{position:relative;display:inline-block;max-width:100%;height:auto;cursor:pointer;transition:all .2s ease;border:2px solid transparent;border-radius:8px}:host ::ng-deep .ProseMirror img:hover{border-color:#e2e8f0;box-shadow:0 2px 4px #0000001a}:host ::ng-deep .ProseMirror img.ProseMirror-selectednode{border-color:#3182ce;box-shadow:0 0 0 3px #3182ce1a;transition:all .2s ease}:host ::ng-deep .ProseMirror .tiptap-image{max-width:100%;height:auto;border-radius:16px;box-shadow:0 4px 20px #00000014;margin:.5em 0;cursor:pointer;transition:all .3s cubic-bezier(.4,0,.2,1);display:block;filter:brightness(1) contrast(1)}:host ::ng-deep .ProseMirror .tiptap-image:hover{box-shadow:0 8px 30px #0000001f;filter:brightness(1.02) contrast(1.02)}:host ::ng-deep .ProseMirror .tiptap-image.ProseMirror-selectednode{outline:2px solid #6366f1;outline-offset:2px;border-radius:16px;box-shadow:0 0 0 4px #6366f11a}:host ::ng-deep .image-container{margin:.5em 0;text-align:center;border-radius:16px;overflow:hidden;transition:all .3s cubic-bezier(.4,0,.2,1)}:host ::ng-deep .image-container.image-align-left{text-align:left}:host ::ng-deep .image-container.image-align-center{text-align:center}:host ::ng-deep .image-container.image-align-right{text-align:right}:host ::ng-deep .image-container img{display:inline-block;max-width:100%;height:auto;border-radius:16px}:host ::ng-deep .resizable-image-container{position:relative;display:inline-block;margin:.5em 0}:host ::ng-deep .resize-controls{position:absolute;inset:0;pointer-events:none;z-index:1000}:host ::ng-deep .resize-handle{position:absolute;width:12px;height:12px;background:#3b82f6;border:2px solid white;border-radius:50%;pointer-events:all;cursor:pointer;z-index:1001;transition:all .15s ease;box-shadow:0 2px 6px #0003}:host ::ng-deep .resize-handle:hover{background:#2563eb;box-shadow:0 3px 8px #0000004d}:host ::ng-deep .resize-handle:active{background:#1d4ed8}:host ::ng-deep .resize-handle-n:hover,:host ::ng-deep .resize-handle-s:hover{transform:translate(-50%) scale(1.2)}:host ::ng-deep .resize-handle-w:hover,:host ::ng-deep .resize-handle-e:hover{transform:translateY(-50%) scale(1.2)}:host ::ng-deep .resize-handle-n:active,:host ::ng-deep .resize-handle-s:active{transform:translate(-50%) scale(.9)}:host ::ng-deep .resize-handle-w:active,:host ::ng-deep .resize-handle-e:active{transform:translateY(-50%) scale(.9)}:host ::ng-deep .resize-handle-nw:hover,:host ::ng-deep .resize-handle-ne:hover,:host ::ng-deep .resize-handle-sw:hover,:host ::ng-deep .resize-handle-se:hover{transform:scale(1.2)}:host ::ng-deep .resize-handle-nw:active,:host ::ng-deep .resize-handle-ne:active,:host ::ng-deep .resize-handle-sw:active,:host ::ng-deep .resize-handle-se:active{transform:scale(.9)}:host ::ng-deep .resize-handle-nw{top:0;left:-6px;cursor:nw-resize}:host ::ng-deep .resize-handle-n{top:0;left:50%;transform:translate(-50%);cursor:n-resize}:host ::ng-deep .resize-handle-ne{top:0;right:-6px;cursor:ne-resize}:host ::ng-deep .resize-handle-w{top:50%;left:-6px;transform:translateY(-50%);cursor:w-resize}:host ::ng-deep .resize-handle-e{top:50%;right:-6px;transform:translateY(-50%);cursor:e-resize}:host ::ng-deep .resize-handle-sw{bottom:0;left:-6px;cursor:sw-resize}:host ::ng-deep .resize-handle-s{bottom:0;left:50%;transform:translate(-50%);cursor:s-resize}:host ::ng-deep .resize-handle-se{bottom:0;right:-6px;cursor:se-resize}:host ::ng-deep body.resizing{-webkit-user-select:none;user-select:none;cursor:crosshair}:host ::ng-deep body.resizing .ProseMirror{pointer-events:none}:host ::ng-deep body.resizing .ProseMirror .tiptap-image{pointer-events:none}:host ::ng-deep .image-size-info{position:absolute;bottom:-20px;left:50%;transform:translate(-50%);background:#000c;color:#fff;padding:2px 6px;border-radius:3px;font-size:11px;white-space:nowrap;opacity:0;transition:opacity .2s ease}:host ::ng-deep .image-container:hover .image-size-info{opacity:1}\n"] }]
5031
+ `, styles: [".tiptap-editor{border:2px solid #e2e8f0;border-radius:8px;background:#fff;overflow:hidden;transition:border-color .2s ease}.tiptap-editor:focus-within{border-color:#3182ce}.tiptap-content{padding:16px;min-height:var(--editor-min-height, 200px);height:var(--editor-height, auto);max-height:var(--editor-max-height, none);overflow-y:var(--editor-overflow, visible);outline:none;position:relative}.tiptap-content.drag-over{background:#f0f8ff;border:2px dashed #3182ce}.character-count{padding:8px 16px;font-size:12px;color:#718096;text-align:right;border-top:1px solid #e2e8f0;background:#f8f9fa}.image-upload-container{position:relative;display:inline-block}:host ::ng-deep .ProseMirror{outline:none;line-height:1.6;color:#2d3748;min-height:100%;height:100%;word-wrap:break-word;overflow-wrap:break-word}:host ::ng-deep .ProseMirror h1{font-size:2em;font-weight:700;margin-top:0;margin-bottom:.5em}:host ::ng-deep .ProseMirror h2{font-size:1.5em;font-weight:700;margin-top:1em;margin-bottom:.5em}:host ::ng-deep .ProseMirror h3{font-size:1.25em;font-weight:700;margin-top:1em;margin-bottom:.5em}:host ::ng-deep .ProseMirror p{margin:.5em 0}:host ::ng-deep .ProseMirror ul,:host ::ng-deep .ProseMirror ol{padding-left:2em;margin:.5em 0}:host ::ng-deep .ProseMirror blockquote{border-left:4px solid #e2e8f0;margin:1em 0;font-style:italic;background:#f8f9fa;padding:.5em 1em;border-radius:0 4px 4px 0}:host ::ng-deep .ProseMirror code{background:#f1f5f9;padding:.2em .4em;border-radius:3px;font-family:Monaco,Consolas,monospace;font-size:.9em}:host ::ng-deep .ProseMirror pre{background:#1a202c;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;margin:1em 0}:host ::ng-deep .ProseMirror pre code{background:none;color:inherit;padding:0}:host ::ng-deep .ProseMirror p.is-editor-empty:first-child:before{content:attr(data-placeholder);color:#a0aec0;pointer-events:none;float:left;height:0}:host ::ng-deep .ProseMirror[contenteditable=false]{pointer-events:none}:host ::ng-deep .ProseMirror[contenteditable=false] img{cursor:default;pointer-events:none}:host ::ng-deep .ProseMirror[contenteditable=false] img:hover{transform:none;box-shadow:0 2px 8px #0000001a}:host ::ng-deep .ProseMirror[contenteditable=false] img.ProseMirror-selectednode{outline:none}:host ::ng-deep .ProseMirror img{position:relative;display:inline-block;max-width:100%;height:auto;cursor:pointer;transition:all .2s ease;border:2px solid transparent;border-radius:8px}:host ::ng-deep .ProseMirror img:hover{border-color:#e2e8f0;box-shadow:0 2px 4px #0000001a}:host ::ng-deep .ProseMirror img.ProseMirror-selectednode{border-color:#3182ce;box-shadow:0 0 0 3px #3182ce1a;transition:all .2s ease}:host ::ng-deep .ProseMirror .tiptap-image{max-width:100%;height:auto;border-radius:16px;box-shadow:0 4px 20px #00000014;margin:.5em 0;cursor:pointer;transition:all .3s cubic-bezier(.4,0,.2,1);display:block;filter:brightness(1) contrast(1)}:host ::ng-deep .ProseMirror .tiptap-image:hover{box-shadow:0 8px 30px #0000001f;filter:brightness(1.02) contrast(1.02)}:host ::ng-deep .ProseMirror .tiptap-image.ProseMirror-selectednode{outline:2px solid #6366f1;outline-offset:2px;border-radius:16px;box-shadow:0 0 0 4px #6366f11a}:host ::ng-deep .image-container{margin:.5em 0;text-align:center;border-radius:16px;overflow:hidden;transition:all .3s cubic-bezier(.4,0,.2,1)}:host ::ng-deep .image-container.image-align-left{text-align:left}:host ::ng-deep .image-container.image-align-center{text-align:center}:host ::ng-deep .image-container.image-align-right{text-align:right}:host ::ng-deep .image-container img{display:inline-block;max-width:100%;height:auto;border-radius:16px}:host ::ng-deep .resizable-image-container{position:relative;display:inline-block;margin:.5em 0}:host ::ng-deep .resize-controls{position:absolute;inset:0;pointer-events:none;z-index:1000}:host ::ng-deep .resize-handle{position:absolute;width:12px;height:12px;background:#3b82f6;border:2px solid white;border-radius:50%;pointer-events:all;cursor:pointer;z-index:1001;transition:all .15s ease;box-shadow:0 2px 6px #0003}:host ::ng-deep .resize-handle:hover{background:#2563eb;box-shadow:0 3px 8px #0000004d}:host ::ng-deep .resize-handle:active{background:#1d4ed8}:host ::ng-deep .resize-handle-n:hover,:host ::ng-deep .resize-handle-s:hover{transform:translate(-50%) scale(1.2)}:host ::ng-deep .resize-handle-w:hover,:host ::ng-deep .resize-handle-e:hover{transform:translateY(-50%) scale(1.2)}:host ::ng-deep .resize-handle-n:active,:host ::ng-deep .resize-handle-s:active{transform:translate(-50%) scale(.9)}:host ::ng-deep .resize-handle-w:active,:host ::ng-deep .resize-handle-e:active{transform:translateY(-50%) scale(.9)}:host ::ng-deep .resize-handle-nw:hover,:host ::ng-deep .resize-handle-ne:hover,:host ::ng-deep .resize-handle-sw:hover,:host ::ng-deep .resize-handle-se:hover{transform:scale(1.2)}:host ::ng-deep .resize-handle-nw:active,:host ::ng-deep .resize-handle-ne:active,:host ::ng-deep .resize-handle-sw:active,:host ::ng-deep .resize-handle-se:active{transform:scale(.9)}:host ::ng-deep .resize-handle-nw{top:0;left:-6px;cursor:nw-resize}:host ::ng-deep .resize-handle-n{top:0;left:50%;transform:translate(-50%);cursor:n-resize}:host ::ng-deep .resize-handle-ne{top:0;right:-6px;cursor:ne-resize}:host ::ng-deep .resize-handle-w{top:50%;left:-6px;transform:translateY(-50%);cursor:w-resize}:host ::ng-deep .resize-handle-e{top:50%;right:-6px;transform:translateY(-50%);cursor:e-resize}:host ::ng-deep .resize-handle-sw{bottom:0;left:-6px;cursor:sw-resize}:host ::ng-deep .resize-handle-s{bottom:0;left:50%;transform:translate(-50%);cursor:s-resize}:host ::ng-deep .resize-handle-se{bottom:0;right:-6px;cursor:se-resize}:host ::ng-deep body.resizing{-webkit-user-select:none;user-select:none;cursor:crosshair}:host ::ng-deep body.resizing .ProseMirror{pointer-events:none}:host ::ng-deep body.resizing .ProseMirror .tiptap-image{pointer-events:none}:host ::ng-deep .image-size-info{position:absolute;bottom:-20px;left:50%;transform:translate(-50%);background:#000c;color:#fff;padding:2px 6px;border-radius:3px;font-size:11px;white-space:nowrap;opacity:0;transition:opacity .2s ease}:host ::ng-deep .image-container:hover .image-size-info{opacity:1}:host ::ng-deep .ProseMirror table{border-collapse:separate;border-spacing:0;margin:0;table-layout:fixed;width:100%;border-radius:8px;overflow:hidden}:host ::ng-deep .ProseMirror table td,:host ::ng-deep .ProseMirror table th{border:none;border-right:1px solid #e2e8f0;border-bottom:1px solid #e2e8f0;box-sizing:border-box;min-width:1em;padding:8px 12px;position:relative;vertical-align:top;background:#fff}:host ::ng-deep .ProseMirror table td:first-child,:host ::ng-deep .ProseMirror table th:first-child{border-left:1px solid #e2e8f0}:host ::ng-deep .ProseMirror table tr:first-child td,:host ::ng-deep .ProseMirror table tr:first-child th{border-top:1px solid #e2e8f0}:host ::ng-deep .ProseMirror table tr:first-child th:first-child{border-top-left-radius:8px}:host ::ng-deep .ProseMirror table tr:first-child th:last-child{border-top-right-radius:8px}:host ::ng-deep .ProseMirror table tr:last-child td:first-child{border-bottom-left-radius:8px}:host ::ng-deep .ProseMirror table tr:last-child td:last-child{border-bottom-right-radius:8px}:host ::ng-deep .ProseMirror table th{background:#f8f9fa;font-weight:600;color:#374151}:host ::ng-deep .ProseMirror table .selectedCell:after{background:#c8c8ff66;content:\"\";inset:0;pointer-events:none;position:absolute;z-index:2}:host ::ng-deep .ProseMirror table .column-resize-handle{position:absolute;right:-2px;top:0;bottom:0;width:4px;background-color:#6366f1;opacity:0;transition:opacity .2s ease}:host ::ng-deep .ProseMirror table:hover .column-resize-handle{opacity:1}:host ::ng-deep .ProseMirror table .column-resize-handle:hover{background-color:#4f46e5}:host ::ng-deep .ProseMirror .tableWrapper{overflow-x:auto;margin:1em 0;border-radius:8px}:host ::ng-deep .ProseMirror .tableWrapper table{margin:0;border-radius:8px;min-width:600px;overflow:hidden}:host ::ng-deep .ProseMirror table p{margin:0}:host ::ng-deep .ProseMirror table .selectedCell{background-color:#6366f11a}:host ::ng-deep .ProseMirror table th{background-color:#f8f9fa;font-weight:600;color:#374151;text-align:left}:host ::ng-deep .ProseMirror table tbody tr:nth-child(2n){background-color:#fafbfc}:host ::ng-deep .ProseMirror table tbody tr:hover{background-color:#f1f5f9}\n"] }]
4066
5032
  }], ctorParameters: () => [] });
4067
5033
 
5034
+ /**
5035
+ * Clés des commandes dans l'ordre de création
5036
+ */
5037
+ const SLASH_COMMAND_KEYS = [
5038
+ "heading1",
5039
+ "heading2",
5040
+ "heading3",
5041
+ "bulletList",
5042
+ "orderedList",
5043
+ "blockquote",
5044
+ "code",
5045
+ "image",
5046
+ "horizontalRule",
5047
+ "table",
5048
+ ];
4068
5049
  /**
4069
5050
  * Factory function pour créer les slash commands traduits
4070
5051
  */
@@ -4202,22 +5183,28 @@ function createI18nSlashCommands(i18nService) {
4202
5183
  keywords: slashCommands.horizontalRule.keywords,
4203
5184
  command: (editor) => editor.chain().focus().setHorizontalRule().run(),
4204
5185
  },
5186
+ {
5187
+ title: slashCommands.table.title,
5188
+ description: slashCommands.table.description,
5189
+ icon: "table_view",
5190
+ keywords: slashCommands.table.keywords,
5191
+ command: (editor) => editor.chain().focus().insertTable({ rows: 3, cols: 3 }).run(),
5192
+ },
4205
5193
  ];
4206
5194
  }
4207
5195
  /**
4208
- * Mapping des clés de commandes pour la compatibilité
5196
+ * Fonction utilitaire pour filtrer les slash commands selon les commandes actives
5197
+ * Utilise le service i18n injecté en interne
4209
5198
  */
4210
- const SLASH_COMMAND_KEYS = {
4211
- heading1: "heading1",
4212
- heading2: "heading2",
4213
- heading3: "heading3",
4214
- bulletList: "bulletList",
4215
- orderedList: "orderedList",
4216
- blockquote: "blockquote",
4217
- code: "code",
4218
- image: "image",
4219
- horizontalRule: "horizontalRule",
4220
- };
5199
+ function filterSlashCommands(activeCommands) {
5200
+ // Injecter le service i18n en interne
5201
+ const i18nService = inject(TiptapI18nService);
5202
+ const allCommands = createI18nSlashCommands(i18nService);
5203
+ return allCommands.filter((command, index) => {
5204
+ const commandKey = SLASH_COMMAND_KEYS[index];
5205
+ return commandKey && activeCommands.has(commandKey);
5206
+ });
5207
+ }
4221
5208
 
4222
5209
  /*
4223
5210
  * Public API Surface of tiptap-editor
@@ -4228,5 +5215,5 @@ const SLASH_COMMAND_KEYS = {
4228
5215
  * Generated bundle index. Do not edit.
4229
5216
  */
4230
5217
 
4231
- export { AngularTiptapEditorComponent, DEFAULT_BUBBLE_MENU_CONFIG, DEFAULT_IMAGE_BUBBLE_MENU_CONFIG, DEFAULT_SLASH_COMMANDS, DEFAULT_TOOLBAR_CONFIG, NoopValueAccessorDirective, TiptapI18nService, createI18nSlashCommands };
5218
+ export { AngularTiptapEditorComponent, DEFAULT_BUBBLE_MENU_CONFIG, DEFAULT_CELL_MENU_CONFIG, DEFAULT_IMAGE_BUBBLE_MENU_CONFIG, DEFAULT_SLASH_COMMANDS, DEFAULT_TABLE_MENU_CONFIG, DEFAULT_TOOLBAR_CONFIG, EditorCommandsService, ImageService, NoopValueAccessorDirective, SLASH_COMMAND_KEYS, TiptapI18nService, createI18nSlashCommands, filterSlashCommands };
4232
5219
  //# sourceMappingURL=flogeez-angular-tiptap-editor.mjs.map