A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 780 lines 36 kB view raw
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};