diff --git a/.idea/misc.xml b/.idea/misc.xml index bc4ec316b..8524fae44 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,7 +3,7 @@ - + \ No newline at end of file diff --git a/db/migrations/0196__rename_bulk_actions.sql b/db/migrations/0196__rename_bulk_actions.sql new file mode 100644 index 000000000..ad4c5febe --- /dev/null +++ b/db/migrations/0196__rename_bulk_actions.sql @@ -0,0 +1,2 @@ +UPDATE attributes SET value = replace(value, 'setLabelValue', 'updateLabelValue') WHERE name = 'action' AND type = 'label'; +UPDATE attributes SET value = replace(value, 'setRelationTarget', 'updateRelationTarget') WHERE name = 'action' AND type = 'label'; diff --git a/package-lock.json b/package-lock.json index a873e3fa7..f71861ec6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "trilium", - "version": "0.52.1-beta", + "version": "0.52.0-beta", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "trilium", - "version": "0.52.1-beta", + "version": "0.52.0-beta", "hasInstallScript": true, "license": "AGPL-3.0-only", "dependencies": { @@ -21,7 +21,7 @@ "commonmark": "0.30.0", "cookie-parser": "1.4.6", "csurf": "1.11.0", - "dayjs": "1.11.3", + "dayjs": "1.11.2", "ejs": "3.1.8", "electron-debug": "3.2.0", "electron-dl": "3.3.1", @@ -44,8 +44,8 @@ "joplin-turndown-plugin-gfm": "1.0.12", "jsdom": "19.0.0", "mime-types": "2.1.35", - "multer": "1.4.4", - "node-abi": "3.21.0", + "multer": "1.4.5-lts.1", + "node-abi": "3.22.0", "normalize-strings": "1.1.1", "open": "8.4.0", "portscanner": "2.2.0", @@ -2161,38 +2161,16 @@ } }, "node_modules/busboy": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", - "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", "dependencies": { - "dicer": "0.2.5", - "readable-stream": "1.1.x" + "streamsearch": "^1.1.0" }, "engines": { - "node": ">=0.8.0" + "node": ">=10.16.0" } }, - "node_modules/busboy/node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "node_modules/busboy/node_modules/readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "node_modules/busboy/node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -3115,9 +3093,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.3.tgz", - "integrity": "sha512-xxwlswWOlGhzgQ4TKzASQkUhqERI3egRNqgV4ScR8wlANA/A9tZ7miXa44vTTKEq5l7vWoL5G57bG3zA+Kow0A==" + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.2.tgz", + "integrity": "sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw==" }, "node_modules/debug": { "version": "4.3.4", @@ -3267,39 +3245,6 @@ "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", "optional": true }, - "node_modules/dicer": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", - "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", - "dependencies": { - "readable-stream": "1.1.x", - "streamsearch": "0.1.2" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/dicer/node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "node_modules/dicer/node_modules/readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "node_modules/dicer/node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, "node_modules/dir-compare": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-2.4.0.tgz", @@ -7359,21 +7304,20 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "node_modules/multer": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4.tgz", - "integrity": "sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==", + "version": "1.4.5-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", + "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", "dependencies": { "append-field": "^1.0.0", - "busboy": "^0.2.11", + "busboy": "^1.0.0", "concat-stream": "^1.5.2", "mkdirp": "^0.5.4", "object-assign": "^4.1.1", - "on-finished": "^2.3.0", "type-is": "^1.6.4", "xtend": "^4.0.0" }, "engines": { - "node": ">= 0.10.0" + "node": ">= 6.0.0" } }, "node_modules/nanoid": { @@ -7407,9 +7351,9 @@ "dev": true }, "node_modules/node-abi": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.21.0.tgz", - "integrity": "sha512-0ChvtQmmNYzXju0fjG0Vfg72q2D8FxUhluvV9uqivtXsKblSekJE2juxfg+9HoSgqPMqCmVEC/GHHtGzi4xYTg==", + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.22.0.tgz", + "integrity": "sha512-u4uAs/4Zzmp/jjsD9cyFYDXeISfUWaAVWshPmDZOFOv4Xl4SbzTXm53I04C2uRueYJ+0t5PEtLH/owbn2Npf/w==", "dependencies": { "semver": "^7.3.5" }, @@ -9463,11 +9407,11 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, "node_modules/streamsearch": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", - "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", "engines": { - "node": ">=0.8.0" + "node": ">=10.0.0" } }, "node_modules/string_decoder": { @@ -12550,35 +12494,11 @@ } }, "busboy": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", - "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", "requires": { - "dicer": "0.2.5", - "readable-stream": "1.1.x" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - } + "streamsearch": "^1.1.0" } }, "bytes": { @@ -13271,9 +13191,9 @@ } }, "dayjs": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.3.tgz", - "integrity": "sha512-xxwlswWOlGhzgQ4TKzASQkUhqERI3egRNqgV4ScR8wlANA/A9tZ7miXa44vTTKEq5l7vWoL5G57bG3zA+Kow0A==" + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.2.tgz", + "integrity": "sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw==" }, "debug": { "version": "4.3.4", @@ -13383,38 +13303,6 @@ "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", "optional": true }, - "dicer": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", - "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", - "requires": { - "readable-stream": "1.1.x", - "streamsearch": "0.1.2" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - } - } - }, "dir-compare": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-2.4.0.tgz", @@ -16530,16 +16418,15 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "multer": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4.tgz", - "integrity": "sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==", + "version": "1.4.5-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", + "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", "requires": { "append-field": "^1.0.0", - "busboy": "^0.2.11", + "busboy": "^1.0.0", "concat-stream": "^1.5.2", "mkdirp": "^0.5.4", "object-assign": "^4.1.1", - "on-finished": "^2.3.0", "type-is": "^1.6.4", "xtend": "^4.0.0" } @@ -16566,9 +16453,9 @@ "dev": true }, "node-abi": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.21.0.tgz", - "integrity": "sha512-0ChvtQmmNYzXju0fjG0Vfg72q2D8FxUhluvV9uqivtXsKblSekJE2juxfg+9HoSgqPMqCmVEC/GHHtGzi4xYTg==", + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.22.0.tgz", + "integrity": "sha512-u4uAs/4Zzmp/jjsD9cyFYDXeISfUWaAVWshPmDZOFOv4Xl4SbzTXm53I04C2uRueYJ+0t5PEtLH/owbn2Npf/w==", "requires": { "semver": "^7.3.5" } @@ -18169,9 +18056,9 @@ } }, "streamsearch": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", - "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==" }, "string_decoder": { "version": "1.1.1", diff --git a/package.json b/package.json index 45824650a..a7930f36d 100644 --- a/package.json +++ b/package.json @@ -59,8 +59,8 @@ "joplin-turndown-plugin-gfm": "1.0.12", "jsdom": "19.0.0", "mime-types": "2.1.35", - "multer": "1.4.4", - "node-abi": "3.21.0", + "multer": "1.4.5-lts.1", + "node-abi": "3.22.0", "normalize-strings": "1.1.1", "open": "8.4.0", "portscanner": "2.2.0", diff --git a/src/public/app/desktop.js b/src/public/app/desktop.js index 6ab84b3b3..5c7eb8996 100644 --- a/src/public/app/desktop.js +++ b/src/public/app/desktop.js @@ -52,13 +52,13 @@ if (utils.isElectron()) { title: suggestion, command: "replaceMisspelling", spellingSuggestion: suggestion, - uiIcon: "empty" + uiIcon: "bx bx-empty" }); } items.push({ title: `Add "${params.misspelledWord}" to dictionary`, - uiIcon: "plus", + uiIcon: "bx bx-plus", handler: () => webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord) }); @@ -69,7 +69,7 @@ if (utils.isElectron()) { items.push({ enabled: editFlags.canCut && hasText, title: `Cut ${platformModifier}+X`, - uiIcon: "cut", + uiIcon: "bx bx-cut", handler: () => webContents.cut() }); } @@ -78,7 +78,7 @@ if (utils.isElectron()) { items.push({ enabled: editFlags.canCopy && hasText, title: `Copy ${platformModifier}+C`, - uiIcon: "copy", + uiIcon: "bx bx-copy", handler: () => webContents.copy() }); } @@ -86,7 +86,7 @@ if (utils.isElectron()) { if (!["", "javascript:", "about:blank#blocked"].includes(params.linkURL) && params.mediaType === 'none') { items.push({ title: `Copy link`, - uiIcon: "copy", + uiIcon: "bx bx-copy", handler: () => { electron.clipboard.write({ bookmark: params.linkText, @@ -100,7 +100,7 @@ if (utils.isElectron()) { items.push({ enabled: editFlags.canPaste, title: `Paste ${platformModifier}+V`, - uiIcon: "paste", + uiIcon: "bx bx-paste", handler: () => webContents.paste() }); } @@ -109,7 +109,7 @@ if (utils.isElectron()) { items.push({ enabled: editFlags.canPaste, title: `Paste as plain text ${platformModifier}+Shift+V`, - uiIcon: "paste", + uiIcon: "bx bx-paste", handler: () => webContents.pasteAndMatchStyle() }); } @@ -122,7 +122,7 @@ if (utils.isElectron()) { items.push({ enabled: editFlags.canPaste, title: `Search for "${shortenedSelection}" with DuckDuckGo`, - uiIcon: "search-alt", + uiIcon: "bx bx-search-alt", handler: () => electron.shell.openExternal(`https://duckduckgo.com/?q=${encodeURIComponent(params.selectionText)}`) }); } diff --git a/src/public/app/dialogs/bulk_assign_attributes.js b/src/public/app/dialogs/bulk_assign_attributes.js new file mode 100644 index 000000000..d6a31a4dd --- /dev/null +++ b/src/public/app/dialogs/bulk_assign_attributes.js @@ -0,0 +1,48 @@ +import utils from "../services/utils.js"; +import bulkActionService from "../services/bulk_action.js"; +import froca from "../services/froca.js"; + +const $dialog = $("#bulk-assign-attributes-dialog"); +const $availableActionList = $("#bulk-available-action-list"); +const $existingActionList = $("#bulk-existing-action-list"); + +$dialog.on('click', '[data-action-add]', async event => { + const actionName = $(event.target).attr('data-action-add'); + + await bulkActionService.addAction('bulkaction', actionName); + + await refresh(); +}); + +for (const actionGroup of bulkActionService.ACTION_GROUPS) { + const $actionGroupList = $(""); + const $actionGroup = $("") + .append($("").text(actionGroup.title + ": ")) + .append($actionGroupList); + + for (const action of actionGroup.actions) { + $actionGroupList.append( + $(' - + @@ -193,24 +167,11 @@ const OPTION_CLASSES = [ Debug ]; -const ACTION_CLASSES = {}; - -for (const clazz of [ - MoveNoteSearchAction, - DeleteNoteSearchAction, - DeleteNoteRevisionsSearchAction, - DeleteLabelSearchAction, - DeleteRelationSearchAction, - RenameLabelSearchAction, - RenameRelationSearchAction, - SetLabelValueSearchAction, - SetRelationTargetSearchAction, - ExecuteScriptSearchAction -]) { - ACTION_CLASSES[clazz.actionName] = clazz; -} - export default class SearchDefinitionWidget extends NoteContextAwareWidget { + get name() { + return "searchDefinition"; + } + isEnabled() { return this.note && this.note.type === 'search'; } @@ -228,6 +189,19 @@ export default class SearchDefinitionWidget extends NoteContextAwareWidget { this.$widget = $(TPL); this.contentSized(); this.$component = this.$widget.find('.search-definition-widget'); + this.$actionList = this.$widget.find('.action-list'); + + for (const actionGroup of bulkActionService.ACTION_GROUPS) { + this.$actionList.append($(' + * - malformed headings when using raw HTML

