diff --git a/src/public/app/services/note_list_renderer.ts b/src/public/app/services/note_list_renderer.ts index 06d2c1a48..6fd9d2ec7 100644 --- a/src/public/app/services/note_list_renderer.ts +++ b/src/public/app/services/note_list_renderer.ts @@ -1,404 +1,17 @@ -import linkService from "./link.js"; -import contentRenderer from "./content_renderer.js"; -import froca from "./froca.js"; -import attributeRenderer from "./attribute_renderer.js"; -import libraryLoader from "./library_loader.js"; -import treeService from "./tree.js"; -import utils from "./utils.js"; import type FNote from "../entities/fnote.js"; +import ListOrGridView from "../widgets/view_widgets/list_or_grid_view.js"; +import type ViewMode from "../widgets/view_widgets/view_mode.js"; -const TPL = ` -
- - -
-
- - - -
-
-
`; - -class NoteListRenderer { - private $noteList: JQuery; - - private parentNote: FNote; - private noteIds: string[]; - private page?: number; - private pageSize?: number; - private viewType?: string | null; - private showNotePath?: boolean; - private highlightRegex?: RegExp | null; - - /* - * We're using noteIds so that it's not necessary to load all notes at once when paging - */ constructor($parent: JQuery, parentNote: FNote, noteIds: string[], showNotePath: boolean = false) { - this.$noteList = $(TPL); - - // note list must be added to the DOM immediately, otherwise some functionality scripting (canvas) won't work - $parent.empty(); - - this.parentNote = parentNote; - const includedNoteIds = this.getIncludedNoteIds(); - - this.noteIds = noteIds.filter((noteId) => !includedNoteIds.has(noteId) && noteId !== "_hidden"); - - if (this.noteIds.length === 0) { - return; - } - - $parent.append(this.$noteList); - - this.page = 1; - this.pageSize = parseInt(parentNote.getLabelValue("pageSize") || ""); - - if (!this.pageSize || this.pageSize < 1) { - this.pageSize = 20; - } - - this.viewType = parentNote.getLabelValue("viewType"); - - if (!["list", "grid"].includes(this.viewType || "")) { - // when not explicitly set, decide based on the note type - this.viewType = parentNote.type === "search" ? "list" : "grid"; - } - - this.$noteList.addClass(`${this.viewType}-view`); - - this.showNotePath = showNotePath; - } - - /** @returns {Set} list of noteIds included (images, included notes) in the parent note and which - * don't have to be shown in the note list. */ - getIncludedNoteIds() { - const includedLinks = this.parentNote ? this.parentNote.getRelations().filter((rel) => rel.name === "imageLink" || rel.name === "includeNoteLink") : []; - - return new Set(includedLinks.map((rel) => rel.value)); + this.viewMode = new ListOrGridView($parent, parentNote, noteIds, showNotePath); } async renderList() { - if (this.noteIds.length === 0 || !this.page || !this.pageSize) { - this.$noteList.hide(); - return; - } - - const highlightedTokens = this.parentNote.highlightedTokens || []; - if (highlightedTokens.length > 0) { - await libraryLoader.requireLibrary(libraryLoader.MARKJS); - - const regex = highlightedTokens.map((token) => utils.escapeRegExp(token)).join("|"); - - this.highlightRegex = new RegExp(regex, "gi"); - } else { - this.highlightRegex = null; - } - - this.$noteList.show(); - - const $container = this.$noteList.find(".note-list-container").empty(); - - const startIdx = (this.page - 1) * this.pageSize; - const endIdx = startIdx + this.pageSize; - - const pageNoteIds = this.noteIds.slice(startIdx, Math.min(endIdx, this.noteIds.length)); - const pageNotes = await froca.getNotes(pageNoteIds); - - for (const note of pageNotes) { - const $card = await this.renderNote(note, this.parentNote.isLabelTruthy("expanded")); - - $container.append($card); - } - - this.renderPager(); - - return this.$noteList; + return await this.viewMode.renderList(); } - renderPager() { - const $pager = this.$noteList.find(".note-list-pager").empty(); - if (!this.page || !this.pageSize) { - return; - } - - const pageCount = Math.ceil(this.noteIds.length / this.pageSize); - - $pager.toggle(pageCount > 1); - - let lastPrinted; - - for (let i = 1; i <= pageCount; i++) { - if (pageCount < 20 || i <= 5 || pageCount - i <= 5 || Math.abs(this.page - i) <= 2) { - lastPrinted = true; - - const startIndex = (i - 1) * this.pageSize + 1; - const endIndex = Math.min(this.noteIds.length, i * this.pageSize); - - $pager.append( - i === this.page - ? $("").text(i).css("text-decoration", "underline").css("font-weight", "bold") - : $('') - .text(i) - .attr("title", `Page of ${startIndex} - ${endIndex}`) - .on("click", () => { - this.page = i; - this.renderList(); - }), - "   " - ); - } else if (lastPrinted) { - $pager.append("...   "); - - lastPrinted = false; - } - } - - // no need to distinguish "note" vs "notes" since in case of one result, there's no paging at all - $pager.append(`(${this.noteIds.length} notes)`); - } - - async renderNote(note: FNote, expand: boolean = false) { - const $expander = $(''); - - const { $renderedAttributes } = await attributeRenderer.renderNormalAttributes(note); - const notePath = - this.parentNote.type === "search" - ? note.noteId // for search note parent, we want to display a non-search path - : `${this.parentNote.noteId}/${note.noteId}`; - - const $card = $('
') - .attr("data-note-id", note.noteId) - .append( - $('
') - .append($expander) - .append($('').addClass(note.getIcon())) - .append( - this.viewType === "grid" - ? $('').text(await treeService.getNoteTitle(note.noteId, this.parentNote.noteId)) - : (await linkService.createLink(notePath, { showTooltip: false, showNotePath: this.showNotePath })).addClass("note-book-title") - ) - .append($renderedAttributes) - ); - - if (this.viewType === "grid") { - $card - .addClass("block-link") - .attr("data-href", `#${notePath}`) - .on("click", (e) => linkService.goToLink(e)); - } - - $expander.on("click", () => this.toggleContent($card, note, !$card.hasClass("expanded"))); - - if (this.highlightRegex) { - $card.find(".note-book-title").markRegExp(this.highlightRegex, { - element: "span", - className: "ck-find-result", - separateWordSearch: false, - caseSensitive: false - }); - } - - await this.toggleContent($card, note, expand); - - return $card; - } - - async toggleContent($card: JQuery, note: FNote, expand: boolean) { - if (this.viewType === "list" && ((expand && $card.hasClass("expanded")) || (!expand && !$card.hasClass("expanded")))) { - return; - } - - const $expander = $card.find("> .note-book-header .note-expander"); - - if (expand || this.viewType === "grid") { - $card.addClass("expanded"); - $expander.addClass("bx-chevron-down").removeClass("bx-chevron-right"); - } else { - $card.removeClass("expanded"); - $expander.addClass("bx-chevron-right").removeClass("bx-chevron-down"); - } - - if ((expand || this.viewType === "grid") && $card.find(".note-book-content").length === 0) { - $card.append(await this.renderNoteContent(note)); - } - } - - async renderNoteContent(note: FNote) { - const $content = $('
'); - - try { - const { $renderedContent, type } = await contentRenderer.getRenderedContent(note, { - trim: this.viewType === "grid" // for grid only short content is needed - }); - - if (this.highlightRegex) { - $renderedContent.markRegExp(this.highlightRegex, { - element: "span", - className: "ck-find-result", - separateWordSearch: false, - caseSensitive: false - }); - } - - $content.append($renderedContent); - $content.addClass(`type-${type}`); - } catch (e) { - console.warn(`Caught error while rendering note '${note.noteId}' of type '${note.type}'`); - console.error(e); - - $content.append("rendering error"); - } - - if (this.viewType === "list") { - const imageLinks = note.getRelations("imageLink"); - - const childNotes = (await note.getChildNotes()).filter((childNote) => !imageLinks.find((rel) => rel.value === childNote.noteId)); - - for (const childNote of childNotes) { - $content.append(await this.renderNote(childNote)); - } - } - - return $content; - } } - -export default NoteListRenderer; diff --git a/src/public/app/widgets/view_widgets/list_or_grid_view.ts b/src/public/app/widgets/view_widgets/list_or_grid_view.ts new file mode 100644 index 000000000..96d4037a4 --- /dev/null +++ b/src/public/app/widgets/view_widgets/list_or_grid_view.ts @@ -0,0 +1,406 @@ +import linkService from "../../services/link.js"; +import contentRenderer from "../../services/content_renderer.js"; +import froca from "../../services/froca.js"; +import attributeRenderer from "../../services/attribute_renderer.js"; +import libraryLoader from "../../services/library_loader.js"; +import treeService from "../../services/tree.js"; +import utils from "../../services/utils.js"; +import type FNote from "../../entities/fnote.js"; +import ViewMode from "./view_mode.js"; + +const TPL = ` +
+ + +
+
+ + + +
+
+
`; + +class ListOrGridView extends ViewMode { + private $noteList: JQuery; + + private parentNote: FNote; + private noteIds: string[]; + private page?: number; + private pageSize?: number; + private viewType?: string | null; + private showNotePath?: boolean; + private highlightRegex?: RegExp | null; + + /* + * We're using noteIds so that it's not necessary to load all notes at once when paging + */ + constructor($parent: JQuery, parentNote: FNote, noteIds: string[], showNotePath: boolean = false) { + super($parent, parentNote, noteIds, showNotePath); + this.$noteList = $(TPL); + + // note list must be added to the DOM immediately, otherwise some functionality scripting (canvas) won't work + $parent.empty(); + + this.parentNote = parentNote; + const includedNoteIds = this.getIncludedNoteIds(); + + this.noteIds = noteIds.filter((noteId) => !includedNoteIds.has(noteId) && noteId !== "_hidden"); + + if (this.noteIds.length === 0) { + return; + } + + $parent.append(this.$noteList); + + this.page = 1; + this.pageSize = parseInt(parentNote.getLabelValue("pageSize") || ""); + + if (!this.pageSize || this.pageSize < 1) { + this.pageSize = 20; + } + + this.viewType = parentNote.getLabelValue("viewType"); + + if (!["list", "grid"].includes(this.viewType || "")) { + // when not explicitly set, decide based on the note type + this.viewType = parentNote.type === "search" ? "list" : "grid"; + } + + this.$noteList.addClass(`${this.viewType}-view`); + + this.showNotePath = showNotePath; + } + + /** @returns {Set} list of noteIds included (images, included notes) in the parent note and which + * don't have to be shown in the note list. */ + getIncludedNoteIds() { + const includedLinks = this.parentNote ? this.parentNote.getRelations().filter((rel) => rel.name === "imageLink" || rel.name === "includeNoteLink") : []; + + return new Set(includedLinks.map((rel) => rel.value)); + } + + async renderList() { + if (this.noteIds.length === 0 || !this.page || !this.pageSize) { + this.$noteList.hide(); + return; + } + + const highlightedTokens = this.parentNote.highlightedTokens || []; + if (highlightedTokens.length > 0) { + await libraryLoader.requireLibrary(libraryLoader.MARKJS); + + const regex = highlightedTokens.map((token) => utils.escapeRegExp(token)).join("|"); + + this.highlightRegex = new RegExp(regex, "gi"); + } else { + this.highlightRegex = null; + } + + this.$noteList.show(); + + const $container = this.$noteList.find(".note-list-container").empty(); + + const startIdx = (this.page - 1) * this.pageSize; + const endIdx = startIdx + this.pageSize; + + const pageNoteIds = this.noteIds.slice(startIdx, Math.min(endIdx, this.noteIds.length)); + const pageNotes = await froca.getNotes(pageNoteIds); + + for (const note of pageNotes) { + const $card = await this.renderNote(note, this.parentNote.isLabelTruthy("expanded")); + + $container.append($card); + } + + this.renderPager(); + + return this.$noteList; + } + + renderPager() { + const $pager = this.$noteList.find(".note-list-pager").empty(); + if (!this.page || !this.pageSize) { + return; + } + + const pageCount = Math.ceil(this.noteIds.length / this.pageSize); + + $pager.toggle(pageCount > 1); + + let lastPrinted; + + for (let i = 1; i <= pageCount; i++) { + if (pageCount < 20 || i <= 5 || pageCount - i <= 5 || Math.abs(this.page - i) <= 2) { + lastPrinted = true; + + const startIndex = (i - 1) * this.pageSize + 1; + const endIndex = Math.min(this.noteIds.length, i * this.pageSize); + + $pager.append( + i === this.page + ? $("").text(i).css("text-decoration", "underline").css("font-weight", "bold") + : $('
') + .text(i) + .attr("title", `Page of ${startIndex} - ${endIndex}`) + .on("click", () => { + this.page = i; + this.renderList(); + }), + "   " + ); + } else if (lastPrinted) { + $pager.append("...   "); + + lastPrinted = false; + } + } + + // no need to distinguish "note" vs "notes" since in case of one result, there's no paging at all + $pager.append(`(${this.noteIds.length} notes)`); + } + + async renderNote(note: FNote, expand: boolean = false) { + const $expander = $(''); + + const { $renderedAttributes } = await attributeRenderer.renderNormalAttributes(note); + const notePath = + this.parentNote.type === "search" + ? note.noteId // for search note parent, we want to display a non-search path + : `${this.parentNote.noteId}/${note.noteId}`; + + const $card = $('
') + .attr("data-note-id", note.noteId) + .append( + $('
') + .append($expander) + .append($('').addClass(note.getIcon())) + .append( + this.viewType === "grid" + ? $('').text(await treeService.getNoteTitle(note.noteId, this.parentNote.noteId)) + : (await linkService.createLink(notePath, { showTooltip: false, showNotePath: this.showNotePath })).addClass("note-book-title") + ) + .append($renderedAttributes) + ); + + if (this.viewType === "grid") { + $card + .addClass("block-link") + .attr("data-href", `#${notePath}`) + .on("click", (e) => linkService.goToLink(e)); + } + + $expander.on("click", () => this.toggleContent($card, note, !$card.hasClass("expanded"))); + + if (this.highlightRegex) { + $card.find(".note-book-title").markRegExp(this.highlightRegex, { + element: "span", + className: "ck-find-result", + separateWordSearch: false, + caseSensitive: false + }); + } + + await this.toggleContent($card, note, expand); + + return $card; + } + + async toggleContent($card: JQuery, note: FNote, expand: boolean) { + if (this.viewType === "list" && ((expand && $card.hasClass("expanded")) || (!expand && !$card.hasClass("expanded")))) { + return; + } + + const $expander = $card.find("> .note-book-header .note-expander"); + + if (expand || this.viewType === "grid") { + $card.addClass("expanded"); + $expander.addClass("bx-chevron-down").removeClass("bx-chevron-right"); + } else { + $card.removeClass("expanded"); + $expander.addClass("bx-chevron-right").removeClass("bx-chevron-down"); + } + + if ((expand || this.viewType === "grid") && $card.find(".note-book-content").length === 0) { + $card.append(await this.renderNoteContent(note)); + } + } + + async renderNoteContent(note: FNote) { + const $content = $('
'); + + try { + const { $renderedContent, type } = await contentRenderer.getRenderedContent(note, { + trim: this.viewType === "grid" // for grid only short content is needed + }); + + if (this.highlightRegex) { + $renderedContent.markRegExp(this.highlightRegex, { + element: "span", + className: "ck-find-result", + separateWordSearch: false, + caseSensitive: false + }); + } + + $content.append($renderedContent); + $content.addClass(`type-${type}`); + } catch (e) { + console.warn(`Caught error while rendering note '${note.noteId}' of type '${note.type}'`); + console.error(e); + + $content.append("rendering error"); + } + + if (this.viewType === "list") { + const imageLinks = note.getRelations("imageLink"); + + const childNotes = (await note.getChildNotes()).filter((childNote) => !imageLinks.find((rel) => rel.value === childNote.noteId)); + + for (const childNote of childNotes) { + $content.append(await this.renderNote(childNote)); + } + } + + return $content; + } +} + +export default ListOrGridView; diff --git a/src/public/app/widgets/view_widgets/view_mode.ts b/src/public/app/widgets/view_widgets/view_mode.ts new file mode 100644 index 000000000..c854bd37f --- /dev/null +++ b/src/public/app/widgets/view_widgets/view_mode.ts @@ -0,0 +1,11 @@ +import type FNote from "../../entities/fnote.js"; + +export default abstract class ViewMode { + + constructor($parent: JQuery, parentNote: FNote, noteIds: string[], showNotePath: boolean = false) { + + } + + abstract renderList(): Promise | undefined>; + +}