A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
1import {updateTransaction} from "../wysiwyg/transaction";
2import {getSelectionOffset, focusByWbr, focusByRange, focusBlock} from "./selection";
3import {hasClosestBlock, hasClosestByClassName, hasClosestByTag} from "./hasClosest";
4import {matchHotKey} from "./hotKey";
5import {isNotCtrl} from "./compatibility";
6import {scrollCenter} from "../../util/highlightById";
7import {insertEmptyBlock} from "../../block/util";
8import {removeBlock} from "../wysiwyg/remove";
9import {hasNextSibling, hasPreviousSibling} from "../wysiwyg/getBlock";
10import * as dayjs from "dayjs";
11
12const scrollToView = (nodeElement: Element, rowElement: HTMLElement, protyle: IProtyle) => {
13 if (nodeElement.getAttribute("custom-pinthead") === "true") {
14 const tableElement = nodeElement.querySelector("table");
15 if (tableElement.clientHeight + tableElement.scrollTop < rowElement.offsetTop + rowElement.clientHeight) {
16 tableElement.scrollTop = rowElement.offsetTop - tableElement.clientHeight + rowElement.clientHeight + 1;
17 } else if (tableElement.scrollTop > rowElement.offsetTop - rowElement.clientHeight) {
18 tableElement.scrollTop = rowElement.offsetTop - rowElement.clientHeight + 1;
19 }
20 } else {
21 scrollCenter(protyle, rowElement);
22 }
23};
24
25export const getColIndex = (cellElement: HTMLElement) => {
26 let previousElement = cellElement.previousElementSibling;
27 let index = 0;
28 while (previousElement) {
29 index++;
30 previousElement = previousElement.previousElementSibling;
31 }
32 return index;
33};
34
35// 光标设置到前一个表格中
36const goPreviousCell = (cellElement: HTMLElement, range: Range, isSelected = true) => {
37 let previousElement = cellElement.previousElementSibling;
38 if (!previousElement) {
39 if (cellElement.parentElement.previousElementSibling) {
40 previousElement = cellElement.parentElement.previousElementSibling.lastElementChild;
41 } else if (cellElement.parentElement.parentElement.tagName === "TBODY" &&
42 cellElement.parentElement.parentElement.previousElementSibling) {
43 previousElement = cellElement.parentElement
44 .parentElement.previousElementSibling.lastElementChild.lastElementChild;
45 } else {
46 previousElement = null;
47 }
48 }
49 if (previousElement) {
50 range.selectNodeContents(previousElement);
51 if (!isSelected) {
52 range.collapse(false);
53 }
54 focusByRange(range);
55 }
56 return previousElement;
57};
58
59export const setTableAlign = (protyle: IProtyle, cellElements: HTMLElement[], nodeElement: Element, type: string, range: Range) => {
60 range.insertNode(document.createElement("wbr"));
61 const html = nodeElement.outerHTML;
62
63 const tableElement = nodeElement.querySelector("table");
64 const columnCnt = tableElement.rows[0].cells.length;
65 const rowCnt = tableElement.rows.length;
66 const currentColumns: number[] = [];
67
68 for (let i = 0; i < rowCnt; i++) {
69 for (let j = 0; j < columnCnt; j++) {
70 if (tableElement.rows[i].cells[j] === cellElements[currentColumns.length]) {
71 currentColumns.push(j);
72 }
73 }
74 if (currentColumns.length > 0) {
75 break;
76 }
77 }
78 for (let k = 0; k < rowCnt; k++) {
79 currentColumns.forEach(item => {
80 tableElement.rows[k].cells[item].setAttribute("align", type);
81 });
82 }
83 updateTransaction(protyle, nodeElement.getAttribute("data-node-id"), nodeElement.outerHTML, html);
84 focusByWbr(tableElement, range);
85};
86
87export const insertRow = (protyle: IProtyle, range: Range, cellElement: HTMLElement, nodeElement: Element) => {
88 const wbrElement = document.createElement("wbr");
89 range.insertNode(wbrElement);
90 const html = nodeElement.outerHTML;
91 wbrElement.remove();
92
93 let rowHTML = "";
94 for (let m = 0; m < cellElement.parentElement.childElementCount; m++) {
95 rowHTML += `<td align="${cellElement.parentElement.children[m].getAttribute("align") || ""}"></td>`;
96 }
97 let newRowElememt: HTMLTableRowElement;
98 if (cellElement.tagName === "TH") {
99 const tbodyElement = nodeElement.querySelector("tbody");
100 if (tbodyElement) {
101 tbodyElement.insertAdjacentHTML("afterbegin", `<tr>${rowHTML}</tr>`);
102 newRowElememt = tbodyElement.firstElementChild as HTMLTableRowElement;
103 } else {
104 cellElement.parentElement.parentElement.insertAdjacentHTML("afterend", `<tbody><tr>${rowHTML}</tr></tbody>`);
105 newRowElememt = cellElement.parentElement.parentElement.nextElementSibling.firstElementChild as HTMLTableRowElement;
106 }
107 } else {
108 cellElement.parentElement.insertAdjacentHTML("afterend", `<tr>${rowHTML}</tr>`);
109 newRowElememt = cellElement.parentElement.nextElementSibling as HTMLTableRowElement;
110 }
111 range.selectNodeContents(newRowElememt.cells[getColIndex(cellElement)]);
112 range.collapse(true);
113 focusByRange(range);
114 updateTransaction(protyle, nodeElement.getAttribute("data-node-id"), nodeElement.outerHTML, html);
115 scrollToView(nodeElement, newRowElememt, protyle);
116};
117
118export const insertRowAbove = (protyle: IProtyle, range: Range, cellElement: HTMLElement, nodeElement: Element) => {
119 const wbrElement = document.createElement("wbr");
120 range.insertNode(wbrElement);
121 const html = nodeElement.outerHTML;
122 wbrElement.remove();
123 let rowHTML = "";
124 let hasNone = false;
125
126 for (let m = 0; m < cellElement.parentElement.childElementCount; m++) {
127 const currentCellElement = cellElement.parentElement.children[m] as HTMLTableCellElement;
128 const className = currentCellElement.className;
129 if (className === "fn__none") {
130 hasNone = true;
131 }
132 // 不需要空格,否则列宽调整后在空格后插入图片会换行 https://github.com/siyuan-note/siyuan/issues/7631
133 if (cellElement.tagName === "TH") {
134 rowHTML += `<th class="${currentCellElement.className}" colspan="${currentCellElement.colSpan}" align="${currentCellElement.getAttribute("align")}"></th>`;
135 } else {
136 rowHTML += `<td class="${currentCellElement.className}" colspan="${currentCellElement.colSpan}" align="${currentCellElement.getAttribute("align")}"></td>`;
137 }
138 }
139
140 if (hasNone) {
141 let previousTrElement = cellElement.parentElement.previousElementSibling;
142 let rowCount = 1;
143 while (previousTrElement) {
144 rowCount++;
145 Array.from(previousTrElement.children).forEach((cell: HTMLTableCellElement) => {
146 if (cell.rowSpan >= rowCount && cell.rowSpan > 1) {
147 cell.rowSpan = cell.rowSpan + 1;
148 }
149 });
150 previousTrElement = previousTrElement.previousElementSibling;
151 }
152 }
153 let newRowElememt: HTMLTableRowElement;
154 if (cellElement.parentElement.parentElement.tagName === "THEAD" && !cellElement.parentElement.previousElementSibling) {
155 cellElement.parentElement.parentElement.insertAdjacentHTML("beforebegin", `<thead><tr>${rowHTML}</tr></thead>`);
156 newRowElememt = nodeElement.querySelector("thead tr");
157 cellElement.parentElement.parentElement.nextElementSibling.insertAdjacentHTML("afterbegin", cellElement.parentElement.parentElement.innerHTML.replace(/<th/g, "<td").replace(/<\/th>/g, "</td>"));
158 cellElement.parentElement.parentElement.remove();
159 } else {
160 cellElement.parentElement.insertAdjacentHTML("beforebegin", `<tr>${rowHTML}</tr>`);
161 newRowElememt = cellElement.parentElement.previousElementSibling as HTMLTableRowElement;
162 }
163 range.selectNodeContents(newRowElememt.cells[getColIndex(cellElement)]);
164 range.collapse(true);
165 focusByRange(range);
166 updateTransaction(protyle, nodeElement.getAttribute("data-node-id"), nodeElement.outerHTML, html);
167 scrollToView(nodeElement, newRowElememt, protyle);
168};
169
170export const insertColumn = (protyle: IProtyle, nodeElement: Element, cellElement: HTMLElement, type: InsertPosition, range: Range) => {
171 const wbrElement = document.createElement("wbr");
172 range.insertNode(wbrElement);
173 const html = nodeElement.outerHTML;
174 wbrElement.remove();
175 const index = getColIndex(cellElement);
176 const tableElement = nodeElement.querySelector("table");
177 for (let i = 0; i < tableElement.rows.length; i++) {
178 const colCellElement = tableElement.rows[i].cells[index];
179 const newCellElement = document.createElement(colCellElement.tagName);
180 colCellElement.insertAdjacentElement(type, newCellElement);
181 if (colCellElement === cellElement) {
182 newCellElement.innerHTML = "<wbr> ";
183 // 滚动条横向定位
184 if (newCellElement.offsetLeft + newCellElement.clientWidth > nodeElement.firstElementChild.scrollLeft + nodeElement.firstElementChild.clientWidth) {
185 nodeElement.firstElementChild.scrollLeft = newCellElement.offsetLeft + newCellElement.clientWidth - nodeElement.firstElementChild.clientWidth;
186 }
187 } else {
188 newCellElement.textContent = " ";
189 }
190 }
191 tableElement.querySelectorAll("col")[index].insertAdjacentHTML(type, "<col style='min-width: 60px;'>");
192 focusByWbr(nodeElement, range);
193 updateTransaction(protyle, nodeElement.getAttribute("data-node-id"), nodeElement.outerHTML, html);
194};
195
196export const deleteRow = (protyle: IProtyle, range: Range, cellElement: HTMLElement, nodeElement: Element) => {
197 if (cellElement.parentElement.parentElement.tagName !== "THEAD") {
198 const wbrElement = document.createElement("wbr");
199 range.insertNode(wbrElement);
200 const html = nodeElement.outerHTML;
201 wbrElement.remove();
202 const index = getColIndex(cellElement);
203 const tbodyElement = cellElement.parentElement.parentElement;
204 let previousTrElement = tbodyElement.previousElementSibling.lastElementChild as HTMLTableRowElement;
205 if (cellElement.parentElement.previousElementSibling) {
206 previousTrElement = cellElement.parentElement.previousElementSibling as HTMLTableRowElement;
207 }
208
209 if (tbodyElement.childElementCount === 1) {
210 tbodyElement.remove();
211 } else {
212 cellElement.parentElement.remove();
213 }
214 range.selectNodeContents(previousTrElement.cells[index]);
215 range.collapse(true);
216 focusByRange(range);
217 scrollToView(nodeElement, previousTrElement, protyle);
218 updateTransaction(protyle, nodeElement.getAttribute("data-node-id"), nodeElement.outerHTML, html);
219 }
220};
221
222export const deleteColumn = (protyle: IProtyle, range: Range, nodeElement: Element, cellElement: HTMLElement) => {
223 const wbrElement = document.createElement("wbr");
224 range.insertNode(wbrElement);
225 const html = nodeElement.outerHTML;
226 wbrElement.remove();
227 const index = getColIndex(cellElement);
228 const sideCellElement = (cellElement.previousElementSibling || cellElement.nextElementSibling) as HTMLElement;
229 if (sideCellElement) {
230 range.selectNodeContents(sideCellElement);
231 range.collapse(true);
232 // 滚动条横向定位
233 if (sideCellElement.offsetLeft + sideCellElement.clientWidth > nodeElement.firstElementChild.scrollLeft + nodeElement.firstElementChild.clientWidth) {
234 nodeElement.firstElementChild.scrollLeft = sideCellElement.offsetLeft + sideCellElement.clientWidth - nodeElement.firstElementChild.clientWidth;
235 }
236 } else {
237 nodeElement.classList.add("protyle-wysiwyg--select");
238 removeBlock(protyle, nodeElement, range, "remove");
239 return;
240 }
241 const tableElement = nodeElement.querySelector("table");
242 for (let i = 0; i < tableElement.rows.length; i++) {
243 const cells = tableElement.rows[i].cells;
244 if (cells.length === 1) {
245 tableElement.remove();
246 break;
247 }
248 cells[index].remove();
249 }
250 nodeElement.querySelectorAll("col")[index]?.remove();
251 updateTransaction(protyle, nodeElement.getAttribute("data-node-id"), nodeElement.outerHTML, html);
252 focusByRange(range);
253};
254
255export const moveRowToUp = (protyle: IProtyle, range: Range, cellElement: HTMLElement, nodeElement: Element) => {
256 const rowElement = cellElement.parentElement;
257 if (rowElement.parentElement.tagName === "THEAD") {
258 return;
259 }
260 range.insertNode(document.createElement("wbr"));
261 const html = nodeElement.outerHTML;
262 if (rowElement.previousElementSibling) {
263 rowElement.after(rowElement.previousElementSibling);
264 } else {
265 const headElement = rowElement.parentElement.previousElementSibling.firstElementChild;
266 headElement.querySelectorAll("th").forEach(item => {
267 const tdElement = document.createElement("td");
268 tdElement.innerHTML = item.innerHTML;
269 item.parentNode.replaceChild(tdElement, item);
270 });
271 rowElement.querySelectorAll("td").forEach(item => {
272 const thElement = document.createElement("th");
273 thElement.innerHTML = item.innerHTML;
274 item.parentNode.replaceChild(thElement, item);
275 });
276 rowElement.after(headElement);
277 rowElement.parentElement.previousElementSibling.append(rowElement);
278 }
279 updateTransaction(protyle, nodeElement.getAttribute("data-node-id"), nodeElement.outerHTML, html);
280 focusByWbr(nodeElement, range);
281 scrollCenter(protyle, rowElement);
282};
283
284export const moveRowToDown = (protyle: IProtyle, range: Range, cellElement: HTMLElement, nodeElement: Element) => {
285 const rowElement = cellElement.parentElement;
286 if ((rowElement.parentElement.tagName === "TBODY" && !rowElement.nextElementSibling) ||
287 (rowElement.parentElement.tagName === "THEAD" && !rowElement.parentElement.nextElementSibling)) {
288 return;
289 }
290 range.insertNode(document.createElement("wbr"));
291 const html = nodeElement.outerHTML;
292 if (rowElement.nextElementSibling) {
293 rowElement.before(rowElement.nextElementSibling);
294 } else {
295 const firstRowElement = rowElement.parentElement.nextElementSibling.firstElementChild;
296 firstRowElement.querySelectorAll("td").forEach(item => {
297 const thElement = document.createElement("th");
298 thElement.innerHTML = item.innerHTML;
299 item.parentNode.replaceChild(thElement, item);
300 });
301 rowElement.querySelectorAll("th").forEach(item => {
302 const tdElement = document.createElement("td");
303 tdElement.innerHTML = item.innerHTML;
304 item.parentNode.replaceChild(tdElement, item);
305 });
306 rowElement.after(firstRowElement);
307 rowElement.parentElement.nextElementSibling.insertAdjacentElement("afterbegin", rowElement);
308 }
309 updateTransaction(protyle, nodeElement.getAttribute("data-node-id"), nodeElement.outerHTML, html);
310 focusByWbr(nodeElement, range);
311 scrollCenter(protyle, rowElement);
312};
313
314export const moveColumnToLeft = (protyle: IProtyle, range: Range, cellElement: HTMLElement, nodeElement: Element) => {
315 if (!cellElement.previousElementSibling) {
316 return;
317 }
318 range.insertNode(document.createElement("wbr"));
319 const html = nodeElement.outerHTML;
320 let cellIndex = 0;
321 Array.from(cellElement.parentElement.children).find((item, index) => {
322 if (cellElement === item) {
323 cellIndex = index;
324 return true;
325 }
326 });
327
328 nodeElement.querySelectorAll("tr").forEach((trElement) => {
329 trElement.cells[cellIndex].after(trElement.cells[cellIndex - 1]);
330 });
331 // 滚动条横向定位
332 if (cellElement.offsetLeft < nodeElement.firstElementChild.scrollLeft) {
333 nodeElement.firstElementChild.scrollLeft = cellElement.offsetLeft;
334 }
335 const colElements = nodeElement.querySelectorAll("col");
336 colElements[cellIndex].after(colElements[cellIndex - 1]);
337 updateTransaction(protyle, nodeElement.getAttribute("data-node-id"), nodeElement.outerHTML, html);
338 focusByWbr(nodeElement, range);
339};
340
341export const moveColumnToRight = (protyle: IProtyle, range: Range, cellElement: HTMLElement, nodeElement: Element) => {
342 if (!cellElement.nextElementSibling) {
343 return;
344 }
345 range.insertNode(document.createElement("wbr"));
346 const html = nodeElement.outerHTML;
347 let cellIndex = 0;
348 Array.from(cellElement.parentElement.children).find((item, index) => {
349 if (cellElement === item) {
350 cellIndex = index;
351 return true;
352 }
353 });
354 nodeElement.querySelectorAll("tr").forEach((trElement) => {
355 trElement.cells[cellIndex].before(trElement.cells[cellIndex + 1]);
356 });
357 // 滚动条横向定位
358 if (cellElement.offsetLeft + cellElement.clientWidth > nodeElement.firstElementChild.scrollLeft + nodeElement.firstElementChild.clientWidth) {
359 nodeElement.firstElementChild.scrollLeft = cellElement.offsetLeft + cellElement.clientWidth - nodeElement.firstElementChild.clientWidth;
360 }
361 const colElements = nodeElement.querySelectorAll("col");
362 colElements[cellIndex].before(colElements[cellIndex + 1]);
363 updateTransaction(protyle, nodeElement.getAttribute("data-node-id"), nodeElement.outerHTML, html);
364 focusByWbr(nodeElement, range);
365};
366
367export const fixTable = (protyle: IProtyle, event: KeyboardEvent, range: Range) => {
368 const cellElement = hasClosestByTag(range.startContainer, "TD") || hasClosestByTag(range.startContainer, "TH");
369 const nodeElement = hasClosestBlock(range.startContainer) as HTMLTableElement;
370 if (!cellElement || !nodeElement) {
371 return false;
372 }
373
374 if (event.key === "Backspace" && range.toString() === "") {
375 const previousElement = hasPreviousSibling(range.startContainer) as Element;
376 if (range.startOffset === 1 && previousElement.nodeType === 1 && previousElement.tagName === "BR" &&
377 range.startContainer.textContent.length === 1 && !hasNextSibling(range.startContainer)) {
378 previousElement.insertAdjacentHTML("beforebegin", "<br>");
379 return false;
380 }
381 }
382
383 // shift+enter 软换行
384 if (event.key === "Enter" && event.shiftKey && isNotCtrl(event) && !event.altKey) {
385 const wbrElement = document.createElement("wbr");
386 range.insertNode(wbrElement);
387 const oldHTML = nodeElement.outerHTML;
388 wbrElement.remove();
389 if (cellElement && !cellElement.innerHTML.endsWith("<br>")) {
390 cellElement.insertAdjacentHTML("beforeend", "<br>");
391 }
392 range.extractContents();
393 const types = protyle.toolbar.getCurrentType(range);
394 if (types.includes("code") && range.startContainer.nodeType !== 3) {
395 // https://github.com/siyuan-note/siyuan/issues/4169
396 const brElement = document.createElement("br");
397 (range.startContainer as HTMLElement).after(brElement);
398 range.setStartAfter(brElement);
399 } else {
400 range.insertNode(document.createElement("br"));
401 }
402 range.collapse(false);
403 scrollCenter(protyle);
404 updateTransaction(protyle, nodeElement.getAttribute("data-node-id"), nodeElement.outerHTML, oldHTML);
405 event.preventDefault();
406 return true;
407 }
408
409 if (!nodeElement.classList.contains("protyle-wysiwyg--select") && !hasClosestByClassName(nodeElement, "protyle-wysiwyg--select")) {
410 // enter 光标跳转到下一行同列
411 if (isNotCtrl(event) && !event.shiftKey && !event.altKey && event.key === "Enter") {
412 event.preventDefault();
413 const trElement = cellElement.parentElement as HTMLTableRowElement;
414 if ((!trElement.nextElementSibling && trElement.parentElement.tagName === "TBODY") ||
415 (trElement.parentElement.tagName === "THEAD" && !trElement.parentElement.nextElementSibling)) {
416 insertEmptyBlock(protyle, "afterend", nodeElement.getAttribute("data-node-id"));
417 return true;
418 }
419 let nextElement = trElement.nextElementSibling as HTMLTableRowElement;
420 if (!nextElement) {
421 nextElement = trElement.parentElement.nextElementSibling.firstChild as HTMLTableRowElement;
422 }
423 if (!nextElement) {
424 return true;
425 }
426 range.selectNodeContents(nextElement.cells[getColIndex(cellElement)]);
427 range.collapse(true);
428 scrollCenter(protyle);
429 return true;
430 }
431 // 表格后无内容时,按右键需新建空块
432 if (event.key === "ArrowRight" && range.toString() === "" &&
433 !nodeElement.nextElementSibling &&
434 cellElement === nodeElement.querySelector("table").lastElementChild.lastElementChild.lastElementChild &&
435 getSelectionOffset(cellElement, protyle.wysiwyg.element, range).start === cellElement.textContent.length) {
436 event.preventDefault();
437 insertEmptyBlock(protyle, "afterend", nodeElement.getAttribute("data-node-id"));
438 return true;
439 }
440 // tab:光标移向下一个 cell
441 if (event.key === "Tab" && isNotCtrl(event)) {
442 if (event.shiftKey) {
443 // shift + tab 光标移动到前一个 cell
444 goPreviousCell(cellElement, range);
445 event.preventDefault();
446 return true;
447 }
448
449 let nextElement = cellElement.nextElementSibling;
450 if (!nextElement) {
451 if (cellElement.parentElement.nextElementSibling) {
452 nextElement = cellElement.parentElement.nextElementSibling.firstElementChild;
453 } else if (cellElement.parentElement.parentElement.tagName === "THEAD" &&
454 cellElement.parentElement.parentElement.nextElementSibling) {
455 nextElement =
456 cellElement.parentElement.parentElement.nextElementSibling.firstElementChild.firstElementChild;
457 } else {
458 nextElement = null;
459 }
460 }
461 if (nextElement) {
462 range.selectNodeContents(nextElement);
463 } else {
464 insertRow(protyle, range, cellElement.parentElement.firstElementChild as HTMLTableCellElement, nodeElement);
465 }
466 event.preventDefault();
467 return true;
468 }
469
470 if (event.key === "ArrowUp" && isNotCtrl(event) && !event.shiftKey && !event.altKey) {
471 const startContainer = range.startContainer as HTMLElement;
472 let previousBrElement;
473 if (startContainer.nodeType !== 3 && (startContainer.tagName === "TH" || startContainer.tagName === "TD")) {
474 previousBrElement = (startContainer.childNodes[Math.min(range.startOffset, startContainer.childNodes.length - 1)] as HTMLElement);
475 } else if (startContainer.parentElement.tagName === "SPAN") {
476 previousBrElement = startContainer.parentElement.previousElementSibling;
477 } else {
478 previousBrElement = startContainer.previousElementSibling;
479 }
480 while (previousBrElement) {
481 if (previousBrElement.tagName === "BR" && hasPreviousSibling(previousBrElement)) {
482 return false;
483 }
484 previousBrElement = previousBrElement.previousElementSibling;
485 }
486 const trElement = cellElement.parentElement as HTMLTableRowElement;
487 let previousElement = trElement.previousElementSibling as HTMLTableRowElement;
488 if (!previousElement) {
489 previousElement = trElement.parentElement.previousElementSibling.lastElementChild as HTMLTableRowElement;
490 }
491 if (!previousElement || previousElement?.tagName === "COL") {
492 return false;
493 }
494 range.selectNodeContents(previousElement.cells[getColIndex(cellElement)]);
495 range.collapse(false);
496 scrollCenter(protyle);
497 event.preventDefault();
498 return true;
499 }
500
501 if (event.key === "ArrowDown" && isNotCtrl(event) && !event.shiftKey && !event.altKey) {
502 const endContainer = range.endContainer as HTMLElement;
503 let nextBrElement;
504 if (endContainer.nodeType !== 3 && (endContainer.tagName === "TH" || endContainer.tagName === "TD")) {
505 nextBrElement = (endContainer.childNodes[Math.max(0, range.endOffset - 1)] as HTMLElement)?.nextElementSibling;
506 } else if (endContainer.parentElement.tagName === "SPAN") {
507 nextBrElement = endContainer.parentElement.nextElementSibling;
508 } else {
509 nextBrElement = endContainer.nextElementSibling;
510 }
511 while (nextBrElement) {
512 if (nextBrElement.tagName === "BR" && nextBrElement.nextSibling) {
513 return false;
514 }
515 nextBrElement = nextBrElement.nextElementSibling;
516 }
517 const trElement = cellElement.parentElement as HTMLTableRowElement;
518 if ((!trElement.nextElementSibling && trElement.parentElement.tagName === "TBODY") ||
519 (trElement.parentElement.tagName === "THEAD" && !trElement.parentElement.nextElementSibling)) {
520 return false;
521 }
522 let nextElement = trElement.nextElementSibling as HTMLTableRowElement;
523 if (!nextElement) {
524 nextElement = trElement.parentElement.nextElementSibling.firstChild as HTMLTableRowElement;
525 }
526 if (!nextElement) {
527 return false;
528 }
529 range.selectNodeContents(nextElement.cells[getColIndex(cellElement)]);
530 range.collapse(true);
531 scrollCenter(protyle);
532 event.preventDefault();
533 return true;
534 }
535
536 // Backspace:光标移动到前一个 cell
537 if (isNotCtrl(event) && !event.shiftKey && !event.altKey && event.key === "Backspace"
538 && getSelectionOffset(cellElement, protyle.wysiwyg.element, range).start === 0 && range.toString() === "" &&
539 // 空换行无法删除 https://github.com/siyuan-note/siyuan/issues/2732
540 (range.startOffset === 0 || (range.startOffset === 1 && cellElement.querySelectorAll("br").length === 1))) {
541 const previousCellElement = goPreviousCell(cellElement, range, false);
542 if (!previousCellElement && nodeElement.previousElementSibling) {
543 focusBlock(nodeElement.previousElementSibling, undefined, false);
544 }
545 scrollCenter(protyle);
546 event.preventDefault();
547 return true;
548 }
549
550 // 居左
551 if (matchHotKey(window.siyuan.config.keymap.editor.general.alignLeft.custom, event)) {
552 setTableAlign(protyle, [cellElement], nodeElement, "left", range);
553 event.preventDefault();
554 return true;
555 }
556 // 居中
557 if (matchHotKey(window.siyuan.config.keymap.editor.general.alignCenter.custom, event)) {
558 setTableAlign(protyle, [cellElement], nodeElement, "center", range);
559 event.preventDefault();
560 return true;
561 }
562 // 居右
563 if (matchHotKey(window.siyuan.config.keymap.editor.general.alignRight.custom, event)) {
564 setTableAlign(protyle, [cellElement], nodeElement, "right", range);
565 event.preventDefault();
566 return true;
567 }
568 }
569
570 const tableElement = nodeElement.querySelector("table");
571 const hasNone = cellElement.parentElement.querySelector(".fn__none");
572 let hasColSpan = false;
573 let hasRowSpan = false;
574 Array.from(cellElement.parentElement.children).forEach((item: HTMLTableCellElement) => {
575 if (item.colSpan > 1) {
576 hasColSpan = true;
577 }
578 if (item.rowSpan > 1) {
579 hasRowSpan = true;
580 }
581 });
582 let previousHasNone: false | Element = false;
583 let previousHasColSpan = false;
584 let previousHasRowSpan = false;
585 let previousRowElement = cellElement.parentElement.previousElementSibling;
586 if (!previousRowElement && cellElement.parentElement.parentElement.tagName === "TBODY") {
587 previousRowElement = tableElement.querySelector("thead").lastElementChild;
588 }
589 if (previousRowElement) {
590 previousHasNone = previousRowElement.querySelector(".fn__none");
591 Array.from(previousRowElement.children).forEach((item: HTMLTableCellElement) => {
592 if (item.colSpan > 1) {
593 previousHasColSpan = true;
594 }
595 if (item.rowSpan > 1) {
596 previousHasRowSpan = true;
597 }
598 });
599 }
600 let nextHasNone: false | Element = false;
601 let nextHasColSpan = false;
602 let nextHasRowSpan = false;
603 let nextRowElement = cellElement.parentElement.nextElementSibling;
604 if (!nextRowElement && cellElement.parentElement.parentElement.tagName === "THEAD") {
605 nextRowElement = tableElement.querySelector("tbody")?.firstElementChild;
606 }
607 if (nextRowElement) {
608 nextHasNone = nextRowElement.querySelector(".fn__none");
609 Array.from(nextRowElement.children).forEach((item: HTMLTableCellElement) => {
610 if (item.colSpan > 1) {
611 nextHasColSpan = true;
612 }
613 if (item.rowSpan > 1) {
614 nextHasRowSpan = true;
615 }
616 });
617 }
618 const colIndex = getColIndex(cellElement);
619 let colIsPure = true;
620 Array.from(tableElement.rows).find(item => {
621 const cellElement = item.cells[colIndex];
622 if (cellElement.classList.contains("fn__none") || cellElement.colSpan > 1 || cellElement.rowSpan > 1) {
623 colIsPure = false;
624 return true;
625 }
626 });
627 let nextColIsPure = true;
628 Array.from(tableElement.rows).find(item => {
629 const cellElement = item.cells[colIndex + 1];
630 if (cellElement && (cellElement.classList.contains("fn__none") || cellElement.colSpan > 1 || cellElement.rowSpan > 1)) {
631 nextColIsPure = false;
632 return true;
633 }
634 });
635 let previousColIsPure = true;
636 Array.from(tableElement.rows).find(item => {
637 const cellElement = item.cells[colIndex - 1];
638 if (cellElement && (cellElement.classList.contains("fn__none") || cellElement.colSpan > 1 || cellElement.rowSpan > 1)) {
639 previousColIsPure = false;
640 return true;
641 }
642 });
643 if (matchHotKey(window.siyuan.config.keymap.editor.table.moveToUp.custom, event)) {
644 if ((!hasNone || (hasNone && !hasRowSpan && hasColSpan)) &&
645 (!previousHasNone || (previousHasNone && !previousHasRowSpan && previousHasColSpan))) {
646 moveRowToUp(protyle, range, cellElement, nodeElement);
647 }
648 event.preventDefault();
649 return true;
650 }
651
652 if (matchHotKey(window.siyuan.config.keymap.editor.table.moveToDown.custom, event)) {
653 if ((!hasNone || (hasNone && !hasRowSpan && hasColSpan)) &&
654 (!nextHasNone || (nextHasNone && !nextHasRowSpan && nextHasColSpan))) {
655 moveRowToDown(protyle, range, cellElement, nodeElement);
656 }
657 event.preventDefault();
658 return true;
659 }
660
661 if (matchHotKey(window.siyuan.config.keymap.editor.table.moveToLeft.custom, event)) {
662 if (colIsPure && previousColIsPure) {
663 moveColumnToLeft(protyle, range, cellElement, nodeElement);
664 }
665 event.preventDefault();
666 return true;
667 }
668
669 if (matchHotKey(window.siyuan.config.keymap.editor.table.moveToRight.custom, event)) {
670 if (colIsPure && nextColIsPure) {
671 moveColumnToRight(protyle, range, cellElement, nodeElement);
672 }
673 event.preventDefault();
674 return true;
675 }
676
677 // 上方新添加一行
678 if (matchHotKey(window.siyuan.config.keymap.editor.table.insertRowAbove.custom, event)) {
679 insertRowAbove(protyle, range, cellElement, nodeElement);
680 event.preventDefault();
681 event.stopPropagation();
682 return true;
683 }
684
685 // 下方新添加一行 https://github.com/Vanessa219/vditor/issues/46
686 if (matchHotKey(window.siyuan.config.keymap.editor.table.insertRowBelow.custom, event)) {
687 if (!nextHasNone || (nextHasNone && !nextHasRowSpan && nextHasColSpan)) {
688 insertRow(protyle, range, cellElement, nodeElement);
689 }
690 event.preventDefault();
691 return true;
692 }
693
694 // 左方新添加一列
695 if (matchHotKey(window.siyuan.config.keymap.editor.table.insertColumnLeft.custom, event)) {
696 if (colIsPure || previousColIsPure) {
697 insertColumn(protyle, nodeElement, cellElement, "beforebegin", range);
698 }
699 event.preventDefault();
700 return true;
701 }
702
703 // 后方新添加一列
704 if (matchHotKey(window.siyuan.config.keymap.editor.table.insertColumnRight.custom, event)) {
705 if (colIsPure || nextColIsPure) {
706 insertColumn(protyle, nodeElement, cellElement, "afterend", range);
707 }
708 event.preventDefault();
709 return true;
710 }
711
712 // 删除当前行
713 if (matchHotKey(window.siyuan.config.keymap.editor.table["delete-row"].custom, event)) {
714 if ((!hasNone && !hasRowSpan) || //https://github.com/siyuan-note/siyuan/issues/5045
715 (hasNone && !hasRowSpan && hasColSpan)) {
716 deleteRow(protyle, range, cellElement, nodeElement);
717 }
718 event.preventDefault();
719 event.stopPropagation();
720 return true;
721 }
722
723 // 删除当前列
724 if (matchHotKey(window.siyuan.config.keymap.editor.table["delete-column"].custom, event)) {
725 if (colIsPure) {
726 deleteColumn(protyle, range, nodeElement, cellElement);
727 }
728 event.preventDefault();
729 return true;
730 }
731};
732
733export const isIncludeCell = (options: {
734 tableSelectElement: HTMLElement,
735 scrollLeft: number,
736 scrollTop: number,
737 item: HTMLTableCellElement,
738}) => {
739 if (options.item.offsetLeft + 6 > options.tableSelectElement.offsetLeft + options.scrollLeft &&
740 options.item.offsetLeft + options.item.clientWidth - 6 < options.tableSelectElement.offsetLeft + options.scrollLeft + options.tableSelectElement.clientWidth &&
741 options.item.offsetTop + 6 > options.tableSelectElement.offsetTop + options.scrollTop &&
742 options.item.offsetTop + options.item.clientHeight - 6 < options.tableSelectElement.offsetTop + options.scrollTop + options.tableSelectElement.clientHeight) {
743 return true;
744 }
745 return false;
746};
747
748export const clearTableCell = (protyle: IProtyle, tableBlockElement: HTMLElement) => {
749 if (!tableBlockElement) {
750 return;
751 }
752 const tableSelectElement = tableBlockElement.querySelector(".table__select") as HTMLElement;
753 const selectCellElements: HTMLTableCellElement[] = [];
754 const scrollLeft = tableBlockElement.firstElementChild.scrollLeft;
755 const scrollTop = tableBlockElement.querySelector("table").scrollTop;
756 tableBlockElement.querySelectorAll("th, td").forEach((item: HTMLTableCellElement) => {
757 if (!item.classList.contains("fn__none") && isIncludeCell({
758 tableSelectElement,
759 scrollLeft,
760 scrollTop,
761 item,
762 })) {
763 selectCellElements.push(item);
764 }
765 });
766 tableSelectElement.removeAttribute("style");
767 if (getSelection().rangeCount > 0) {
768 const range = getSelection().getRangeAt(0);
769 if (tableBlockElement.contains(range.startContainer)) {
770 range.insertNode(document.createElement("wbr"));
771 }
772 }
773 const oldHTML = tableBlockElement.outerHTML;
774 tableBlockElement.querySelector("wbr")?.remove();
775 tableBlockElement.setAttribute("updated", dayjs().format("YYYYMMDDHHmmss"));
776 selectCellElements.forEach(item => {
777 item.innerHTML = "";
778 });
779 updateTransaction(protyle, tableBlockElement.getAttribute("data-node-id"), tableBlockElement.outerHTML, oldHTML);
780};