import {transaction} from "../../wysiwyg/transaction"; import {hasClosestBlock, hasClosestByClassName} from "../../util/hasClosest"; import {openMenuPanel} from "./openMenuPanel"; import {updateAttrViewCellAnimation} from "./action"; import {isNotCtrl} from "../../util/compatibility"; import {objEquals} from "../../../util/functions"; import {fetchPost} from "../../../util/fetch"; import {focusBlock, focusByRange} from "../../util/selection"; import * as dayjs from "dayjs"; import {unicode2Emoji} from "../../../emoji"; import {getColIconByType} from "./col"; import {genAVValueHTML} from "./blockAttr"; import {Constants} from "../../../constants"; import {hintRef} from "../../hint/extend"; export const getCellText = (cellElement: HTMLElement | false) => { if (!cellElement) { return ""; } let cellText = ""; const textElement = cellElement.querySelector(".av__celltext"); if (textElement) { if (textElement.querySelector(".av__cellicon")) { cellText = `${textElement.firstChild.textContent} → ${textElement.lastChild.textContent}`; } else { cellText = textElement.textContent; } } else { cellText = cellElement.textContent; } return cellText; }; export const genCellValueByElement = (colType: TAVCol, cellElement: HTMLElement) => { const cellValue: IAVCellValue = { type: colType, id: cellElement.dataset.id, }; if (colType === "number") { const value = cellElement.querySelector(".av__celltext").getAttribute("data-content"); cellValue.number = { content: parseFloat(value) || 0, isNotEmpty: !!value }; } else if (["text", "block", "url", "phone", "email", "template"].includes(colType)) { cellValue[colType as "text"] = { content: cellElement.querySelector(".av__celltext").textContent.trim() }; } else if (colType === "mSelect" || colType === "select") { const mSelect: IAVCellSelectValue[] = []; cellElement.querySelectorAll(".b3-chip").forEach((item: HTMLElement) => { mSelect.push({ content: item.textContent.trim(), color: item.style.color.replace("var(--b3-font-color", "").replace(")", "") }); }); cellValue.mSelect = mSelect; } else if (["date", "created", "updated"].includes(colType)) { cellValue[colType as "date"] = JSON.parse(cellElement.querySelector(".av__celltext").getAttribute("data-value")); } else if (colType === "checkbox") { cellValue.checkbox = { checked: cellElement.querySelector("use").getAttribute("xlink:href") === "#iconCheck" ? true : false }; } else if (colType === "relation") { cellValue.relation = { blockIDs: Array.from(cellElement.querySelectorAll("span")).map((item: HTMLElement) => item.getAttribute("data-id")), contents: Array.from(cellElement.querySelectorAll("span")).map((item: HTMLElement) => item.textContent), }; } else if (colType === "mAsset") { const mAsset: IAVCellAssetValue[] = []; Array.from(cellElement.children).forEach((item) => { const isImg = item.classList.contains("av__cellassetimg"); mAsset.push({ type: isImg ? "image" : "file", content: isImg ? item.getAttribute("src") : item.getAttribute("data-url"), name: isImg ? "" : item.textContent }); }); cellValue.mAsset = mAsset; } if (colType === "block") { cellValue.isDetached = cellElement.dataset.detached === "true"; } return cellValue; }; export const genCellValue = (colType: TAVCol, value: string | any) => { let cellValue: IAVCellValue = { type: colType, [colType === "select" ? "mSelect" : colType]: value as IAVCellDateValue }; if (typeof value === "string" && value && colType !== "mAsset") { if (colType === "number") { cellValue = { type: colType, number: { content: parseFloat(value) || 0, isNotEmpty: true } }; } else if (["text", "block", "url", "phone", "email", "template"].includes(colType)) { cellValue = { type: colType, [colType]: { content: value } }; } else if (colType === "mSelect" || colType === "select") { cellValue = { type: colType, mSelect: [{ content: value, color: "1" }] }; } else if (colType === "checkbox") { cellValue = { type: colType, checkbox: { checked: true } }; } else if (colType === "relation") { cellValue = { type: colType, relation: {blockIDs: [], contents: [value]} }; } } else if (typeof value === "undefined" || !value) { if (colType === "number") { cellValue = { type: colType, number: { content: 0, isNotEmpty: true } }; } else if (["text", "block", "url", "phone", "email", "template"].includes(colType)) { cellValue = { type: colType, [colType]: { content: "" } }; } else if (colType === "mSelect" || colType === "select" || colType === "mAsset") { cellValue = { type: colType, [colType === "select" ? "mSelect" : colType]: [] }; } else if (["date", "created", "updated"].includes(colType)) { cellValue = { type: colType, [colType]: { content: null, isNotEmpty: false, content2: null, isNotEmpty2: false, hasEndDate: false, isNotTime: true, } }; } else if (colType === "checkbox") { cellValue = { type: colType, checkbox: { checked: false } }; } else if (colType === "relation") { cellValue = { type: colType, relation: {blockIDs: [], contents: []} }; } } if (colType === "block") { cellValue.isDetached = true; } return cellValue; }; export const cellScrollIntoView = (blockElement: HTMLElement, cellElement: Element, onlyHeight = true) => { const cellRect = cellElement.getBoundingClientRect(); if (!onlyHeight) { const avScrollElement = blockElement.querySelector(".av__scroll"); if (avScrollElement) { const avScrollRect = avScrollElement.getBoundingClientRect(); if (avScrollRect.right < cellRect.right) { avScrollElement.scrollLeft = avScrollElement.scrollLeft + cellRect.right - avScrollRect.right; } else { const rowElement = hasClosestByClassName(cellElement, "av__row"); if (rowElement) { const stickyElement = rowElement.querySelector(".av__colsticky"); if (stickyElement) { const stickyRight = stickyElement.getBoundingClientRect().right; if (stickyRight > cellRect.left) { avScrollElement.scrollLeft = avScrollElement.scrollLeft + cellRect.left - stickyRight; } } else if (avScrollRect.left > cellRect.left) { avScrollElement.scrollLeft = avScrollElement.scrollLeft + cellRect.left - avScrollRect.left; } } } } } if (!blockElement.querySelector(".av__header")) { // 属性面板 return; } const avHeaderRect = blockElement.querySelector(".av__row--header").getBoundingClientRect(); if (avHeaderRect.bottom > cellRect.top) { const contentElement = hasClosestByClassName(blockElement, "protyle-content", true); if (contentElement) { contentElement.scrollTop = contentElement.scrollTop + cellRect.top - avHeaderRect.bottom; } } else { const avFooterRect = blockElement.querySelector(".av__row--footer").getBoundingClientRect(); if (avFooterRect.top < cellRect.bottom) { const contentElement = hasClosestByClassName(blockElement, "protyle-content", true); if (contentElement) { contentElement.scrollTop = contentElement.scrollTop + cellRect.bottom - avFooterRect.top; } } } }; export const getTypeByCellElement = (cellElement: Element) => { const scrollElement = hasClosestByClassName(cellElement, "av__scroll"); if (!scrollElement) { return; } return scrollElement.querySelector(".av__row--header").querySelector(`[data-col-id="${cellElement.getAttribute("data-col-id")}"]`).getAttribute("data-dtype") as TAVCol; }; export const popTextCell = (protyle: IProtyle, cellElements: HTMLElement[], type?: TAVCol) => { if (cellElements.length === 0 || (cellElements.length === 1 && !cellElements[0])) { return; } if (!type) { type = getTypeByCellElement(cellElements[0]); } if (type === "updated" || type === "created" || document.querySelector(".av__mask")) { return; } if (type === "block" && (cellElements.length > 1 || !cellElements[0].getAttribute("data-detached"))) { return; } const blockElement = hasClosestBlock(cellElements[0]); let cellRect = cellElements[0].getBoundingClientRect(); if (blockElement) { /// #if MOBILE const contentElement = hasClosestByClassName(blockElement, "protyle-content", true); if (contentElement) { contentElement.scrollTop = contentElement.scrollTop + cellRect.top - 110; } /// #else cellScrollIntoView(blockElement, cellElements[0], false); /// #endif } cellRect = cellElements[0].getBoundingClientRect(); let html = ""; const style = `style="padding-top: 6.5px;position:absolute;left: ${cellRect.left}px;top: ${cellRect.top}px;width:${Math.max(cellRect.width, 25)}px;height: ${cellRect.height}px"`; if (["text", "url", "email", "phone", "block", "template"].includes(type)) { html = ``; } else if (type === "number") { html = ``; } else if (blockElement) { if (["select", "mSelect"].includes(type)) { openMenuPanel({protyle, blockElement, type: "select", cellElements}); } else if (type === "mAsset") { openMenuPanel({protyle, blockElement, type: "asset", cellElements}); focusBlock(blockElement); } else if (type === "date") { openMenuPanel({protyle, blockElement, type: "date", cellElements}); } else if (type === "checkbox") { updateCellValueByInput(protyle, type, cellElements); } else if (type === "relation") { openMenuPanel({protyle, blockElement, type: "relation", cellElements}); } else if (type === "rollup") { openMenuPanel({protyle, blockElement, type: "rollup", cellElements, colId: cellElements[0].dataset.colId}); } if (!hasClosestByClassName(cellElements[0], "custom-attr")) { cellElements[0].classList.add("av__cell--select"); } return; } window.siyuan.menus.menu.remove(); document.body.insertAdjacentHTML("beforeend", `
${html}
`); const avMaskElement = document.querySelector(".av__mask"); const inputElement = avMaskElement.querySelector(".b3-text-field") as HTMLInputElement; if (inputElement) { inputElement.select(); inputElement.focus(); if (blockElement && type === "template") { fetchPost("/api/av/renderAttributeView", {id: blockElement.dataset.avId}, (response) => { response.data.view.columns.find((item: IAVColumn) => { if (item.id === cellElements[0].dataset.colId) { inputElement.value = item.template; inputElement.dataset.template = item.template; return true; } }); }); } if (type === "block") { inputElement.addEventListener("input", (event: InputEvent) => { if (Constants.BLOCK_HINT_KEYS.includes(inputElement.value.substring(0, 2))) { protyle.toolbar.range = document.createRange(); protyle.toolbar.range.selectNodeContents(cellElements[0].lastChild); focusByRange(protyle.toolbar.range); hintRef(inputElement.value.substring(2), protyle, "av"); avMaskElement?.remove(); event.preventDefault(); event.stopPropagation(); } }); } inputElement.addEventListener("keydown", (event) => { if (event.isComposing) { return; } if (event.key === "Escape" || event.key === "Tab" || (event.key === "Enter" && !event.shiftKey && isNotCtrl(event))) { updateCellValueByInput(protyle, type, cellElements); if (event.key === "Tab") { protyle.wysiwyg.element.dispatchEvent(new KeyboardEvent("keydown", { shiftKey: event.shiftKey, ctrlKey: event.ctrlKey, altKey: event.altKey, metaKey: event.metaKey, key: "Tab", keyCode: 9 })); } event.preventDefault(); event.stopPropagation(); } }); } avMaskElement.addEventListener("click", (event) => { if ((event.target as HTMLElement).classList.contains("av__mask")) { updateCellValueByInput(protyle, type, cellElements); avMaskElement?.remove(); } }); }; const updateCellValueByInput = (protyle: IProtyle, type: TAVCol, cellElements: HTMLElement[]) => { const rowElement = hasClosestByClassName(cellElements[0], "av__row"); if (!rowElement) { return; } if (!document.contains(cellElements[0]) && cellElements.length === 1) { // 原始 cell 已被更新 const avid = rowElement.dataset.avid; if (avid) { // 新增行后弹出的输入框 const previousId = rowElement.dataset.previousId; cellElements[0] = protyle.wysiwyg.element.querySelector(previousId ? `[data-av-id="${avid}"] .av__row[data-id="${previousId}"]` : `[data-av-id="${avid}"] .av__row--header`).nextElementSibling.querySelector('[data-detached="true"]'); } else { // 修改单元格后立即修改其他单元格 let tempElement = protyle.wysiwyg.element.querySelector(`.av__cell[data-id="${cellElements[0].dataset.id}"]`) as HTMLElement; if (!tempElement) { // 修改单元格后修改其他没有内容的单元格(id 会随机) tempElement = protyle.wysiwyg.element.querySelector(`.av__row[data-id="${rowElement.dataset.id}"] .av__cell[data-col-id="${cellElements[0].dataset.colId}"]`) as HTMLElement; } if (!tempElement) { return; } cellElements[0] = tempElement; } } if (cellElements.length === 1 && cellElements[0].dataset.detached === "true" && !rowElement.dataset.id) { return; } const blockElement = hasClosestBlock(cellElements[0]); if (!blockElement) { return; } const avMaskElement = document.querySelector(".av__mask"); const doOperations: IOperation[] = []; const undoOperations: IOperation[] = []; const avID = blockElement.getAttribute("data-av-id"); const id = blockElement.getAttribute("data-node-id"); if (type === "template") { const colId = cellElements[0].getAttribute("data-col-id"); const textElement = avMaskElement.querySelector(".b3-text-field") as HTMLInputElement; if (textElement.value !== textElement.dataset.template) { transaction(protyle, [{ action: "updateAttrViewColTemplate", id: colId, avID, data: textElement.value, type: "template", }], [{ action: "updateAttrViewColTemplate", id: colId, avID, data: textElement.dataset.template, type: "template", }]); } } else { cellElements.forEach((item) => { const rowElement = hasClosestByClassName(item, "av__row"); if (!rowElement) { return; } const rowID = rowElement.getAttribute("data-id"); const cellId = item.getAttribute("data-id"); const colId = item.getAttribute("data-col-id"); const inputValue: { content?: string | number, isNotEmpty?: boolean, checked?: boolean, } = {}; const oldValue: { content?: string | number, isNotEmpty?: boolean, checked?: boolean, } = {}; if (type === "number") { oldValue.content = parseFloat(item.textContent.trim()); oldValue.isNotEmpty = typeof oldValue.content === "number" && !isNaN(oldValue.content); inputValue.content = parseFloat((avMaskElement.querySelector(".b3-text-field") as HTMLInputElement).value); inputValue.isNotEmpty = typeof inputValue.content === "number" && !isNaN(inputValue.content); if (!inputValue.isNotEmpty) { // 后端仅支持传入数字,因此在为空的时候需要设置为 0 inputValue.content = 0; } } else if (type === "checkbox") { const useElement = item.querySelector("use"); inputValue.checked = useElement.getAttribute("xlink:href") === "#iconUncheck"; oldValue.checked = !inputValue.checked; useElement.setAttribute("xlink:href", inputValue.checked ? "#iconCheck" : "#iconUncheck"); } else { inputValue.content = (avMaskElement.querySelector(".b3-text-field") as HTMLInputElement).value; oldValue.content = type === "block" ? item.firstElementChild.textContent.trim() : item.textContent.trim(); } if (objEquals(inputValue, oldValue)) { return; } doOperations.push({ action: "updateAttrViewCell", id: cellId, avID, keyID: colId, rowID, data: { [type]: inputValue, isDetached: true, } }, { action: "doUpdateUpdated", id, data: dayjs().format("YYYYMMDDHHmmss"), }); undoOperations.push({ action: "updateAttrViewCell", id: cellId, avID, keyID: colId, rowID, data: { [type]: oldValue, isDetached: true, } }, { action: "doUpdateUpdated", id, data: blockElement.getAttribute("updated"), }); if (!hasClosestByClassName(cellElements[0], "custom-attr")) { updateAttrViewCellAnimation(item, { [type]: inputValue, isDetached: true, type }); } }); } if (doOperations.length > 0) { transaction(protyle, doOperations, undoOperations); } if (!hasClosestByClassName(cellElements[0], "custom-attr")) { cellElements[0].classList.add("av__cell--select"); } if (blockElement && // 单元格编辑中 ctrl+p 光标定位 !document.querySelector(".b3-dialog")) { focusBlock(blockElement); } document.querySelectorAll(".av__mask").forEach((item) => { item.remove(); }); }; export const updateCellsValue = (protyle: IProtyle, nodeElement: HTMLElement, value?: any, cElements?: HTMLElement[]) => { const doOperations: IOperation[] = []; const undoOperations: IOperation[] = []; const avID = nodeElement.dataset.avId; const id = nodeElement.dataset.nodeId; let text = ""; let cellElements: Element[]; if (cElements?.length > 0) { cellElements = cElements; } else { cellElements = Array.from(nodeElement.querySelectorAll(".av__cell--select")); if (cellElements.length === 0) { nodeElement.querySelectorAll(".av__row--select:not(.av__row--header)").forEach(rowElement => { rowElement.querySelectorAll(".av__cell").forEach(cellElement => { cellElements.push(cellElement); }); }); } } cellElements.forEach((item: HTMLElement, elementIndex) => { if (!nodeElement.contains(item)) { item = cellElements[elementIndex] = nodeElement.querySelector(`.av__cell[data-id="${item.dataset.id}"]`) as HTMLElement; } const rowElement = hasClosestByClassName(item, "av__row"); if (!rowElement) { return; } const type = getTypeByCellElement(item) || item.dataset.type as TAVCol; if (["created", "updated", "template"].includes(type)) { return; } const rowID = rowElement.getAttribute("data-id"); const cellId = item.getAttribute("data-id"); const colId = item.getAttribute("data-col-id"); text += getCellText(item); const oldValue = genCellValueByElement(type, item); // relation 为全部更新,以下类型为添加 if (type === "mAsset") { if (Array.isArray(value)) { value = oldValue.mAsset.concat(value); } else if (typeof value !== "undefined") { // 不传入为删除,传入字符串不进行处理 return; } } else if (type === "mSelect") { // 不传入为删除 if (typeof value === "string") { value = oldValue.mSelect.concat({ content: value, color: (oldValue.mSelect.length + 1).toString() }); } } const cellValue = genCellValue(type, value); doOperations.push({ action: "updateAttrViewCell", id: cellId, avID, keyID: colId, rowID, data: cellValue }); undoOperations.push({ action: "updateAttrViewCell", id: cellId, avID, keyID: colId, rowID, data: oldValue }); if (!hasClosestByClassName(cellElements[0], "custom-attr")) { updateAttrViewCellAnimation(item, cellValue); } else { item.innerHTML = genAVValueHTML(cellValue); } }); if (doOperations.length > 0) { doOperations.push({ action: "doUpdateUpdated", id, data: dayjs().format("YYYYMMDDHHmmss"), }); undoOperations.push({ action: "doUpdateUpdated", id, data: nodeElement.getAttribute("updated"), }); transaction(protyle, doOperations, undoOperations); } return text; }; export const renderCell = (cellValue: IAVCellValue, wrap: boolean) => { let text = ""; if (["text", "template"].includes(cellValue.type)) { text = `${cellValue ? (cellValue[cellValue.type as "text"].content || "") : ""}`; } else if (["url", "email", "phone"].includes(cellValue.type)) { const urlContent = cellValue ? cellValue[cellValue.type as "url"].content : ""; // https://github.com/siyuan-note/siyuan/issues/9291 let urlAttr = ""; if (cellValue.type === "url") { urlAttr = ` data-href="${urlContent}"`; } text = `${urlContent}`; } else if (cellValue.type === "block") { if (cellValue?.isDetached) { text = `${cellValue.block.content || ""} ${window.siyuan.languages.more}`; } else { text = `${cellValue.block.content || ""} ${window.siyuan.languages.update}`; } } else if (cellValue.type === "number") { text = `${cellValue?.number.formattedContent || cellValue?.number.content || ""}`; } else if (cellValue.type === "mSelect" || cellValue.type === "select") { cellValue?.mSelect?.forEach((item) => { text += `${item.content}`; }); } else if (cellValue.type === "date") { const dataValue = cellValue ? cellValue.date : null; text = ``; if (dataValue && dataValue.isNotEmpty) { text += dayjs(dataValue.content).format(dataValue.isNotTime ? "YYYY-MM-DD" : "YYYY-MM-DD HH:mm"); } if (dataValue && dataValue.hasEndDate && dataValue.isNotEmpty && dataValue.isNotEmpty2) { text += `${dayjs(dataValue.content2).format(dataValue.isNotTime ? "YYYY-MM-DD" : "YYYY-MM-DD HH:mm")}`; } text += ""; } else if (["created", "updated"].includes(cellValue.type)) { const dataValue = cellValue ? cellValue[cellValue.type as "date"] : null; text = ``; if (dataValue && dataValue.isNotEmpty) { text += dayjs(dataValue.content).format("YYYY-MM-DD HH:mm"); } text += ""; } else if (cellValue.type === "mAsset") { cellValue?.mAsset?.forEach((item) => { if (item.type === "image") { text += ``; } else { text += `${item.name}`; } }); } else if (cellValue.type === "checkbox") { text += ``; } else if (cellValue.type === "rollup") { cellValue?.rollup?.contents?.forEach((item, index) => { const rollupText = ["select", "mSelect", "mAsset", "checkbox", "relation"].includes(item.type) ? renderCell(item, wrap) : renderRollup(item); if (!rollupText && text) { text = text.substring(0, text.length - 2); } else { text += rollupText + ((index === cellValue.rollup.contents.length - 1 || !rollupText) ? "" : ", "); } }); } else if (cellValue.type === "relation") { cellValue?.relation?.contents?.forEach((item, index) => { text += `${item}`; }); } if (["text", "template", "url", "email", "phone", "number", "date", "created", "updated"].includes(cellValue.type) && cellValue && cellValue[cellValue.type as "url"].content) { text += ``; } return text; }; const renderRollup = (cellValue: IAVCellValue) => { let text = ""; if (["text"].includes(cellValue.type)) { text = cellValue ? (cellValue[cellValue.type as "text"].content || "") : ""; } else if (["url", "email", "phone"].includes(cellValue.type)) { const urlContent = cellValue ? cellValue[cellValue.type as "url"].content : ""; if (urlContent) { let urlAttr = ""; if (cellValue.type === "url") { urlAttr = ` data-href="${urlContent}"`; } text = `${urlContent}`; } } else if (cellValue.type === "block") { if (cellValue?.isDetached) { text = `${cellValue.block?.content || ""}`; } else { text = `${cellValue.block?.content || ""}`; } } else if (cellValue.type === "number") { text = cellValue?.number.formattedContent || cellValue?.number.content.toString() || ""; } else if (cellValue.type === "date") { const dataValue = cellValue ? cellValue.date : null; if (dataValue && dataValue.isNotEmpty) { text += dayjs(dataValue.content).format(dataValue.isNotTime ? "YYYY-MM-DD" : "YYYY-MM-DD HH:mm"); } if (dataValue && dataValue.hasEndDate && dataValue.isNotEmpty && dataValue.isNotEmpty2) { text += `${dayjs(dataValue.content2).format(dataValue.isNotTime ? "YYYY-MM-DD" : "YYYY-MM-DD HH:mm")}`; } if (text) { text = `${text}`; } } return text; }; export const updateHeaderCell = (cellElement: HTMLElement, headerValue: { icon?: string, name?: string, pin?: boolean, }) => { if (typeof headerValue.icon !== "undefined") { cellElement.dataset.icon = headerValue.icon; cellElement.querySelector(".av__cellheadericon").outerHTML = headerValue.icon ? unicode2Emoji(headerValue.icon, "av__cellheadericon", true) : ``; } if (typeof headerValue.name !== "undefined") { cellElement.querySelector(".av__celltext").textContent = headerValue.name; } if (typeof headerValue.pin !== "undefined") { const textElement = cellElement.querySelector(".av__celltext"); if (headerValue.pin) { if (!textElement.nextElementSibling) { textElement.insertAdjacentHTML("afterend", '
'); } } else { if (textElement.nextElementSibling) { textElement.nextElementSibling.nextElementSibling.remove(); textElement.nextElementSibling.remove(); } } } };