import Quill from 'quill';
import type {
QuillTableBetter,
TableCell,
TableColgroup
} from '../types';
import {
getCorrectWidth,
setElementProperty,
setElementAttribute,
updateTableWidth
} from '../utils';
interface Options {
tableNode: HTMLElement;
cellNode: Element;
mousePosition: {
clientX: number;
clientY: number;
}
}
const MIN_WIDTH = 30;
const MIN_HEIGHT = 22;
const TOUCH_TOLERANCE = 15;
const DRAG_BLOCK_HEIGHT = 8;
const DRAG_BLOCK_WIDTH = 8;
const LINE_CONTAINER_HEIGHT = 5;
const LINE_CONTAINER_WIDTH = 5;
class OperateLine {
quill: Quill;
options: Options | null;
drag: boolean;
line: HTMLElement | null;
dragBlock: HTMLElement | null;
dragTable: HTMLElement | null;
direction: string | null;
tableBetter: QuillTableBetter;
constructor(quill: Quill, tableBetter?: QuillTableBetter) {
this.quill = quill;
this.options = null;
this.drag = false;
this.line = null;
this.dragBlock = null;
this.dragTable = null;
this.direction = null; // 1.level 2.vertical
this.tableBetter = tableBetter;
this.quill.container.addEventListener('mousemove', this.handleMouseMove.bind(this));
}
// Créer la le petit cadre bleu pendant le drag (redimenssionnement)
createDragBlock() {
const dragBlock = document.createElement('div');
dragBlock.classList.add('ql-operate-block');
const { dragBlockProps } = this.getProperty(this.options);
setElementProperty(dragBlock, dragBlockProps);
this.dragBlock = dragBlock;
this.quill.container.appendChild(dragBlock);
this.updateCell(dragBlock);
}
// Crée la ligne bleue pointillée qui indique la future position de la bordure
createDragTable(table: Element) {
const dragTable = document.createElement('div');
const properties = this.getDragTableProperty(table);
dragTable.classList.add('ql-operate-drag-table');
setElementProperty(dragTable, properties);
this.dragTable = dragTable;
this.quill.container.appendChild(dragTable);
}
// Crée la ligne bleue pointillée qui indique la future position de la bordure
createOperateLine() {
const container = document.createElement('div');
const line = document.createElement('div');
container.classList.add('ql-operate-line-container');
const { containerProps, lineProps } = this.getProperty(this.options);
setElementProperty(container, containerProps);
setElementProperty(line, lineProps);
container.appendChild(line);
this.quill.container.appendChild(container);
this.line = container;
this.updateCell(container);
}
// Utilitaire pour trouver la bonne colonne dans un
en tenant compte des fusions
getCorrectCol(colgroup: TableColgroup, sum: number) {
let child = colgroup.children.head;
while (child && --sum) {
child = child.next;
}
return child;
}
// Calcule la position et la taille du rectangle fantôme (pointillés)
// Utilise toujours le 'scale' pour convertir l'écran (zoomé) en taille logique (réelle du DOM)
getDragTableProperty(table: Element) {
const scale = this.tableBetter.scale || 1;
const rect = table.getBoundingClientRect();
const containerRect = this.quill.container.getBoundingClientRect();
//Conversion simple à la place des scale * 100
const width = Math.round(rect.width / scale);
const height = Math.round(rect.height / scale);
const left = Math.round((rect.left - containerRect.left) / scale);
const top = Math.round((rect.top - containerRect.top) / scale)
return {
left: `${left}px`,
top: `${top}px`,
width: `${width}px`,
height: `${height}px`,
display: 'block'
}
}
/**
* Calcule l'index visuel de la colonne (en prenant en compte les colspan/rowspan).
* C'est essentiel pour savoir quelle bordure on touche dans un tableau.
*/
getLevelColSum(cell: Element) {
const row = cell.parentElement;
// @ts-ignore
if (!row || row.tagName !== 'TR') return 0;
const table = row.closest('table') as HTMLTableElement;
if (!table) return 0;
// On récupère toutes les lignes pour reconstruire la structure virtuelle
const rows = Array.from(table.rows);
const targetRowIndex = rows.indexOf(row as HTMLTableRowElement);
if (targetRowIndex === -1) return 0;
// blockedUntil[colIdx] = Index de la ligne où la colonne se libère
const blockedUntil: number[] = [];
for (let r = 0; r <= targetRowIndex; r++) {
const currentRow = rows[r];
const cells = Array.from(currentRow.children) as Element[];
let c = 0; // Index DOM (balise td)
let colIdx = 0; // Index VISUEL (colonne grille)
while (c < cells.length) {
// 1. Sauter les colonnes visuelles occupées par une fusion verticale venant du haut
while (blockedUntil[colIdx] > r) {
colIdx++;
}
const currentCell = cells[c];
const colspan = parseInt(currentCell.getAttribute('colspan')) || 1;
const rowspan = parseInt(currentCell.getAttribute('rowspan')) || 1;
// 2. Si c'est notre cellule cible, on retourne son index de FIN (bordure droite)
if (currentCell === cell) {
return colIdx + colspan;
}
// 3. Enregistrer le blocage pour les lignes futures si rowspan > 1
if (rowspan > 1) {
for (let k = 0; k < colspan; k++) {
blockedUntil[colIdx + k] = r + rowspan;
}
}
colIdx += colspan;
c++;
}
}
return 0;
}
getMaxColNum(cell: Element) {
const table = cell.closest('table') as HTMLTableElement;
if (!table) return 0;
const firstRow = table.rows[0];
// @ts-ignore
if (firstRow && firstRow.lastElementChild) {
// @ts-ignore
return this.getLevelColSum(firstRow.lastElementChild);
}
return 0;
}
// Détermine si on doit afficher la ligne de redimensionnement (horizontal ou vertical)
// en fonction de la position de la souris par rapport aux bords de la cellule.
getProperty(options: Options) {
const scale = this.tableBetter.scale || 1;
const containerRect = this.quill.container.getBoundingClientRect();
const { tableNode, cellNode, mousePosition } = options;
const tableRect = tableNode.getBoundingClientRect();
const cellRect = cellNode.getBoundingClientRect();
const toLogical = (val: number) => Math.round(val / scale);
const sContainerRect = {
left: toLogical(containerRect.left),
top: toLogical(containerRect.top),
width: toLogical(containerRect.width),
height: toLogical(containerRect.height)
};
const sCellRect = {
left: toLogical(cellRect.left),
top: toLogical(cellRect.top),
width: toLogical(cellRect.width),
height: toLogical(cellRect.height)
};
const x = sCellRect.left + sCellRect.width;
const y = sCellRect.top + sCellRect.height;
const clientX = toLogical(mousePosition.clientX);
const clientY = toLogical(mousePosition.clientY);
const sTableRect = {
right: toLogical(tableRect.right),
bottom: toLogical(tableRect.bottom)
}
const dragBlockProps = {
width: `${DRAG_BLOCK_WIDTH}px`,
height: `${DRAG_BLOCK_HEIGHT}px`,
top: `${sTableRect.bottom - sContainerRect.top}px`,
left: `${sTableRect.right - sContainerRect.left}px`,
display: 'block'
}
if (Math.abs(x - clientX) <= TOUCH_TOLERANCE) {
this.direction = 'level';
return {
dragBlockProps,
containerProps: {
width: `${LINE_CONTAINER_WIDTH}px`,
height: `${sCellRect.height}px`,
top: `${sCellRect.top - sContainerRect.top}px`,
left: `${x - sContainerRect.left - LINE_CONTAINER_WIDTH / 2}px`,
display: 'flex',
cursor: 'col-resize'
},
lineProps: {
width: '1px',
height: '100%'
}
}
} else if (Math.abs(y - clientY) <= TOUCH_TOLERANCE) {
this.direction = 'vertical';
return {
dragBlockProps,
containerProps: {
width: `${sCellRect.width}px`,
height: `${LINE_CONTAINER_HEIGHT}px`,
top: `${y - sContainerRect.top - LINE_CONTAINER_HEIGHT / 2}px`,
left: `${sCellRect.left - sContainerRect.left}px`,
display: 'flex',
cursor: 'row-resize'
},
lineProps: {
width: '100%',
height: '1px'
}
}
} else {
this.hideLine();
}
return { dragBlockProps };
}
// Récupère les cellules impactées verticalement quand on a un rowspan
getVerticalCells(cell: Element, rowspan: number) {
let row = cell.parentElement;
while (rowspan > 1 && row) {
// @ts-ignore
row = row.nextSibling;
rowspan--;
}
return row.children;
}
// Événement principal d'écoute de la souris.
// Cache ou affiche les outils dynamiquement selon là où se trouve la souris.
handleMouseMove(e: MouseEvent) {
if (!this.quill.isEnabled()) return;
const target = e.target as Element;
if (
target === this.dragBlock ||
(this.line && this.line.contains(target)) ||
target.classList.contains('ql-operate-block') ||
target.classList.contains('ql-operate-line-container')
) {
return;
}
const tableNode = target.closest('table');
if (tableNode && !this.quill.root.contains(tableNode)) return;
const cellNode = target.closest('td,th');
const mousePosition = {
clientX: e.clientX,
clientY: e.clientY
}
if (!tableNode || !cellNode) {
if (this.options && this.options.tableNode) {
const rect = this.options.tableNode.getBoundingClientRect();
const dist = Math.sqrt(
Math.pow(e.clientX - rect.right, 2) +
Math.pow(e.clientY - rect.bottom, 2)
);
if (dist < 40) {
return;
}
}
if (this.line && !this.drag && this.options) {
const extendedOptions = {
tableNode: this.options.tableNode,
cellNode: this.options.cellNode,
mousePosition
};
const { containerProps } = this.getProperty(extendedOptions);
if (containerProps) {
this.updateProperty(extendedOptions);
return;
}
}
if (this.line && !this.drag) {
this.hideLine();
this.hideDragBlock();
}
return;
}
const options = { tableNode, cellNode, mousePosition };
if (!this.line) {
this.options = options;
this.createOperateLine();
this.createDragBlock();
} else {
if (this.drag || !cellNode) return;
this.updateProperty(options);
}
}
hideDragBlock() {
this.dragBlock && setElementProperty(this.dragBlock, { display: 'none' });
}
hideDragTable() {
this.dragTable && setElementProperty(this.dragTable, { display: 'none' });
}
hideLine() {
this.line && setElementProperty(this.line, { display: 'none' });
}
isLine(node: Element) {
return node.classList.contains('ql-operate-line-container');
}
/**
* Redimensionne la largeur des colonnes via le Drag & Drop d'une bordure verticale.
* Cette fonction gère deux écosystèmes : les tableaux modernes (avec )
* et les tableaux legacy (modification cellule par cellule via une matrice virtuelle).
*/
setCellLevelRect(cell: Element, clientX: number) {
// Le "scale" permet de corriger les calculs si l'éditeur a un zoom appliqué (ex: 1.5 pour 150%)
const scale = this.tableBetter.scale || 1;
const tableBlot = (Quill.find(cell) as TableCell).table();
const isPercent = tableBlot.isPercent();
// Vérifie si le tableau possède l'élément HTML (la norme propre pour redimensionner)
const colgroup = tableBlot.colgroup() as TableColgroup;
// Récupère l'index VISUEL (la vraie colonne dans la grille, ignorant les fusions HTML) de la bordure droite
// @ts-ignore
const colSum = this.getLevelColSum(cell);
let bounds = tableBlot.domNode.getBoundingClientRect();
/**
* Utilitaire local pour appliquer proprement la nouvelle largeur.
* Met à jour à la fois l'attribut HTML (width="X") et le style CSS (width: Xpx).
*/
const applyWidth = (node: Element, logicalW: number, desc: string = '') => {
const w = logicalW;
const el = node as HTMLElement;
setElementAttribute(node, { width: String(w) });
const sWidth = isPercent ? getCorrectWidth(w, isPercent) : `${w}px`;
setElementProperty(node as HTMLElement, { width: sWidth });
if (!isPercent) {
(node as HTMLElement).style.setProperty('width', sWidth, 'important');
(node as HTMLElement).style.removeProperty('min-width');
}
};
// si Le tableau utilise un
if (colgroup) {
// On cible les balises invisibles qui contrôlent toute la colonne
const col = this.getCorrectCol(colgroup, colSum); // Colonne à gauche de la bordure
const nextCol = col.next; // Colonne à droite de la bordure
const { left: colLeft } = col.domNode.getBoundingClientRect();
// Drag de la toute dernière bordure à droite du tableau
if (!nextCol) {
let newW = (clientX - colLeft) / scale;
if (newW < MIN_WIDTH) newW = MIN_WIDTH;
let currentRectW = col.domNode.getBoundingClientRect().width / scale;
let diff = newW - currentRectW; // Différence de largeur pour ajuster la boîte globale
// Si le mouvement est significatif (> 0.5px), on redimensionne le tableau ENTIER
if (Math.abs(diff) > 0.5) {
applyWidth(col.domNode, newW, 'Col Unique');
bounds.width = bounds.width / scale;
const tableNode = tableBlot.domNode;
const newTotalW = bounds.width + diff;
updateTableWidth(tableNode, bounds, diff);
tableNode.style.setProperty('min-width', `${newTotalW}px`, 'important');
if (!isPercent) tableNode.style.setProperty('width', `${newTotalW}px`, 'important');
}
return;
}
// Redimensionnement interne (entre deux colonnes)
const rectB = nextCol.domNode.getBoundingClientRect();
const w1Visual = col.domNode.getBoundingClientRect().width;
const w2Visual = rectB.width;
const totalLogical = (w1Visual + w2Visual) / scale; // Espace total partagé par les 2 colonnes
let newW1 = (clientX - colLeft) / scale;
if (newW1 < MIN_WIDTH) newW1 = MIN_WIDTH;
if (newW1 > totalLogical - MIN_WIDTH) newW1 = totalLogical - MIN_WIDTH;
let newW2 = totalLogical - newW1; // Ce qui reste va à la colonne de droite
applyWidth(col.domNode, newW1, 'Col G');
applyWidth(nextCol.domNode, newW2, 'Col D');
} else { // Tableau sans colgroup
const tableNode = tableBlot.domNode as HTMLTableElement;
const maxCols = this.getMaxColNum(cell);
const cellEndIndex = this.getLevelColSum(cell);
const isLastVisualColumn = cellEndIndex >= maxCols;
// Drag de la dernière colonne du tableau
if (isLastVisualColumn) {
const { left: colLeft } = cell.getBoundingClientRect();
let newW = (clientX - colLeft) / scale;
if (newW < MIN_WIDTH) newW = MIN_WIDTH;
const firstCell = cell;
let currentRectW = firstCell.getBoundingClientRect().width / scale;
let diff = newW - currentRectW;
if (Math.abs(diff) > 0.5) {
bounds.width = bounds.width / scale;
const newTotalW = bounds.width + diff;
updateTableWidth(tableNode, bounds, diff);
tableNode.style.setProperty('min-width', `${newTotalW}px`, 'important');
if (!isPercent) tableNode.style.setProperty('width', `${newTotalW}px`, 'important');
const rows = Array.from(tableNode.rows);
// Le tableau `blockedUntil` modélise une grille virtuelle pour détecter les cases vides
// causées par des fusions verticales (rowspan) venant du haut.
const blockedUntil: number[] = [];
for (let r = 0; r < rows.length; r++) {
const row = rows[r];
const cells = Array.from(row.children) as Element[];
let c = 0;
let colIdx = 0;
while (c < cells.length) {
while (blockedUntil[colIdx] > r) colIdx++; // On saute les cases occupées par un rowspan
const currentCell = cells[c];
const colspan = parseInt(currentCell.getAttribute('colspan')) || 1;
const rowspan = parseInt(currentCell.getAttribute('rowspan')) || 1;
if (rowspan > 1) {
for (let k = 0; k < colspan; k++) blockedUntil[colIdx + k] = r + rowspan;
}
// On applique la largeur uniquement aux cellules de la toute dernière colonne visuelle
if (colIdx + colspan >= maxCols) {
applyWidth(currentCell, newW, `R${r}-Last`);
}
colIdx += colspan;
c++;
}
}
}
}
// Redimensionnement d'une bordure interne
else {
const rows = Array.from(tableNode.rows);
const blockedUntil: number[] = [];
// masterWidths stocke la décision de redimensionnement de la PREMIÈRE ligne.
// Les lignes suivantes copieront exactement ces valeurs pour garder la colonne droite
// et éviter les effets "d'escalier" si certaines cellules ont un padding différent.
let masterWidths: { w1: number, w2: number } | null = null;
for (let r = 0; r < rows.length; r++) {
const row = rows[r];
const cells = Array.from(row.children) as Element[];
let c = 0;
let colIdx = 0;
let leftCell: Element | null = null;
let rightCell: Element | null = null;
// Parsing de la grille virtuelle
while (c < cells.length) {
while (blockedUntil[colIdx] > r) colIdx++;
const currentCell = cells[c];
const colspan = parseInt(currentCell.getAttribute('colspan')) || 1;
const rowspan = parseInt(currentCell.getAttribute('rowspan')) || 1;
if (rowspan > 1) {
for (let k = 0; k < colspan; k++) blockedUntil[colIdx + k] = r + rowspan;
}
// On identifie qui est à gauche et à droite de la bordure manipulée (colSum)
if (colIdx + colspan === colSum) leftCell = currentCell;
if (colIdx === colSum) rightCell = currentCell;
colIdx += colspan;
c++;
}
if (leftCell && rightCell) {
let newW1, newW2;
if (masterWidths === null) {
// récupération colspans
const leftColspan = parseInt(leftCell.getAttribute('colspan')) || 1;
const rightColspan = parseInt(rightCell.getAttribute('colspan')) || 1;
// calcul de la valeur minimale
const dynamicMinW1 = MIN_WIDTH * leftColspan;
const dynamicMinW2 = MIN_WIDTH * rightColspan;
const { left: cLeft, width: w1V } = leftCell.getBoundingClientRect();
const { width: w2V } = rightCell.getBoundingClientRect();
// (Avec les Math.round pour garder l'alignement strict)
const totalLogical = Math.round((w1V + w2V) / scale);
let rawW1 = (clientX - cLeft) / scale;
newW1 = Math.round(rawW1);
// Application valeur minimale
if (newW1 < dynamicMinW1) newW1 = dynamicMinW1;
if (newW1 > totalLogical - dynamicMinW2) newW1 = totalLogical - dynamicMinW2;
newW2 = totalLogical - newW1;
masterWidths = { w1: newW1, w2: newW2 };
} else { // Lignes suivantes : on copie les valeurs du parent
newW1 = masterWidths.w1;
newW2 = masterWidths.w2;
}
applyWidth(leftCell, newW1, `R${r}-G`);
applyWidth(rightCell, newW2, `R${r}-D`);
}
else if (leftCell && !rightCell) {
// si on n'a qu'une cellule à redimensionner
const leftColspan = parseInt(leftCell.getAttribute('colspan')) || 1;
const dynamicMinW = MIN_WIDTH * leftColspan;
const { left: cLeft } = leftCell.getBoundingClientRect();
let newW = Math.round((clientX - cLeft) / scale);
if (newW < dynamicMinW) newW = dynamicMinW;
applyWidth(leftCell, newW, `R${r}-U`);
}
}
}
}
}
setCellRect(cell: Element, clientX: number, clientY: number) {
if (this.direction === 'level') {
this.setCellLevelRect(cell, clientX);
} else if (this.direction === 'vertical') {
this.setCellVerticalRect(cell, clientY);
}
}
/**
* REDIMENSIONNEMENT GLOBAL DU TABLEAU (Drag du coin bas-droit)
* Répartit l'agrandissement/rétrécissement de la souris de façon égale sur toutes les colonnes/lignes.
*/
setCellsRect(cell: Element, changeX: number, changeY: number) {
const scale = this.tableBetter.scale || 1;
const tableBlot = (Quill.find(cell) as TableCell).table();
const isPercent = tableBlot.isPercent();
const colgroup = tableBlot.colgroup() as TableColgroup;
const tableNode = tableBlot.domNode as HTMLTableElement;
let bounds = tableNode.getBoundingClientRect();
const logicalChangeX = changeX / scale;
const logicalChangeY = changeY / scale;
const rows = Array.from(cell.parentElement.parentElement.children) as HTMLElement[];
let rowHeights = rows.map(r => r.getBoundingClientRect().height / scale);
let remainingChangeY = logicalChangeY;
let safetyY = 0; // Sécurité anti-boucle infinie
// Algorithme de distribution
while (Math.abs(remainingChangeY) > 1 && safetyY < 50) {
safetyY++;
let activeRows = [];
// On cherche les lignes qui ont encore le droit de rétrécir
for (let i = 0; i < rows.length; i++) {
if (remainingChangeY < 0 && rowHeights[i] > MIN_HEIGHT + 0.5) {
activeRows.push(i);
} else if (remainingChangeY > 0) {
activeRows.push(i); // Si on agrandit, toutes les lignes sont actives
}
}
if (activeRows.length === 0) break; // Fin si tout est compressé au maximum
let stepY = remainingChangeY / activeRows.length;
remainingChangeY = 0; // On vide la réserve, on verra si on doit en remettre
for (let i of activeRows) {
let newH = rowHeights[i] + stepY;
if (newH < MIN_HEIGHT) {
// La ligne touche le fond : on récupère le rétrécissement non utilisé
remainingChangeY += (newH - MIN_HEIGHT);
rowHeights[i] = MIN_HEIGHT;
} else {
rowHeights[i] = newH;
}
}
}
// Application physique des hauteurs (TR et TD)
for (let i = 0; i < rows.length; i++) {
let cHeight = Math.round(rowHeights[i]);
if (cHeight < MIN_HEIGHT) cHeight = MIN_HEIGHT;
const sHeight = `${cHeight}px`;
const row = rows[i];
row.setAttribute('height', String(cHeight));
row.style.setProperty('height', sHeight, 'important');
row.style.removeProperty('min-height');
row.style.setProperty('max-height', sHeight, 'important');
const cells = Array.from(row.children) as HTMLElement[];
for (const c of cells) {
c.setAttribute('height', String(cHeight));
c.style.setProperty('height', sHeight, 'important');
c.style.removeProperty('min-height');
c.style.setProperty('max-height', sHeight, 'important');
}
}
let leftoverX = 0; // Garde en mémoire ce qu'on n'a pas pu rétrécir
if (colgroup) {
// --- TABLEAU AVEC COLGROUP ---
let cols = [];
let col = colgroup.children.head;
while (col) { cols.push(col); col = col.next; }
let colWidths = cols.map(c => c.domNode.getBoundingClientRect().width / scale);
let remainingChangeX = logicalChangeX;
let safetyX = 0;
while (Math.abs(remainingChangeX) > 1 && safetyX < 50) {
safetyX++;
let activeCols = [];
for (let i = 0; i < cols.length; i++) {
if (remainingChangeX < 0 && colWidths[i] > MIN_WIDTH + 0.5) {
activeCols.push(i);
} else if (remainingChangeX > 0) {
activeCols.push(i);
}
}
if (activeCols.length === 0) break;
let stepX = remainingChangeX / activeCols.length;
remainingChangeX = 0;
for (let i of activeCols) {
let newW = colWidths[i] + stepX;
if (newW < MIN_WIDTH) {
remainingChangeX += (newW - MIN_WIDTH);
colWidths[i] = MIN_WIDTH;
} else {
colWidths[i] = newW;
}
}
}
leftoverX = remainingChangeX; // Sauvegarde de l'excédent non applicable
for (let i = 0; i < cols.length; i++) {
let cWidth = Math.round(colWidths[i]);
if (cWidth < MIN_WIDTH) cWidth = MIN_WIDTH;
this.setColWidth(cols[i].domNode, String(cWidth), isPercent);
}
} else {
// --- TABLEAU SANS COLGROUP (Cellule par cellule) ---
for (let r = 0; r < rows.length; r++) {
const cells = Array.from(rows[r].children) as HTMLElement[];
let cellWidths = cells.map(c => c.getBoundingClientRect().width / scale);
let colspans = cells.map(c => parseInt(c.getAttribute('colspan')) || 1);
let remainingChangeX = logicalChangeX;
let safetyX = 0;
// Même principe, mais on gère le poids (colspan) de chaque cellule
while (Math.abs(remainingChangeX) > 1 && safetyX < 50) {
safetyX++;
let activeIndices = [];
let activeColspanSum = 0;
for (let i = 0; i < cells.length; i++) {
let minW = MIN_WIDTH * colspans[i];
if (remainingChangeX < 0 && cellWidths[i] > minW + 0.5) {
activeIndices.push(i);
activeColspanSum += colspans[i];
} else if (remainingChangeX > 0) {
activeIndices.push(i);
activeColspanSum += colspans[i];
}
}
if (activeIndices.length === 0) break;
let currentRemaining = remainingChangeX;
remainingChangeX = 0;
for (let i of activeIndices) {
// Les grandes cellules (colspan élevés) absorbent plus de changement
let portion = currentRemaining * (colspans[i] / activeColspanSum);
let minW = MIN_WIDTH * colspans[i];
let newW = cellWidths[i] + portion;
if (newW < minW) {
remainingChangeX += (newW - minW);
cellWidths[i] = minW;
} else {
cellWidths[i] = newW;
}
}
}
if (r === 0) leftoverX = remainingChangeX; // On prend la ligne 0 comme référence pour la boîte globale
for (let i = 0; i < cells.length; i++) {
let cWidth = Math.round(cellWidths[i]);
let minW = MIN_WIDTH * colspans[i];
if (cWidth < minW) cWidth = minW;
const sWidth = isPercent ? getCorrectWidth(cWidth, isPercent) : `${cWidth}px`;
const c = cells[i];
setElementAttribute(c, { width: String(cWidth) });
setElementProperty(c, { width: sWidth });
if (!isPercent) {
c.style.setProperty('width', sWidth, 'important');
c.style.removeProperty('min-width');
}
}
}
}
const appliedChangeX = logicalChangeX - leftoverX;
const currentTableWidth = Math.round(bounds.width / scale);
const logicalBounds = { ...bounds, width: currentTableWidth };
const newTotalW = currentTableWidth + Math.round(appliedChangeX);
updateTableWidth(tableNode, logicalBounds, Math.round(appliedChangeX));
tableNode.style.setProperty('min-width', `${newTotalW}px`, 'important');
tableNode.style.removeProperty('height');
tableNode.removeAttribute('height');
const tempBlot = tableBlot.temporary();
if (tempBlot && tempBlot.domNode) {
tempBlot.domNode.style.removeProperty('height');
tempBlot.domNode.removeAttribute('height');
}
}
// Utilitaire pour appliquer une largeur propre (% ou px)
setColWidth(domNode: HTMLElement, width: string, isPercent: boolean) {
if (isPercent) {
width = getCorrectWidth(parseFloat(width), isPercent);
domNode.style.setProperty('width', width);
} else {
let intVal = width;
if (!width.endsWith('%')) {
const val = parseFloat(width.replace('px', ''));
intVal = `${Math.round(val)}px`;
}
setElementAttribute(domNode, { width: intVal.replace('px', '') });
domNode.style.setProperty('width', intVal, 'important');
domNode.style.removeProperty('min-width');
}
}
// Redimensionnement manuel (Drag) d'une ligne précise (Axe Vertical)
setCellVerticalRect(cell: Element, clientY: number) {
const scale = this.tableBetter.scale || 1;
const rowspan = ~~cell.getAttribute('rowspan') || 1;
const cellsCollection = rowspan > 1 ? this.getVerticalCells(cell, rowspan) : cell.parentElement.children;
const cells = Array.from(cellsCollection) as HTMLElement[];
const row = cells[0].parentElement as HTMLElement;
const { top: rowTop } = row.getBoundingClientRect();
// 1. Calcul Arrondi
let newHeight = Math.round((clientY - rowTop) / scale);
if (newHeight < MIN_HEIGHT) newHeight = MIN_HEIGHT;
// 2. Application HTML (Attribut) en Entier
setElementAttribute(row, { height: String(newHeight) });
// 3. Application CSS en Entier
row.style.setProperty('height', `${newHeight}px`);
for (const c of cells) {
const cRowspan = ~~c.getAttribute('rowspan') || 1;
if (rowspan === 1 && cRowspan > 1) {
continue;
}
setElementAttribute(c, { height: String(newHeight) });
c.style.setProperty('height', `${newHeight}px`);
}
}
toggleLineChildClass(isAdd: boolean) {
const node = this.line.firstElementChild;
if (isAdd) {
node.classList.add('ql-operate-line');
} else {
node.classList.remove('ql-operate-line');
}
}
/**
* Initialise les écouteurs d'événements pour les outils de redimensionnement (ligne ou carré).
* Gère le cycle de vie du glisser-déposer (Drag & Drop) : mousedown, mousemove, mouseup.
*/
updateCell(node: Element) {
if (!node) return;
const isLine = this.isLine(node); // Détermine si on tire une bordure (ligne) ou tout le tableau (carré)
const handleDrag = (e: MouseEvent) => {
e.preventDefault();
if (this.drag) {
// Déplacement du fantome (la ligne bleue ou le cadre en pointillés).
if (isLine) {
this.updateDragLine(e.clientX, e.clientY);
this.hideDragBlock();
} else {
this.updateDragBlock(e.clientX, e.clientY);
this.hideLine();
}
}
}
const handleMouseup = (e: MouseEvent) => {
e.preventDefault();
const { cellNode, tableNode } = this.options;
// Calcul et Application de la nouvelle taille
if (isLine) {
// Redimensionnement d'une seule colonne/ligne
this.setCellRect(cellNode, e.clientX, e.clientY);
this.toggleLineChildClass(false);
} else {
// Redimensionnement global du tableau depuis le coin inférieur droit
const { right, bottom } = tableNode.getBoundingClientRect();
const changeX = e.clientX - right;
const changeY = e.clientY - bottom;
this.setCellsRect(cellNode, changeX, changeY);
this.dragBlock.classList.remove('ql-operate-block-move');
this.hideDragBlock();
this.hideDragTable();
}
// Synchronisation finale (Arrondi)
setTimeout(() => {
if (tableNode) {
const scale = this.tableBetter.scale || 1;
const tableRect = tableNode.getBoundingClientRect();
const tableW = Math.round(tableRect.width / scale);
// On force la largeur globale en un nombre entier pour éviter le flou d'affichage
tableNode.setAttribute('width', String(tableW));
tableNode.style.width = `${tableW}px`;
tableNode.style.minWidth = `${tableW}px`;
const tableBlot = (Quill.find(cellNode) as TableCell).table();
const colgroup = tableBlot.colgroup();
// On arrondit également toutes les colonnes internes pour que la somme soit parfaite
if (colgroup) {
let col = colgroup.children.head;
while (col) {
const colNode = col.domNode;
const w = Math.round(colNode.getBoundingClientRect().width / scale);
colNode.setAttribute('width', String(w));
colNode.style.width = `${w}px`;
col = col.next;
}
} else {
// S'il n'y a pas de colgroup, on utilise la première ligne comme parent
// et on force toutes les autres lignes à copier ses largeurs entières.
const rows = Array.from((tableNode as HTMLTableElement).rows);
const firstRow = rows[0];
if (firstRow) {
const referenceWidths: number[] = [];
Array.from(firstRow.children).forEach((cell: HTMLElement) => {
const w = Math.round(cell.getBoundingClientRect().width / scale);
referenceWidths.push(w);
});
rows.forEach(row => {
Array.from(row.children).forEach((cell: HTMLElement, index) => {
if (referenceWidths[index] !== undefined) {
const w = referenceWidths[index];
cell.setAttribute('width', String(w));
cell.style.width = `${w}px`;
}
});
});
}
}
}
}, 0);
this.drag = false;
document.removeEventListener('mousemove', handleDrag, false);
document.removeEventListener('mouseup', handleMouseup, false);
this.tableBetter.tableMenus.updateMenus(tableNode);
}
const handleMousedown = (e: MouseEvent) => {
e.preventDefault();
const { tableNode, cellNode } = this.options;
const scale = this.tableBetter.scale || 1;
if (tableNode) {
tableNode.style.removeProperty('min-width');
// Snapshot Table : On fige la largeur globale actuelle du tableau en pixels entiers
const tableRect = tableNode.getBoundingClientRect();
const tableW = Math.round(tableRect.width / scale);
tableNode.setAttribute('width', String(tableW));
tableNode.style.width = `${tableW}px`;
// Snapshot Colonne : On fige la largeur de chaque colonne.
const tableBlot = (Quill.find(cellNode) as TableCell).table();
const colgroup = tableBlot.colgroup();
const rows = Array.from((tableNode as HTMLTableElement).rows);
const firstRow = rows[0];
if (colgroup) {
let col = colgroup.children.head;
while (col) {
const w = Math.round(col.domNode.getBoundingClientRect().width / scale);
col.domNode.setAttribute('width', String(w));
col.domNode.style.width = `${w}px`;
col = col.next;
}
}
else if (firstRow) {
// On capture les largeurs de la PREMIÈRE ligne (Row 0)
const referenceWidths: number[] = [];
Array.from(firstRow.children).forEach((cell: HTMLElement) => {
const w = Math.round(cell.getBoundingClientRect().width / scale);
referenceWidths.push(w);
});
// On FORCE ces largeurs sur TOUTES les cellules de TOUTES les lignes
rows.forEach(row => {
Array.from(row.children).forEach((cell: HTMLElement, index) => {
if (referenceWidths[index] !== undefined) {
const w = referenceWidths[index];
cell.setAttribute('width', String(w));
cell.style.width = `${w}px`;
cell.style.boxSizing = 'border-box';
}
});
});
}
}
// Apparition des éléments visuels de drag
if (isLine) {
this.toggleLineChildClass(true);
} else {
if (this.dragTable) {
const properties = this.getDragTableProperty(tableNode);
setElementProperty(this.dragTable, properties);
} else {
this.createDragTable(tableNode);
}
}
this.drag = true;
document.addEventListener('mousemove', handleDrag);
document.addEventListener('mouseup', handleMouseup);
}
node.addEventListener('mousedown', handleMousedown);
}
updateDragBlock(clientX: number, clientY: number) {
const scale = this.tableBetter.scale || 1;
// Conversion Souris écran --> Souris Logique
const sClientX = Math.round(clientX / scale);
const sClientY = Math.round(clientY / scale);
const containerRect = this.quill.container.getBoundingClientRect();
const sContainerRect = {
top: Math.round(containerRect.top / scale),
left: Math.round(containerRect.left / scale)
};
this.dragBlock.classList.add('ql-operate-block-move');
setElementProperty(this.dragBlock, {
top: `${sClientY - sContainerRect.top - DRAG_BLOCK_HEIGHT / 2}px`,
left: `${sClientX - sContainerRect.left - DRAG_BLOCK_WIDTH / 2}px`
});
this.updateDragTable(clientX, clientY);
}
updateDragLine(clientX: number, clientY: number) {
const scale = this.tableBetter.scale || 1;
// Conversion (toujours la même chose normalement ça devrait aller pour comprendre)
const sClientX = Math.round(clientX / scale);
const sClientY = Math.round(clientY / scale);
const containerRect = this.quill.container.getBoundingClientRect();
const sContainerRect = {
top: Math.round(containerRect.top / scale),
left: Math.round(containerRect.left / scale)
};
if (this.direction === 'level') {
setElementProperty(this.line, {
left: `${sClientX - sContainerRect.left - LINE_CONTAINER_WIDTH / 2}px`
});
} else if (this.direction === 'vertical') {
setElementProperty(this.line, {
top: `${sClientY - sContainerRect.top - LINE_CONTAINER_HEIGHT / 2}px`
});
}
}
updateDragTable(clientX: number, clientY: number) {
const scale = this.tableBetter.scale || 1;
const sClientX = Math.round(clientX / scale);
const sClientY = Math.round(clientY / scale);
let { top, left } = this.dragTable.getBoundingClientRect();
top = Math.round(top / scale);
left = Math.round(left / scale);
const width = sClientX - left;
const height = sClientY - top;
setElementProperty(this.dragTable, {
width: `${width}px`,
height: `${height}px`,
display: 'block'
});
}
updateProperty(options: Options) {
const { containerProps, lineProps, dragBlockProps } = this.getProperty(options);
if (!containerProps || !lineProps) return;
this.options = options;
setElementProperty(this.line, containerProps);
setElementProperty(this.line.firstChild as HTMLElement, lineProps);
setElementProperty(this.dragBlock, dragBlockProps);
}
}
export default OperateLine;