A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 457 lines 18 kB view raw
1import {hasClosestBlock, hasClosestByClassName} from "../../util/hasClosest"; 2import {focusBlock} from "../../util/selection"; 3import {Menu} from "../../../plugin/Menu"; 4import {transaction} from "../../wysiwyg/transaction"; 5import { 6 genCellValue, 7 genCellValueByElement, 8 getTypeByCellElement, 9 renderCell, 10 renderCellAttr 11} from "./cell"; 12import {fetchPost} from "../../../util/fetch"; 13import * as dayjs from "dayjs"; 14import {Constants} from "../../../constants"; 15import {insertGalleryItemAnimation} from "./gallery/item"; 16import {clearSelect} from "../../util/clearSelect"; 17import {isCustomAttr} from "./blockAttr"; 18 19export const getFieldIdByCellElement = (cellElement: Element, viewType: TAVView): string => { 20 if (isCustomAttr(cellElement)) { 21 return cellElement.getAttribute("data-row-id"); 22 } 23 return (hasClosestByClassName(cellElement, viewType === "table" ? "av__row" : "av__gallery-item") as HTMLElement).dataset.id; 24}; 25 26export const selectRow = (checkElement: Element, type: "toggle" | "select" | "unselect" | "unselectAll") => { 27 const rowElement = hasClosestByClassName(checkElement, "av__row"); 28 if (!rowElement) { 29 return; 30 } 31 const useElement = checkElement.querySelector("use"); 32 if (rowElement.classList.contains("av__row--header") || type === "unselectAll") { 33 if ("#iconCheck" === useElement.getAttribute("xlink:href") || type === "unselectAll") { 34 rowElement.parentElement.querySelectorAll(".av__firstcol").forEach(item => { 35 item.querySelector("use").setAttribute("xlink:href", "#iconUncheck"); 36 const rowItemElement = hasClosestByClassName(item, "av__row"); 37 if (rowItemElement) { 38 rowItemElement.classList.remove("av__row--select"); 39 } 40 }); 41 } else { 42 rowElement.parentElement.querySelectorAll(".av__firstcol").forEach(item => { 43 item.querySelector("use").setAttribute("xlink:href", "#iconCheck"); 44 const rowItemElement = hasClosestByClassName(item, "av__row"); 45 if (rowItemElement) { 46 rowItemElement.classList.add("av__row--select"); 47 } 48 }); 49 } 50 } else { 51 if (type === "select" || (useElement.getAttribute("xlink:href") === "#iconUncheck" && type === "toggle")) { 52 rowElement.classList.add("av__row--select"); 53 useElement.setAttribute("xlink:href", "#iconCheck"); 54 } else if (type === "unselect" || (useElement.getAttribute("xlink:href") === "#iconCheck" && type === "toggle")) { 55 rowElement.classList.remove("av__row--select"); 56 useElement.setAttribute("xlink:href", "#iconUncheck"); 57 } 58 } 59 focusBlock(hasClosestBlock(rowElement) as HTMLElement); 60 updateHeader(rowElement); 61}; 62 63export const updateHeader = (rowElement: HTMLElement) => { 64 const blockElement = hasClosestBlock(rowElement); 65 if (!blockElement) { 66 return; 67 } 68 const selectCount = rowElement.parentElement.querySelectorAll(".av__row--select:not(.av__row--header)").length; 69 const count = rowElement.parentElement.querySelectorAll(".av__row:not(.av__row--header)").length; 70 71 const headElement = rowElement.parentElement.firstElementChild; 72 const headUseElement = headElement.querySelector("use"); 73 74 if (count === selectCount && count !== 0) { 75 headElement.classList.add("av__row--select"); 76 headUseElement.setAttribute("xlink:href", "#iconCheck"); 77 } else if (selectCount === 0) { 78 headElement.classList.remove("av__row--select"); 79 headUseElement.setAttribute("xlink:href", "#iconUncheck"); 80 } else if (selectCount > 0) { 81 headElement.classList.add("av__row--select"); 82 headUseElement.setAttribute("xlink:href", "#iconIndeterminateCheck"); 83 } 84 85 const counterElement = blockElement.querySelector(".av__counter"); 86 const allCount = blockElement.querySelectorAll(".av__row--select:not(.av__row--header)").length; 87 if (allCount === 0) { 88 counterElement.classList.add("fn__none"); 89 return; 90 } 91 counterElement.classList.remove("fn__none"); 92 counterElement.innerHTML = `${allCount} ${window.siyuan.languages.selected}`; 93}; 94 95export const setPage = (blockElement: Element) => { 96 const avType = blockElement.getAttribute("data-av-type") as TAVView; 97 blockElement.querySelectorAll(".av__body").forEach((item: HTMLElement) => { 98 const pageSize = item.dataset.pageSize; 99 if (pageSize) { 100 const currentCount = item.querySelectorAll(avType === "table" ? ".av__row:not(.av__row--header)" : ".av__gallery-item").length; 101 if (parseInt(pageSize) < currentCount) { 102 item.dataset.pageSize = currentCount.toString(); 103 } 104 } 105 }); 106}; 107 108/** 109 * 前端插入一假行 110 * @param options.protyle 111 * @param options.blockElement 112 * @param options.srcIDs 113 * @param options.previousId 114 * @param options.avId 存在为新增否则为拖拽插入 115 */ 116export const insertAttrViewBlockAnimation = (options: { 117 protyle: IProtyle, 118 blockElement: Element, 119 srcIDs: string[], // node id 120 previousId: string, 121 groupID?: string 122}) => { 123 (options.blockElement.querySelector('[data-type="av-search"]') as HTMLInputElement).value = ""; 124 const groupQuery = options.groupID ? `.av__body[data-group-id="${options.groupID}"] ` : ""; 125 let previousElement = options.blockElement.querySelector(groupQuery + `.av__row[data-id="${options.previousId}"]`) || options.blockElement.querySelector(groupQuery + ".av__row--header"); 126 // 有排序需要加入最后一行 127 const hasSort = options.blockElement.querySelector('.av__views [data-type="av-sort"]').classList.contains("block__icon--active"); 128 if (hasSort) { 129 previousElement = options.blockElement.querySelector(groupQuery + ".av__row--util").previousElementSibling; 130 } 131 const bodyElement = options.blockElement.querySelector(`.av__body[data-group-id="${options.groupID}"] `); 132 if (bodyElement && ["updated", "created"].includes(bodyElement.getAttribute("data-dtype")) && 133 bodyElement.getAttribute("data-content") !== "_@today@_") { 134 previousElement = options.blockElement.querySelector('.av__body[data-content="_@today@_"] .av__row--util')?.previousElementSibling; 135 } 136 if (!previousElement) { 137 return; 138 } 139 let cellsHTML = '<div class="av__colsticky"><div class="av__firstcol"><svg><use xlink:href="#iconUncheck"></use></svg></div></div>'; 140 const pinIndex = previousElement.querySelectorAll(".av__colsticky .av__cell").length - 1; 141 if (pinIndex > -1) { 142 cellsHTML = '<div class="av__colsticky"><div class="av__firstcol"><svg><use xlink:href="#iconUncheck"></use></svg></div>'; 143 } 144 previousElement.querySelectorAll(".av__cell").forEach((item: HTMLElement, index) => { 145 let lineNumber = 1; 146 const colType = getTypeByCellElement(item); 147 if (colType === "lineNumber") { 148 const lineNumberValue = item.querySelector(".av__celltext")?.getAttribute("data-value"); 149 if (lineNumberValue) { 150 lineNumber = parseInt(lineNumberValue); 151 } 152 } 153 cellsHTML += `<div class="av__cell${colType === "checkbox" ? " av__cell-uncheck" : ""}" data-col-id="${item.dataset.colId}" 154data-wrap="${item.dataset.wrap}" 155data-dtype="${item.dataset.dtype}" 156style="width: ${item.style.width};${item.dataset.dtype === "number" ? "text-align: right;" : ""}" 157${colType === "block" ? ' data-detached="true"' : ""}>${renderCell(genCellValue(colType, null), lineNumber)}</div>`; 158 if (pinIndex === index) { 159 cellsHTML += "</div>"; 160 } 161 }); 162 let html = ""; 163 clearSelect(["cell", "row"], options.blockElement); 164 options.srcIDs.forEach(() => { 165 html += `<div class="av__row" data-type="ghost"> 166 ${cellsHTML} 167</div>`; 168 }); 169 previousElement.insertAdjacentHTML("afterend", html); 170 fetchPost("/api/av/getAttributeViewAddingBlockDefaultValues", { 171 avID: options.blockElement.getAttribute("data-av-id"), 172 viewID: options.blockElement.getAttribute(Constants.CUSTOM_SY_AV_VIEW), 173 groupID: options.groupID, 174 previousID: options.previousId, 175 }, (response) => { 176 if (response.data.values) { 177 let popCellElement: HTMLElement; 178 const updateIds = Object.keys(response.data.values); 179 options.blockElement.querySelectorAll('[data-type="ghost"]').forEach(rowItem => { 180 rowItem.querySelectorAll(".av__cell").forEach((cellItem: HTMLElement) => { 181 if (!popCellElement && cellItem.getAttribute("data-detached") === "true") { 182 popCellElement = cellItem; 183 } 184 if (updateIds.includes(cellItem.dataset.colId)) { 185 const cellValue = response.data.values[cellItem.dataset.colId]; 186 cellItem.innerHTML = renderCell(cellValue); 187 renderCellAttr(cellItem, cellValue); 188 } 189 }); 190 }); 191 } 192 setPage(options.blockElement); 193 }); 194}; 195 196export const stickyRow = (blockElement: HTMLElement, elementRect: DOMRect, status: "top" | "bottom" | "all") => { 197 if (blockElement.dataset.avType !== "table") { 198 return; 199 } 200 // 只读模式下也需固定 https://github.com/siyuan-note/siyuan/issues/11338 201 const headerElements = blockElement.querySelectorAll(".av__row--header"); 202 if (headerElements.length > 0 && (status === "top" || status === "all")) { 203 headerElements.forEach((item: HTMLElement) => { 204 const bodyRect = item.parentElement.getBoundingClientRect(); 205 const distance = Math.floor(elementRect.top - bodyRect.top); 206 if (distance > 0 && distance < bodyRect.height - item.clientHeight) { 207 item.style.transform = `translateY(${distance}px)`; 208 } else { 209 item.style.transform = ""; 210 } 211 }); 212 } 213 214 const footerElements = blockElement.querySelectorAll(".av__row--footer"); 215 if (footerElements.length > 0 && (status === "bottom" || status === "all")) { 216 footerElements.forEach((item: HTMLElement) => { 217 if (item.querySelector(".av__calc--ashow")) { 218 const bodyRect = item.parentElement.getBoundingClientRect(); 219 const distance = Math.ceil(elementRect.bottom - bodyRect.bottom); 220 if (distance < 0 && -distance < bodyRect.height - item.clientHeight) { 221 item.style.transform = `translateY(${distance}px)`; 222 } else { 223 item.style.transform = ""; 224 } 225 } else { 226 item.style.transform = ""; 227 } 228 }); 229 } 230}; 231 232const updatePageSize = (options: { 233 currentPageSize: string, 234 newPageSize: string, 235 protyle: IProtyle, 236 avID: string, 237 nodeElement: Element 238}) => { 239 if (options.currentPageSize === options.newPageSize) { 240 return; 241 } 242 options.nodeElement.querySelectorAll(".av__body").forEach((item: HTMLElement) => { 243 item.dataset.pageSize = options.newPageSize; 244 }); 245 const blockID = options.nodeElement.getAttribute("data-node-id"); 246 transaction(options.protyle, [{ 247 action: "setAttrViewPageSize", 248 avID: options.avID, 249 data: parseInt(options.newPageSize), 250 blockID 251 }], [{ 252 action: "setAttrViewPageSize", 253 data: parseInt(options.currentPageSize), 254 avID: options.avID, 255 blockID 256 }]); 257 document.querySelector(".av__panel")?.remove(); 258}; 259 260export const setPageSize = (options: { 261 target: HTMLElement, 262 protyle: IProtyle, 263 avID: string, 264 nodeElement: Element 265}) => { 266 const menu = new Menu(Constants.MENU_AV_PAGE_SIZE); 267 if (menu.isOpen) { 268 return; 269 } 270 const currentPageSize = options.target.dataset.size; 271 menu.addItem({ 272 iconHTML: "", 273 label: "10", 274 checked: currentPageSize === "10", 275 click() { 276 updatePageSize({ 277 currentPageSize, 278 newPageSize: "10", 279 protyle: options.protyle, 280 avID: options.avID, 281 nodeElement: options.nodeElement 282 }); 283 } 284 }); 285 menu.addItem({ 286 iconHTML: "", 287 checked: currentPageSize === "25", 288 label: "25", 289 click() { 290 updatePageSize({ 291 currentPageSize, 292 newPageSize: "25", 293 protyle: options.protyle, 294 avID: options.avID, 295 nodeElement: options.nodeElement 296 }); 297 } 298 }); 299 menu.addItem({ 300 iconHTML: "", 301 checked: currentPageSize === "50", 302 label: "50", 303 click() { 304 updatePageSize({ 305 currentPageSize, 306 newPageSize: "50", 307 protyle: options.protyle, 308 avID: options.avID, 309 nodeElement: options.nodeElement 310 }); 311 } 312 }); 313 menu.addItem({ 314 iconHTML: "", 315 checked: currentPageSize === "100", 316 label: "100", 317 click() { 318 updatePageSize({ 319 currentPageSize, 320 newPageSize: "100", 321 protyle: options.protyle, 322 avID: options.avID, 323 nodeElement: options.nodeElement 324 }); 325 } 326 }); 327 menu.addItem({ 328 iconHTML: "", 329 checked: currentPageSize === Constants.SIZE_DATABASE_MAZ_SIZE.toString(), 330 label: window.siyuan.languages.all, 331 click() { 332 updatePageSize({ 333 currentPageSize, 334 newPageSize: Constants.SIZE_DATABASE_MAZ_SIZE.toString(), 335 protyle: options.protyle, 336 avID: options.avID, 337 nodeElement: options.nodeElement 338 }); 339 } 340 }); 341 const rect = options.target.getBoundingClientRect(); 342 menu.open({ 343 x: rect.left, 344 y: rect.bottom 345 }); 346}; 347 348export const deleteRow = (blockElement: HTMLElement, protyle: IProtyle) => { 349 const rowElements = blockElement.querySelectorAll(".av__row--select:not(.av__row--header), .av__gallery-item--select"); 350 if (rowElements.length === 0) { 351 return; 352 } 353 const avID = blockElement.getAttribute("data-av-id"); 354 const undoOperations: IOperation[] = []; 355 const blockIds: string[] = []; 356 rowElements.forEach(item => { 357 blockIds.push(item.getAttribute("data-id")); 358 }); 359 rowElements.forEach(item => { 360 const blockValue = genCellValueByElement("block", item.querySelector('.av__cell[data-dtype="block"]')); 361 undoOperations.push({ 362 action: "insertAttrViewBlock", 363 avID, 364 previousID: item.previousElementSibling?.getAttribute("data-id") || "", 365 srcs: [{ 366 itemID: Lute.NewNodeID(), 367 id: item.getAttribute("data-id"), 368 isDetached: blockValue.isDetached, 369 content: blockValue.block.content 370 }], 371 blockID: blockElement.dataset.nodeId, 372 groupID: item.parentElement.getAttribute("data-group-id") 373 }); 374 }); 375 const newUpdated = dayjs().format("YYYYMMDDHHmmss"); 376 undoOperations.push({ 377 action: "doUpdateUpdated", 378 id: blockElement.dataset.nodeId, 379 data: blockElement.getAttribute("updated") 380 }); 381 transaction(protyle, [{ 382 action: "removeAttrViewBlock", 383 srcIDs: blockIds, 384 avID, 385 }, { 386 action: "doUpdateUpdated", 387 id: blockElement.dataset.nodeId, 388 data: newUpdated, 389 }], undoOperations); 390 rowElements.forEach(item => { 391 item.remove(); 392 }); 393 stickyRow(blockElement, protyle.contentElement.getBoundingClientRect(), "all"); 394 updateHeader(blockElement.querySelector(".av__row")); 395 blockElement.setAttribute("updated", newUpdated); 396}; 397 398export const insertRows = (options: { 399 blockElement: HTMLElement, 400 protyle: IProtyle, 401 count: number, 402 previousID: string, 403 groupID?: string 404}) => { 405 const avID = options.blockElement.getAttribute("data-av-id"); 406 const srcIDs: string[] = []; 407 const srcs: IOperationSrcs[] = []; 408 new Array(options.count).fill(0).forEach(() => { 409 const newNodeID = Lute.NewNodeID(); 410 srcIDs.push(newNodeID); 411 srcs.push({ 412 itemID: Lute.NewNodeID(), 413 id: newNodeID, 414 isDetached: true, 415 content: "", 416 }); 417 }); 418 const newUpdated = dayjs().format("YYYYMMDDHHmmss"); 419 transaction(options.protyle, [{ 420 action: "insertAttrViewBlock", 421 avID, 422 previousID: options.previousID, 423 srcs, 424 blockID: options.blockElement.dataset.nodeId, 425 groupID: options.groupID 426 }, { 427 action: "doUpdateUpdated", 428 id: options.blockElement.dataset.nodeId, 429 data: newUpdated, 430 }], [{ 431 action: "removeAttrViewBlock", 432 srcIDs, 433 avID, 434 }, { 435 action: "doUpdateUpdated", 436 id: options.blockElement.dataset.nodeId, 437 data: options.blockElement.getAttribute("updated") 438 }]); 439 if (options.blockElement.getAttribute("data-av-type") === "gallery") { 440 insertGalleryItemAnimation({ 441 blockElement: options.blockElement, 442 protyle: options.protyle, 443 srcIDs, 444 previousId: options.previousID, 445 groupID: options.groupID 446 }); 447 } else { 448 insertAttrViewBlockAnimation({ 449 protyle: options.protyle, 450 blockElement: options.blockElement, 451 srcIDs, 452 previousId: options.previousID, 453 groupID: options.groupID 454 }); 455 } 456 options.blockElement.setAttribute("updated", newUpdated); 457};