diff --git a/app/src/assets/scss/component/_menu.scss b/app/src/assets/scss/component/_menu.scss index 6bc11d7ec..dd4668a6f 100644 --- a/app/src/assets/scss/component/_menu.scss +++ b/app/src/assets/scss/component/_menu.scss @@ -154,6 +154,7 @@ color: var(--b3-theme-on-background); position: relative; box-sizing: border-box; + margin: 1px 0; &[disabled="disabled"] { cursor: not-allowed; diff --git a/app/src/protyle/render/av/cell.ts b/app/src/protyle/render/av/cell.ts index 3ab8b3783..2f6966b4b 100644 --- a/app/src/protyle/render/av/cell.ts +++ b/app/src/protyle/render/av/cell.ts @@ -1,15 +1,20 @@ import {transaction} from "../../wysiwyg/transaction"; import {hasClosestBlock, hasClosestByClassName} from "../../util/hasClosest"; +import {openMenuPanel} from "./openMenuPanel"; export const popTextCell = (protyle: IProtyle, cellElement: HTMLElement) => { - const type = cellElement.parentElement.parentElement.firstElementChild.children[parseInt(cellElement.getAttribute("data-index")) + 1].getAttribute("data-dtype") as TAVCol; + const type = cellElement.parentElement.parentElement.firstElementChild.querySelector(`[data-col-id="${cellElement.getAttribute("data-col-id")}"]`).getAttribute("data-dtype") as TAVCol; const cellRect = cellElement.getBoundingClientRect(); let html = ""; const style = `style="position:absolute;left: ${cellRect.left}px;top: ${cellRect.top}px;width:${Math.max(cellRect.width, 200)}px;height: ${cellRect.height}px"` + const blockElement = hasClosestBlock(cellElement); if (type === "block" || type === "text") { html = ``; } else if (type === "number") { html = ``; + } else if (type === "select" && blockElement) { + openMenuPanel(protyle, blockElement, "select", {cellElement}); + return; } document.body.insertAdjacentHTML("beforeend", `
${html} diff --git a/app/src/protyle/render/av/col.ts b/app/src/protyle/render/av/col.ts index b7996538d..fb8b4fc14 100644 --- a/app/src/protyle/render/av/col.ts +++ b/app/src/protyle/render/av/col.ts @@ -50,20 +50,21 @@ export const updateHeader = (rowElement: HTMLElement) => { avHeadElement.style.position = "sticky"; }; -const removeCol = (cellElement: HTMLElement) => { - const index = cellElement.getAttribute("data-index"); +const removeCol = (cellElement:HTMLElement) => { const blockElement = hasClosestBlock(cellElement); if (!blockElement) { return false; } + const colId = cellElement.getAttribute("data-col-id"); blockElement.querySelectorAll(".av__row").forEach((item) => { - item.querySelector(`[data-index="${index}"]`).remove(); + item.querySelector(`[data-col-id="${colId}"]`).remove(); }); + cellElement.remove(); }; export const showColMenu = (protyle: IProtyle, blockElement: HTMLElement, cellElement: HTMLElement) => { const type = cellElement.getAttribute("data-dtype") as TAVCol; - const colId = cellElement.getAttribute("data-id"); + const colId = cellElement.getAttribute("data-col-id"); const avId = blockElement.getAttribute("data-av-id"); const menu = new Menu("av-header-cell", () => { const newValue = (window.siyuan.menus.menu.element.querySelector(".b3-text-field") as HTMLInputElement).value; diff --git a/app/src/protyle/render/av/filter.ts b/app/src/protyle/render/av/filter.ts new file mode 100644 index 000000000..754a40761 --- /dev/null +++ b/app/src/protyle/render/av/filter.ts @@ -0,0 +1,222 @@ +import {Menu} from "../../../plugin/Menu"; +import {transaction} from "../../wysiwyg/transaction"; +import {hasClosestByClassName} from "../../util/hasClosest"; +import {getColIconByType} from "./col"; +import {setPosition} from "../../../util/setPosition"; + +export const setFilter = (options: { + filter: IAVFilter, + protyle: IProtyle, + data: IAV, + target: HTMLElement, +}) => { + const colType = Object.keys(options.filter.value)[0] as TAVCol; + const rectTarget = options.target.getBoundingClientRect(); + const menu = new Menu("set-filter-" + options.filter.column, () => { + const oldFilters = JSON.parse(JSON.stringify(options.data.filters)); + options.data.filters.find((filter) => { + if (filter.column === options.filter.column) { + let cellValue: IAVCellValue; + if (colType === "number") { + if (textElement.value) { + cellValue = { + content: parseFloat(textElement.value), + isNotEmpty: true + } + } else { + cellValue = {} + } + } else { + cellValue = { + content: textElement.value + } + } + filter.value[colType] = cellValue; + filter.operator = (window.siyuan.menus.menu.element.querySelector(".b3-select") as HTMLSelectElement).value as TAVFilterOperator; + return true; + } + }); + transaction(options.protyle, [{ + action: "setAttrView", + id: options.data.id, + data: { + filters: options.data.filters + } + }], [{ + action: "setAttrView", + id: options.data.id, + data: { + filters: oldFilters + } + }]); + const menuElement = hasClosestByClassName(options.target, "b3-menu"); + if (menuElement) { + menuElement.innerHTML = getFiltersHTML(options.data); + } + }); + if (menu.isOpen) { + return; + } + let selectHTML = ""; + switch (colType) { + case "text": + selectHTML = ` + + + + + + + +`; + break; + case "number": + selectHTML = ` + + + + + + + +`; + break; + } + menu.addItem({ + iconHTML: "", + label: `` + }); + menu.addItem({ + iconHTML: "", + label: `` + }); + const textElement = (window.siyuan.menus.menu.element.querySelector(".b3-text-field") as HTMLInputElement); + textElement.addEventListener("keydown", (event) => { + if (event.isComposing) { + event.preventDefault(); + return; + } + if (event.key === "Enter") { + menu.close(); + event.preventDefault(); + } + }); + menu.open({x: rectTarget.left, y: rectTarget.bottom}); + textElement.select(); +}; + +export const addFilter = (options: { + data: IAV, + rect: DOMRect, + menuElement: HTMLElement, + tabRect: DOMRect, + avId: string, + protyle: IProtyle +}) => { + const menu = new Menu("av-add-filter"); + options.data.columns.forEach((column) => { + let hasFilter = false; + options.data.filters.find((filter) => { + if (filter.column === column.id) { + hasFilter = true; + return true; + } + }); + if (!hasFilter) { + menu.addItem({ + label: column.name, + icon: getColIconByType(column.type), + click: () => { + const oldFilters = Object.assign([], options.data.filters); + let cellValue = {} + if (column.type !== "number") { + cellValue = {content: ""} + } + options.data.filters.push({ + column: column.id, + operator: "Contains", + value: { + [column.type]: cellValue + }, + }); + transaction(options.protyle, [{ + action: "setAttrView", + id: options.avId, + data: { + filters: options.data.filters + } + }], [{ + action: "setAttrView", + id: options.avId, + data: { + filters: oldFilters + } + }]); + options.menuElement.innerHTML = getFiltersHTML(options.data); + setPosition(options.menuElement, options.tabRect.right - options.menuElement.clientWidth, options.tabRect.bottom, options.tabRect.height); + const filterElement = options.menuElement.querySelector(`[data-id="${column.id}"] .b3-chip`) as HTMLElement; + const colType = filterElement.getAttribute("data-coltype") as TAVCol; + setFilter({ + filter: { + operator: filterElement.dataset.op as TAVFilterOperator, + column: filterElement.parentElement.parentElement.dataset.id, + value: { + [colType]: {content: filterElement.dataset.value} + } + }, + protyle: options.protyle, + data: options.data, + target: filterElement + }); + } + }); + } + }); + menu.open({ + x: options.rect.left, + y: options.rect.bottom, + h: options.rect.height, + }); +}; + +export const getFiltersHTML = (data: IAV) => { + let html = ""; + const genFilterItem = (filter: IAVFilter) => { + let filterHTML = ""; + data.columns.find((item) => { + if (item.id === filter.column) { + const filterValue = (filter.value && filter.value[item.type] && filter.value[item.type].content) ? filter.value[item.type].content : ""; + filterHTML += ` + + ${item.name}${filterValue ? ": " + filterValue : ""} +`; + return true; + } + }); + return filterHTML; + }; + data.filters.forEach((item: IAVFilter) => { + html += ``; + }); + return ` + +${html} + +`; +}; diff --git a/app/src/protyle/render/av/openMenuPanel.ts b/app/src/protyle/render/av/openMenuPanel.ts index 07a93fc01..b0ee488a4 100644 --- a/app/src/protyle/render/av/openMenuPanel.ts +++ b/app/src/protyle/render/av/openMenuPanel.ts @@ -3,10 +3,15 @@ import {fetchPost} from "../../../util/fetch"; import {addCol} from "./addCol"; import {getColIconByType} from "./col"; import {setPosition} from "../../../util/setPosition"; -import {Menu} from "../../../plugin/Menu"; -import {hasClosestByAttribute, hasClosestByClassName} from "../../util/hasClosest"; +import {hasClosestByAttribute} from "../../util/hasClosest"; +import {bindSelectEvent, getSelectHTML, setSelectOption} from "./select"; +import {addFilter, getFiltersHTML} from "./filter"; +import {addSort, bindSortsEvent, getSortsHTML} from "./sort"; -export const openMenuPanel = (protyle: IProtyle, blockElement: HTMLElement, type: "properties" | "config" | "sorts" | "filters" = "config") => { +export const openMenuPanel = (protyle: IProtyle, + blockElement: HTMLElement, + type: "select" | "properties" | "config" | "sorts" | "filters" = "config", + options?: any) => { let avPanelElement = document.querySelector(".av__panel"); if (avPanelElement) { avPanelElement.remove(); @@ -25,16 +30,26 @@ export const openMenuPanel = (protyle: IProtyle, blockElement: HTMLElement, type html = getSortsHTML(data); } else if (type === "filters") { html = getFiltersHTML(data); + } else if (type === "select") { + html = getSelectHTML(data, options); } + document.body.insertAdjacentHTML("beforeend", `
${html}
`); - avPanelElement = document.querySelector(".av__panel"); const menuElement = avPanelElement.lastElementChild as HTMLElement; const tabRect = blockElement.querySelector(".layout-tab-bar").getBoundingClientRect(); - setPosition(menuElement, tabRect.right - menuElement.clientWidth, tabRect.bottom, tabRect.height); + if (options && options.cellElement) { + const cellRect = options.cellElement.getBoundingClientRect(); + setPosition(menuElement, cellRect.left, cellRect.bottom, cellRect.height); + bindSelectEvent(protyle, data, menuElement, options); + menuElement.querySelector("input").select(); + menuElement.querySelector("input").focus(); + } else { + setPosition(menuElement, tabRect.right - menuElement.clientWidth, tabRect.bottom, tabRect.height); + } bindSortsEvent(protyle, menuElement, data); avPanelElement.addEventListener("dragstart", (event) => { @@ -155,7 +170,6 @@ export const openMenuPanel = (protyle: IProtyle, blockElement: HTMLElement, type } dragoverElement = targetElement; }); - avPanelElement.addEventListener("dragleave", (event) => { const target = event.target as HTMLElement; const targetElement = hasClosestByAttribute(target, "data-id", null); @@ -169,6 +183,7 @@ export const openMenuPanel = (protyle: IProtyle, blockElement: HTMLElement, type window.siyuan.dragElement = undefined; } }); + avPanelElement.addEventListener("click", (event) => { event.preventDefault(); let target = event.target as HTMLElement; @@ -416,6 +431,10 @@ export const openMenuPanel = (protyle: IProtyle, blockElement: HTMLElement, type setPosition(menuElement, tabRect.right - menuElement.clientWidth, tabRect.bottom, tabRect.height); event.stopPropagation(); break; + } else if (type === "editOption") { + setSelectOption(protyle, data, options, target) + event.stopPropagation(); + break; } target = target.parentElement; } @@ -423,358 +442,12 @@ export const openMenuPanel = (protyle: IProtyle, blockElement: HTMLElement, type }); }; -const addSort = (options: { - data: IAV, - rect: DOMRect, - menuElement: HTMLElement, - tabRect: DOMRect, - avId: string, - protyle: IProtyle -}) => { - const menu = new Menu("av-add-sort"); - options.data.columns.forEach((column) => { - let hasSort = false; - options.data.sorts.find((sort) => { - if (sort.column === column.id) { - hasSort = true; - return true; - } - }); - if (!hasSort) { - menu.addItem({ - label: column.name, - icon: getColIconByType(column.type), - click: () => { - const oldSorts = Object.assign([], options.data.sorts); - options.data.sorts.push({ - column: column.id, - order: "ASC", - }); - transaction(options.protyle, [{ - action: "setAttrView", - id: options.avId, - data: { - sorts: options.data.sorts - } - }], [{ - action: "setAttrView", - id: options.avId, - data: { - sorts: oldSorts - } - }]); - options.menuElement.innerHTML = getSortsHTML(options.data); - bindSortsEvent(options.protyle, options.menuElement, options.data); - setPosition(options.menuElement, options.tabRect.right - options.menuElement.clientWidth, options.tabRect.bottom, options.tabRect.height); - } - }); - } - }); - menu.open({ - x: options.rect.left, - y: options.rect.bottom, - h: options.rect.height, - }); -}; - -const bindSortsEvent = (protyle: IProtyle, menuElement: HTMLElement, data: IAV) => { - menuElement.querySelectorAll("select").forEach((item: HTMLSelectElement) => { - item.addEventListener("change", () => { - const colId = item.parentElement.getAttribute("data-id"); - const oldSort = JSON.parse(JSON.stringify(data.sorts)); - if (item.previousElementSibling.classList.contains("b3-menu__icon")) { - data.sorts.find((sort: IAVSort) => { - if (sort.column === colId) { - sort.column = item.value; - item.parentElement.setAttribute("data-id", item.value); - return true; - } - }); - } else { - data.sorts.find((sort: IAVSort) => sort.column === colId).order = item.value as "ASC" | "DESC"; - } - transaction(protyle, [{ - action: "setAttrView", - id: data.id, - data: { - sorts: data.sorts - } - }], [{ - action: "setAttrView", - id: data.id, - data: { - sorts: oldSort - } - }]); - }); - }); -}; - -const getSortsHTML = (data: IAV) => { - let html = ""; - const genSortItem = (id: string) => { - let sortHTML = ""; - data.columns.forEach((item) => { - sortHTML += ``; - }); - return sortHTML; - }; - data.sorts.forEach((item: IAVSort) => { - html += ``; - }); - return ` - -${html} - -`; -}; - -export const setFilter = (options: { - filter: IAVFilter, - protyle: IProtyle, - data: IAV, - target: HTMLElement, -}) => { - const colType = Object.keys(options.filter.value)[0] as TAVCol; - const rectTarget = options.target.getBoundingClientRect(); - const menu = new Menu("set-filter-" + options.filter.column, () => { - const oldFilters = JSON.parse(JSON.stringify(options.data.filters)); - options.data.filters.find((filter) => { - if (filter.column === options.filter.column) { - let cellValue: IAVCellValue; - if (colType === "number") { - if (textElement.value) { - cellValue = { - content: parseFloat(textElement.value), - isNotEmpty: true - } - } else { - cellValue = {} - } - } else { - cellValue = { - content: textElement.value - } - } - filter.value[colType] = cellValue; - filter.operator = (window.siyuan.menus.menu.element.querySelector(".b3-select") as HTMLSelectElement).value as TAVFilterOperator; - return true; - } - }); - transaction(options.protyle, [{ - action: "setAttrView", - id: options.data.id, - data: { - filters: options.data.filters - } - }], [{ - action: "setAttrView", - id: options.data.id, - data: { - filters: oldFilters - } - }]); - const menuElement = hasClosestByClassName(options.target, "b3-menu"); - if (menuElement) { - menuElement.innerHTML = getFiltersHTML(options.data); - } - }); - if (menu.isOpen) { - return; - } - let selectHTML = ""; - switch (colType) { - case "text": - selectHTML = ` - - - - - - - -`; - break; - case "number": - selectHTML = ` - - - - - - - -`; - break; - } - menu.addItem({ - iconHTML: "", - label: `` - }); - menu.addItem({ - iconHTML: "", - label: `` - }); - const textElement = (window.siyuan.menus.menu.element.querySelector(".b3-text-field") as HTMLInputElement); - textElement.addEventListener("keydown", (event) => { - if (event.isComposing) { - event.preventDefault(); - return; - } - if (event.key === "Enter") { - menu.close(); - event.preventDefault(); - } - }); - menu.open({x: rectTarget.left, y: rectTarget.bottom}); - textElement.select(); -}; - -const addFilter = (options: { - data: IAV, - rect: DOMRect, - menuElement: HTMLElement, - tabRect: DOMRect, - avId: string, - protyle: IProtyle -}) => { - const menu = new Menu("av-add-filter"); - options.data.columns.forEach((column) => { - let hasFilter = false; - options.data.filters.find((filter) => { - if (filter.column === column.id) { - hasFilter = true; - return true; - } - }); - if (!hasFilter) { - menu.addItem({ - label: column.name, - icon: getColIconByType(column.type), - click: () => { - const oldFilters = Object.assign([], options.data.filters); - let cellValue = {} - if (column.type !== "number") { - cellValue = {content: ""} - } - options.data.filters.push({ - column: column.id, - operator: "Contains", - value: { - [column.type]: cellValue - }, - }); - transaction(options.protyle, [{ - action: "setAttrView", - id: options.avId, - data: { - filters: options.data.filters - } - }], [{ - action: "setAttrView", - id: options.avId, - data: { - filters: oldFilters - } - }]); - options.menuElement.innerHTML = getFiltersHTML(options.data); - setPosition(options.menuElement, options.tabRect.right - options.menuElement.clientWidth, options.tabRect.bottom, options.tabRect.height); - const filterElement = options.menuElement.querySelector(`[data-id="${column.id}"] .b3-chip`) as HTMLElement; - const colType = filterElement.getAttribute("data-coltype") as TAVCol; - setFilter({ - filter: { - operator: filterElement.dataset.op as TAVFilterOperator, - column: filterElement.parentElement.parentElement.dataset.id, - value: { - [colType]: {content: filterElement.dataset.value} - } - }, - protyle: options.protyle, - data: options.data, - target: filterElement - }); - } - }); - } - }); - menu.open({ - x: options.rect.left, - y: options.rect.bottom, - h: options.rect.height, - }); -}; - -const getFiltersHTML = (data: IAV) => { - let html = ""; - const genFilterItem = (filter: IAVFilter) => { - let filterHTML = ""; - data.columns.find((item) => { - if (item.id === filter.column) { - const filterValue = (filter.value && filter.value[item.type] && filter.value[item.type].content) ? filter.value[item.type].content : ""; - filterHTML += ` - - ${item.name}${filterValue ? ": " + filterValue : ""} -`; - return true; - } - }); - return filterHTML; - }; - data.filters.forEach((item: IAVFilter) => { - html += ``; - }); - return ` - -${html} - -`; -}; - const getPropertiesHTML = (data: IAV) => { let showHTML = ""; let hideHTML = ""; data.columns.forEach((item: IAVColumn) => { if (item.hidden) { - hideHTML += ``; } else { - showHTML += ``; + } + if (key === item.name) { + hasMatch = true; + } + }) + } + if (!hasMatch && key) { + const colorIndex = (options?.length || 0) % 13 + 1; + html += `` + } + return html; +} + +export const setSelectOption = (protyle: IProtyle, data: IAV, options: { + cellElement: HTMLElement; +}, target: HTMLElement,) => { + const name = target.parentElement.dataset.name; + const menu = new Menu("av-select-option", () => { + transaction(protyle, [{ + action: "updateAttrViewColOption", + id: colId, + parentID: data.id, + data: { + oldName: name, + newName: inputElement.value + }, + }], [{ + action: "updateAttrViewColOption", + id: colId, + parentID: data.id, + data: { + oldName: inputElement.value, + newName: name + }, + }]); + }) + if (menu.isOpen) { + return; + } + const menuElement = hasClosestByClassName(target, "b3-menu"); + if (!menuElement) { + return + } + const color = target.parentElement.dataset.color; + const colId = options.cellElement.dataset.colId; + menu.addItem({ + iconHTML: "", + label: `` + }) + menu.addItem({ + label: window.siyuan.languages.delete, + icon: "iconTrashcan", + click() { + confirmDialog(window.siyuan.languages.deleteOpConfirm, window.siyuan.languages.confirmDelete, () => { + let colOptions: { name: string, color: string }[] = [] + data.columns.find(column => { + if (column.id === colId) { + colOptions = column.options; + return true; + } + }) + const newName = target.parentElement.dataset.name + transaction(protyle, [{ + action: "removeAttrViewColOption", + id: colId, + parentID: data.id, + data: newName, + }], [{ + action: "updateAttrViewColOptions", + id: colId, + parentID: data.id, + data: colOptions + }]); + colOptions.find((item, index) => { + if (item.name === newName) { + colOptions.splice(index, 1); + return true; + } + }) + menuElement.innerHTML = getSelectHTML(data, options); + const cellRect = options.cellElement.getBoundingClientRect(); + setPosition(menuElement, cellRect.left, cellRect.bottom, cellRect.height); + bindSelectEvent(protyle, data, menuElement, options); + }) + } + }) + menu.addSeparator() + Array.from(Array(13).keys()).forEach(index => { + menu.addItem({ + current: parseInt(color) === index + 1, + iconHTML: "", + label: `A`, + click() { + transaction(protyle, [{ + action: "updateAttrViewColOption", + id: colId, + parentID: data.id, + data: { + oldColor: color, + newColor: (index + 1).toString() + }, + }], [{ + action: "updateAttrViewColOption", + id: colId, + parentID: data.id, + data: { + oldColor: (index + 1).toString(), + newColor: color + }, + }]); + } + }) + }) + const rect = target.getBoundingClientRect(); + menu.open({ + x: rect.left, + y: rect.bottom, + h: rect.height, + }) + const inputElement = window.siyuan.menus.menu.element.querySelector("input"); + inputElement.select(); +} + +export const bindSelectEvent = (protyle: IProtyle, data: IAV, menuElement: HTMLElement, options: { + cellElement: HTMLElement +}) => { + const inputElement = menuElement.querySelector("input"); + const rowId = options.cellElement.parentElement.dataset.id + const colId = options.cellElement.dataset.colId; + const cellId = options.cellElement.dataset.id + let colData: IAVColumn; + data.columns.find((item: IAVColumn) => { + if (item.id === colId) { + colData = item; + return; + } + }) + if (!colData.options) { + colData.options = []; + } + let cellData: IAVCell + data.rows.find(row => { + if (row.id === rowId) { + row.cells.find(cell => { + if (cell.id === cellId) { + cellData = cell + return true; + } + }) + return true; + } + }) + inputElement.addEventListener("input", (event: InputEvent) => { + if (event.isComposing) { + return + } + menuElement.lastElementChild.innerHTML = filterSelectHTML(inputElement.value, colData.options); + }) + inputElement.addEventListener("compositionend", (event: InputEvent) => { + if (event.isComposing) { + return + } + menuElement.lastElementChild.innerHTML = filterSelectHTML(inputElement.value, colData.options); + }) + inputElement.addEventListener("keydown", (event: KeyboardEvent) => { + if (event.isComposing) { + return + } + let currentElement = upDownHint(menuElement.lastElementChild, event, "b3-menu__item--current"); + if (event.key === "Enter") { + if (!currentElement) { + currentElement = menuElement.querySelector(".b3-menu__item--current"); + } + let oldValue + if (colData.type !== "select") { + oldValue = Object.assign([], cellData.value.mSelect.content); + cellData.value.mSelect.content.push({ + color: currentElement.dataset.color, + content: currentElement.dataset.name + }) + } else { + oldValue = Object.assign({}, cellData.value.select); + cellData.value.select = { + color: currentElement.dataset.color, + content: currentElement.dataset.name + } + } + if (currentElement.querySelector(".b3-menu__accelerator")) { + colData.options.push({ + color: currentElement.dataset.color, + name: currentElement.dataset.name + }) + transaction(protyle, [{ + action: "updateAttrViewColOptions", + id: colId, + parentID: data.id, + data: colData.options + }, { + action: "updateAttrViewCell", + id: cellId, + rowID: rowId, + parentID: data.id, + data: { + [colData.type]: cellData.value + } + }], [{ + action: "removeAttrViewColOption", + id: colId, + parentID: data.id, + data: currentElement.dataset.name, + }]); + } else { + transaction(protyle, [{ + action: "updateAttrViewCell", + id: cellId, + rowID: rowId, + parentID: data.id, + data: cellData.value + }], [{ + action: "updateAttrViewCell", + id: cellId, + rowID: rowId, + parentID: data.id, + data: { + [colData.type]: oldValue + } + }]); + } + if (colData.type === "select") { + menuElement.parentElement.remove(); + } + } + }) +} + +export const getSelectHTML = (data: IAV, options: { cellElement: HTMLElement }) => { + const cellId = options.cellElement.dataset.id + const colId = options.cellElement.dataset["colId"] + const colData = data.columns.find(item => { + if (item.id === colId) { + return item + } + }) + let selectedHTML = "" + data.rows.find(row => { + if (options.cellElement.parentElement.dataset.id === row.id) { + row.cells.find(cell => { + if (cell.id === cellId && cell.value) { + if (colData.type === "mSelect") { + cell.value.mSelect?.content.forEach((value: string) => { + let colorIndex = "" + colData.options.find(option => { + if (option.name === value) { + colorIndex = option.color + } + }) + selectedHTML += `
${value}
` + }) + } else { + selectedHTML += `
${options.cellElement.textContent.trim()}
` + } + return true; + } + }) + return true + } + }) + return `
+ ${selectedHTML} + +
+
${filterSelectHTML("", colData.options)}
` +} diff --git a/app/src/protyle/render/av/sort.ts b/app/src/protyle/render/av/sort.ts new file mode 100644 index 000000000..ed35047ae --- /dev/null +++ b/app/src/protyle/render/av/sort.ts @@ -0,0 +1,133 @@ +import {Menu} from "../../../plugin/Menu"; +import {getColIconByType} from "./col"; +import {transaction} from "../../wysiwyg/transaction"; +import {setPosition} from "../../../util/setPosition"; + +export const addSort = (options: { + data: IAV, + rect: DOMRect, + menuElement: HTMLElement, + tabRect: DOMRect, + avId: string, + protyle: IProtyle +}) => { + const menu = new Menu("av-add-sort"); + options.data.columns.forEach((column) => { + let hasSort = false; + options.data.sorts.find((sort) => { + if (sort.column === column.id) { + hasSort = true; + return true; + } + }); + if (!hasSort) { + menu.addItem({ + label: column.name, + icon: getColIconByType(column.type), + click: () => { + const oldSorts = Object.assign([], options.data.sorts); + options.data.sorts.push({ + column: column.id, + order: "ASC", + }); + transaction(options.protyle, [{ + action: "setAttrView", + id: options.avId, + data: { + sorts: options.data.sorts + } + }], [{ + action: "setAttrView", + id: options.avId, + data: { + sorts: oldSorts + } + }]); + options.menuElement.innerHTML = getSortsHTML(options.data); + bindSortsEvent(options.protyle, options.menuElement, options.data); + setPosition(options.menuElement, options.tabRect.right - options.menuElement.clientWidth, options.tabRect.bottom, options.tabRect.height); + } + }); + } + }); + menu.open({ + x: options.rect.left, + y: options.rect.bottom, + h: options.rect.height, + }); +}; + +export const bindSortsEvent = (protyle: IProtyle, menuElement: HTMLElement, data: IAV) => { + menuElement.querySelectorAll("select").forEach((item: HTMLSelectElement) => { + item.addEventListener("change", () => { + const colId = item.parentElement.getAttribute("data-id"); + const oldSort = JSON.parse(JSON.stringify(data.sorts)); + if (item.previousElementSibling.classList.contains("b3-menu__icon")) { + data.sorts.find((sort: IAVSort) => { + if (sort.column === colId) { + sort.column = item.value; + item.parentElement.setAttribute("data-id", item.value); + return true; + } + }); + } else { + data.sorts.find((sort: IAVSort) => sort.column === colId).order = item.value as "ASC" | "DESC"; + } + transaction(protyle, [{ + action: "setAttrView", + id: data.id, + data: { + sorts: data.sorts + } + }], [{ + action: "setAttrView", + id: data.id, + data: { + sorts: oldSort + } + }]); + }); + }); +}; + +export const getSortsHTML = (data: IAV) => { + let html = ""; + const genSortItem = (id: string) => { + let sortHTML = ""; + data.columns.forEach((item) => { + sortHTML += ``; + }); + return sortHTML; + }; + data.sorts.forEach((item: IAVSort) => { + html += ``; + }); + return ` + +${html} + +`; +}; diff --git a/app/src/protyle/wysiwyg/index.ts b/app/src/protyle/wysiwyg/index.ts index fb1b20fe0..100c6372f 100644 --- a/app/src/protyle/wysiwyg/index.ts +++ b/app/src/protyle/wysiwyg/index.ts @@ -355,12 +355,12 @@ export class WYSIWYG { const avId = nodeElement.getAttribute("data-av-id"); const dragElement = target.parentElement; const oldWidth = dragElement.clientWidth; - const dragIndex = dragElement.getAttribute("data-index"); + const dragColId = dragElement.getAttribute("data-col-id"); let newWidth: string; documentSelf.onmousemove = (moveEvent: MouseEvent) => { newWidth = oldWidth + (moveEvent.clientX - event.clientX) + "px"; dragElement.parentElement.parentElement.querySelectorAll(".av__row").forEach(item => { - (item.querySelector(`[data-index="${dragIndex}"]`) as HTMLElement).style.width = newWidth; + (item.querySelector(`[data-col-id="${dragColId}"]`) as HTMLElement).style.width = newWidth; }); }; @@ -372,12 +372,12 @@ export class WYSIWYG { documentSelf.onselect = null; transaction(protyle, [{ action: "setAttrViewColWidth", - id: dragElement.getAttribute("data-id"), + id: dragColId, parentID: avId, data: newWidth }], [{ action: "setAttrViewColWidth", - id: dragElement.getAttribute("data-id"), + id: dragColId, parentID: avId, data: oldWidth + "px" }]); diff --git a/app/src/protyle/wysiwyg/transaction.ts b/app/src/protyle/wysiwyg/transaction.ts index 19c39283d..c899b7c4b 100644 --- a/app/src/protyle/wysiwyg/transaction.ts +++ b/app/src/protyle/wysiwyg/transaction.ts @@ -651,7 +651,7 @@ export const onTransaction = (protyle: IProtyle, operation: IOperation, focus: b updateRef(protyle, operation.id); } else if (operation.action === "append") { reloadProtyle(protyle, false); - } else if (["addAttrViewCol", "insertAttrViewBlock", "updateAttrViewCol", "updateAttrViewCell", "sortAttrViewRow", + } else if (["addAttrViewCol", "insertAttrViewBlock", "updateAttrViewCol", "updateAttrViewColOptions", "updateAttrViewCell", "sortAttrViewRow", "sortAttrViewCol", "setAttrViewColHidden", "setAttrViewColWrap", "setAttrViewColWidth", "setAttrView"].includes(operation.action)) { refreshAV(protyle, operation); } diff --git a/app/src/types/index.d.ts b/app/src/types/index.d.ts index 4951f5ea5..d4bad73c4 100644 --- a/app/src/types/index.d.ts +++ b/app/src/types/index.d.ts @@ -29,6 +29,9 @@ type TOperation = | "setAttrViewColHidden" | "setAttrViewColWrap" | "setAttrViewColWidth" + | "updateAttrViewColOptions" + | "removeAttrViewColOption" + | "updateAttrViewColOption" | "setAttrView" type TBazaarType = "templates" | "icons" | "widgets" | "themes" | "plugins" type TCardType = "doc" | "notebook" | "all" @@ -869,6 +872,11 @@ interface IAVColumn { wrap: boolean, hidden: boolean, type: TAVCol, + // 选项列表 + options?: { + name: string, + color: string, + }[] } interface IAVRow { @@ -888,7 +896,7 @@ interface IAVCell { interface IAVCellValue { content?: any - content2?: string + content2?: string // 用于日期 color?: string id?: string isNotEmpty?: boolean diff --git a/app/src/util/upDownHint.ts b/app/src/util/upDownHint.ts index 7f4a68b5a..10f69dfcb 100644 --- a/app/src/util/upDownHint.ts +++ b/app/src/util/upDownHint.ts @@ -1,19 +1,20 @@ -export const upDownHint = (listElement: Element, event: KeyboardEvent) => { - let currentHintElement: HTMLElement = listElement.querySelector(".b3-list-item--focus"); +export const upDownHint = (listElement: Element, event: KeyboardEvent, classActiveName = "b3-list-item--focus") => { + let currentHintElement: HTMLElement = listElement.querySelector("." + classActiveName); + const className = classActiveName.split("--")[0]; if (event.key === "ArrowDown") { event.preventDefault(); event.stopPropagation(); - currentHintElement.classList.remove("b3-list-item--focus"); + currentHintElement.classList.remove(classActiveName); if (!currentHintElement.nextElementSibling) { - listElement.children[0].classList.add("b3-list-item--focus"); + listElement.children[0].classList.add(classActiveName); } else { - if (currentHintElement.nextElementSibling.classList.contains("b3-list-item")) { - currentHintElement.nextElementSibling.classList.add("b3-list-item--focus"); + if (currentHintElement.nextElementSibling.classList.contains(className)) { + currentHintElement.nextElementSibling.classList.add(classActiveName); } else { - currentHintElement.nextElementSibling.nextElementSibling.classList.add("b3-list-item--focus"); + currentHintElement.nextElementSibling.nextElementSibling.classList.add(classActiveName); } } - currentHintElement = listElement.querySelector(".b3-list-item--focus"); + currentHintElement = listElement.querySelector("." + classActiveName); if (listElement.scrollTop < currentHintElement.offsetTop - listElement.clientHeight + currentHintElement.clientHeight || listElement.scrollTop > currentHintElement.offsetTop) { currentHintElement.scrollIntoView(listElement.scrollTop > currentHintElement.offsetTop); @@ -22,18 +23,18 @@ export const upDownHint = (listElement: Element, event: KeyboardEvent) => { } else if (event.key === "ArrowUp") { event.preventDefault(); event.stopPropagation(); - currentHintElement.classList.remove("b3-list-item--focus"); + currentHintElement.classList.remove(classActiveName); if (!currentHintElement.previousElementSibling) { const length = listElement.children.length; - listElement.children[length - 1].classList.add("b3-list-item--focus"); + listElement.children[length - 1].classList.add(classActiveName); } else { - if (currentHintElement.previousElementSibling.classList.contains("b3-list-item")) { - currentHintElement.previousElementSibling.classList.add("b3-list-item--focus"); + if (currentHintElement.previousElementSibling.classList.contains(className)) { + currentHintElement.previousElementSibling.classList.add(classActiveName); } else { - currentHintElement.previousElementSibling.previousElementSibling.classList.add("b3-list-item--focus"); + currentHintElement.previousElementSibling.previousElementSibling.classList.add(classActiveName); } } - currentHintElement = listElement.querySelector(".b3-list-item--focus"); + currentHintElement = listElement.querySelector("." + classActiveName); if (listElement.scrollTop < currentHintElement.offsetTop - listElement.clientHeight + currentHintElement.clientHeight || listElement.scrollTop > currentHintElement.offsetTop - currentHintElement.clientHeight * 2) { currentHintElement.scrollIntoView(listElement.scrollTop > currentHintElement.offsetTop - currentHintElement.clientHeight * 2);