A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
1import {hasClosestBlock, hasClosestByAttribute, hasClosestByClassName} from "../../../util/hasClosest";
2import {Constants} from "../../../../constants";
3import {fetchSyncPost} from "../../../../util/fetch";
4import {escapeAttr} from "../../../../util/escape";
5import {unicode2Emoji} from "../../../../emoji";
6import {cellValueIsEmpty, renderCell} from "../cell";
7import {focusBlock} from "../../../util/selection";
8import {electronUndo} from "../../../undo";
9import {addClearButton} from "../../../../util/addClearButton";
10import {avRender, genTabHeaderHTML, getGroupTitleHTML, updateSearch} from "../render";
11import {processRender} from "../../../util/processCode";
12import {getColIconByType, getColNameByType} from "../col";
13import {getCompressURL} from "../../../../util/image";
14import {getPageSize} from "../groups";
15
16interface IIds {
17 groupId: string,
18 fieldId: string,
19}
20
21interface ITableOptions {
22 protyle: IProtyle,
23 blockElement: HTMLElement,
24 cb: (data: IAV) => void,
25 data: IAV,
26 renderAll: boolean,
27 resetData: {
28 alignSelf: string,
29 selectItemIds: IIds[],
30 editIds: IIds[],
31 isSearching: boolean,
32 pageSizes: { [key: string]: string },
33 query: string,
34 oldOffset: number,
35 }
36}
37
38const getGalleryHTML = (data: IAVGallery) => {
39 let galleryHTML = "";
40 // body
41 data.cards.forEach((item: IAVGalleryItem, rowIndex: number) => {
42 galleryHTML += `<div data-id="${item.id}" draggable="true" class="av__gallery-item">`;
43 if (data.coverFrom !== 0) {
44 const coverClass = "av__gallery-cover av__gallery-cover--" + data.cardAspectRatio;
45 if (item.coverURL) {
46 if (item.coverURL.startsWith("background")) {
47 galleryHTML += `<div class="${coverClass}"><img class="av__gallery-img" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" style="${item.coverURL}"></div>`;
48 } else {
49 galleryHTML += `<div class="${coverClass}"><img loading="lazy" class="av__gallery-img${data.fitImage ? " av__gallery-img--fit" : ""}" src="${getCompressURL(item.coverURL)}"></div>`;
50 }
51 } else if (item.coverContent) {
52 galleryHTML += `<div class="${coverClass}"><div class="av__gallery-content">${item.coverContent}</div><div></div></div>`;
53 } else {
54 galleryHTML += `<div class="${coverClass}"></div>`;
55 }
56 }
57 galleryHTML += '<div class="av__gallery-fields">';
58 item.values.forEach((cell, fieldsIndex) => {
59 if (data.fields[fieldsIndex].hidden) {
60 return;
61 }
62 let checkClass = "";
63 if (cell.valueType === "checkbox") {
64 checkClass = cell.value?.checkbox?.checked ? " av__cell-check" : " av__cell-uncheck";
65 }
66 const isEmpty = cellValueIsEmpty(cell.value);
67 // NOTE: innerHTML 中不能换行否则 https://github.com/siyuan-note/siyuan/issues/15132
68 let ariaLabel = escapeAttr(data.fields[fieldsIndex].name) || getColNameByType(data.fields[fieldsIndex].type);
69 if (data.fields[fieldsIndex].desc) {
70 ariaLabel += escapeAttr(`<div class="ft__on-surface">${data.fields[fieldsIndex].desc}</div>`);
71 }
72
73 if (cell.valueType === "checkbox" && !data.displayFieldName) {
74 cell.value.checkbox.content = data.fields[fieldsIndex].name || getColNameByType(data.fields[fieldsIndex].type);
75 }
76 const cellHTML = `<div class="av__cell${checkClass}${data.displayFieldName ? "" : " ariaLabel"}"
77data-wrap="${data.fields[fieldsIndex].wrap}"
78aria-label="${ariaLabel}"
79data-position="5west"
80data-id="${cell.id}"
81data-field-id="${data.fields[fieldsIndex].id}"
82data-dtype="${cell.valueType}"
83${cell.value?.isDetached ? ' data-detached="true"' : ""}
84style="${cell.bgColor ? `background-color:${cell.bgColor};` : ""}
85${cell.color ? `color:${cell.color};` : ""}">${renderCell(cell.value, rowIndex, data.showIcon, "gallery")}</div>`;
86 if (data.displayFieldName) {
87 galleryHTML += `<div class="av__gallery-field av__gallery-field--name" data-empty="${isEmpty}">
88 <div class="av__gallery-name">
89 ${data.fields[fieldsIndex].icon ? unicode2Emoji(data.fields[fieldsIndex].icon, undefined, true) : `<svg><use xlink:href="#${getColIconByType(data.fields[fieldsIndex].type)}"></use></svg>`}${Lute.EscapeHTMLStr(data.fields[fieldsIndex].name)}
90 ${data.fields[fieldsIndex].desc ? `<svg aria-label="${data.fields[fieldsIndex].desc}" data-position="north" class="ariaLabel"><use xlink:href="#iconInfo"></use></svg>` : ""}
91 </div>
92 ${cellHTML}
93</div>`;
94 } else {
95 galleryHTML += `<div class="av__gallery-field" data-empty="${isEmpty}">
96 <div class="av__gallery-tip">
97 ${data.fields[fieldsIndex].icon ? unicode2Emoji(data.fields[fieldsIndex].icon, undefined, true) : `<svg><use xlink:href="#${getColIconByType(data.fields[fieldsIndex].type)}"></use></svg>`}${window.siyuan.languages.edit} ${Lute.EscapeHTMLStr(data.fields[fieldsIndex].name)}
98 </div>
99 ${cellHTML}
100</div>`;
101 }
102 });
103 galleryHTML += `</div>
104 <div class="av__gallery-actions">
105 <span class="protyle-icon protyle-icon--first b3-tooltips b3-tooltips__n" aria-label="${window.siyuan.languages.displayEmptyFields}" data-type="av-gallery-edit"><svg><use xlink:href="#iconEdit"></use></svg></span>
106 <span class="protyle-icon protyle-icon--last b3-tooltips b3-tooltips__n" aria-label="${window.siyuan.languages.more}" data-type="av-gallery-more"><svg><use xlink:href="#iconMore"></use></svg></span>
107 </div>
108</div>`;
109 });
110 galleryHTML += `<div class="av__gallery-add" data-type="av-add-bottom"><svg class="svg"><use xlink:href="#iconAdd"></use></svg><span class="fn__space"></span>${window.siyuan.languages.newRow}</div>`;
111 return `<div class="av__gallery${data.cardSize === 0 ? " av__gallery--small" : (data.cardSize === 2 ? " av__gallery--big" : "")}">
112 ${galleryHTML}
113</div>
114<div class="av__gallery-load${data.cardCount > data.cards.length ? "" : " fn__none"}">
115 <button class="b3-button av__button" data-type="av-load-more">
116 <svg><use xlink:href="#iconArrowDown"></use></svg>
117 <span>${window.siyuan.languages.loadMore}</span>
118 <svg data-type="set-page-size" data-size="${data.pageSize}"><use xlink:href="#iconMore"></use></svg>
119 </button>
120</div>`;
121};
122
123const renderGroupGallery = (options: ITableOptions) => {
124 const searchInputElement = options.blockElement.querySelector('[data-type="av-search"]') as HTMLInputElement;
125 const isSearching = searchInputElement && document.activeElement === searchInputElement;
126 const query = searchInputElement?.value || "";
127
128 let avBodyHTML = "";
129 options.data.view.groups.forEach((group: IAVGallery) => {
130 if (group.groupHidden === 0) {
131 avBodyHTML += `${getGroupTitleHTML(group, group.cards.length)}
132<div data-group-id="${group.id}" data-page-size="${group.pageSize}" data-dtype="${group.groupKey.type}" data-content="${Lute.EscapeHTMLStr(group.groupValue.text?.content)}" class="av__body${group.groupFolded ? " fn__none" : ""}">${getGalleryHTML(group)}</div>`;
133 }
134 });
135 if (options.renderAll) {
136 options.blockElement.firstElementChild.outerHTML = `<div class="av__container fn__block">
137 ${genTabHeaderHTML(options.data, isSearching || !!query, !options.protyle.disabled && !hasClosestByAttribute(options.blockElement, "data-type", "NodeBlockQueryEmbed"))}
138 <div>
139 ${avBodyHTML}
140 </div>
141 <div class="av__cursor" contenteditable="true">${Constants.ZWSP}</div>
142</div>`;
143 } else {
144 options.blockElement.querySelector(".av__header").nextElementSibling.innerHTML = avBodyHTML;
145 }
146 afterRenderGallery(options);
147};
148
149const afterRenderGallery = (options: ITableOptions) => {
150 const view = options.data.view as IAVGallery;
151 if (view.coverFrom === 1 || view.coverFrom === 3) {
152 processRender(options.blockElement);
153 }
154 if (typeof options.resetData.oldOffset === "number") {
155 options.protyle.contentElement.scrollTop = options.resetData.oldOffset;
156 }
157 if (options.blockElement.getAttribute("data-need-focus") === "true") {
158 focusBlock(options.blockElement);
159 options.blockElement.removeAttribute("data-need-focus");
160 }
161 options.blockElement.setAttribute("data-render", "true");
162 if (options.resetData.alignSelf) {
163 options.blockElement.style.alignSelf = options.resetData.alignSelf;
164 }
165 options.resetData.selectItemIds.find(selectId => {
166 let itemElement = options.blockElement.querySelector(`.av__body[data-group-id="${selectId.groupId}"] .av__gallery-item[data-id="${selectId.fieldId}"]`) as HTMLElement;
167 if (!itemElement) {
168 itemElement = options.blockElement.querySelector(`.av__gallery-item[data-id="${selectId.fieldId}"]`) as HTMLElement;
169 }
170 if (itemElement) {
171 itemElement.classList.add("av__gallery-item--select");
172 }
173 });
174 options.resetData.editIds.find(selectId => {
175 let itemElement = options.blockElement.querySelector(`.av__body[data-group-id="${selectId.groupId}"] .av__gallery-item[data-id="${selectId.fieldId}"]`) as HTMLElement;
176 if (!itemElement) {
177 itemElement = options.blockElement.querySelector(`.av__gallery-item[data-id="${selectId.fieldId}"]`) as HTMLElement;
178 }
179 if (itemElement) {
180 itemElement.querySelector(".av__gallery-fields").classList.add("av__gallery-fields--edit");
181 }
182 });
183 Object.keys(options.resetData.pageSizes).forEach((groupId) => {
184 const bodyElement = options.blockElement.querySelector(`.av__body[data-group-id="${groupId === "unGroup" ? "" : groupId}"]`) as HTMLElement;
185 if (bodyElement) {
186 bodyElement.dataset.pageSize = options.resetData.pageSizes[groupId];
187 }
188 });
189 if (getSelection().rangeCount > 0) {
190 // 修改表头后光标重新定位
191 const range = getSelection().getRangeAt(0);
192 if (!hasClosestByClassName(range.startContainer, "av__title")) {
193 const blockElement = hasClosestBlock(range.startContainer);
194 if (blockElement && options.blockElement === blockElement && !options.resetData.isSearching) {
195 focusBlock(options.blockElement);
196 }
197 }
198 }
199 options.blockElement.querySelector(".layout-tab-bar").scrollLeft = (options.blockElement.querySelector(".layout-tab-bar .item--focus") as HTMLElement).offsetLeft - 30;
200 if (options.cb) {
201 options.cb(options.data);
202 }
203 if (!options.renderAll) {
204 return;
205 }
206 const viewsElement = options.blockElement.querySelector(".av__views") as HTMLElement;
207 const searchInputElement = options.blockElement.querySelector('[data-type="av-search"]') as HTMLInputElement;
208 searchInputElement.value = options.resetData.query || "";
209 if (options.resetData.isSearching) {
210 searchInputElement.focus();
211 }
212 searchInputElement.addEventListener("compositionstart", (event: KeyboardEvent) => {
213 event.stopPropagation();
214 });
215 searchInputElement.addEventListener("keydown", (event: KeyboardEvent) => {
216 if (event.isComposing) {
217 return;
218 }
219 electronUndo(event);
220 });
221 searchInputElement.addEventListener("input", (event: KeyboardEvent) => {
222 event.stopPropagation();
223 if (event.isComposing) {
224 return;
225 }
226 if (searchInputElement.value || document.activeElement === searchInputElement) {
227 viewsElement.classList.add("av__views--show");
228 } else {
229 viewsElement.classList.remove("av__views--show");
230 }
231 updateSearch(options.blockElement, options.protyle);
232 });
233 searchInputElement.addEventListener("compositionend", () => {
234 updateSearch(options.blockElement, options.protyle);
235 });
236 searchInputElement.addEventListener("blur", (event: KeyboardEvent) => {
237 if (event.isComposing) {
238 return;
239 }
240 if (!searchInputElement.value) {
241 viewsElement.classList.remove("av__views--show");
242 searchInputElement.style.width = "0";
243 searchInputElement.style.paddingLeft = "0";
244 searchInputElement.style.paddingRight = "0";
245 }
246 });
247 addClearButton({
248 inputElement: searchInputElement,
249 right: 0,
250 width: "1em",
251 height: searchInputElement.clientHeight,
252 clearCB() {
253 viewsElement.classList.remove("av__views--show");
254 searchInputElement.style.width = "0";
255 searchInputElement.style.paddingLeft = "0";
256 searchInputElement.style.paddingRight = "0";
257 focusBlock(options.blockElement);
258 updateSearch(options.blockElement, options.protyle);
259 }
260 });
261};
262
263export const renderGallery = async (options: {
264 blockElement: HTMLElement,
265 protyle: IProtyle,
266 cb?: (data: IAV) => void,
267 renderAll: boolean,
268 data?: IAV,
269}) => {
270 const searchInputElement = options.blockElement.querySelector('[data-type="av-search"]') as HTMLInputElement;
271 const editIds: IIds[] = [];
272 options.blockElement.querySelectorAll(".av__gallery-fields--edit").forEach(item => {
273 editIds.push({
274 groupId: (hasClosestByClassName(item, "av__body") as HTMLElement).dataset.groupId || "",
275 fieldId: item.parentElement.getAttribute("data-id"),
276 });
277 });
278 const selectItemIds: IIds[] = [];
279 options.blockElement.querySelectorAll(".av__gallery-item--select").forEach(galleryItem => {
280 const fieldId = galleryItem.getAttribute("data-id");
281 if (fieldId) {
282 selectItemIds.push({
283 groupId: (hasClosestByClassName(galleryItem, "av__body") as HTMLElement).dataset.groupId || "",
284 fieldId
285 });
286 }
287 });
288 const pageSizes: { [key: string]: string } = {};
289 options.blockElement.querySelectorAll(".av__body").forEach((item: HTMLElement) => {
290 pageSizes[item.dataset.groupId || "unGroup"] = item.dataset.pageSize;
291 });
292 const resetData = {
293 isSearching: searchInputElement && document.activeElement === searchInputElement,
294 query: searchInputElement?.value || "",
295 alignSelf: options.blockElement.style.alignSelf,
296 oldOffset: options.protyle.contentElement.scrollTop,
297 editIds,
298 selectItemIds,
299 pageSizes,
300 };
301 if (options.blockElement.firstElementChild.innerHTML === "") {
302 options.blockElement.style.alignSelf = "";
303 options.blockElement.firstElementChild.outerHTML = `<div class="av__gallery">
304 <span style="width: 100%;height: 178px;" class="av__pulse"></span>
305 <span style="width: 100%;height: 178px;" class="av__pulse"></span>
306 <span style="width: 100%;height: 178px;" class="av__pulse"></span>
307</div>`;
308 }
309 const created = options.protyle.options.history?.created;
310 const snapshot = options.protyle.options.history?.snapshot;
311
312 let data: IAV = options.data;
313 if (!data) {
314 const avPageSize = getPageSize(options.blockElement);
315 const response = await fetchSyncPost(created ? "/api/av/renderHistoryAttributeView" : (snapshot ? "/api/av/renderSnapshotAttributeView" : "/api/av/renderAttributeView"), {
316 id: options.blockElement.getAttribute("data-av-id"),
317 created,
318 snapshot,
319 pageSize: avPageSize.unGroupPageSize,
320 groupPaging: avPageSize.groupPageSize,
321 viewID: options.blockElement.getAttribute(Constants.CUSTOM_SY_AV_VIEW) || "",
322 query: resetData.query.trim()
323 });
324 data = response.data;
325 }
326 if (data.viewType === "table") {
327 options.blockElement.setAttribute("data-av-type", "table");
328 avRender(options.blockElement, options.protyle, options.cb, options.renderAll);
329 return;
330 }
331 const view: IAVGallery = data.view as IAVGallery;
332 if (view.groups?.length > 0) {
333 renderGroupGallery({
334 blockElement: options.blockElement,
335 protyle: options.protyle,
336 cb: options.cb,
337 renderAll: options.renderAll,
338 data,
339 resetData
340 });
341 return;
342 }
343 const bodyHTML = getGalleryHTML(view);
344 if (options.renderAll) {
345 options.blockElement.firstElementChild.outerHTML = `<div class="av__container fn__block">
346 ${genTabHeaderHTML(data, resetData.isSearching || !!resetData.query, !options.protyle.disabled && !hasClosestByAttribute(options.blockElement, "data-type", "NodeBlockQueryEmbed"))}
347 <div>
348 <div class="av__body" data-group-id="" data-page-size="${view.pageSize}">
349 ${bodyHTML}
350 </div>
351 </div>
352 <div class="av__cursor" contenteditable="true">${Constants.ZWSP}</div>
353</div>`;
354 } else {
355 const bodyElement = options.blockElement.querySelector(".av__body") as HTMLElement;
356 bodyElement.innerHTML = bodyHTML;
357 bodyElement.dataset.pageSize = view.pageSize.toString();
358 }
359 afterRenderGallery({
360 resetData,
361 renderAll: options.renderAll,
362 data,
363 cb: options.cb,
364 protyle: options.protyle,
365 blockElement: options.blockElement,
366 });
367 if (view.hideAttrViewName) {
368 options.blockElement.querySelector(".av__gallery").classList.add("av__gallery--top");
369 }
370};