mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +01:00 
			
		
		
		
	Compare commits
	
		
			20 Commits
		
	
	
		
			v0.49.1-be
			...
			v0.49.2-be
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 5a85fe92aa | ||
|  | feffd57f24 | ||
|  | faf81ae056 | ||
|  | 003fec4b11 | ||
|  | 5ecb603e86 | ||
|  | 1fed71a92e | ||
|  | dad82ea4e8 | ||
|  | 067251861d | ||
|  | 6bc8773d5f | ||
|  | a910034c96 | ||
|  | 257cc66f62 | ||
|  | 00f24bdb63 | ||
|  | fada3fe623 | ||
|  | d97e454463 | ||
|  | 3128a7d62f | ||
|  | b8fe9a41db | ||
|  | 8366a94bde | ||
|  | f56123b864 | ||
|  | ae951bfe23 | ||
|  | 265401775b | 
							
								
								
									
										5734
									
								
								libraries/codemirror/keymap/vim.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										5734
									
								
								libraries/codemirror/keymap/vim.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -2,7 +2,7 @@ | ||||
|   "name": "trilium", | ||||
|   "productName": "Trilium Notes", | ||||
|   "description": "Trilium Notes", | ||||
|   "version": "0.49.1-beta", | ||||
|   "version": "0.49.2-beta", | ||||
|   "license": "AGPL-3.0-only", | ||||
|   "main": "electron.js", | ||||
|   "bin": { | ||||
|   | ||||
| @@ -1114,7 +1114,7 @@ class Note extends AbstractEntity { | ||||
|  | ||||
|         const branch = this.becca.getNote(parentNoteId).getParentBranches()[0]; | ||||
|  | ||||
|         return cloningService.cloneNoteToParent(this.noteId, branch.branchId); | ||||
|         return cloningService.cloneNoteToBranch(this.noteId, branch.branchId); | ||||
|     } | ||||
|  | ||||
|     decrypt() { | ||||
|   | ||||
| @@ -48,7 +48,7 @@ async function cloneNotesTo(notePath) { | ||||
|     const targetBranchId = await froca.getBranchId(parentNoteId, noteId); | ||||
|  | ||||
|     for (const cloneNoteId of clonedNoteIds) { | ||||
|         await branchService.cloneNoteTo(cloneNoteId, targetBranchId, $clonePrefix.val()); | ||||
|         await branchService.cloneNoteToBranch(cloneNoteId, targetBranchId, $clonePrefix.val()); | ||||
|  | ||||
|         const clonedNote = await froca.getNote(cloneNoteId); | ||||
|         const targetNote = await froca.getBranch(targetBranchId).getNote(); | ||||
|   | ||||
| @@ -1,7 +1,15 @@ | ||||
| import mimeTypesService from "../../services/mime_types.js"; | ||||
| import options from "../../services/options.js"; | ||||
| import server from "../../services/server.js"; | ||||
| import toastService from "../../services/toast.js"; | ||||
| import utils from "../../services/utils.js"; | ||||
|  | ||||
| const TPL = ` | ||||
| <h4>Use vim keybindings in CodeNotes (no ex mode)</h4> | ||||
| <div class="custom-control custom-checkbox"> | ||||
|     <input type="checkbox" class="custom-control-input" id="vim-keymap-enabled"> | ||||
|     <label class="custom-control-label" for="vim-keymap-enabled">Enable Vim Keybindings</label> | ||||
| </div> | ||||
| <h4>Available MIME types in the dropdown</h4> | ||||
|  | ||||
| <ul id="options-mime-types" style="max-height: 500px; overflow: auto; list-style-type: none;"></ul>`; | ||||
| @@ -10,12 +18,18 @@ export default class CodeNotesOptions { | ||||
|     constructor() { | ||||
|         $("#options-code-notes").html(TPL); | ||||
|  | ||||
|         this.$vimKeymapEnabled = $("#vim-keymap-enabled"); | ||||
|         this.$vimKeymapEnabled.on('change', () => { | ||||
|             const opts = { 'vimKeymapEnabled': this.$vimKeymapEnabled.is(":checked") ? "true" : "false" }; | ||||
|             server.put('options', opts).then(() => toastService.showMessage("Options change have been saved.")); | ||||
|             return false; | ||||
|         }); | ||||
|         this.$mimeTypes = $("#options-mime-types"); | ||||
|     } | ||||
|  | ||||
|     async optionsLoaded() { | ||||
|     async optionsLoaded(options) { | ||||
|         this.$mimeTypes.empty(); | ||||
|  | ||||
|         this.$vimKeymapEnabled.prop("checked", options['vimKeymapEnabled'] === 'true'); | ||||
|         let idCtr = 1; | ||||
|  | ||||
|         for (const mimeType of await mimeTypesService.getMimeTypes()) { | ||||
|   | ||||
| @@ -196,8 +196,18 @@ ws.subscribeToMessages(async message => { | ||||
|     } | ||||
| }); | ||||
|  | ||||
| async function cloneNoteTo(childNoteId, parentBranchId, prefix) { | ||||
|     const resp = await server.put(`notes/${childNoteId}/clone-to/${parentBranchId}`, { | ||||
| async function cloneNoteToBranch(childNoteId, parentBranchId, prefix) { | ||||
|     const resp = await server.put(`notes/${childNoteId}/clone-to-branch/${parentBranchId}`, { | ||||
|         prefix: prefix | ||||
|     }); | ||||
|  | ||||
|     if (!resp.success) { | ||||
|         alert(resp.message); | ||||
|     } | ||||
| } | ||||
|  | ||||
| async function cloneNoteToNote(childNoteId, parentNoteId, prefix) { | ||||
|     const resp = await server.put(`notes/${childNoteId}/clone-to-note/${parentNoteId}`, { | ||||
|         prefix: prefix | ||||
|     }); | ||||
|  | ||||
| @@ -222,5 +232,6 @@ export default { | ||||
|     deleteNotes, | ||||
|     moveNodeUpInHierarchy, | ||||
|     cloneNoteAfter, | ||||
|     cloneNoteTo | ||||
|     cloneNoteToBranch, | ||||
|     cloneNoteToNote, | ||||
| }; | ||||
|   | ||||
| @@ -51,7 +51,7 @@ async function pasteInto(parentBranchId) { | ||||
|         for (const clipboardBranch of clipboardBranches) { | ||||
|             const clipboardNote = await clipboardBranch.getNote(); | ||||
|  | ||||
|             await branchService.cloneNoteTo(clipboardNote.noteId, parentBranchId); | ||||
|             await branchService.cloneNoteToBranch(clipboardNote.noteId, parentBranchId); | ||||
|         } | ||||
|  | ||||
|         // copy will keep clipboardBranchIds and clipboardMode so it's possible to paste into multiple places | ||||
|   | ||||
| @@ -10,6 +10,7 @@ const CODE_MIRROR = { | ||||
|         "libraries/codemirror/addon/edit/matchtags.js", | ||||
|         "libraries/codemirror/addon/search/match-highlighter.js", | ||||
|         "libraries/codemirror/mode/meta.js", | ||||
|         "libraries/codemirror/keymap/vim.js", | ||||
|         "libraries/codemirror/addon/lint/lint.js", | ||||
|         "libraries/codemirror/addon/lint/eslint.js" | ||||
|     ], | ||||
|   | ||||
							
								
								
									
										21
									
								
								src/public/app/share.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/public/app/share.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| /** | ||||
|  * Fetch note with given ID from backend | ||||
|  * | ||||
|  * @param noteId of the given note to be fetched. If falsy, fetches current note. | ||||
|  */ | ||||
| async function fetchNote(noteId = null) { | ||||
|     if (!noteId) { | ||||
|         noteId = document.body.getAttribute("data-note-id"); | ||||
|     } | ||||
|  | ||||
|     const resp = await fetch(`api/notes/${noteId}`); | ||||
|  | ||||
|     return await resp.json(); | ||||
| } | ||||
|  | ||||
| document.addEventListener('DOMContentLoaded', () => { | ||||
|     const toggleMenuButton = document.getElementById('toggleMenuButton'); | ||||
|     const layout = document.getElementById('layout'); | ||||
|  | ||||
|     toggleMenuButton.addEventListener('click', () => layout.classList.toggle('showMenu')); | ||||
| }, false); | ||||
| @@ -226,6 +226,8 @@ const ATTR_HELP = { | ||||
|         "renderNote": 'notes of type "render HTML note" will be rendered using a code note (HTML or script) and it is necessary to point using this relation to which note should be rendered', | ||||
|         "widget": "target of this relation will be executed and rendered as a widget in the sidebar", | ||||
|         "shareCss": "CSS note which will be injected into the share page. CSS note must be in the shared sub-tree as well. Consider using 'shareHiddenFromTree' and 'shareOmitDefaultCss' as well.", | ||||
|         "shareJs": "JavaScript note which will be injected into the share page. JS note must be in the shared sub-tree as well. Consider using 'shareHiddenFromTree'.", | ||||
|         "shareFavicon": "Favicon note to be set in the shared page. Typically you want to set it to share root and make it inheritable. Favicon note must be in the shared sub-tree as well. Consider using 'shareHiddenFromTree'.", | ||||
|     } | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -22,7 +22,7 @@ export default class SharedSwitchWidget extends SwitchWidget { | ||||
|     } | ||||
|  | ||||
|     switchOn() { | ||||
|         branchService.cloneNoteTo(this.noteId, 'share'); | ||||
|         branchService.cloneNoteToNote(this.noteId, 'share'); | ||||
|     } | ||||
|  | ||||
|     async switchOff() { | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import ws from "../../services/ws.js"; | ||||
| import appContext from "../../services/app_context.js"; | ||||
| import toastService from "../../services/toast.js"; | ||||
| import treeService from "../../services/tree.js"; | ||||
| import options from "../../services/options.js"; | ||||
|  | ||||
| const TPL = ` | ||||
| <div class="note-detail-code note-detail-printable"> | ||||
| @@ -94,6 +95,7 @@ export default class EditableCodeTypeWidget extends TypeWidget { | ||||
|             viewportMargin: Infinity, | ||||
|             indentUnit: 4, | ||||
|             matchBrackets: true, | ||||
|             keyMap: options.is('vimKeymapEnabled') ? "vim": "default", | ||||
|             matchTags: {bothTags: true}, | ||||
|             highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: false}, | ||||
|             lint: true, | ||||
|   | ||||
| @@ -32,15 +32,18 @@ body { | ||||
| #main { | ||||
|     flex-basis: 0; | ||||
|     flex-grow: 3; | ||||
|     overflow: auto; | ||||
|     padding: 10px 20px 20px 20px; | ||||
| } | ||||
|  | ||||
| #parentLink { | ||||
|     float: right; | ||||
|     margin-top: 20px; | ||||
| } | ||||
|  | ||||
| #title { | ||||
|     margin: 0; | ||||
|     padding: 20px 20px 0 20px; | ||||
| } | ||||
|  | ||||
| #content { | ||||
|     padding: 20px; | ||||
|     padding-top: 10px; | ||||
| } | ||||
|  | ||||
| img { | ||||
| @@ -57,7 +60,7 @@ iframe.pdf-view { | ||||
|     height: 800px; | ||||
| } | ||||
|  | ||||
| #menuButton { | ||||
| #toggleMenuButton { | ||||
|     display: none; | ||||
|     position: fixed; | ||||
|     top: 8px; | ||||
| @@ -72,23 +75,74 @@ iframe.pdf-view { | ||||
|     cursor: pointer; | ||||
| } | ||||
|  | ||||
| #menuButton::after { | ||||
| #childLinks.grid ul { | ||||
|     list-style-type: none; | ||||
|     display: flex; | ||||
|     flex-wrap: wrap; | ||||
|     padding: 0; | ||||
| } | ||||
|  | ||||
| #childLinks.grid ul li { | ||||
|     width: 180px; | ||||
|     height: 140px; | ||||
|     padding: 10px; | ||||
| } | ||||
|  | ||||
| #childLinks.grid ul li a { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     height: 100%; | ||||
|     width: 100%; | ||||
|     border: 1px solid #ddd; | ||||
|     border-radius: 5px; | ||||
|     justify-content: center; | ||||
|     align-content: center; | ||||
|     text-align: center; | ||||
|     font-size: large; | ||||
| } | ||||
|  | ||||
| #childLinks.grid ul li a:hover { | ||||
|     background: #eee; | ||||
| } | ||||
|  | ||||
| #childLinks.list ul { | ||||
|     list-style-type: none; | ||||
|     display: inline-flex; | ||||
|     flex-wrap: wrap; | ||||
|     padding: 0; | ||||
|     margin-top: 5px; | ||||
| } | ||||
|  | ||||
| #childLinks.list ul li { | ||||
|     margin-right: 20px; | ||||
| } | ||||
|  | ||||
| #noteClippedFrom { | ||||
|     padding: 10px 0 10px 0; | ||||
|     margin: 20px 0 20px 0; | ||||
|     color: #666; | ||||
|     border: 1px solid #ddd; | ||||
|     border-left: 0; | ||||
|     border-right: 0; | ||||
| } | ||||
|  | ||||
| #toggleMenuButton::after { | ||||
|     position: relative; | ||||
|     top: -2px; | ||||
|     left: 1px; | ||||
| } | ||||
|  | ||||
| @media (max-width: 48em) { | ||||
|     #layout.navMenu #menu { | ||||
|     #layout.showMenu #menu { | ||||
|         display: block; | ||||
|         margin-top: 40px; | ||||
|     } | ||||
|  | ||||
|     #menuButton { | ||||
|     #toggleMenuButton { | ||||
|         display: block; | ||||
|     } | ||||
|  | ||||
|     #layout.navMenu #main { | ||||
|     #layout.showMenu #main { | ||||
|         display: none; | ||||
|     } | ||||
|  | ||||
| @@ -96,11 +150,11 @@ iframe.pdf-view { | ||||
|         padding-left: 60px; | ||||
|     } | ||||
|  | ||||
|     #layout.navMenu #menuButton::after { | ||||
|     #layout.showMenu #toggleMenuButton::after { | ||||
|         content: "«"; | ||||
|     } | ||||
|  | ||||
|     #menuButton::after { | ||||
|     #toggleMenuButton::after { | ||||
|         content: "»"; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -2,11 +2,18 @@ | ||||
|  | ||||
| const cloningService = require('../../services/cloning'); | ||||
|  | ||||
| function cloneNoteToParent(req) { | ||||
| function cloneNoteToBranch(req) { | ||||
|     const {noteId, parentBranchId} = req.params; | ||||
|     const {prefix} = req.body; | ||||
|  | ||||
|     return cloningService.cloneNoteToParent(noteId, parentBranchId, prefix); | ||||
|     return cloningService.cloneNoteToBranch(noteId, parentBranchId, prefix); | ||||
| } | ||||
|  | ||||
| function cloneNoteToNote(req) { | ||||
|     const {noteId, parentNoteId} = req.params; | ||||
|     const {prefix} = req.body; | ||||
|  | ||||
|     return cloningService.cloneNoteToNote(noteId, parentNoteId, prefix); | ||||
| } | ||||
|  | ||||
| function cloneNoteAfter(req) { | ||||
| @@ -16,6 +23,7 @@ function cloneNoteAfter(req) { | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     cloneNoteToParent, | ||||
|     cloneNoteToBranch, | ||||
|     cloneNoteToNote, | ||||
|     cloneNoteAfter | ||||
| }; | ||||
|   | ||||
| @@ -33,6 +33,7 @@ const ALLOWED_OPTIONS = new Set([ | ||||
|     'similarNotesWidget', | ||||
|     'editedNotesWidget', | ||||
|     'calendarWidget', | ||||
|     'vimKeymapEnabled', | ||||
|     'codeNotesMimeTypes', | ||||
|     'spellCheckEnabled', | ||||
|     'spellCheckLanguageCode', | ||||
|   | ||||
| @@ -229,7 +229,8 @@ function register(app) { | ||||
|  | ||||
|     apiRoute(GET, '/api/edited-notes/:date', noteRevisionsApiRoute.getEditedNotesOnDate); | ||||
|  | ||||
|     apiRoute(PUT, '/api/notes/:noteId/clone-to/:parentBranchId', cloningApiRoute.cloneNoteToParent); | ||||
|     apiRoute(PUT, '/api/notes/:noteId/clone-to-branch/:parentBranchId', cloningApiRoute.cloneNoteToBranch); | ||||
|     apiRoute(PUT, '/api/notes/:noteId/clone-to-note/:parentNoteId', cloningApiRoute.cloneNoteToNote); | ||||
|     apiRoute(PUT, '/api/notes/:noteId/clone-after/:afterBranchId', cloningApiRoute.cloneNoteAfter); | ||||
|  | ||||
|     route(GET, '/api/notes/:branchId/export/:type/:format/:version/:taskId', [auth.checkApiAuthOrElectron], exportRoute.exportBranch); | ||||
|   | ||||
| @@ -67,6 +67,8 @@ const BUILTIN_ATTRIBUTES = [ | ||||
|     { type: 'relation', name: 'widget', isDangerous: true }, | ||||
|     { type: 'relation', name: 'renderNote', isDangerous: true }, | ||||
|     { type: 'relation', name: 'shareCss', isDangerous: false }, | ||||
|     { type: 'relation', name: 'shareJs', isDangerous: false }, | ||||
|     { type: 'relation', name: 'shareFavicon', isDangerous: false }, | ||||
| ]; | ||||
|  | ||||
| /** @returns {Note[]} */ | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| module.exports = { buildDate:"2021-12-24T23:05:10+01:00", buildRevision: "0217b1c85de9a2824e7f07d07a357064c5803383" }; | ||||
| module.exports = { buildDate:"2022-01-02T22:43:30+01:00", buildRevision: "feffd57f240438d107c1ed1c1772545611a97dee" }; | ||||
|   | ||||
| @@ -10,20 +10,18 @@ const utils = require('./utils'); | ||||
| const becca = require("../becca/becca"); | ||||
| const beccaService = require("../becca/becca_service"); | ||||
|  | ||||
| function cloneNoteToParent(noteId, parentBranchId, prefix) { | ||||
|     if (parentBranchId === 'share') { | ||||
| function cloneNoteToNote(noteId, parentNoteId, prefix) { | ||||
|     if (parentNoteId === 'share') { | ||||
|         const specialNotesService = require('./special_notes'); | ||||
|         // share root note is created lazily | ||||
|         specialNotesService.getShareRoot(); | ||||
|     } | ||||
|  | ||||
|     const parentBranch = becca.getBranch(parentBranchId); | ||||
|  | ||||
|     if (isNoteDeleted(noteId) || isNoteDeleted(parentBranch.noteId)) { | ||||
|     if (isNoteDeleted(noteId) || isNoteDeleted(parentNoteId)) { | ||||
|         return { success: false, message: 'Note is deleted.' }; | ||||
|     } | ||||
|  | ||||
|     const validationResult = treeService.validateParentChild(parentBranch.noteId, noteId); | ||||
|     const validationResult = treeService.validateParentChild(parentNoteId, noteId); | ||||
|  | ||||
|     if (!validationResult.success) { | ||||
|         return validationResult; | ||||
| @@ -31,21 +29,33 @@ function cloneNoteToParent(noteId, parentBranchId, prefix) { | ||||
|  | ||||
|     const branch = new Branch({ | ||||
|         noteId: noteId, | ||||
|         parentNoteId: parentBranch.noteId, | ||||
|         parentNoteId: parentNoteId, | ||||
|         prefix: prefix, | ||||
|         isExpanded: 0 | ||||
|     }).save(); | ||||
|  | ||||
|     parentBranch.isExpanded = true; // the new target should be expanded so it immediately shows up to the user | ||||
|     parentBranch.save(); | ||||
|  | ||||
|     return { | ||||
|         success: true, | ||||
|         branchId: branch.branchId, | ||||
|         notePath: beccaService.getNotePath(parentBranch.noteId).path + "/" + noteId | ||||
|         notePath: beccaService.getNotePath(parentNoteId).path + "/" + noteId | ||||
|     }; | ||||
| } | ||||
|  | ||||
| function cloneNoteToBranch(noteId, parentBranchId, prefix) { | ||||
|     const parentBranch = becca.getBranch(parentBranchId); | ||||
|  | ||||
|     if (!parentBranch) { | ||||
|         return { success: false, message: `Parent branch ${parentBranchId} does not exist.` }; | ||||
|     } | ||||
|  | ||||
|     const ret = cloneNoteToNote(noteId, parentBranch.noteId, prefix); | ||||
|  | ||||
|     parentBranch.isExpanded = true; // the new target should be expanded so it immediately shows up to the user | ||||
|     parentBranch.save(); | ||||
|  | ||||
|     return ret; | ||||
| } | ||||
|  | ||||
| function ensureNoteIsPresentInParent(noteId, parentNoteId, prefix) { | ||||
|     if (isNoteDeleted(noteId) || isNoteDeleted(parentNoteId)) { | ||||
|         return { success: false, message: 'Note is deleted.' }; | ||||
| @@ -121,7 +131,8 @@ function isNoteDeleted(noteId) { | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     cloneNoteToParent, | ||||
|     cloneNoteToBranch, | ||||
|     cloneNoteToNote, | ||||
|     ensureNoteIsPresentInParent, | ||||
|     ensureNoteIsAbsentFromParent, | ||||
|     toggleNoteInParent, | ||||
|   | ||||
| @@ -259,7 +259,7 @@ class ConsistencyChecks { | ||||
|                              WHERE noteId = ? | ||||
|                                and parentNoteId = ? | ||||
|                                and isDeleted = 0 | ||||
|                              ORDER BY utcDateCreated`, [noteId, parentNoteId]); | ||||
|                              ORDER BY utcDateModified`, [noteId, parentNoteId]); | ||||
|  | ||||
|                     const branches = branchIds.map(branchId => becca.getBranch(branchId)); | ||||
|  | ||||
|   | ||||
| @@ -54,7 +54,7 @@ function getYearNote(dateStr, rootNote) { | ||||
|         rootNote = getRootCalendarNote(); | ||||
|     } | ||||
|  | ||||
|     const yearStr = dateStr.substr(0, 4); | ||||
|     const yearStr = dateStr.trim().substr(0, 4); | ||||
|  | ||||
|     let yearNote = attributeService.getNoteWithLabel(YEAR_LABEL, yearStr); | ||||
|  | ||||
| @@ -138,6 +138,8 @@ function getDateNoteTitle(rootNote, dayNumber, dateObj) { | ||||
|  | ||||
| /** @returns {Note} */ | ||||
| function getDateNote(dateStr) { | ||||
|     dateStr = dateStr.trim().substr(0, 10); | ||||
|  | ||||
|     let dateNote = attributeService.getNoteWithLabel(DATE_LABEL, dateStr); | ||||
|  | ||||
|     if (dateNote) { | ||||
|   | ||||
| @@ -3,6 +3,10 @@ const sanitizeHtml = require('sanitize-html'); | ||||
| // intended mainly as protection against XSS via import | ||||
| // secondarily it (partly) protects against "CSS takeover" | ||||
| function sanitize(dirtyHtml) { | ||||
|     if (!dirtyHtml) { | ||||
|         return dirtyHtml; | ||||
|     } | ||||
|  | ||||
|     // avoid H1 per https://github.com/zadam/trilium/issues/1552 | ||||
|     // demote H1, and if that conflicts with existing H2, demote that, etc | ||||
|     const transformTags = {}; | ||||
|   | ||||
| @@ -51,7 +51,7 @@ async function importOpml(taskContext, fileBuffer, parentNote) { | ||||
|             throw new Error("Unrecognized OPML version " + opmlVersion); | ||||
|         } | ||||
|  | ||||
|         content = htmlSanitizer.sanitize(content); | ||||
|         content = htmlSanitizer.sanitize(content || ""); | ||||
|  | ||||
|         const {note} = noteService.createNewNote({ | ||||
|             parentNoteId, | ||||
|   | ||||
| @@ -240,13 +240,15 @@ async function importZip(taskContext, fileBuffer, importRootNote) { | ||||
|         } | ||||
|  | ||||
|         if (noteMeta && noteMeta.isClone) { | ||||
|             new Branch({ | ||||
|                 noteId, | ||||
|                 parentNoteId, | ||||
|                 isExpanded: noteMeta.isExpanded, | ||||
|                 prefix: noteMeta.prefix, | ||||
|                 notePosition: noteMeta.notePosition | ||||
|             }).save(); | ||||
|             if (!becca.getBranchFromChildAndParent(noteId, parentNoteId)) { | ||||
|                 new Branch({ | ||||
|                     noteId, | ||||
|                     parentNoteId, | ||||
|                     isExpanded: noteMeta.isExpanded, | ||||
|                     prefix: noteMeta.prefix, | ||||
|                     notePosition: noteMeta.notePosition | ||||
|                 }).save(); | ||||
|             } | ||||
|  | ||||
|             return; | ||||
|         } | ||||
| @@ -365,6 +367,16 @@ async function importZip(taskContext, fileBuffer, importRootNote) { | ||||
|             } | ||||
|  | ||||
|             note.setContent(content); | ||||
|  | ||||
|             if (!becca.getBranchFromChildAndParent(noteId, parentNoteId)) { | ||||
|                 new Branch({ | ||||
|                     noteId, | ||||
|                     parentNoteId, | ||||
|                     isExpanded: noteMeta.isExpanded, | ||||
|                     prefix: noteMeta.prefix, | ||||
|                     notePosition: noteMeta.notePosition | ||||
|                 }).save(); | ||||
|             } | ||||
|         } | ||||
|         else { | ||||
|             ({note} = noteService.createNewNote({ | ||||
|   | ||||
| @@ -360,7 +360,7 @@ function downloadImages(noteId, content) { | ||||
|             // which upon the download of all the images will update the note if the links have not been fixed before | ||||
|  | ||||
|             sql.transactional(() => { | ||||
|                 const imageNotes = becca.getNotes(Object.values(imageUrlToNoteIdMapping)); | ||||
|                 const imageNotes = becca.getNotes(Object.values(imageUrlToNoteIdMapping), true); | ||||
|  | ||||
|                 const origNote = becca.getNote(noteId); | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,16 @@ | ||||
| const becca = require('../becca/becca'); | ||||
| const sql = require("./sql.js"); | ||||
|  | ||||
| function getOption(name) { | ||||
|     const option = require('../becca/becca').getOption(name); | ||||
|     let option; | ||||
|  | ||||
|     if (becca.loaded) { | ||||
|         option = becca.getOption(name); | ||||
|     } | ||||
|     else { | ||||
|         // e.g. in initial sync becca is not loaded because DB is not initialized | ||||
|         option = sql.getRow("SELECT * FROM options WHERE name = ?", name); | ||||
|     } | ||||
|  | ||||
|     if (!option) { | ||||
|         throw new Error(`Option "${name}" doesn't exist`); | ||||
| @@ -39,12 +48,12 @@ function getOptionBool(name) { | ||||
| } | ||||
|  | ||||
| function setOption(name, value) { | ||||
|     const option = becca.getOption(name); | ||||
|  | ||||
|     if (value === true || value === false) { | ||||
|         value = value.toString(); | ||||
|     } | ||||
|  | ||||
|     const option = becca.getOption(name); | ||||
|  | ||||
|     if (option) { | ||||
|         option.value = value; | ||||
|  | ||||
|   | ||||
| @@ -70,6 +70,7 @@ const defaultOptions = [ | ||||
|     { name: 'imageMaxWidthHeight', value: '2000', isSynced: true }, | ||||
|     { name: 'imageJpegQuality', value: '75', isSynced: true }, | ||||
|     { name: 'autoFixConsistencyIssues', value: 'true', isSynced: false }, | ||||
|     { name: 'vimKeymapEnabled', value: 'false', isSynced: false }, | ||||
|     { name: 'codeNotesMimeTypes', value: '["text/x-csrc","text/x-c++src","text/x-csharp","text/css","text/x-go","text/x-groovy","text/x-haskell","text/html","message/http","text/x-java","application/javascript;env=frontend","application/javascript;env=backend","application/json","text/x-kotlin","text/x-markdown","text/x-perl","text/x-php","text/x-python","text/x-ruby",null,"text/x-sql","text/x-sqlite;schema=trilium","text/x-swift","text/xml","text/x-yaml"]', isSynced: true }, | ||||
|     { name: 'leftPaneWidth', value: '25', isSynced: false }, | ||||
|     { name: 'leftPaneVisible', value: 'true', isSynced: false }, | ||||
|   | ||||
| @@ -371,7 +371,10 @@ function getLastSyncedPull() { | ||||
|  | ||||
| function setLastSyncedPull(entityChangeId) { | ||||
|     const lastSyncedPullOption = becca.getOption('lastSyncedPull'); | ||||
|     lastSyncedPullOption.value = entityChangeId + ''; | ||||
|  | ||||
|     if (lastSyncedPullOption) { // might be null in initial sync when becca is not loaded | ||||
|         lastSyncedPullOption.value = entityChangeId + ''; | ||||
|     } | ||||
|  | ||||
|     // this way we avoid updating entity_changes which otherwise means that we've never pushed all entity_changes | ||||
|     sql.execute("UPDATE options SET value = ? WHERE name = ?", [entityChangeId, 'lastSyncedPull']); | ||||
| @@ -389,7 +392,10 @@ function setLastSyncedPush(entityChangeId) { | ||||
|     ws.setLastSyncedPush(entityChangeId); | ||||
|  | ||||
|     const lastSyncedPushOption = becca.getOption('lastSyncedPush'); | ||||
|     lastSyncedPushOption.value = entityChangeId + ''; | ||||
|  | ||||
|     if (lastSyncedPushOption) { // might be null in initial sync when becca is not loaded | ||||
|         lastSyncedPushOption.value = entityChangeId + ''; | ||||
|     } | ||||
|  | ||||
|     // this way we avoid updating entity_changes which otherwise means that we've never pushed all entity_changes | ||||
|     sql.execute("UPDATE options SET value = ? WHERE name = ?", [entityChangeId, 'lastSyncedPush']); | ||||
|   | ||||
| @@ -16,7 +16,10 @@ let mainWindow; | ||||
| let setupWindow; | ||||
|  | ||||
| async function createExtraWindow(notePath, hoistedNoteId = 'root') { | ||||
|     const spellcheckEnabled = optionService.getOptionBool('spellCheckEnabled'); | ||||
|  | ||||
|     const {BrowserWindow} = require('electron'); | ||||
|  | ||||
|     const win = new BrowserWindow({ | ||||
|         width: 1000, | ||||
|         height: 800, | ||||
| @@ -25,7 +28,7 @@ async function createExtraWindow(notePath, hoistedNoteId = 'root') { | ||||
|             enableRemoteModule: true, | ||||
|             nodeIntegration: true, | ||||
|             contextIsolation: false, | ||||
|             spellcheck: optionService.getOptionBool('spellCheckEnabled') | ||||
|             spellcheck: spellcheckEnabled | ||||
|         }, | ||||
|         frame: optionService.getOptionBool('nativeTitleBarVisible'), | ||||
|         icon: getIcon() | ||||
| @@ -33,6 +36,8 @@ async function createExtraWindow(notePath, hoistedNoteId = 'root') { | ||||
|  | ||||
|     win.setMenuBarVisibility(false); | ||||
|     win.loadURL('http://127.0.0.1:' + await port + '/?extra=1&extraHoistedNoteId=' + hoistedNoteId + '#' + notePath); | ||||
|  | ||||
|     configureWebContents(win.webContents, spellcheckEnabled); | ||||
| } | ||||
|  | ||||
| ipcMain.on('create-extra-window', (event, arg) => { | ||||
| @@ -74,8 +79,10 @@ async function createMainWindow() { | ||||
|     mainWindow.loadURL('http://127.0.0.1:' + await port); | ||||
|     mainWindow.on('closed', () => mainWindow = null); | ||||
|  | ||||
|     const {webContents} = mainWindow; | ||||
|     configureWebContents(mainWindow.webContents, spellcheckEnabled); | ||||
| } | ||||
|  | ||||
| function configureWebContents(webContents, spellcheckEnabled) { | ||||
|     require("@electron/remote/main").enable(webContents); | ||||
|  | ||||
|     webContents.on('new-window', (e, url) => { | ||||
|   | ||||
| @@ -1,43 +1,18 @@ | ||||
| const {JSDOM} = require("jsdom"); | ||||
| const NO_CONTENT = '<p>This note has no content.</p>'; | ||||
| const shaca = require("./shaca/shaca"); | ||||
|  | ||||
| function getChildrenList(note) { | ||||
|     if (note.hasChildren()) { | ||||
|         const document = new JSDOM().window.document; | ||||
|  | ||||
|         const ulEl = document.createElement("ul"); | ||||
|  | ||||
|         for (const childNote of note.getChildNotes()) { | ||||
|             const li = document.createElement("li"); | ||||
|             const link = document.createElement("a"); | ||||
|             link.appendChild(document.createTextNode(childNote.title)); | ||||
|             link.setAttribute("href", childNote.noteId); | ||||
|  | ||||
|             li.appendChild(link); | ||||
|             ulEl.appendChild(li); | ||||
|         } | ||||
|  | ||||
|         return '<p>Child notes:</p>' + ulEl.outerHTML; | ||||
|     } | ||||
|     else { | ||||
|         return ''; | ||||
|     } | ||||
| } | ||||
|  | ||||
| function getContent(note) { | ||||
|     let content = note.getContent(); | ||||
|     let header = ''; | ||||
|     let isEmpty = false; | ||||
|  | ||||
|     if (note.type === 'text') { | ||||
|         const document = new JSDOM(content || "").window.document; | ||||
|  | ||||
|         const isEmpty = document.body.textContent.trim().length === 0 | ||||
|         isEmpty = document.body.textContent.trim().length === 0 | ||||
|             && document.querySelectorAll("img").length === 0; | ||||
|  | ||||
|         if (isEmpty) { | ||||
|             content = NO_CONTENT + getChildrenList(note); | ||||
|         } | ||||
|         else { | ||||
|         if (!isEmpty) { | ||||
|             for (const linkEl of document.querySelectorAll("a")) { | ||||
|                 const href = linkEl.getAttribute("href"); | ||||
|  | ||||
| @@ -49,6 +24,7 @@ function getContent(note) { | ||||
|  | ||||
|                     if (linkedNote) { | ||||
|                         linkEl.setAttribute("href", linkedNote.shareId); | ||||
|                         linkEl.classList.add("type-" + linkedNote.type); | ||||
|                     } | ||||
|                     else { | ||||
|                         linkEl.removeAttribute("href"); | ||||
| @@ -57,11 +33,24 @@ function getContent(note) { | ||||
|             } | ||||
|  | ||||
|             content = document.body.innerHTML; | ||||
|  | ||||
|             if (content.includes(`<span class="math-tex">`)) { | ||||
|                 header += ` | ||||
| <script src="../../libraries/katex/katex.min.js"></script> | ||||
| <link rel="stylesheet" href="../../libraries/katex/katex.min.css"> | ||||
| <script src="../../libraries/katex/auto-render.min.js"></script> | ||||
| <script src="../../libraries/katex/mhchem.min.js"></script> | ||||
| <script> | ||||
| document.addEventListener("DOMContentLoaded", function() { | ||||
|     renderMathInElement(document.getElementById('content')); | ||||
| }); | ||||
| </script>`; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     else if (note.type === 'code' || note.type === 'mermaid') { | ||||
|     else if (note.type === 'code') { | ||||
|         if (!content?.trim()) { | ||||
|             content = NO_CONTENT + getChildrenList(note); | ||||
|             isEmpty = true; | ||||
|         } | ||||
|         else { | ||||
|             const document = new JSDOM().window.document; | ||||
| @@ -72,6 +61,16 @@ function getContent(note) { | ||||
|             content = preEl.outerHTML; | ||||
|         } | ||||
|     } | ||||
|     else if (note.type === 'mermaid') { | ||||
|         content = ` | ||||
| <div class="mermaid">${content}</div> | ||||
| <hr> | ||||
| <details> | ||||
|     <summary>Chart source</summary> | ||||
|     <pre>${content}</pre> | ||||
| </details>` | ||||
|         header += `<script src="../../libraries/mermaid.min.js"></script>`; | ||||
|     } | ||||
|     else if (note.type === 'image') { | ||||
|         content = `<img src="api/images/${note.noteId}/${note.title}?${note.utcDateModified}">`; | ||||
|     } | ||||
| @@ -84,15 +83,23 @@ function getContent(note) { | ||||
|         } | ||||
|     } | ||||
|     else if (note.type === 'book') { | ||||
|         content = getChildrenList(note); | ||||
|         isEmpty = true; | ||||
|     } | ||||
|     else { | ||||
|         content = '<p>This note type cannot be displayed.</p>' + getChildrenList(note); | ||||
|         content = '<p>This note type cannot be displayed.</p>'; | ||||
|     } | ||||
|  | ||||
|     return content; | ||||
|     return { | ||||
|         header, | ||||
|         content, | ||||
|         isEmpty | ||||
|     }; | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     getContent | ||||
| }; | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -29,13 +29,15 @@ function register(router) { | ||||
|         const note = shaca.aliasToNote[shareId] || shaca.notes[shareId]; | ||||
|  | ||||
|         if (note) { | ||||
|             const content = contentRenderer.getContent(note); | ||||
|             const {header, content, isEmpty} = contentRenderer.getContent(note); | ||||
|  | ||||
|             const subRoot = getSharedSubTreeRoot(note); | ||||
|  | ||||
|             res.render("share/page", { | ||||
|                 note, | ||||
|                 header, | ||||
|                 content, | ||||
|                 isEmpty, | ||||
|                 subRoot | ||||
|             }); | ||||
|         } | ||||
| @@ -44,19 +46,15 @@ function register(router) { | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     router.get('/share/api/images/:noteId/:filename', (req, res, next) => { | ||||
|         const image = shaca.getNote(req.params.noteId); | ||||
|     router.get('/share/api/notes/:noteId', (req, res, next) => { | ||||
|         const {noteId} = req.params; | ||||
|         const note = shaca.getNote(noteId); | ||||
|  | ||||
|         if (!image) { | ||||
|             return res.status(404).send("Not found"); | ||||
|         } | ||||
|         else if (image.type !== 'image') { | ||||
|             return res.status(400).send("Requested note is not an image"); | ||||
|         if (!note) { | ||||
|             return res.status(404).send(`Note ${noteId} not found`); | ||||
|         } | ||||
|  | ||||
|         res.set('Content-Type', image.mime); | ||||
|  | ||||
|         res.send(image.getContent()); | ||||
|         res.json(note.getPojoWithAttributes()); | ||||
|     }); | ||||
|  | ||||
|     router.get('/share/api/notes/:noteId/download', (req, res, next) => { | ||||
| @@ -64,7 +62,7 @@ function register(router) { | ||||
|         const note = shaca.getNote(noteId); | ||||
|  | ||||
|         if (!note) { | ||||
|             return res.status(404).send(`Not found`); | ||||
|             return res.status(404).send(`Note ${noteId} not found`); | ||||
|         } | ||||
|  | ||||
|         const utils = require("../services/utils"); | ||||
| @@ -79,20 +77,30 @@ function register(router) { | ||||
|         res.send(note.getContent()); | ||||
|     }); | ||||
|  | ||||
|     router.get('/share/api/images/:noteId/:filename', (req, res, next) => { | ||||
|         const image = shaca.getNote(req.params.noteId); | ||||
|  | ||||
|         if (!image) { | ||||
|             return res.status(404).send(`Note ${noteId} not found`); | ||||
|         } | ||||
|         else if (image.type !== 'image') { | ||||
|             return res.status(400).send("Requested note is not an image"); | ||||
|         } | ||||
|  | ||||
|         res.set('Content-Type', image.mime); | ||||
|  | ||||
|         res.send(image.getContent()); | ||||
|     }); | ||||
|  | ||||
|     // used for PDF viewing | ||||
|     router.get('/share/api/notes/:noteId/view', (req, res, next) => { | ||||
|         const {noteId} = req.params; | ||||
|         const note = shaca.getNote(noteId); | ||||
|  | ||||
|         if (!note) { | ||||
|             return res.status(404).send(`Not found`); | ||||
|             return res.status(404).send(`Note ${noteId} not found`); | ||||
|         } | ||||
|  | ||||
|         const utils = require("../services/utils"); | ||||
|  | ||||
|         const filename = utils.formatDownloadTitle(note.title, note.type, note.mime); | ||||
|  | ||||
|         // res.setHeader('Content-Disposition', utils.getContentDisposition(filename)); | ||||
|  | ||||
|         res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); | ||||
|         res.setHeader('Content-Type', note.mime); | ||||
|  | ||||
|   | ||||
| @@ -89,6 +89,18 @@ class Attribute extends AbstractEntity { | ||||
|  | ||||
|         return this.shaca.getNote(this.value); | ||||
|     } | ||||
|  | ||||
|     getPojo() { | ||||
|         return { | ||||
|             attributeId: this.attributeId, | ||||
|             noteId: this.noteId, | ||||
|             type: this.type, | ||||
|             name: this.name, | ||||
|             position: this.position, | ||||
|             value: this.value, | ||||
|             isInheritable: this.isInheritable | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|  | ||||
| module.exports = Attribute; | ||||
|   | ||||
| @@ -410,6 +410,19 @@ class Note extends AbstractEntity { | ||||
|  | ||||
|         return sharedAlias || this.noteId; | ||||
|     } | ||||
|  | ||||
|     getPojoWithAttributes() { | ||||
|         return { | ||||
|             noteId: this.noteId, | ||||
|             title: this.title, | ||||
|             type: this.type, | ||||
|             mime: this.mime, | ||||
|             utcDateModified: this.utcDateModified, | ||||
|             attributes: this.getAttributes().map(attr => attr.getPojo()), | ||||
|             parentNoteIds: this.parents.map(parentNote => parentNote.noteId), | ||||
|             childNoteIds: this.children.map(child => child.noteId) | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|  | ||||
| module.exports = Note; | ||||
|   | ||||
| @@ -59,11 +59,7 @@ function load() { | ||||
|         SELECT attributeId, noteId, type, name, value, isInheritable, position, utcDateModified  | ||||
|         FROM attributes  | ||||
|         WHERE isDeleted = 0  | ||||
|           AND noteId IN (${noteIdStr}) | ||||
|           AND ( | ||||
|               (type = 'label' AND name IN ('archived', 'shareHiddenFromTree', 'shareAlias', 'shareOmitDefaultCss'))  | ||||
|               OR (type = 'relation' AND name IN ('imageLink', 'template', 'shareCss')) | ||||
|           )`, []); | ||||
|           AND noteId IN (${noteIdStr})`); | ||||
|  | ||||
|     for (const row of rawAttributeRows) { | ||||
|         new Attribute(row); | ||||
|   | ||||
| @@ -61,7 +61,7 @@ async function start() { | ||||
|             const parentNoteId = getRandomNoteId(); | ||||
|             const prefix = Math.random() > 0.8 ? "prefix" : null; | ||||
|  | ||||
|             const result = await cloningService.cloneNoteToParent(noteIdToClone, parentNoteId, prefix); | ||||
|             const result = await cloningService.cloneNoteToBranch(noteIdToClone, parentNoteId, prefix); | ||||
|  | ||||
|             console.log(`Cloning ${i}:`, result.success ? "succeeded" : "FAILED"); | ||||
|         } | ||||
|   | ||||
| @@ -2,45 +2,79 @@ | ||||
| <html lang="en"> | ||||
| <head> | ||||
|     <meta charset="utf-8"> | ||||
|     <% if (note.hasRelation("shareFavicon")) { %> | ||||
|     <link rel="shortcut icon" href="api/notes/<%= note.getRelation("shareFavicon").value %>/download"> | ||||
|     <% } else { %> | ||||
|     <link rel="shortcut icon" href="../favicon.ico"> | ||||
|     <% } %> | ||||
|     <script src="../app/share.js"></script> | ||||
|     <% if (!note.hasLabel("shareOmitDefaultCss")) { %> | ||||
|     <link href="../libraries/normalize.min.css" rel="stylesheet"> | ||||
|     <link href="../stylesheets/share.css" rel="stylesheet"> | ||||
|         <link href="../libraries/normalize.min.css" rel="stylesheet"> | ||||
|         <link href="../stylesheets/share.css" rel="stylesheet"> | ||||
|     <% } %> | ||||
|     <% if (note.type === 'text' || note.type === 'book') { %> | ||||
|     <link href="../libraries/ckeditor/ckeditor-content.css" rel="stylesheet"> | ||||
|         <link href="../libraries/ckeditor/ckeditor-content.css" rel="stylesheet"> | ||||
|     <% } %> | ||||
|     <% for (const cssRelation of note.getRelations("shareCss")) { %> | ||||
|     <link href="api/notes/<%= cssRelation.value %>/download" rel="stylesheet"> | ||||
|         <link href="api/notes/<%= cssRelation.value %>/download" rel="stylesheet"> | ||||
|     <% } %> | ||||
|     <% for (const jsRelation of note.getRelations("shareJs")) { %> | ||||
|         <script type="module" src="api/notes/<%= jsRelation.value %>/download"></script> | ||||
|     <% } %> | ||||
|     <%- header %> | ||||
|     <title><%= note.title %></title> | ||||
| </head> | ||||
| <body> | ||||
|     <div id="layout"> | ||||
|         <div id="main"> | ||||
|             <h1 id="title"><%= note.title %></h1> | ||||
| <body data-note-id="<%= note.noteId %>"> | ||||
| <div id="layout"> | ||||
|     <div id="main"> | ||||
|         <% if (note.parents[0].noteId !== 'share' && note.parents.length !== 0) { %> | ||||
|             <nav id="parentLink"> | ||||
|                 parent: <a href="<%= note.parents[0].noteId %>" | ||||
|                            class="type-<%= note.parents[0].type %>"><%= note.parents[0].title %></a> | ||||
|             </nav> | ||||
|         <% } %> | ||||
|  | ||||
|             <div id="content" class="note-<%= note.type %> <% if (note.type === 'text') { %>ck-content<% } %>"> | ||||
|         <h1 id="title"><%= note.title %></h1> | ||||
|  | ||||
|         <% if (note.hasLabel("pageUrl")) { %> | ||||
|             <div id="noteClippedFrom">This note was originally clipped from <a href="<%= note.getLabelValue("pageUrl") %>"><%= note.getLabelValue("pageUrl") %></a></div> | ||||
|         <% } %> | ||||
|  | ||||
|         <% if (note.type === 'book') { %> | ||||
|         <% } else if (isEmpty) { %> | ||||
|             <p>This note has no content.</p> | ||||
|         <% } else { %> | ||||
|             <div id="content" class="type-<%= note.type %><% if (note.type === 'text') { %> ck-content<% } %>"> | ||||
|                 <%- content %> | ||||
|             </div> | ||||
|         </div> | ||||
|         <% } %> | ||||
|  | ||||
|         <% if (subRoot.hasChildren()) { %> | ||||
|         <button id="menuButton"></button> | ||||
|         <% if (note.hasChildren()) { %> | ||||
|             <nav id="childLinks" class="<% if (isEmpty) { %>grid<% } else { %>list<% } %>"> | ||||
|                 <% if (!isEmpty) { %> | ||||
|                     <hr> | ||||
|                     <span>Child notes: </span> | ||||
|                 <% } %> | ||||
|  | ||||
|                 <ul> | ||||
|                     <% for (const childNote of note.getChildNotes()) { %> | ||||
|                         <li> | ||||
|                             <a href="<%= childNote.shareId %>" | ||||
|                                class="type-<%= childNote.type %>"><%= childNote.title %></a> | ||||
|                         </li> | ||||
|                     <% } %> | ||||
|                 </ul> | ||||
|             </nav> | ||||
|         <% } %> | ||||
|     </div> | ||||
|  | ||||
|     <% if (subRoot.hasChildren()) { %> | ||||
|         <button id="toggleMenuButton"></button> | ||||
|  | ||||
|         <nav id="menu"> | ||||
|             <%- include('tree_item', {note: subRoot, activeNote: note}) %> | ||||
|         </nav> | ||||
|         <% } %> | ||||
|     </div> | ||||
|  | ||||
|     <script> | ||||
|         (function () { | ||||
|             const menuButton = document.getElementById('menuButton'); | ||||
|             const layout = document.getElementById('layout'); | ||||
|  | ||||
|             menuButton.addEventListener('click', () => layout.classList.toggle('navMenu')); | ||||
|         }()); | ||||
|     </script> | ||||
|     <% } %> | ||||
| </div> | ||||
| </body> | ||||
| </html> | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
|     <% if (activeNote.noteId === note.noteId) { %> | ||||
|     <strong><%= note.title %></strong> | ||||
|     <% } else { %> | ||||
|     <a href="./<%= note.shareId %>"><%= note.title %></a> | ||||
|     <a class="type-<%= note.type %>" href="./<%= note.shareId %>"><%= note.title %></a> | ||||
|     <% } %> | ||||
| </p> | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user