├── .gitignore ├── CHANGELOG.md ├── images ├── setting1.png ├── setting2.png └── screenshot.png ├── keymaps └── sidetoc.json ├── lib ├── dispatcher.js ├── types.js ├── pane-state.js ├── sidetoc.js ├── ripper.js ├── settings.js └── sidetoc-pane.js ├── .prettierrc ├── src ├── dispatcher.ts ├── types.ts ├── pane-state.ts ├── sidetoc.ts ├── settings.ts ├── ripper.ts └── sidetoc-pane.tsx ├── .babelrc ├── .eslintrc.yml ├── tsconfig.json ├── menus └── sidetoc.json ├── styles └── sidetoc.less ├── package.json ├── LICENSE.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.0 - First Release 2 | * Every feature added 3 | * Every bug fixed 4 | -------------------------------------------------------------------------------- /images/setting1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basyura/inkdrop-sidetoc/HEAD/images/setting1.png -------------------------------------------------------------------------------- /images/setting2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basyura/inkdrop-sidetoc/HEAD/images/setting2.png -------------------------------------------------------------------------------- /images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basyura/inkdrop-sidetoc/HEAD/images/screenshot.png -------------------------------------------------------------------------------- /keymaps/sidetoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": { 3 | "ctrl-l": "sidetoc:sidetoc-toggle" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /lib/dispatcher.js: -------------------------------------------------------------------------------- 1 | "use babel"; 2 | import * as Flux from "flux"; 3 | export default new Flux.Dispatcher(); 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | trailingComma: "es5" 2 | tabWidth: 2 3 | semi: true 4 | singleQuote: false 5 | arrow_parens: "always" 6 | printWidth: 100 7 | -------------------------------------------------------------------------------- /src/dispatcher.ts: -------------------------------------------------------------------------------- 1 | "use babel"; 2 | 3 | import * as Flux from "flux"; 4 | import { DispatchAction } from "./types"; 5 | 6 | export default new Flux.Dispatcher(); 7 | -------------------------------------------------------------------------------- /lib/types.js: -------------------------------------------------------------------------------- 1 | "use babel"; 2 | export var WidthChangeMode; 3 | (function (WidthChangeMode) { 4 | WidthChangeMode[WidthChangeMode["Reset"] = 0] = "Reset"; 5 | WidthChangeMode[WidthChangeMode["Increase"] = 1] = "Increase"; 6 | WidthChangeMode[WidthChangeMode["Decrease"] = 2] = "Decrease"; 7 | })(WidthChangeMode || (WidthChangeMode = {})); 8 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { "electron": "6.0.11" }, 7 | "useBuiltIns": "usage", 8 | "debug": false, 9 | "corejs": 3 10 | } 11 | ], 12 | "@babel/preset-react" 13 | ], 14 | "plugins": ["@babel/plugin-proposal-class-properties"] 15 | } 16 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | root: true 2 | plugins: 3 | - react 4 | - prettier 5 | extends: 6 | - plugin:react/recommended 7 | - plugin:prettier/recommended 8 | parser: babel-eslint 9 | env: {} 10 | globals: 11 | inkdrop: readonly 12 | rules: 13 | no-useless-escape: 0 14 | prettier/prettier: 15 | - 2 16 | - trailingComma: none 17 | singleQuote: true 18 | semi: false 19 | prefer-const: 2 20 | no-unused-vars: 21 | - 2 22 | - argsIgnorePattern: ^_ 23 | varsIgnorePattern: ^_ 24 | 25 | settings: 26 | react: 27 | version: "16.8" 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "outDir": "lib", 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "jsx": "react", 21 | "newLine": "lf" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /menus/sidetoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "menu": [ 3 | { 4 | "label": "Plugins", 5 | "submenu": [ 6 | { 7 | "label": "sidetoc", 8 | "submenu": [ 9 | { 10 | "label": "Toggle SideToc", 11 | "command": "sidetoc:sidetoc-toggle" 12 | }, 13 | { 14 | "label": "Increase width", 15 | "command": "sidetoc:width-increase" 16 | }, 17 | { 18 | "label": "Decrease width", 19 | "command": "sidetoc:width-decrease" 20 | }, 21 | { 22 | "label": "Reset width", 23 | "command": "sidetoc:width-reset" 24 | } 25 | ] 26 | } 27 | ] 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /styles/sidetoc.less: -------------------------------------------------------------------------------- 1 | .sidetoc-pane { 2 | padding: var(--inkdrop-sidetoc-padding); 3 | min-width: var(--inkdrop-sidetoc-width); 4 | max-width: var(--inkdrop-sidetoc-width); 5 | li { 6 | padding-left: 10px; 7 | line-height: 180%; 8 | list-style: none; 9 | word-break: break-all; 10 | } 11 | li:hover { 12 | background-color: var(--inkdrop-sidetoc-highlight-bg-color); 13 | color: var(--inkdrop-sidetoc-highlight-fg-color); 14 | } 15 | } 16 | 17 | .sidetoc-pane-hide { 18 | display: none; 19 | } 20 | 21 | .sidetoc-pane-nowrap { 22 | li { 23 | overflow: hidden; 24 | text-overflow: ellipsis; 25 | white-space: nowrap; 26 | } 27 | } 28 | 29 | .sidetoc-pane-wrapper { 30 | position: absolute; 31 | overflow-y: auto; 32 | width: var(--inkdrop-sidetoc-pane-wrapper-width); 33 | } 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sidetoc", 3 | "main": "./lib/sidetoc", 4 | "version": "2.5.0", 5 | "description": "It adds an outline view on the right side of the editor and preview", 6 | "keywords": [], 7 | "repository": "git@github.com:basyura/inkdrop-sidetoc.git", 8 | "license": "MIT", 9 | "engines": { 10 | "inkdrop": "^4.x" 11 | }, 12 | "scripts": { 13 | "build": "tsc", 14 | "build-watch": "tsc --watch" 15 | }, 16 | "dependencies": { 17 | "flux": "^4.0.3", 18 | "inkdrop-model": "^2.7.1" 19 | }, 20 | "devDependencies": { 21 | "@types/event-kit": "^2.4.1", 22 | "@types/flux": "^3.1.11", 23 | "@types/node": "^14.18.21", 24 | "@types/react": "^16.14.26", 25 | "@types/react-codemirror": "^1.0.7", 26 | "@types/react-dom": "^16.9.16", 27 | "event-kit": "^2.5.3", 28 | "react": "^16.14.0", 29 | "typescript": "^5.8.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | "use babel"; 2 | import CodeMirror from "codemirror"; 3 | import type { Note } from "inkdrop-model"; 4 | 5 | export interface Inkdrop { 6 | window: any; 7 | commands: any; 8 | config: any; 9 | components: any; 10 | layouts: any; 11 | store: any; 12 | getActiveEditor(): Editor; 13 | onEditorLoad(callback: (e: Editor) => void): void; 14 | } 15 | 16 | export interface Editor { 17 | cm: CodeMirror.Editor; 18 | forceUpdate(): any; 19 | } 20 | 21 | export interface DispatchAction { 22 | type: string; 23 | } 24 | 25 | export interface HeaderItem { 26 | count: number; 27 | str: string; 28 | rowStart: number; 29 | rowEnd: number; 30 | index: number; 31 | } 32 | 33 | export interface Props { 34 | editingNote: Note; 35 | } 36 | 37 | export interface State { 38 | visibility: boolean; 39 | headers: HeaderItem[]; 40 | currentHeader: HeaderItem | null; 41 | min: number; 42 | len: number; 43 | } 44 | 45 | export interface ParseResult { 46 | headers: HeaderItem[]; 47 | min: number; 48 | } 49 | 50 | export enum WidthChangeMode { 51 | Reset, 52 | Increase, 53 | Decrease, 54 | } 55 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/pane-state.js: -------------------------------------------------------------------------------- 1 | "use babel"; 2 | import * as React from "react"; 3 | var PaneState = /** @class */ (function () { 4 | function PaneState() { 5 | // render optimization 6 | this.lastRenderHeaders = null; 7 | this.lastRenderVisibility = null; 8 | this.lastRenderCurrentHeader = null; 9 | // event listener references for proper cleanup 10 | this.previewElement = null; 11 | // DOM element cache for performance 12 | this.cachedPaneElement = null; 13 | this.cachedEditorElement = null; 14 | // Style cache for object pooling 15 | this.styleCache = new Map(); 16 | this.lastLine = -1; 17 | this.noteId = ""; 18 | this.isPreview = false; 19 | // preview headers. this value is cleared with null when mode change to preview. 20 | this.previewHeaders = []; 21 | this.previewCurrent = ""; 22 | // ref to scrollIntoView 23 | this.curSectionRef = React.createRef(); 24 | // for handle event 25 | this.dispatchId = ""; 26 | this.cursorTime = null; 27 | this.observer = null; 28 | this.bodyObserver = null; 29 | this.firstPreview = true; 30 | // render optimization 31 | this.lastRenderHeaders = null; 32 | this.lastRenderVisibility = null; 33 | this.lastRenderCurrentHeader = null; 34 | // event listener references 35 | this.previewElement = null; 36 | // DOM element cache 37 | this.cachedPaneElement = null; 38 | this.cachedEditorElement = null; 39 | // Style cache 40 | this.styleCache = new Map(); 41 | } 42 | return PaneState; 43 | }()); 44 | export { PaneState }; 45 | -------------------------------------------------------------------------------- /src/pane-state.ts: -------------------------------------------------------------------------------- 1 | "use babel"; 2 | 3 | import * as React from "react"; 4 | 5 | export class PaneState { 6 | // rendered cache 7 | content: any; 8 | lastLine: number; 9 | noteId: string; 10 | isPreview: boolean; 11 | // preview headers. this value is cleared with null when mode change to preview. 12 | previewHeaders: HTMLElement[]; 13 | previewCurrent: string; 14 | // ref to scrollIntoView 15 | curSectionRef: React.RefObject; 16 | // for handle event 17 | dispatchId: string; 18 | cursorTime: Date | null; 19 | observer: MutationObserver | null; 20 | bodyObserver: MutationObserver | null; 21 | firstPreview: boolean; 22 | 23 | // render optimization 24 | lastRenderHeaders: any = null; 25 | lastRenderVisibility: boolean | null = null; 26 | lastRenderCurrentHeader: any = null; 27 | // event listener references for proper cleanup 28 | previewElement: Element | null = null; 29 | // DOM element cache for performance 30 | cachedPaneElement: HTMLElement | null = null; 31 | cachedEditorElement: Element | null = null; 32 | // Style cache for object pooling 33 | styleCache: Map = new Map(); 34 | 35 | constructor() { 36 | this.lastLine = -1; 37 | this.noteId = ""; 38 | this.isPreview = false; 39 | // preview headers. this value is cleared with null when mode change to preview. 40 | this.previewHeaders = []; 41 | this.previewCurrent = ""; 42 | // ref to scrollIntoView 43 | this.curSectionRef = React.createRef(); 44 | // for handle event 45 | this.dispatchId = ""; 46 | this.cursorTime = null; 47 | this.observer = null; 48 | this.bodyObserver = null; 49 | this.firstPreview = true; 50 | // render optimization 51 | this.lastRenderHeaders = null; 52 | this.lastRenderVisibility = null; 53 | this.lastRenderCurrentHeader = null; 54 | // event listener references 55 | this.previewElement = null; 56 | // DOM element cache 57 | this.cachedPaneElement = null; 58 | this.cachedEditorElement = null; 59 | // Style cache 60 | this.styleCache = new Map(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Inkdrop SideToc Plugin 2 | 3 | ![](https://inkdrop-plugin-badge.vercel.app/api/version/sidetoc) 4 | ![](https://inkdrop-plugin-badge.vercel.app/api/downloads/sidetoc) 5 | 6 | It adds an outline view on the right side of the editor and preview. 7 | 8 | https://my.inkdrop.app/plugins/sidetoc 9 | 10 | ![Screenshot](https://raw.githubusercontent.com/basyura/inkdrop-sidetoc/master/images/screenshot.png) 11 | 12 | ## Features 13 | 14 | - Show TOC of headers. 15 | - Highlight current header. 16 | - Follow cursor movement and scroll 17 | - Toggle side toc pane. 18 | - Jump to header on click. 19 | - Jump to next (or previous) header by key. 20 | - Support Preview Mode 21 | 22 | ## Install 23 | 24 | ``` 25 | ipm install sidetoc 26 | ``` 27 | 28 | ## Keybindings 29 | 30 | | Command | Explanation | 31 | | ---------------------- | --------------------------------- | 32 | | sidetoc:sidetoc-toggle | Toggle side toc pane. | 33 | | sidetoc:jump-next | Jump to next header. | 34 | | sidetoc:jump-prev | Jump to previous header. | 35 | | sidetoc:width-increase | increase width. | 36 | | sidetoc:width-decrease | decrease width. | 37 | | sidetoc:width-reset | reset width. | 38 | | sidetoc:wraptext-toggle| Toggle wrap/nowrap overflow text. | 39 | 40 | 41 | 42 | 43 | keymap.cson 44 | 45 | ```cson 46 | 'body': 47 | 'ctrl-l': 'sidetoc:sidetoc-toggle' 48 | 'ctrl-n': 'sidetoc:jump-next' 49 | 'ctrl-p': 'sidetoc:jump-prev' 50 | 'ctrl-L': 'sidetoc:width-decrease' 51 | 'ctrl-K': 'sidetoc:width-increase' 52 | 'ctrl-0': 'sidetoc:width-reset' 53 | 'ctrl-t': 'sidetoc:wraptext-toggle' 54 | 55 | '.mde-preview': 56 | 'ctrl-n': 'sidetoc:jump-next' 57 | 'ctrl-p': 'sidetoc:jump-prev' 58 | ``` 59 | 60 | ## Settings 61 | 62 | | key | default | 63 | | ------------------ | --------------------------------------- | 64 | | ~~highlightColor~~ | ~~#C5EAFB~~ - Obsolete!! | 65 | | highlightBgColor | --note-list-view-item-active-background | 66 | | highlightFgColor | --note-list-view-item-active-color | 67 | | width | 200 | 68 | | textwrap | true | 69 | | defaultVisible | true | 70 | | showIfNoHeader | false | 71 | 72 | config.cson 73 | 74 | ```cson 75 | sidetoc: 76 | highlightBgColor: "#C5EAFB" 77 | highlightFgColor: "black" 78 | width: 200 79 | textwrap: false 80 | defaultVisible: true 81 | showIfNoHeader: true 82 | ``` 83 | 84 | Settings UI 85 | 86 | 87 | ![setting1](https://raw.githubusercontent.com/basyura/inkdrop-sidetoc/master/images/setting1.png) 88 | ![setting2](https://raw.githubusercontent.com/basyura/inkdrop-sidetoc/master/images/setting2.png) 89 | 90 | ## Style Tweaks 91 | 92 | https://docs.inkdrop.app/manual/style-tweaks 93 | 94 | > If you want to apply quick-and-dirty personal styling changes without creating an entire theme that you intend to publish, 95 | > you can add styles to the styles.less file in your data directory. It does not exist by default. 96 | 97 | ## Not supported 98 | 99 | * Content in html tags (syntax). 100 | 101 | ## Changelog 102 | 103 | * https://github.com/basyura/inkdrop-sidetoc/commits/master/ 104 | -------------------------------------------------------------------------------- /src/sidetoc.ts: -------------------------------------------------------------------------------- 1 | "use babel"; 2 | 3 | import { CompositeDisposable } from "event-kit"; 4 | import * as SideTocPane from "./sidetoc-pane"; 5 | import dispatcher from "./dispatcher"; 6 | import { Inkdrop, DispatchAction } from "./types"; 7 | 8 | const componentName = "SideTocPane"; 9 | const layoutName = "mde"; 10 | 11 | declare var inkdrop: Inkdrop; 12 | 13 | class SideTocPlugin { 14 | subscriptions = new CompositeDisposable(); 15 | 16 | activate() { 17 | const { components, commands } = inkdrop; 18 | components.registerClass(SideTocPane.default); 19 | show(); 20 | 21 | this.subscriptions.add( 22 | commands.add(document.body, { 23 | "sidetoc:sidetoc-toggle": toggle, 24 | "sidetoc:jump-next": jumpToNext, 25 | "sidetoc:jump-prev": jumpToPrev, 26 | "sidetoc:width-increase": increaseWidth, 27 | "sidetoc:width-decrease": decreaseWidth, 28 | "sidetoc:width-reset": resetWidth, 29 | "sidetoc:wraptext-toggle": toggleTextwrap, 30 | }) 31 | ); 32 | } 33 | 34 | deactivate() { 35 | dispatcher.dispatch({ type: "Deactivate" }); 36 | 37 | this.subscriptions.dispose(); 38 | 39 | const { components, layouts } = inkdrop; 40 | layouts.removeComponentFromLayout(layoutName, componentName); 41 | components.deleteClass(SideTocPane); 42 | } 43 | } 44 | 45 | const show = () => { 46 | inkdrop.layouts.insertComponentToLayoutAfter(layoutName, "Editor", componentName); 47 | dispatcher.dispatch({ type: "Activate" }); 48 | }; 49 | 50 | /* dispachers */ 51 | const toggle = () => dispatcher.dispatch({ type: "Toggle" }); 52 | const jumpToNext = () => dispatcher.dispatch({ type: "JumpToNext" }); 53 | const jumpToPrev = () => dispatcher.dispatch({ type: "JumpToPrev" }); 54 | const increaseWidth = () => dispatcher.dispatch({ type: "IncreaseWidth" }); 55 | const decreaseWidth = () => dispatcher.dispatch({ type: "DecreaseWidth" }); 56 | const resetWidth = () => dispatcher.dispatch({ type: "ResetWidth" }); 57 | const toggleTextwrap = () => dispatcher.dispatch({ type: "ToggleTextwrap" }); 58 | 59 | const plugin = new SideTocPlugin(); 60 | module.exports = { 61 | config: { 62 | // #C5EAFB 63 | highlightBgColor: { 64 | title: "highlight background color (css variables | color name | #FFFFFF)", 65 | type: "string", 66 | default: "--note-list-view-item-active-background", 67 | }, 68 | highlightFgColor: { 69 | title: "highlight foreground color (css variables | color name | #FFFFFF)", 70 | type: "string", 71 | default: "--note-list-view-item-active-color", 72 | }, 73 | width: { 74 | title: "side pane width", 75 | type: "integer", 76 | default: 200, 77 | }, 78 | increaseWidth: { 79 | title: "increase pane width", 80 | type: "integer", 81 | default: 10, 82 | }, 83 | textwrap: { 84 | title: "wrap overflow text", 85 | type: "boolean", 86 | default: true, 87 | }, 88 | defaultVisible: { 89 | title: "show sidetoc by default", 90 | type: "boolean", 91 | default: true, 92 | }, 93 | showIfNoHeader: { 94 | title: "show the sidetoc pane even if there is no header", 95 | type: "boolean", 96 | default: false, 97 | }, 98 | }, 99 | activate() { 100 | plugin.activate(); 101 | }, 102 | deactivate() { 103 | plugin.deactivate(); 104 | }, 105 | }; 106 | -------------------------------------------------------------------------------- /lib/sidetoc.js: -------------------------------------------------------------------------------- 1 | "use babel"; 2 | import { CompositeDisposable } from "event-kit"; 3 | import * as SideTocPane from "./sidetoc-pane"; 4 | import dispatcher from "./dispatcher"; 5 | var componentName = "SideTocPane"; 6 | var layoutName = "mde"; 7 | var SideTocPlugin = /** @class */ (function () { 8 | function SideTocPlugin() { 9 | this.subscriptions = new CompositeDisposable(); 10 | } 11 | SideTocPlugin.prototype.activate = function () { 12 | var components = inkdrop.components, commands = inkdrop.commands; 13 | components.registerClass(SideTocPane.default); 14 | show(); 15 | this.subscriptions.add(commands.add(document.body, { 16 | "sidetoc:sidetoc-toggle": toggle, 17 | "sidetoc:jump-next": jumpToNext, 18 | "sidetoc:jump-prev": jumpToPrev, 19 | "sidetoc:width-increase": increaseWidth, 20 | "sidetoc:width-decrease": decreaseWidth, 21 | "sidetoc:width-reset": resetWidth, 22 | "sidetoc:wraptext-toggle": toggleTextwrap, 23 | })); 24 | }; 25 | SideTocPlugin.prototype.deactivate = function () { 26 | dispatcher.dispatch({ type: "Deactivate" }); 27 | this.subscriptions.dispose(); 28 | var components = inkdrop.components, layouts = inkdrop.layouts; 29 | layouts.removeComponentFromLayout(layoutName, componentName); 30 | components.deleteClass(SideTocPane); 31 | }; 32 | return SideTocPlugin; 33 | }()); 34 | var show = function () { 35 | inkdrop.layouts.insertComponentToLayoutAfter(layoutName, "Editor", componentName); 36 | dispatcher.dispatch({ type: "Activate" }); 37 | }; 38 | /* dispachers */ 39 | var toggle = function () { return dispatcher.dispatch({ type: "Toggle" }); }; 40 | var jumpToNext = function () { return dispatcher.dispatch({ type: "JumpToNext" }); }; 41 | var jumpToPrev = function () { return dispatcher.dispatch({ type: "JumpToPrev" }); }; 42 | var increaseWidth = function () { return dispatcher.dispatch({ type: "IncreaseWidth" }); }; 43 | var decreaseWidth = function () { return dispatcher.dispatch({ type: "DecreaseWidth" }); }; 44 | var resetWidth = function () { return dispatcher.dispatch({ type: "ResetWidth" }); }; 45 | var toggleTextwrap = function () { return dispatcher.dispatch({ type: "ToggleTextwrap" }); }; 46 | var plugin = new SideTocPlugin(); 47 | module.exports = { 48 | config: { 49 | // #C5EAFB 50 | highlightBgColor: { 51 | title: "highlight background color (css variables | color name | #FFFFFF)", 52 | type: "string", 53 | default: "--note-list-view-item-active-background", 54 | }, 55 | highlightFgColor: { 56 | title: "highlight foreground color (css variables | color name | #FFFFFF)", 57 | type: "string", 58 | default: "--note-list-view-item-active-color", 59 | }, 60 | width: { 61 | title: "side pane width", 62 | type: "integer", 63 | default: 200, 64 | }, 65 | increaseWidth: { 66 | title: "increase pane width", 67 | type: "integer", 68 | default: 10, 69 | }, 70 | textwrap: { 71 | title: "wrap overflow text", 72 | type: "boolean", 73 | default: true, 74 | }, 75 | defaultVisible: { 76 | title: "show sidetoc by default", 77 | type: "boolean", 78 | default: true, 79 | }, 80 | showIfNoHeader: { 81 | title: "show the sidetoc pane even if there is no header", 82 | type: "boolean", 83 | default: false, 84 | }, 85 | }, 86 | activate: function () { 87 | plugin.activate(); 88 | }, 89 | deactivate: function () { 90 | plugin.deactivate(); 91 | }, 92 | }; 93 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | "use babel"; 2 | 3 | import { Inkdrop, WidthChangeMode } from "./types"; 4 | 5 | declare var inkdrop: Inkdrop; 6 | 7 | class Settings { 8 | DefaultWidth = 200; 9 | fontFamily: string = ""; 10 | hiBgColor: string = ""; 11 | hiFgColor: string = ""; 12 | currentWidth: number = 0; 13 | _settingWidth: number = 0; 14 | isTextwrap: boolean = true; 15 | isDefaultVisible: boolean = true; 16 | isShowIfNoHeader: boolean = false; 17 | sidetocPanePadding = 10; 18 | computedStyle: CSSStyleDeclaration; 19 | 20 | constructor() { 21 | // to get css value 22 | this.computedStyle = getComputedStyle(document.body); 23 | // fontFamily 24 | inkdrop.config.observe("editor.fontFamily", (newValue: string) => { 25 | this.fontFamily = newValue; 26 | }); 27 | // highlight BG 28 | inkdrop.config.observe("sidetoc.highlightBgColor", (newValue: string) => { 29 | this._changeBgColor(newValue); 30 | }); 31 | // highlight FG 32 | inkdrop.config.observe("sidetoc.highlightFgColor", (newValue: string) => { 33 | this._changeFgColor(newValue); 34 | }); 35 | // width 36 | inkdrop.config.observe("sidetoc.width", (newValue: number) => { 37 | if (newValue == null || newValue < 10) { 38 | newValue = this.DefaultWidth; 39 | } 40 | this._settingWidth = newValue; 41 | this.currentWidth = newValue; 42 | this.changeCurrentWidth(WidthChangeMode.Reset); 43 | }); 44 | // ellipsis 45 | inkdrop.config.observe("sidetoc.textwrap", (newValue: boolean) => { 46 | if (newValue == undefined) { 47 | newValue = true; 48 | } 49 | this.isTextwrap = newValue; 50 | }); 51 | // visibility 52 | inkdrop.config.observe("sidetoc.defaultVisible", (newValue: boolean) => { 53 | if (newValue == undefined) { 54 | newValue = true; 55 | } 56 | this.isDefaultVisible = newValue; 57 | }); 58 | // show if no header 59 | inkdrop.config.observe("sidetoc.showIfNoHeader", (newValue: boolean) => { 60 | if (newValue == undefined) { 61 | newValue = false; 62 | } 63 | this.isShowIfNoHeader = newValue; 64 | }); 65 | // wrapper's padding 66 | document.documentElement.style.setProperty( 67 | "--inkdrop-sidetoc-padding", 68 | this.sidetocPanePadding.toString(10) + "px" 69 | ); 70 | } 71 | /* 72 | * 73 | */ 74 | refresh() { 75 | // to get css value 76 | this.computedStyle = getComputedStyle(document.body); 77 | this._changeBgColor(inkdrop.config.get("sidetoc.highlightBgColor")); 78 | this._changeFgColor(inkdrop.config.get("sidetoc.highlightFgColor")); 79 | } 80 | /* 81 | * 82 | */ 83 | changeCurrentWidth = (mode: WidthChangeMode) => { 84 | let width = inkdrop.config.get("sidetoc.IncreaseWidth"); 85 | // check settings 86 | if (width == null || width == 0) { 87 | width = 10; 88 | } 89 | 90 | if (mode == WidthChangeMode.Reset) { 91 | this.currentWidth = this._settingWidth; 92 | } else if (mode == WidthChangeMode.Increase) { 93 | this.currentWidth += width; 94 | } else if (mode == WidthChangeMode.Decrease) { 95 | this.currentWidth -= width; 96 | } 97 | 98 | document.documentElement.style.setProperty( 99 | "--inkdrop-sidetoc-width", 100 | this.currentWidth.toString(10) + "px" 101 | ); 102 | 103 | document.documentElement.style.setProperty( 104 | "--inkdrop-sidetoc-pane-wrapper-width", 105 | (this.currentWidth - 2 * this.sidetocPanePadding).toString(10) + "px" 106 | ); 107 | }; 108 | /* 109 | * 110 | */ 111 | toggleTextWrap = () => { 112 | this.isTextwrap = !this.isTextwrap; 113 | }; 114 | /* 115 | * 116 | */ 117 | _changeBgColor(newValue: string) { 118 | if (newValue == null) { 119 | return; 120 | } 121 | if (newValue.startsWith("--")) { 122 | newValue = this.computedStyle.getPropertyValue(newValue); 123 | } 124 | this.hiBgColor = newValue; 125 | document.documentElement.style.setProperty("--inkdrop-sidetoc-highlight-bg-color", newValue); 126 | } 127 | /* 128 | * 129 | */ 130 | _changeFgColor(newValue: string) { 131 | if (newValue == null) { 132 | return null; 133 | } 134 | if (newValue.startsWith("--")) { 135 | newValue = this.computedStyle.getPropertyValue(newValue); 136 | } 137 | this.hiFgColor = newValue; 138 | document.documentElement.style.setProperty("--inkdrop-sidetoc-highlight-fg-color", newValue); 139 | } 140 | } 141 | 142 | export default new Settings(); 143 | -------------------------------------------------------------------------------- /src/ripper.ts: -------------------------------------------------------------------------------- 1 | "use babel"; 2 | 3 | import { HeaderItem, ParseResult, Props } from "./types"; 4 | 5 | // Cache for parsed headers 6 | const headerCache = new Map(); 7 | let lastBodyHash: number | null = null; 8 | 9 | /* 10 | * Create an optimized hash of the content for cache invalidation 11 | * Uses sampling strategy for large files to avoid O(n) complexity on every character 12 | */ 13 | function hashCode(str: string): number { 14 | const length = str.length; 15 | if (length === 0) return 0; 16 | 17 | let hash = length; // Start with length as base 18 | 19 | // For small strings (< 1000 chars), hash every character 20 | if (length < 1000) { 21 | for (let i = 0; i < length; i++) { 22 | hash = (hash << 5) - hash + str.charCodeAt(i); 23 | hash = hash & hash; // Convert to 32bit integer 24 | } 25 | } else { 26 | // For large strings, sample strategically: 27 | // - First and last 100 characters (header/footer detection) 28 | // - Every 50th character in the middle (content changes) 29 | // - Total sample size: ~200-400 characters vs entire file 30 | 31 | // Hash first 100 characters 32 | const start = Math.min(100, length); 33 | for (let i = 0; i < start; i++) { 34 | hash = (hash << 5) - hash + str.charCodeAt(i); 35 | hash = hash & hash; 36 | } 37 | 38 | // Hash last 100 characters 39 | const end = Math.max(length - 100, start); 40 | for (let i = end; i < length; i++) { 41 | hash = (hash << 5) - hash + str.charCodeAt(i); 42 | hash = hash & hash; 43 | } 44 | 45 | // Sample middle section every 50 characters 46 | for (let i = start; i < end; i += 50) { 47 | hash = (hash << 5) - hash + str.charCodeAt(i); 48 | hash = hash & hash; 49 | } 50 | } 51 | 52 | return hash; 53 | } 54 | 55 | /* 56 | * extract # headers with caching 57 | */ 58 | export function parse(props: Props): ParseResult { 59 | const body = props.editingNote.body; 60 | const bodyHash = hashCode(body); 61 | 62 | // Return cached result if content hasn't changed 63 | if (lastBodyHash === bodyHash && headerCache.has(bodyHash)) { 64 | return headerCache.get(bodyHash)!; 65 | } 66 | 67 | // section list which starting with #. 68 | let headers: HeaderItem[] = []; 69 | // minimum section level 70 | let min = 999; 71 | let row = -1; 72 | let before: HeaderItem | null = null; 73 | let index = 0; 74 | let isInCodeBlock = false; 75 | const codeBlockReg = /^\s{0,}```/; 76 | 77 | // Split only once and cache the lines 78 | const lines = body.split("\n"); 79 | const lineCount = lines.length; 80 | 81 | for (let lineIndex = 0; lineIndex < lineCount; lineIndex++) { 82 | const v = lines[lineIndex]; 83 | row++; 84 | 85 | // skip code block 86 | if (codeBlockReg.test(v)) { 87 | isInCodeBlock = !isInCodeBlock; 88 | continue; 89 | } else if (isInCodeBlock) { 90 | continue; 91 | } 92 | 93 | // check 94 | if (!isValid(v)) { 95 | continue; 96 | } 97 | 98 | // count of # - optimized loop 99 | let i = 0; 100 | const vLength = v.length; 101 | while (i < vLength && v[i] === "#") { 102 | i++; 103 | } 104 | 105 | // create header item - optimized string processing 106 | let headerStartIndex = i; 107 | while (headerStartIndex < vLength && v[headerStartIndex] === " ") { 108 | headerStartIndex++; 109 | } 110 | 111 | const header: HeaderItem = { 112 | count: i, 113 | str: v.substring(headerStartIndex), 114 | rowStart: row, 115 | rowEnd: 0, 116 | index: index, 117 | }; 118 | index++; 119 | 120 | // apply header end row 121 | if (before != null) { 122 | before.rowEnd = row - 1; 123 | } 124 | before = header; 125 | headers.push(header); 126 | 127 | if (i < min) { 128 | min = i; 129 | } 130 | } 131 | 132 | if (headers.length != 0) { 133 | headers[headers.length - 1].rowEnd = row; 134 | } 135 | 136 | const result: ParseResult = { headers: headers, min: min }; 137 | 138 | // Cache the result 139 | headerCache.set(bodyHash, result); 140 | lastBodyHash = bodyHash; 141 | 142 | // Keep cache size reasonable (last 10 documents) 143 | if (headerCache.size > 10) { 144 | const firstKey = headerCache.keys().next().value; 145 | if (firstKey !== undefined) { 146 | headerCache.delete(firstKey); 147 | } 148 | } 149 | 150 | return result; 151 | } 152 | /* 153 | * 154 | */ 155 | function isValid(v: string): boolean { 156 | if (!v.startsWith("#")) { 157 | return false; 158 | } 159 | if (!v.match(/^#+\s+\S+/)) { 160 | return false; 161 | } 162 | 163 | return true; 164 | } 165 | -------------------------------------------------------------------------------- /lib/ripper.js: -------------------------------------------------------------------------------- 1 | "use babel"; 2 | // Cache for parsed headers 3 | var headerCache = new Map(); 4 | var lastBodyHash = null; 5 | /* 6 | * Create an optimized hash of the content for cache invalidation 7 | * Uses sampling strategy for large files to avoid O(n) complexity on every character 8 | */ 9 | function hashCode(str) { 10 | var length = str.length; 11 | if (length === 0) 12 | return 0; 13 | var hash = length; // Start with length as base 14 | // For small strings (< 1000 chars), hash every character 15 | if (length < 1000) { 16 | for (var i = 0; i < length; i++) { 17 | hash = (hash << 5) - hash + str.charCodeAt(i); 18 | hash = hash & hash; // Convert to 32bit integer 19 | } 20 | } 21 | else { 22 | // For large strings, sample strategically: 23 | // - First and last 100 characters (header/footer detection) 24 | // - Every 50th character in the middle (content changes) 25 | // - Total sample size: ~200-400 characters vs entire file 26 | // Hash first 100 characters 27 | var start = Math.min(100, length); 28 | for (var i = 0; i < start; i++) { 29 | hash = (hash << 5) - hash + str.charCodeAt(i); 30 | hash = hash & hash; 31 | } 32 | // Hash last 100 characters 33 | var end = Math.max(length - 100, start); 34 | for (var i = end; i < length; i++) { 35 | hash = (hash << 5) - hash + str.charCodeAt(i); 36 | hash = hash & hash; 37 | } 38 | // Sample middle section every 50 characters 39 | for (var i = start; i < end; i += 50) { 40 | hash = (hash << 5) - hash + str.charCodeAt(i); 41 | hash = hash & hash; 42 | } 43 | } 44 | return hash; 45 | } 46 | /* 47 | * extract # headers with caching 48 | */ 49 | export function parse(props) { 50 | var body = props.editingNote.body; 51 | var bodyHash = hashCode(body); 52 | // Return cached result if content hasn't changed 53 | if (lastBodyHash === bodyHash && headerCache.has(bodyHash)) { 54 | return headerCache.get(bodyHash); 55 | } 56 | // section list which starting with #. 57 | var headers = []; 58 | // minimum section level 59 | var min = 999; 60 | var row = -1; 61 | var before = null; 62 | var index = 0; 63 | var isInCodeBlock = false; 64 | var codeBlockReg = /^\s{0,}```/; 65 | // Split only once and cache the lines 66 | var lines = body.split("\n"); 67 | var lineCount = lines.length; 68 | for (var lineIndex = 0; lineIndex < lineCount; lineIndex++) { 69 | var v = lines[lineIndex]; 70 | row++; 71 | // skip code block 72 | if (codeBlockReg.test(v)) { 73 | isInCodeBlock = !isInCodeBlock; 74 | continue; 75 | } 76 | else if (isInCodeBlock) { 77 | continue; 78 | } 79 | // check 80 | if (!isValid(v)) { 81 | continue; 82 | } 83 | // count of # - optimized loop 84 | var i = 0; 85 | var vLength = v.length; 86 | while (i < vLength && v[i] === "#") { 87 | i++; 88 | } 89 | // create header item - optimized string processing 90 | var headerStartIndex = i; 91 | while (headerStartIndex < vLength && v[headerStartIndex] === " ") { 92 | headerStartIndex++; 93 | } 94 | var header = { 95 | count: i, 96 | str: v.substring(headerStartIndex), 97 | rowStart: row, 98 | rowEnd: 0, 99 | index: index, 100 | }; 101 | index++; 102 | // apply header end row 103 | if (before != null) { 104 | before.rowEnd = row - 1; 105 | } 106 | before = header; 107 | headers.push(header); 108 | if (i < min) { 109 | min = i; 110 | } 111 | } 112 | if (headers.length != 0) { 113 | headers[headers.length - 1].rowEnd = row; 114 | } 115 | var result = { headers: headers, min: min }; 116 | // Cache the result 117 | headerCache.set(bodyHash, result); 118 | lastBodyHash = bodyHash; 119 | // Keep cache size reasonable (last 10 documents) 120 | if (headerCache.size > 10) { 121 | var firstKey = headerCache.keys().next().value; 122 | if (firstKey !== undefined) { 123 | headerCache.delete(firstKey); 124 | } 125 | } 126 | return result; 127 | } 128 | /* 129 | * 130 | */ 131 | function isValid(v) { 132 | if (!v.startsWith("#")) { 133 | return false; 134 | } 135 | if (!v.match(/^#+\s+\S+/)) { 136 | return false; 137 | } 138 | return true; 139 | } 140 | -------------------------------------------------------------------------------- /lib/settings.js: -------------------------------------------------------------------------------- 1 | "use babel"; 2 | import { WidthChangeMode } from "./types"; 3 | var Settings = /** @class */ (function () { 4 | function Settings() { 5 | var _this = this; 6 | this.DefaultWidth = 200; 7 | this.fontFamily = ""; 8 | this.hiBgColor = ""; 9 | this.hiFgColor = ""; 10 | this.currentWidth = 0; 11 | this._settingWidth = 0; 12 | this.isTextwrap = true; 13 | this.isDefaultVisible = true; 14 | this.isShowIfNoHeader = false; 15 | this.sidetocPanePadding = 10; 16 | /* 17 | * 18 | */ 19 | this.changeCurrentWidth = function (mode) { 20 | var width = inkdrop.config.get("sidetoc.IncreaseWidth"); 21 | // check settings 22 | if (width == null || width == 0) { 23 | width = 10; 24 | } 25 | if (mode == WidthChangeMode.Reset) { 26 | _this.currentWidth = _this._settingWidth; 27 | } 28 | else if (mode == WidthChangeMode.Increase) { 29 | _this.currentWidth += width; 30 | } 31 | else if (mode == WidthChangeMode.Decrease) { 32 | _this.currentWidth -= width; 33 | } 34 | document.documentElement.style.setProperty("--inkdrop-sidetoc-width", _this.currentWidth.toString(10) + "px"); 35 | document.documentElement.style.setProperty("--inkdrop-sidetoc-pane-wrapper-width", (_this.currentWidth - 2 * _this.sidetocPanePadding).toString(10) + "px"); 36 | }; 37 | /* 38 | * 39 | */ 40 | this.toggleTextWrap = function () { 41 | _this.isTextwrap = !_this.isTextwrap; 42 | }; 43 | // to get css value 44 | this.computedStyle = getComputedStyle(document.body); 45 | // fontFamily 46 | inkdrop.config.observe("editor.fontFamily", function (newValue) { 47 | _this.fontFamily = newValue; 48 | }); 49 | // highlight BG 50 | inkdrop.config.observe("sidetoc.highlightBgColor", function (newValue) { 51 | _this._changeBgColor(newValue); 52 | }); 53 | // highlight FG 54 | inkdrop.config.observe("sidetoc.highlightFgColor", function (newValue) { 55 | _this._changeFgColor(newValue); 56 | }); 57 | // width 58 | inkdrop.config.observe("sidetoc.width", function (newValue) { 59 | if (newValue == null || newValue < 10) { 60 | newValue = _this.DefaultWidth; 61 | } 62 | _this._settingWidth = newValue; 63 | _this.currentWidth = newValue; 64 | _this.changeCurrentWidth(WidthChangeMode.Reset); 65 | }); 66 | // ellipsis 67 | inkdrop.config.observe("sidetoc.textwrap", function (newValue) { 68 | if (newValue == undefined) { 69 | newValue = true; 70 | } 71 | _this.isTextwrap = newValue; 72 | }); 73 | // visibility 74 | inkdrop.config.observe("sidetoc.defaultVisible", function (newValue) { 75 | if (newValue == undefined) { 76 | newValue = true; 77 | } 78 | _this.isDefaultVisible = newValue; 79 | }); 80 | // show if no header 81 | inkdrop.config.observe("sidetoc.showIfNoHeader", function (newValue) { 82 | if (newValue == undefined) { 83 | newValue = false; 84 | } 85 | _this.isShowIfNoHeader = newValue; 86 | }); 87 | // wrapper's padding 88 | document.documentElement.style.setProperty("--inkdrop-sidetoc-padding", this.sidetocPanePadding.toString(10) + "px"); 89 | } 90 | /* 91 | * 92 | */ 93 | Settings.prototype.refresh = function () { 94 | // to get css value 95 | this.computedStyle = getComputedStyle(document.body); 96 | this._changeBgColor(inkdrop.config.get("sidetoc.highlightBgColor")); 97 | this._changeFgColor(inkdrop.config.get("sidetoc.highlightFgColor")); 98 | }; 99 | /* 100 | * 101 | */ 102 | Settings.prototype._changeBgColor = function (newValue) { 103 | if (newValue == null) { 104 | return; 105 | } 106 | if (newValue.startsWith("--")) { 107 | newValue = this.computedStyle.getPropertyValue(newValue); 108 | } 109 | this.hiBgColor = newValue; 110 | document.documentElement.style.setProperty("--inkdrop-sidetoc-highlight-bg-color", newValue); 111 | }; 112 | /* 113 | * 114 | */ 115 | Settings.prototype._changeFgColor = function (newValue) { 116 | if (newValue == null) { 117 | return null; 118 | } 119 | if (newValue.startsWith("--")) { 120 | newValue = this.computedStyle.getPropertyValue(newValue); 121 | } 122 | this.hiFgColor = newValue; 123 | document.documentElement.style.setProperty("--inkdrop-sidetoc-highlight-fg-color", newValue); 124 | }; 125 | return Settings; 126 | }()); 127 | export default new Settings(); 128 | -------------------------------------------------------------------------------- /src/sidetoc-pane.tsx: -------------------------------------------------------------------------------- 1 | "use babel"; 2 | 3 | import * as React from "react"; 4 | import * as ripper from "./ripper"; 5 | import CodeMirror from "codemirror"; 6 | import dispatcher from "./dispatcher"; 7 | import Settings from "./settings"; 8 | import { PaneState } from "./pane-state"; 9 | import { 10 | Inkdrop, 11 | Editor, 12 | DispatchAction, 13 | HeaderItem, 14 | Props, 15 | State, 16 | WidthChangeMode, 17 | } from "./types"; 18 | import { settings } from "cluster"; 19 | 20 | const $ = (query: string) => document.querySelector(query); 21 | 22 | declare var inkdrop: Inkdrop; 23 | 24 | // Memoized header list item component for performance 25 | interface HeaderListItemProps { 26 | header: HeaderItem; 27 | style: React.CSSProperties; 28 | onClick: (header: HeaderItem) => void; 29 | isCurrent: boolean; 30 | } 31 | 32 | const HeaderListItem = React.memo( 33 | React.forwardRef( 34 | ({ header, style, onClick, isCurrent }, ref) => ( 35 |
  • onClick(header)} 38 | ref={isCurrent ? ref : null} 39 | > 40 | {header.str} 41 |
  • 42 | ) 43 | ), 44 | (prevProps, nextProps) => { 45 | // Only prevent re-render if header content is the same AND isCurrent status unchanged 46 | // Always re-render when isCurrent changes to update background color 47 | if (prevProps.isCurrent !== nextProps.isCurrent) { 48 | return false; // Force re-render when active state changes 49 | } 50 | 51 | // For performance, skip deep style comparison and only check header identity 52 | return prevProps.header === nextProps.header; 53 | } 54 | ); 55 | 56 | export default class SideTocPane extends React.Component { 57 | // internal state 58 | paneState = new PaneState(); 59 | // element cache 60 | statusBar: Element | null = null; 61 | 62 | // Utility functions for performance optimization 63 | private debounce = void>(func: T, wait: number): T => { 64 | let timeout: NodeJS.Timeout | undefined; 65 | return ((...args: Parameters) => { 66 | const later = () => { 67 | clearTimeout(timeout); 68 | func(...args); 69 | }; 70 | clearTimeout(timeout); 71 | timeout = setTimeout(later, wait); 72 | }) as T; 73 | }; 74 | 75 | private throttle = void>(func: T, limit: number): T => { 76 | let inThrottle: boolean; 77 | return ((...args: Parameters) => { 78 | if (!inThrottle) { 79 | func(...args); 80 | inThrottle = true; 81 | setTimeout(() => inThrottle = false, limit); 82 | } 83 | }) as T; 84 | }; 85 | 86 | // DOM element cache methods for performance 87 | private getPaneElement(): HTMLElement | null { 88 | if (!this.paneState.cachedPaneElement) { 89 | this.paneState.cachedPaneElement = document.querySelector( 90 | "#app-container > .main-layout > .editor-layout > .mde-layout > .sidetoc-pane" 91 | ); 92 | } 93 | return this.paneState.cachedPaneElement; 94 | } 95 | 96 | private getEditorElement(): Element | null { 97 | if (!this.paneState.cachedEditorElement) { 98 | this.paneState.cachedEditorElement = document.querySelector(".editor"); 99 | } 100 | return this.paneState.cachedEditorElement; 101 | } 102 | 103 | private invalidateElementCache(): void { 104 | this.paneState.cachedPaneElement = null; 105 | this.paneState.cachedEditorElement = null; 106 | } 107 | /* 108 | * 109 | */ 110 | componentWillMount() { 111 | // state of this component 112 | this.state = { 113 | visibility: Settings.isDefaultVisible, 114 | headers: [], 115 | currentHeader: null, 116 | min: 0, 117 | len: 0, 118 | }; 119 | 120 | this.paneState.dispatchId = dispatcher.register((action: DispatchAction) => 121 | this.dispatchAction(action) 122 | ); 123 | 124 | const editor = inkdrop.getActiveEditor(); 125 | if (editor != null) { 126 | this.attachEvents(editor); 127 | } else { 128 | inkdrop.onEditorLoad((e) => { 129 | this.attachEvents(e); 130 | }); 131 | } 132 | } 133 | /* 134 | * 135 | */ 136 | componentDidUpdate() { 137 | const cur = this.paneState.curSectionRef.current; 138 | if (cur == null) { 139 | return; 140 | } 141 | 142 | const pane = document.querySelector(".sidetoc-pane-wrapper"); 143 | if (pane != null) { 144 | pane.scrollTop = cur.offsetTop - pane.offsetTop; 145 | } 146 | } 147 | /* 148 | * 149 | */ 150 | componentWillUnmount() { 151 | // unhandle event 152 | dispatcher.unregister(this.paneState.dispatchId); 153 | 154 | const editor = inkdrop.getActiveEditor(); 155 | // for delete note 156 | if (editor == null) { 157 | return; 158 | } 159 | 160 | this.detachEvents(editor); 161 | } 162 | /* 163 | * 164 | */ 165 | dispatchAction(action: DispatchAction) { 166 | switch (action.type) { 167 | case "Toggle": 168 | this.handleVisibility(); 169 | break; 170 | case "JumpToPrev": 171 | this.handleJumpToPrev(); 172 | break; 173 | case "JumpToNext": 174 | this.handleJumpToNext(); 175 | break; 176 | case "Activate": 177 | this.updateState({ visibility: true }); 178 | break; 179 | case "Deactivate": 180 | this.updateState({ visibility: false }); 181 | break; 182 | case "IncreaseWidth": 183 | this.handleChangeWidth(WidthChangeMode.Increase); 184 | break; 185 | case "DecreaseWidth": 186 | this.handleChangeWidth(WidthChangeMode.Decrease); 187 | break; 188 | case "ResetWidth": 189 | this.handleChangeWidth(WidthChangeMode.Reset); 190 | break; 191 | case "ToggleTextwrap": 192 | this.handleToggleTextwrap(); 193 | break; 194 | 195 | default: 196 | } 197 | } 198 | /* 199 | * Optimized render with better caching strategy 200 | */ 201 | render() { 202 | // Check if we need to regenerate content 203 | if (this.paneState.content == null || this.shouldUpdateContent()) { 204 | this.paneState.content = this.createContent(); 205 | this.paneState.lastRenderHeaders = this.state.headers; 206 | this.paneState.lastRenderVisibility = this.state.visibility; 207 | this.paneState.lastRenderCurrentHeader = this.state.currentHeader; 208 | } 209 | 210 | return this.paneState.content; 211 | } 212 | 213 | /* 214 | * Check if content should be updated 215 | */ 216 | private shouldUpdateContent(): boolean { 217 | return this.paneState.lastRenderHeaders !== this.state.headers || 218 | this.paneState.lastRenderVisibility !== this.state.visibility || 219 | this.paneState.lastRenderCurrentHeader !== this.state.currentHeader; 220 | } 221 | /* 222 | * 223 | */ 224 | createContent() { 225 | const isShowPane = this.isShowPane(); 226 | let className = "sidetoc-pane"; 227 | if (!isShowPane) { 228 | className = "sidetoc-pane-hide"; 229 | } else if (!Settings.isTextwrap) { 230 | className += " sidetoc-pane-nowrap"; 231 | } 232 | 233 | const style = { 234 | fontFamily: Settings.fontFamily, 235 | }; 236 | 237 | let wrapperStyle: { [key: string]: any } = {}; 238 | if (isShowPane) { 239 | const pane = this.getPaneElement(); 240 | if (pane != null) { 241 | wrapperStyle.height = pane.offsetHeight - 20; 242 | } 243 | } 244 | 245 | // current header key for preview which join header text with "_". 246 | let current = ""; 247 | 248 | return ( 249 |
    250 |
    251 | {this.state.headers.map((v: HeaderItem, index: number) => { 252 | // Optimized string processing - avoid regex replace 253 | const cleanStr = v.str.split(' ').join(''); 254 | current += "_" + cleanStr; 255 | const { style, isCurrent } = this.toStyleCached(v, current); 256 | const ref = isCurrent ? this.paneState.curSectionRef : null; 257 | return ( 258 | 266 | ); 267 | })} 268 |
    269 |
    270 | ); 271 | } 272 | /*----- private -----*/ 273 | /* 274 | * 275 | */ 276 | attachEvents(editor: Editor) { 277 | this.statusBar = document.querySelector( 278 | "#app-container .main-layout .editor-layout .editor-status-bar-layout" 279 | ); 280 | 281 | // refresh 282 | this.updateState(); 283 | 284 | const { cm } = editor; 285 | cm.on("cursorActivity", this.handleCursorActivity); 286 | cm.on("changes", this.handleCmUpdate); 287 | cm.on("scroll", this.handleCmScroll); 288 | 289 | // for sidetoc overflow-y 290 | inkdrop.window.on("resize", this.handleWindowResize); 291 | 292 | // hook preview scroll 293 | const editorEle = this.getEditorElement(); 294 | if (editorEle == null) { 295 | return; 296 | } 297 | // preview element 298 | const preview = editorEle.querySelector(".mde-preview"); 299 | if (preview == null) { 300 | return; 301 | } 302 | // Store reference for proper cleanup 303 | this.paneState.previewElement = preview; 304 | preview.addEventListener("scroll", this.handlePreviewScroll); 305 | 306 | // check initial view mode 307 | this.paneState.isPreview = editorEle.classList.contains("editor-viewmode-preview"); 308 | // observe preview update 309 | this.paneState.observer = new MutationObserver((_) => this.handlePreviewUpdate(editorEle)); 310 | 311 | this.paneState.observer.observe(preview, { 312 | childList: true, 313 | subtree: true, 314 | attributes: true, 315 | }); 316 | 317 | // observe ui theme 318 | this.paneState.bodyObserver = new MutationObserver((_) => { 319 | Settings.refresh(); 320 | this.updateState(); 321 | }); 322 | this.paneState.bodyObserver.observe(document.body, { attributes: true }); 323 | } 324 | /* 325 | * 326 | */ 327 | detachEvents(editor: Editor) { 328 | const { cm } = editor; 329 | cm.off("cursorActivity", this.handleCursorActivity); 330 | cm.off("update", this.handleCmUpdate); 331 | cm.off("scroll", this.handleCmScroll); 332 | 333 | inkdrop.window.off("resize", this.handleWindowResize); 334 | this.paneState.observer!.disconnect(); 335 | this.paneState.bodyObserver!.disconnect(); 336 | 337 | // Properly remove preview scroll event listener to prevent memory leaks 338 | if (this.paneState.previewElement) { 339 | this.paneState.previewElement.removeEventListener("scroll", this.handlePreviewScroll); 340 | this.paneState.previewElement = null; 341 | } 342 | } 343 | /* 344 | * 345 | */ 346 | updateState = (option = {}) => { 347 | const editor = inkdrop.getActiveEditor(); 348 | if (editor == null) { 349 | return; 350 | } 351 | const ret = ripper.parse(this.props); 352 | // renew state 353 | const newState = Object.assign(option, { 354 | headers: ret.headers, 355 | min: ret.min, 356 | len: editor.cm.lineCount(), 357 | }); 358 | 359 | this.commit(newState); 360 | 361 | return newState; 362 | }; 363 | /* 364 | * 365 | */ 366 | updateSection(line: number): void { 367 | const header = this.getCurrentHeader(line); 368 | if (header != null && this.state.currentHeader != header) { 369 | this.commit({ currentHeader: header }); 370 | } 371 | } 372 | /* 373 | * 374 | */ 375 | handleVisibility = () => { 376 | this.commit({ visibility: !this.state.visibility }); 377 | setTimeout(() => { 378 | inkdrop.commands.dispatch(document.body, "editor:refresh"); 379 | }, 10); 380 | }; 381 | /* 382 | * Handle CodeMirror updates with debouncing 383 | */ 384 | handleCmUpdate = this.debounce(() => { 385 | if (this.props.editingNote._id != this.paneState.noteId) { 386 | this.paneState.noteId = this.props.editingNote._id; 387 | this.paneState.previewCurrent = ""; 388 | const newState = this.updateState(); 389 | if (newState != null && newState.headers.length > 0) { 390 | this.paneState.previewCurrent = "_" + newState.headers[0].str.replace(/ /g, ""); 391 | setTimeout(() => { 392 | this.handleCursorActivity(inkdrop.getActiveEditor().cm, true); 393 | }, 100); 394 | } 395 | return; 396 | } 397 | 398 | const editor = inkdrop.getActiveEditor(); 399 | if (!editor) return; 400 | 401 | const { cm } = editor; 402 | const text = cm.lineInfo(cm.getCursor().line).text as string; 403 | // forcely update when starts with "#" 404 | if (!text.startsWith("#")) { 405 | // edited normal line. 406 | if (this.state.len == editor.cm.lineCount()) { 407 | return; 408 | } 409 | } 410 | 411 | this.updateState(); 412 | }, 200); // 200ms debounce 413 | /* 414 | * 415 | */ 416 | handleCursorActivity = (cm: CodeMirror.Editor, forcibly: boolean = false) => { 417 | const cur = cm.getCursor(); 418 | if (!forcibly && cur.line == this.paneState.lastLine) { 419 | return; 420 | } 421 | this.paneState.lastLine = cur.line; 422 | this.updateSection(cur.line); 423 | this.paneState.cursorTime = new Date(); 424 | }; 425 | /* 426 | * Handle scrolling and refresh highlight section. 427 | */ 428 | handleCmScroll = (cm: CodeMirror.Editor) => { 429 | // prioritize handleCursorActivity 430 | if ( 431 | this.paneState.cursorTime != null && 432 | new Date().getTime() - this.paneState.cursorTime.getTime() < 100 433 | ) { 434 | return; 435 | } 436 | 437 | const info = cm.getScrollInfo(); 438 | // todo: with adjusted value 439 | let top = info.top + inkdrop.config.get("editor.cursorScrollMargin"); 440 | // for scrool to bottom 441 | if (top + info.clientHeight >= info.height) { 442 | top = info.height - 10; 443 | } 444 | const line = cm.lineAtHeight(top, "local"); 445 | 446 | this.updateSection(line); 447 | }; 448 | /* 449 | * 450 | */ 451 | handleJumpToPrev = () => { 452 | // for preview mode 453 | if (this.paneState.isPreview) { 454 | const preview = document.querySelector(".mde-preview")!; 455 | const meta = document.querySelector(".metadata"); 456 | 457 | if (meta != null && meta.clientHeight != 0 && this.paneState.previewHeaders.length > 1) { 458 | const header = this.paneState.previewHeaders[0]; 459 | if (preview.scrollTop <= header.offsetTop) { 460 | preview.scrollTop = 0; 461 | return; 462 | } 463 | } 464 | 465 | for (let i = 1; i < this.paneState.previewHeaders.length; i++) { 466 | const header = this.paneState.previewHeaders[i]; 467 | const top = header.getBoundingClientRect().top; 468 | if (top > 0) { 469 | preview.scrollTop = this.paneState.previewHeaders[i - 1].offsetTop - preview.offsetTop; 470 | return; 471 | } 472 | } 473 | 474 | const header = this.paneState.previewHeaders[this.paneState.previewHeaders.length - 1]; 475 | if (header == null) { 476 | return; 477 | } 478 | preview.scrollTop = header.offsetTop - preview.offsetTop; 479 | return; 480 | } 481 | 482 | // for editor mode 483 | const { cm } = inkdrop.getActiveEditor(); 484 | const { line } = cm.getCursor(); 485 | const header = this.getCurrentHeader(line); 486 | const prev = this.getPrevHeader(header, line); 487 | if (prev != null) { 488 | cm.setCursor(prev.rowStart, 0); 489 | this.handleCursorActivity(cm); 490 | } 491 | }; 492 | /* 493 | * 494 | */ 495 | handleJumpToNext = () => { 496 | // for preview mode 497 | if (this.paneState.isPreview) { 498 | const preview = document.querySelector(".mde-preview")!; 499 | const meta = document.querySelector(".metadata"); 500 | 501 | // meta div 502 | if (meta != null && meta.clientHeight != 0 && this.paneState.previewHeaders.length > 1) { 503 | const header = this.paneState.previewHeaders[0]; 504 | if (preview.scrollTop < header.clientHeight) { 505 | preview.scrollTop = header.offsetTop - preview.offsetTop; 506 | return; 507 | } 508 | } 509 | 510 | const diff = preview.getBoundingClientRect().y; 511 | for (let i = this.paneState.previewHeaders.length - 2; i >= 0; i--) { 512 | const header = this.paneState.previewHeaders[i]; 513 | const top = header.getBoundingClientRect().top; 514 | // maybe under 10 515 | if (top - diff < 50) { 516 | preview.scrollTop = this.paneState.previewHeaders[i + 1].offsetTop - preview.offsetTop; 517 | break; 518 | } 519 | } 520 | return; 521 | } 522 | 523 | // for editor mode 524 | const { cm } = inkdrop.getActiveEditor(); 525 | const { line } = cm.getCursor(); 526 | const header = this.getCurrentHeader(line); 527 | const next = this.getNextHeader(header); 528 | if (next != null) { 529 | const vp = cm.getViewport(); 530 | cm.setCursor(next.rowStart + Math.round((vp.to - vp.from) / 2), 0); 531 | cm.setCursor(next.rowStart, 0); 532 | this.handleCursorActivity(cm); 533 | } 534 | }; 535 | /* 536 | * 537 | */ 538 | handlePreviewUpdate = (editorEle: Element | null) => { 539 | if (editorEle == null) { 540 | return; 541 | } 542 | 543 | this.paneState.isPreview = editorEle.classList.contains("editor-viewmode-preview"); 544 | // skip editor mode 545 | if (!this.paneState.isPreview) { 546 | return; 547 | } 548 | 549 | this.paneState.previewHeaders = []; 550 | const preview = editorEle.querySelector(".mde-preview"); 551 | if (!preview) return; 552 | 553 | // Use efficient CSS selector instead of scanning all elements 554 | const headerElements = preview.querySelectorAll("h1, h2, h3, h4, h5, h6"); 555 | this.paneState.previewHeaders = Array.from(headerElements); 556 | }; 557 | 558 | /* 559 | * Handle preview scroll with throttling. 560 | */ 561 | handlePreviewScroll = this.throttle((_: Event) => { 562 | // skip editor mode 563 | if (!this.paneState.isPreview) { 564 | return; 565 | } 566 | 567 | // for first preview 568 | if (this.paneState.firstPreview) { 569 | this.paneState.firstPreview = false; 570 | this.handlePreviewUpdate(this.getEditorElement()); 571 | } 572 | 573 | const previewElement = document.querySelector(".mde-preview"); 574 | if (!previewElement) return; 575 | 576 | const diff = previewElement.getBoundingClientRect().y; 577 | // analyze current header 578 | for (let i = this.paneState.previewHeaders.length - 1; i >= 0; i--) { 579 | const header = this.paneState.previewHeaders[i]; 580 | const top = header.getBoundingClientRect().top; 581 | // create current header key 582 | if (top - diff < 50) { 583 | let current = ""; 584 | let k = 0; 585 | for (k = 0; k <= i; k++) { 586 | current += "_" + this.paneState.previewHeaders[k].textContent; 587 | } 588 | current = current.replace(/ /g, ""); 589 | // change preview current 590 | if (this.paneState.previewCurrent != current) { 591 | this.paneState.previewCurrent = current; 592 | 593 | // move cursor to active header 594 | const { cm } = inkdrop.getActiveEditor(); 595 | const item = this.state.headers[k - 1]; 596 | if (item != null) { 597 | cm.setCursor(item.rowStart, 0); 598 | this.forceUpdate(); 599 | } 600 | } 601 | 602 | break; 603 | } 604 | } 605 | }, 16); // 60fps throttling 606 | /* 607 | * 608 | */ 609 | handleWindowResize = () => { 610 | // Handle size changed. 611 | if (!this.state.visibility) { 612 | return; 613 | } 614 | 615 | // Invalidate DOM cache on resize as layout may have changed 616 | this.invalidateElementCache(); 617 | this.updateState(); 618 | }; 619 | /* 620 | * scroll to header 621 | */ 622 | handleClick = (header: HeaderItem) => { 623 | // for preview mode 624 | if (this.paneState.isPreview) { 625 | for (let i = 0; i < this.state.headers.length; i++) { 626 | if (this.state.headers[i] == header) { 627 | const preview = document.querySelector(".mde-preview")!; 628 | preview.scrollTop = this.paneState.previewHeaders[i].offsetTop - preview.offsetTop; 629 | inkdrop.commands.dispatch(document.body, "editor:focus"); 630 | break; 631 | } 632 | } 633 | return; 634 | } 635 | 636 | const cm = inkdrop.getActiveEditor().cm; 637 | cm.scrollTo(0, 99999); 638 | cm.setCursor(header.rowStart, 0); 639 | cm.focus(); 640 | }; 641 | /* 642 | * 643 | */ 644 | handleChangeWidth = (mode: WidthChangeMode) => Settings.changeCurrentWidth(mode); 645 | /* 646 | * 647 | */ 648 | handleToggleTextwrap = () => { 649 | Settings.toggleTextWrap(); 650 | this.commit({}); 651 | }; 652 | /* 653 | * convert to style with caching for object pooling 654 | */ 655 | toStyleCached = (header: HeaderItem, current: string) => { 656 | // Generate cache key based on style-affecting properties 657 | const cacheKey = `${header.count}-${this.state.min}-${current}-${this.paneState.isPreview ? this.paneState.previewCurrent : this.state.currentHeader?.index || 'none'}`; 658 | 659 | // Check cache first 660 | if (this.paneState.styleCache.has(cacheKey)) { 661 | return this.paneState.styleCache.get(cacheKey); 662 | } 663 | 664 | // Calculate style 665 | const result = this.toStyle(header, current); 666 | 667 | // Cache the result 668 | this.paneState.styleCache.set(cacheKey, result); 669 | 670 | // Keep cache size reasonable (LRU-like behavior) 671 | if (this.paneState.styleCache.size > 100) { 672 | const firstKey = this.paneState.styleCache.keys().next().value; 673 | if (firstKey !== undefined) { 674 | this.paneState.styleCache.delete(firstKey); 675 | } 676 | } 677 | 678 | return result; 679 | }; 680 | 681 | /* 682 | * convert to style (original implementation) 683 | */ 684 | toStyle = (header: HeaderItem, current: string) => { 685 | let style = { 686 | marginLeft: 20 * (header.count - this.state.min), 687 | cursor: "pointer", 688 | backgroundColor: "", 689 | color: "", 690 | }; 691 | 692 | let isCurrent = false; 693 | 694 | if (this.paneState.isPreview) { 695 | if (this.paneState.previewCurrent == current) { 696 | isCurrent = true; 697 | } 698 | } else if (this.isSameHeader(this.state.currentHeader, header)) { 699 | isCurrent = true; 700 | } 701 | 702 | if (isCurrent) { 703 | style.color = Settings.hiFgColor; 704 | style.backgroundColor = Settings.hiBgColor; 705 | } 706 | 707 | return { style, isCurrent }; 708 | }; 709 | /* 710 | * 711 | */ 712 | isSameHeader(h1: HeaderItem | null, h2: HeaderItem): boolean { 713 | if (h1 == null) { 714 | return false; 715 | } else if (h1 == h2) { 716 | return true; 717 | } 718 | 719 | if ( 720 | h1.count == h2.count && 721 | h1.index == h2.index && 722 | h1.rowStart == h2.rowStart && 723 | h1.str == h2.str 724 | ) { 725 | return true; 726 | } 727 | 728 | return false; 729 | } 730 | /* 731 | * Binary search optimized header lookup - O(log n) instead of O(n) 732 | */ 733 | getCurrentHeader(line: number): HeaderItem | null { 734 | const headers = this.state.headers; 735 | if (headers.length === 0) return null; 736 | 737 | let left = 0; 738 | let right = headers.length - 1; 739 | 740 | // Binary search for the header containing the line 741 | while (left <= right) { 742 | const mid = Math.floor((left + right) / 2); 743 | const header = headers[mid]; 744 | 745 | if (line >= header.rowStart && line <= header.rowEnd) { 746 | return header; 747 | } else if (line < header.rowStart) { 748 | right = mid - 1; 749 | } else { 750 | left = mid + 1; 751 | } 752 | } 753 | 754 | return null; 755 | } 756 | /* 757 | * 758 | */ 759 | getPrevHeader(header: HeaderItem | null, line: number): HeaderItem | null { 760 | if (header == null) { 761 | return null; 762 | } 763 | // first header 764 | if (header.index == 0) { 765 | return header; 766 | } 767 | 768 | if (header.index - 1 >= 0) { 769 | // Jump to prev header 770 | if (header.rowStart == line) { 771 | return this.state.headers[header.index - 1]; 772 | } 773 | // Jump to current header line if cursor is not current header. 774 | return header; 775 | } 776 | 777 | return null; 778 | } 779 | /* 780 | * 781 | */ 782 | getNextHeader(header: HeaderItem | null) { 783 | if (header == null) { 784 | // jump to first header 785 | return this.state.headers.length > 0 ? this.state.headers[0] : null; 786 | } 787 | 788 | if (header.index + 1 < this.state.headers.length) { 789 | return this.state.headers[header.index + 1]; 790 | } 791 | 792 | return null; 793 | } 794 | /* 795 | * Optimized commit with selective content invalidation 796 | */ 797 | commit(state: any): void { 798 | // Only invalidate content if headers, visibility, or currentHeader changed 799 | if (state.headers !== undefined || state.visibility !== undefined || state.currentHeader !== undefined) { 800 | this.paneState.content = null; 801 | } 802 | 803 | // Clear style cache when state changes that affect styling 804 | if (state.headers !== undefined || state.currentHeader !== undefined || state.min !== undefined) { 805 | this.paneState.styleCache.clear(); 806 | } 807 | 808 | this.setState(state); 809 | } 810 | /* 811 | * 812 | */ 813 | isShowPane(): boolean { 814 | if (!this.state.visibility) { 815 | return false; 816 | } 817 | 818 | if (Settings.isShowIfNoHeader) { 819 | return true; 820 | } 821 | 822 | return this.state.headers.length != 0; 823 | } 824 | /* 825 | * 826 | */ 827 | log(_: () => string) { 828 | // console.log(`sidetoc: ${_()}`); 829 | } 830 | } 831 | -------------------------------------------------------------------------------- /lib/sidetoc-pane.js: -------------------------------------------------------------------------------- 1 | "use babel"; 2 | var __extends = (this && this.__extends) || (function () { 3 | var extendStatics = function (d, b) { 4 | extendStatics = Object.setPrototypeOf || 5 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 6 | function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; 7 | return extendStatics(d, b); 8 | }; 9 | return function (d, b) { 10 | if (typeof b !== "function" && b !== null) 11 | throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); 12 | extendStatics(d, b); 13 | function __() { this.constructor = d; } 14 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 15 | }; 16 | })(); 17 | import * as React from "react"; 18 | import * as ripper from "./ripper"; 19 | import dispatcher from "./dispatcher"; 20 | import Settings from "./settings"; 21 | import { PaneState } from "./pane-state"; 22 | import { WidthChangeMode, } from "./types"; 23 | var $ = function (query) { return document.querySelector(query); }; 24 | var HeaderListItem = React.memo(React.forwardRef(function (_a, ref) { 25 | var header = _a.header, style = _a.style, onClick = _a.onClick, isCurrent = _a.isCurrent; 26 | return (React.createElement("li", { style: style, onClick: function () { return onClick(header); }, ref: isCurrent ? ref : null }, header.str)); 27 | }), function (prevProps, nextProps) { 28 | // Only prevent re-render if header content is the same AND isCurrent status unchanged 29 | // Always re-render when isCurrent changes to update background color 30 | if (prevProps.isCurrent !== nextProps.isCurrent) { 31 | return false; // Force re-render when active state changes 32 | } 33 | // For performance, skip deep style comparison and only check header identity 34 | return prevProps.header === nextProps.header; 35 | }); 36 | var SideTocPane = /** @class */ (function (_super) { 37 | __extends(SideTocPane, _super); 38 | function SideTocPane() { 39 | var _this = _super !== null && _super.apply(this, arguments) || this; 40 | // internal state 41 | _this.paneState = new PaneState(); 42 | // element cache 43 | _this.statusBar = null; 44 | // Utility functions for performance optimization 45 | _this.debounce = function (func, wait) { 46 | var timeout; 47 | return (function () { 48 | var args = []; 49 | for (var _i = 0; _i < arguments.length; _i++) { 50 | args[_i] = arguments[_i]; 51 | } 52 | var later = function () { 53 | clearTimeout(timeout); 54 | func.apply(void 0, args); 55 | }; 56 | clearTimeout(timeout); 57 | timeout = setTimeout(later, wait); 58 | }); 59 | }; 60 | _this.throttle = function (func, limit) { 61 | var inThrottle; 62 | return (function () { 63 | var args = []; 64 | for (var _i = 0; _i < arguments.length; _i++) { 65 | args[_i] = arguments[_i]; 66 | } 67 | if (!inThrottle) { 68 | func.apply(void 0, args); 69 | inThrottle = true; 70 | setTimeout(function () { return inThrottle = false; }, limit); 71 | } 72 | }); 73 | }; 74 | /* 75 | * 76 | */ 77 | _this.updateState = function (option) { 78 | if (option === void 0) { option = {}; } 79 | var editor = inkdrop.getActiveEditor(); 80 | if (editor == null) { 81 | return; 82 | } 83 | var ret = ripper.parse(_this.props); 84 | // renew state 85 | var newState = Object.assign(option, { 86 | headers: ret.headers, 87 | min: ret.min, 88 | len: editor.cm.lineCount(), 89 | }); 90 | _this.commit(newState); 91 | return newState; 92 | }; 93 | /* 94 | * 95 | */ 96 | _this.handleVisibility = function () { 97 | _this.commit({ visibility: !_this.state.visibility }); 98 | setTimeout(function () { 99 | inkdrop.commands.dispatch(document.body, "editor:refresh"); 100 | }, 10); 101 | }; 102 | /* 103 | * Handle CodeMirror updates with debouncing 104 | */ 105 | _this.handleCmUpdate = _this.debounce(function () { 106 | if (_this.props.editingNote._id != _this.paneState.noteId) { 107 | _this.paneState.noteId = _this.props.editingNote._id; 108 | _this.paneState.previewCurrent = ""; 109 | var newState = _this.updateState(); 110 | if (newState != null && newState.headers.length > 0) { 111 | _this.paneState.previewCurrent = "_" + newState.headers[0].str.replace(/ /g, ""); 112 | setTimeout(function () { 113 | _this.handleCursorActivity(inkdrop.getActiveEditor().cm, true); 114 | }, 100); 115 | } 116 | return; 117 | } 118 | var editor = inkdrop.getActiveEditor(); 119 | if (!editor) 120 | return; 121 | var cm = editor.cm; 122 | var text = cm.lineInfo(cm.getCursor().line).text; 123 | // forcely update when starts with "#" 124 | if (!text.startsWith("#")) { 125 | // edited normal line. 126 | if (_this.state.len == editor.cm.lineCount()) { 127 | return; 128 | } 129 | } 130 | _this.updateState(); 131 | }, 200); // 200ms debounce 132 | /* 133 | * 134 | */ 135 | _this.handleCursorActivity = function (cm, forcibly) { 136 | if (forcibly === void 0) { forcibly = false; } 137 | var cur = cm.getCursor(); 138 | if (!forcibly && cur.line == _this.paneState.lastLine) { 139 | return; 140 | } 141 | _this.paneState.lastLine = cur.line; 142 | _this.updateSection(cur.line); 143 | _this.paneState.cursorTime = new Date(); 144 | }; 145 | /* 146 | * Handle scrolling and refresh highlight section. 147 | */ 148 | _this.handleCmScroll = function (cm) { 149 | // prioritize handleCursorActivity 150 | if (_this.paneState.cursorTime != null && 151 | new Date().getTime() - _this.paneState.cursorTime.getTime() < 100) { 152 | return; 153 | } 154 | var info = cm.getScrollInfo(); 155 | // todo: with adjusted value 156 | var top = info.top + inkdrop.config.get("editor.cursorScrollMargin"); 157 | // for scrool to bottom 158 | if (top + info.clientHeight >= info.height) { 159 | top = info.height - 10; 160 | } 161 | var line = cm.lineAtHeight(top, "local"); 162 | _this.updateSection(line); 163 | }; 164 | /* 165 | * 166 | */ 167 | _this.handleJumpToPrev = function () { 168 | // for preview mode 169 | if (_this.paneState.isPreview) { 170 | var preview = document.querySelector(".mde-preview"); 171 | var meta = document.querySelector(".metadata"); 172 | if (meta != null && meta.clientHeight != 0 && _this.paneState.previewHeaders.length > 1) { 173 | var header_1 = _this.paneState.previewHeaders[0]; 174 | if (preview.scrollTop <= header_1.offsetTop) { 175 | preview.scrollTop = 0; 176 | return; 177 | } 178 | } 179 | for (var i = 1; i < _this.paneState.previewHeaders.length; i++) { 180 | var header_2 = _this.paneState.previewHeaders[i]; 181 | var top_1 = header_2.getBoundingClientRect().top; 182 | if (top_1 > 0) { 183 | preview.scrollTop = _this.paneState.previewHeaders[i - 1].offsetTop - preview.offsetTop; 184 | return; 185 | } 186 | } 187 | var header_3 = _this.paneState.previewHeaders[_this.paneState.previewHeaders.length - 1]; 188 | if (header_3 == null) { 189 | return; 190 | } 191 | preview.scrollTop = header_3.offsetTop - preview.offsetTop; 192 | return; 193 | } 194 | // for editor mode 195 | var cm = inkdrop.getActiveEditor().cm; 196 | var line = cm.getCursor().line; 197 | var header = _this.getCurrentHeader(line); 198 | var prev = _this.getPrevHeader(header, line); 199 | if (prev != null) { 200 | cm.setCursor(prev.rowStart, 0); 201 | _this.handleCursorActivity(cm); 202 | } 203 | }; 204 | /* 205 | * 206 | */ 207 | _this.handleJumpToNext = function () { 208 | // for preview mode 209 | if (_this.paneState.isPreview) { 210 | var preview = document.querySelector(".mde-preview"); 211 | var meta = document.querySelector(".metadata"); 212 | // meta div 213 | if (meta != null && meta.clientHeight != 0 && _this.paneState.previewHeaders.length > 1) { 214 | var header_4 = _this.paneState.previewHeaders[0]; 215 | if (preview.scrollTop < header_4.clientHeight) { 216 | preview.scrollTop = header_4.offsetTop - preview.offsetTop; 217 | return; 218 | } 219 | } 220 | var diff = preview.getBoundingClientRect().y; 221 | for (var i = _this.paneState.previewHeaders.length - 2; i >= 0; i--) { 222 | var header_5 = _this.paneState.previewHeaders[i]; 223 | var top_2 = header_5.getBoundingClientRect().top; 224 | // maybe under 10 225 | if (top_2 - diff < 50) { 226 | preview.scrollTop = _this.paneState.previewHeaders[i + 1].offsetTop - preview.offsetTop; 227 | break; 228 | } 229 | } 230 | return; 231 | } 232 | // for editor mode 233 | var cm = inkdrop.getActiveEditor().cm; 234 | var line = cm.getCursor().line; 235 | var header = _this.getCurrentHeader(line); 236 | var next = _this.getNextHeader(header); 237 | if (next != null) { 238 | var vp = cm.getViewport(); 239 | cm.setCursor(next.rowStart + Math.round((vp.to - vp.from) / 2), 0); 240 | cm.setCursor(next.rowStart, 0); 241 | _this.handleCursorActivity(cm); 242 | } 243 | }; 244 | /* 245 | * 246 | */ 247 | _this.handlePreviewUpdate = function (editorEle) { 248 | if (editorEle == null) { 249 | return; 250 | } 251 | _this.paneState.isPreview = editorEle.classList.contains("editor-viewmode-preview"); 252 | // skip editor mode 253 | if (!_this.paneState.isPreview) { 254 | return; 255 | } 256 | _this.paneState.previewHeaders = []; 257 | var preview = editorEle.querySelector(".mde-preview"); 258 | if (!preview) 259 | return; 260 | // Use efficient CSS selector instead of scanning all elements 261 | var headerElements = preview.querySelectorAll("h1, h2, h3, h4, h5, h6"); 262 | _this.paneState.previewHeaders = Array.from(headerElements); 263 | }; 264 | /* 265 | * Handle preview scroll with throttling. 266 | */ 267 | _this.handlePreviewScroll = _this.throttle(function (_) { 268 | // skip editor mode 269 | if (!_this.paneState.isPreview) { 270 | return; 271 | } 272 | // for first preview 273 | if (_this.paneState.firstPreview) { 274 | _this.paneState.firstPreview = false; 275 | _this.handlePreviewUpdate(_this.getEditorElement()); 276 | } 277 | var previewElement = document.querySelector(".mde-preview"); 278 | if (!previewElement) 279 | return; 280 | var diff = previewElement.getBoundingClientRect().y; 281 | // analyze current header 282 | for (var i = _this.paneState.previewHeaders.length - 1; i >= 0; i--) { 283 | var header = _this.paneState.previewHeaders[i]; 284 | var top_3 = header.getBoundingClientRect().top; 285 | // create current header key 286 | if (top_3 - diff < 50) { 287 | var current = ""; 288 | var k = 0; 289 | for (k = 0; k <= i; k++) { 290 | current += "_" + _this.paneState.previewHeaders[k].textContent; 291 | } 292 | current = current.replace(/ /g, ""); 293 | // change preview current 294 | if (_this.paneState.previewCurrent != current) { 295 | _this.paneState.previewCurrent = current; 296 | // move cursor to active header 297 | var cm = inkdrop.getActiveEditor().cm; 298 | var item = _this.state.headers[k - 1]; 299 | if (item != null) { 300 | cm.setCursor(item.rowStart, 0); 301 | _this.forceUpdate(); 302 | } 303 | } 304 | break; 305 | } 306 | } 307 | }, 16); // 60fps throttling 308 | /* 309 | * 310 | */ 311 | _this.handleWindowResize = function () { 312 | // Handle size changed. 313 | if (!_this.state.visibility) { 314 | return; 315 | } 316 | // Invalidate DOM cache on resize as layout may have changed 317 | _this.invalidateElementCache(); 318 | _this.updateState(); 319 | }; 320 | /* 321 | * scroll to header 322 | */ 323 | _this.handleClick = function (header) { 324 | // for preview mode 325 | if (_this.paneState.isPreview) { 326 | for (var i = 0; i < _this.state.headers.length; i++) { 327 | if (_this.state.headers[i] == header) { 328 | var preview = document.querySelector(".mde-preview"); 329 | preview.scrollTop = _this.paneState.previewHeaders[i].offsetTop - preview.offsetTop; 330 | inkdrop.commands.dispatch(document.body, "editor:focus"); 331 | break; 332 | } 333 | } 334 | return; 335 | } 336 | var cm = inkdrop.getActiveEditor().cm; 337 | cm.scrollTo(0, 99999); 338 | cm.setCursor(header.rowStart, 0); 339 | cm.focus(); 340 | }; 341 | /* 342 | * 343 | */ 344 | _this.handleChangeWidth = function (mode) { return Settings.changeCurrentWidth(mode); }; 345 | /* 346 | * 347 | */ 348 | _this.handleToggleTextwrap = function () { 349 | Settings.toggleTextWrap(); 350 | _this.commit({}); 351 | }; 352 | /* 353 | * convert to style with caching for object pooling 354 | */ 355 | _this.toStyleCached = function (header, current) { 356 | var _a; 357 | // Generate cache key based on style-affecting properties 358 | var cacheKey = "".concat(header.count, "-").concat(_this.state.min, "-").concat(current, "-").concat(_this.paneState.isPreview ? _this.paneState.previewCurrent : ((_a = _this.state.currentHeader) === null || _a === void 0 ? void 0 : _a.index) || 'none'); 359 | // Check cache first 360 | if (_this.paneState.styleCache.has(cacheKey)) { 361 | return _this.paneState.styleCache.get(cacheKey); 362 | } 363 | // Calculate style 364 | var result = _this.toStyle(header, current); 365 | // Cache the result 366 | _this.paneState.styleCache.set(cacheKey, result); 367 | // Keep cache size reasonable (LRU-like behavior) 368 | if (_this.paneState.styleCache.size > 100) { 369 | var firstKey = _this.paneState.styleCache.keys().next().value; 370 | if (firstKey !== undefined) { 371 | _this.paneState.styleCache.delete(firstKey); 372 | } 373 | } 374 | return result; 375 | }; 376 | /* 377 | * convert to style (original implementation) 378 | */ 379 | _this.toStyle = function (header, current) { 380 | var style = { 381 | marginLeft: 20 * (header.count - _this.state.min), 382 | cursor: "pointer", 383 | backgroundColor: "", 384 | color: "", 385 | }; 386 | var isCurrent = false; 387 | if (_this.paneState.isPreview) { 388 | if (_this.paneState.previewCurrent == current) { 389 | isCurrent = true; 390 | } 391 | } 392 | else if (_this.isSameHeader(_this.state.currentHeader, header)) { 393 | isCurrent = true; 394 | } 395 | if (isCurrent) { 396 | style.color = Settings.hiFgColor; 397 | style.backgroundColor = Settings.hiBgColor; 398 | } 399 | return { style: style, isCurrent: isCurrent }; 400 | }; 401 | return _this; 402 | } 403 | // DOM element cache methods for performance 404 | SideTocPane.prototype.getPaneElement = function () { 405 | if (!this.paneState.cachedPaneElement) { 406 | this.paneState.cachedPaneElement = document.querySelector("#app-container > .main-layout > .editor-layout > .mde-layout > .sidetoc-pane"); 407 | } 408 | return this.paneState.cachedPaneElement; 409 | }; 410 | SideTocPane.prototype.getEditorElement = function () { 411 | if (!this.paneState.cachedEditorElement) { 412 | this.paneState.cachedEditorElement = document.querySelector(".editor"); 413 | } 414 | return this.paneState.cachedEditorElement; 415 | }; 416 | SideTocPane.prototype.invalidateElementCache = function () { 417 | this.paneState.cachedPaneElement = null; 418 | this.paneState.cachedEditorElement = null; 419 | }; 420 | /* 421 | * 422 | */ 423 | SideTocPane.prototype.componentWillMount = function () { 424 | var _this = this; 425 | // state of this component 426 | this.state = { 427 | visibility: Settings.isDefaultVisible, 428 | headers: [], 429 | currentHeader: null, 430 | min: 0, 431 | len: 0, 432 | }; 433 | this.paneState.dispatchId = dispatcher.register(function (action) { 434 | return _this.dispatchAction(action); 435 | }); 436 | var editor = inkdrop.getActiveEditor(); 437 | if (editor != null) { 438 | this.attachEvents(editor); 439 | } 440 | else { 441 | inkdrop.onEditorLoad(function (e) { 442 | _this.attachEvents(e); 443 | }); 444 | } 445 | }; 446 | /* 447 | * 448 | */ 449 | SideTocPane.prototype.componentDidUpdate = function () { 450 | var cur = this.paneState.curSectionRef.current; 451 | if (cur == null) { 452 | return; 453 | } 454 | var pane = document.querySelector(".sidetoc-pane-wrapper"); 455 | if (pane != null) { 456 | pane.scrollTop = cur.offsetTop - pane.offsetTop; 457 | } 458 | }; 459 | /* 460 | * 461 | */ 462 | SideTocPane.prototype.componentWillUnmount = function () { 463 | // unhandle event 464 | dispatcher.unregister(this.paneState.dispatchId); 465 | var editor = inkdrop.getActiveEditor(); 466 | // for delete note 467 | if (editor == null) { 468 | return; 469 | } 470 | this.detachEvents(editor); 471 | }; 472 | /* 473 | * 474 | */ 475 | SideTocPane.prototype.dispatchAction = function (action) { 476 | switch (action.type) { 477 | case "Toggle": 478 | this.handleVisibility(); 479 | break; 480 | case "JumpToPrev": 481 | this.handleJumpToPrev(); 482 | break; 483 | case "JumpToNext": 484 | this.handleJumpToNext(); 485 | break; 486 | case "Activate": 487 | this.updateState({ visibility: true }); 488 | break; 489 | case "Deactivate": 490 | this.updateState({ visibility: false }); 491 | break; 492 | case "IncreaseWidth": 493 | this.handleChangeWidth(WidthChangeMode.Increase); 494 | break; 495 | case "DecreaseWidth": 496 | this.handleChangeWidth(WidthChangeMode.Decrease); 497 | break; 498 | case "ResetWidth": 499 | this.handleChangeWidth(WidthChangeMode.Reset); 500 | break; 501 | case "ToggleTextwrap": 502 | this.handleToggleTextwrap(); 503 | break; 504 | default: 505 | } 506 | }; 507 | /* 508 | * Optimized render with better caching strategy 509 | */ 510 | SideTocPane.prototype.render = function () { 511 | // Check if we need to regenerate content 512 | if (this.paneState.content == null || this.shouldUpdateContent()) { 513 | this.paneState.content = this.createContent(); 514 | this.paneState.lastRenderHeaders = this.state.headers; 515 | this.paneState.lastRenderVisibility = this.state.visibility; 516 | this.paneState.lastRenderCurrentHeader = this.state.currentHeader; 517 | } 518 | return this.paneState.content; 519 | }; 520 | /* 521 | * Check if content should be updated 522 | */ 523 | SideTocPane.prototype.shouldUpdateContent = function () { 524 | return this.paneState.lastRenderHeaders !== this.state.headers || 525 | this.paneState.lastRenderVisibility !== this.state.visibility || 526 | this.paneState.lastRenderCurrentHeader !== this.state.currentHeader; 527 | }; 528 | /* 529 | * 530 | */ 531 | SideTocPane.prototype.createContent = function () { 532 | var _this = this; 533 | var isShowPane = this.isShowPane(); 534 | var className = "sidetoc-pane"; 535 | if (!isShowPane) { 536 | className = "sidetoc-pane-hide"; 537 | } 538 | else if (!Settings.isTextwrap) { 539 | className += " sidetoc-pane-nowrap"; 540 | } 541 | var style = { 542 | fontFamily: Settings.fontFamily, 543 | }; 544 | var wrapperStyle = {}; 545 | if (isShowPane) { 546 | var pane = this.getPaneElement(); 547 | if (pane != null) { 548 | wrapperStyle.height = pane.offsetHeight - 20; 549 | } 550 | } 551 | // current header key for preview which join header text with "_". 552 | var current = ""; 553 | return (React.createElement("div", { className: className, style: style }, 554 | React.createElement("div", { className: "sidetoc-pane-wrapper", style: wrapperStyle }, this.state.headers.map(function (v, index) { 555 | // Optimized string processing - avoid regex replace 556 | var cleanStr = v.str.split(' ').join(''); 557 | current += "_" + cleanStr; 558 | var _a = _this.toStyleCached(v, current), style = _a.style, isCurrent = _a.isCurrent; 559 | var ref = isCurrent ? _this.paneState.curSectionRef : null; 560 | return (React.createElement(HeaderListItem, { key: "".concat(v.index, "-").concat(v.str), header: v, style: style, onClick: _this.handleClick, ref: ref, isCurrent: isCurrent })); 561 | })))); 562 | }; 563 | /*----- private -----*/ 564 | /* 565 | * 566 | */ 567 | SideTocPane.prototype.attachEvents = function (editor) { 568 | var _this = this; 569 | this.statusBar = document.querySelector("#app-container .main-layout .editor-layout .editor-status-bar-layout"); 570 | // refresh 571 | this.updateState(); 572 | var cm = editor.cm; 573 | cm.on("cursorActivity", this.handleCursorActivity); 574 | cm.on("changes", this.handleCmUpdate); 575 | cm.on("scroll", this.handleCmScroll); 576 | // for sidetoc overflow-y 577 | inkdrop.window.on("resize", this.handleWindowResize); 578 | // hook preview scroll 579 | var editorEle = this.getEditorElement(); 580 | if (editorEle == null) { 581 | return; 582 | } 583 | // preview element 584 | var preview = editorEle.querySelector(".mde-preview"); 585 | if (preview == null) { 586 | return; 587 | } 588 | // Store reference for proper cleanup 589 | this.paneState.previewElement = preview; 590 | preview.addEventListener("scroll", this.handlePreviewScroll); 591 | // check initial view mode 592 | this.paneState.isPreview = editorEle.classList.contains("editor-viewmode-preview"); 593 | // observe preview update 594 | this.paneState.observer = new MutationObserver(function (_) { return _this.handlePreviewUpdate(editorEle); }); 595 | this.paneState.observer.observe(preview, { 596 | childList: true, 597 | subtree: true, 598 | attributes: true, 599 | }); 600 | // observe ui theme 601 | this.paneState.bodyObserver = new MutationObserver(function (_) { 602 | Settings.refresh(); 603 | _this.updateState(); 604 | }); 605 | this.paneState.bodyObserver.observe(document.body, { attributes: true }); 606 | }; 607 | /* 608 | * 609 | */ 610 | SideTocPane.prototype.detachEvents = function (editor) { 611 | var cm = editor.cm; 612 | cm.off("cursorActivity", this.handleCursorActivity); 613 | cm.off("update", this.handleCmUpdate); 614 | cm.off("scroll", this.handleCmScroll); 615 | inkdrop.window.off("resize", this.handleWindowResize); 616 | this.paneState.observer.disconnect(); 617 | this.paneState.bodyObserver.disconnect(); 618 | // Properly remove preview scroll event listener to prevent memory leaks 619 | if (this.paneState.previewElement) { 620 | this.paneState.previewElement.removeEventListener("scroll", this.handlePreviewScroll); 621 | this.paneState.previewElement = null; 622 | } 623 | }; 624 | /* 625 | * 626 | */ 627 | SideTocPane.prototype.updateSection = function (line) { 628 | var header = this.getCurrentHeader(line); 629 | if (header != null && this.state.currentHeader != header) { 630 | this.commit({ currentHeader: header }); 631 | } 632 | }; 633 | /* 634 | * 635 | */ 636 | SideTocPane.prototype.isSameHeader = function (h1, h2) { 637 | if (h1 == null) { 638 | return false; 639 | } 640 | else if (h1 == h2) { 641 | return true; 642 | } 643 | if (h1.count == h2.count && 644 | h1.index == h2.index && 645 | h1.rowStart == h2.rowStart && 646 | h1.str == h2.str) { 647 | return true; 648 | } 649 | return false; 650 | }; 651 | /* 652 | * Binary search optimized header lookup - O(log n) instead of O(n) 653 | */ 654 | SideTocPane.prototype.getCurrentHeader = function (line) { 655 | var headers = this.state.headers; 656 | if (headers.length === 0) 657 | return null; 658 | var left = 0; 659 | var right = headers.length - 1; 660 | // Binary search for the header containing the line 661 | while (left <= right) { 662 | var mid = Math.floor((left + right) / 2); 663 | var header = headers[mid]; 664 | if (line >= header.rowStart && line <= header.rowEnd) { 665 | return header; 666 | } 667 | else if (line < header.rowStart) { 668 | right = mid - 1; 669 | } 670 | else { 671 | left = mid + 1; 672 | } 673 | } 674 | return null; 675 | }; 676 | /* 677 | * 678 | */ 679 | SideTocPane.prototype.getPrevHeader = function (header, line) { 680 | if (header == null) { 681 | return null; 682 | } 683 | // first header 684 | if (header.index == 0) { 685 | return header; 686 | } 687 | if (header.index - 1 >= 0) { 688 | // Jump to prev header 689 | if (header.rowStart == line) { 690 | return this.state.headers[header.index - 1]; 691 | } 692 | // Jump to current header line if cursor is not current header. 693 | return header; 694 | } 695 | return null; 696 | }; 697 | /* 698 | * 699 | */ 700 | SideTocPane.prototype.getNextHeader = function (header) { 701 | if (header == null) { 702 | // jump to first header 703 | return this.state.headers.length > 0 ? this.state.headers[0] : null; 704 | } 705 | if (header.index + 1 < this.state.headers.length) { 706 | return this.state.headers[header.index + 1]; 707 | } 708 | return null; 709 | }; 710 | /* 711 | * Optimized commit with selective content invalidation 712 | */ 713 | SideTocPane.prototype.commit = function (state) { 714 | // Only invalidate content if headers, visibility, or currentHeader changed 715 | if (state.headers !== undefined || state.visibility !== undefined || state.currentHeader !== undefined) { 716 | this.paneState.content = null; 717 | } 718 | // Clear style cache when state changes that affect styling 719 | if (state.headers !== undefined || state.currentHeader !== undefined || state.min !== undefined) { 720 | this.paneState.styleCache.clear(); 721 | } 722 | this.setState(state); 723 | }; 724 | /* 725 | * 726 | */ 727 | SideTocPane.prototype.isShowPane = function () { 728 | if (!this.state.visibility) { 729 | return false; 730 | } 731 | if (Settings.isShowIfNoHeader) { 732 | return true; 733 | } 734 | return this.state.headers.length != 0; 735 | }; 736 | /* 737 | * 738 | */ 739 | SideTocPane.prototype.log = function (_) { 740 | // console.log(`sidetoc: ${_()}`); 741 | }; 742 | return SideTocPane; 743 | }(React.Component)); 744 | export default SideTocPane; 745 | --------------------------------------------------------------------------------