import linkService from "./link.js"; import noteContentRenderer from "./note_content_renderer.js"; import froca from "./froca.js"; import attributeRenderer from "./attribute_renderer.js"; import libraryLoader from "./library_loader.js"; const TPL = `
`; class NoteListRenderer { /* * We're using noteIds so that it's not necessary to load all notes at once when paging */ constructor($parent, parentNote, noteIds, showNotePath = false) { this.$noteList = $(TPL); // note list must be added to the DOM immediatelly, 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 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) into a 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.$noteList.hide(); return; } const highlightedTokens = this.parentNote.highlightedTokens || []; if (highlightedTokens.length > 0) { await libraryLoader.requireLibrary(libraryLoader.MARKJS); this.highlightRegex = new RegExp(highlightedTokens.join("|"), '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.hasLabel('expanded')); $container.append($card); } this.renderPager(); return this.$noteList; } renderPager() { const $pager = this.$noteList.find('.note-list-pager').empty(); 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; $pager.append( i === this.page ? $('').text(i).css('text-decoration', 'underline').css('font-weight', "bold") : $('') .text(i) .on('click', () => { this.page = i; this.renderList(); }), "   " ); } else if (lastPrinted) { $pager.append("...   "); lastPrinted = false; } } } async renderNote(note, expand = 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 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(note.title) : (await linkService.createNoteLink(notePath, {showTooltip: false, showNotePath: this.showNotePath})) .addClass("note-book-title") ) .append($renderedAttributes) ); if (this.viewType === 'grid') { $card .addClass("block-link") .attr("data-note-path", 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, note, expand) { 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) { const $content = $('
'); try { const {$renderedContent, type} = await noteContentRenderer.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.log(`Caught error while rendering note ${note.noteId} of type ${note.type}: ${e.message}, stack: ${e.stack}`); $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;