+ * - etc. + * + * In those cases the generated TOC may be incorrect or the navigation may lead + * to the wrong heading (although what "right" means in those cases is not + * clear), but it won't crash. + */ + +import attributeService from "../services/attributes.js"; +import CollapsibleWidget from "./collapsible_widget.js"; + +const TPL = `
+ + + +
`; + +/** + * Find a heading node in the parent's children given its index. + * + * @param {Element} parent Parent node to find a headingIndex'th in. + * @param {uint} headingIndex Index for the heading + * @returns {Element|null} Heading node with the given index, null couldn't be + * found (ie malformed like nested headings, etc) + */ +function findHeadingNodeByIndex(parent, headingIndex) { + let headingNode = null; + for (let i = 0; i < parent.childCount; ++i) { + let child = parent.getChild(i); + + // Headings appear as flattened top level children in the CKEditor + // document named as "heading" plus the level, eg "heading2", + // "heading3", "heading2", etc and not nested wrt the heading level. If + // a heading node is found, decrement the headingIndex until zero is + // reached + if (child.name.startsWith("heading")) { + if (headingIndex === 0) { + headingNode = child; + break; + } + headingIndex--; + } + } + + return headingNode; +} + +function findHeadingElementByIndex(parent, headingIndex) { + let headingElement = null; + for (let i = 0; i < parent.children.length; ++i) { + const child = parent.children[i]; + // Headings appear as flattened top level children in the DOM named as + // "H" plus the level, eg "H2", "H3", "H2", etc and not nested wrt the + // heading level. If a heading node is found, decrement the headingIndex + // until zero is reached + + if (child.tagName.match(/H\d+/i) !== null) { + if (headingIndex === 0) { + headingElement = child; + break; + } + headingIndex--; + } + } + return headingElement; +} + +const MIN_HEADING_COUNT = 3; + +export default class TocWidget extends CollapsibleWidget { + get widgetTitle() { + return "Table of Contents"; + } + + isEnabled() { + return super.isEnabled() + && this.note.type === 'text' + && !this.note.hasLabel('noToc'); + } + + async doRenderBody() { + this.$body.empty().append($(TPL)); + this.$toc = this.$body.find('.toc'); + } + + async refreshWithNote(note) { + let $toc = "", headingCount = 0; + // Check for type text unconditionally in case alwaysShowWidget is set + if (this.note.type === 'text') { + const { content } = await note.getNoteComplement(); + ({$toc, headingCount} = await this.getToc(content)); + } + + this.$toc.html($toc); + this.toggleInt(headingCount >= MIN_HEADING_COUNT); + this.triggerCommand("reevaluateIsEnabled"); + } + + /** + * Builds a jquery table of contents. + * + * @param {String} html Note's html content + * @returns {$toc: jQuery, headingCount: integer} ordered list table of headings, nested by heading level + * with an onclick event that will cause the document to scroll to + * the desired position. + */ + getToc(html) { + // Regular expression for headings

...

using non-greedy + // matching and backreferences + const headingTagsRegex = /(.*?)<\/h\1>/g; + + // Use jquery to build the table rather than html text, since it makes + // it easier to set the onclick event that will be executed with the + // right captured callback context + const $toc = $("
    "); + // Note heading 2 is the first level Trilium makes available to the note + let curLevel = 2; + const $ols = [$toc]; + let headingCount; + for (let m = null, headingIndex = 0; ((m = headingTagsRegex.exec(html)) !== null); headingIndex++) { + // + // Nest/unnest whatever necessary number of ordered lists + // + const newLevel = m[1]; + const levelDelta = newLevel - curLevel; + if (levelDelta > 0) { + // Open as many lists as newLevel - curLevel + for (let i = 0; i < levelDelta; i++) { + const $ol = $("
      "); + $ols[$ols.length - 1].append($ol); + $ols.push($ol); + } + } else if (levelDelta < 0) { + // Close as many lists as curLevel - newLevel + for (let i = 0; i < -levelDelta; ++i) { + $ols.pop(); + } + } + curLevel = newLevel; + + // + // Create the list item and set up the click callback + // + const $li = $('
    1. ' + m[2] + '
    2. '); + // XXX Do this with CSS? How to inject CSS in doRender? + $li.hover(function () { + $(this).css("font-weight", "bold"); + }).mouseout(function () { + $(this).css("font-weight", "normal"); + }); + $li.on("click", () => this.jumpToHeading(headingIndex)); + $ols[$ols.length - 1].append($li); + headingCount = headingIndex; + } + + return { + $toc, + headingCount + }; + } + + async jumpToHeading(headingIndex) { + // A readonly note can change state to "readonly disabled + // temporarily" (ie "edit this note" button) without any + // intervening events, do the readonly calculation at navigation + // time and not at outline creation time + // See https://github.com/zadam/trilium/issues/2828 + const isReadOnly = await this.noteContext.isReadOnly(); + + if (isReadOnly) { + const $readonlyTextContent = await this.noteContext.getContentElement(); + + const headingElement = findHeadingElementByIndex($readonlyTextContent[0], headingIndex); + + if (headingElement != null) { + headingElement.scrollIntoView(); + } + } else { + const textEditor = await this.noteContext.getTextEditor(); + + const model = textEditor.model; + const doc = model.document; + const root = doc.getRoot(); + + const headingNode = findHeadingNodeByIndex(root, headingIndex); + + // headingNode could be null if the html was malformed or + // with headings inside elements, just ignore and don't + // navigate (note that the TOC rendering and other TOC + // entries' navigation could be wrong too) + if (headingNode != null) { + // Setting the selection alone doesn't scroll to the + // caret, needs to be done explicitly and outside of + // the writer change callback so the scroll is + // guaranteed to happen after the selection is + // updated. + + // In addition, scrolling to a caret later in the + // document (ie "forward scrolls"), only scrolls + // barely enough to place the caret at the bottom of + // the screen, which is a usability issue, you would + // like the caret to be placed at the top or center + // of the screen. + + // To work around that issue, first scroll to the + // end of the document, then scroll to the desired + // point. This causes all the scrolls to be + // "backward scrolls" no matter the current caret + // position, which places the caret at the top of + // the screen. + + // XXX This could be fixed in another way by using + // the underlying CKEditor5 + // scrollViewportToShowTarget, which allows to + // provide a larger "viewportOffset", but that + // has coding complications (requires calling an + // internal CKEditor utils funcion and passing + // an HTML element, not a CKEditor node, and + // CKEditor5 doesn't seem to have a + // straightforward way to convert a node to an + // HTML element? (in CKEditor4 this was done + // with $(node.$) ) + + // Scroll to the end of the note to guarantee the + // next scroll is a backwards scroll that places the + // caret at the top of the screen + model.change(writer => { + writer.setSelection(root.getChild(root.childCount - 1), 0); + }); + textEditor.editing.view.scrollToTheSelection(); + // Backwards scroll to the heading + model.change(writer => { + writer.setSelection(headingNode, 0); + }); + textEditor.editing.view.scrollToTheSelection(); + } + } + } + + async entitiesReloadedEvent({loadResults}) { + if (loadResults.isNoteContentReloaded(this.noteId)) { + await this.refresh(); + } else if (loadResults.getAttributes().find(attr => attr.type === 'label' + && (attr.name.toLowerCase().includes('readonly') || attr.name === 'noToc') + && attributeService.isAffecting(attr, this.note))) { + + await this.refresh(); + } + } +} diff --git a/src/public/app/widgets/type_widgets/canvas.js b/src/public/app/widgets/type_widgets/canvas.js index e713e3776..924c0931a 100644 --- a/src/public/app/widgets/type_widgets/canvas.js +++ b/src/public/app/widgets/type_widgets/canvas.js @@ -8,7 +8,7 @@ const {sleep} = utils; const TPL = `
      - + + diff --git a/src/views/dialogs/note_type_chooser.ejs b/src/views/dialogs/note_type_chooser.ejs new file mode 100644 index 000000000..a28ae70c2 --- /dev/null +++ b/src/views/dialogs/note_type_chooser.ejs @@ -0,0 +1,34 @@ + + + diff --git a/src/views/dialogs/sort_child_notes.ejs b/src/views/dialogs/sort_child_notes.ejs index 36c6475e0..ed38a0636 100644 --- a/src/views/dialogs/sort_child_notes.ejs +++ b/src/views/dialogs/sort_child_notes.ejs @@ -50,6 +50,17 @@ descending
      + +
      + +
      Folders
      + +
      + + +