A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 370 lines 18 kB view raw
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};