A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
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};