├── .gitignore ├── plugin.config.json ├── api ├── index.ts ├── Global.d.ts ├── JoplinFilters.d.ts ├── JoplinViewsToolbarButtons.d.ts ├── JoplinViewsMenuItems.d.ts ├── JoplinViewsMenus.d.ts ├── JoplinWindow.d.ts ├── JoplinInterop.d.ts ├── JoplinClipboard.d.ts ├── JoplinViews.d.ts ├── JoplinPlugins.d.ts ├── JoplinViewsNoteList.d.ts ├── JoplinContentScripts.d.ts ├── Joplin.d.ts ├── JoplinViewsDialogs.d.ts ├── JoplinSettings.d.ts ├── JoplinViewsPanels.d.ts ├── JoplinImaging.d.ts ├── JoplinWorkspace.d.ts ├── JoplinCommands.d.ts ├── JoplinViewsEditor.d.ts ├── JoplinData.d.ts ├── noteListType.d.ts ├── noteListType.ts └── types.ts ├── .npmignore ├── docs ├── life-calendar-v1.0.0-screenshot.png └── life-calendar-v1.2.0-screenshot.png ├── tsconfig.json ├── src ├── index.ts ├── manifest.json ├── lifeMdCtrl.js ├── lifeMdRule.js ├── life-calendar.css └── life-calendar.js ├── LICENSE ├── package.json ├── CHANGELOG.md ├── GENERATOR_DOC.md ├── README.md └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | publish/ 4 | -------------------------------------------------------------------------------- /plugin.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extraScripts": ["lifeMdRule.js"] 3 | } -------------------------------------------------------------------------------- /api/index.ts: -------------------------------------------------------------------------------- 1 | import type Joplin from './Joplin'; 2 | 3 | declare const joplin: Joplin; 4 | 5 | export default joplin; 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.md 2 | !README.md 3 | /*.jpl 4 | /api 5 | /src 6 | /dist 7 | tsconfig.json 8 | webpack.config.js 9 | /docs 10 | -------------------------------------------------------------------------------- /docs/life-calendar-v1.0.0-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hieuthi/joplin-plugin-life-calendar/HEAD/docs/life-calendar-v1.0.0-screenshot.png -------------------------------------------------------------------------------- /docs/life-calendar-v1.2.0-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hieuthi/joplin-plugin-life-calendar/HEAD/docs/life-calendar-v1.2.0-screenshot.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "module": "commonjs", 5 | "target": "es2015", 6 | "jsx": "react", 7 | "allowJs": true, 8 | "baseUrl": "." 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /api/Global.d.ts: -------------------------------------------------------------------------------- 1 | import Plugin from '../Plugin'; 2 | import Joplin from './Joplin'; 3 | /** 4 | * @ignore 5 | */ 6 | /** 7 | * @ignore 8 | */ 9 | export default class Global { 10 | private joplin_; 11 | constructor(implementation: any, plugin: Plugin, store: any); 12 | get joplin(): Joplin; 13 | get process(): any; 14 | } 15 | -------------------------------------------------------------------------------- /api/JoplinFilters.d.ts: -------------------------------------------------------------------------------- 1 | import { FilterHandler } from '../../../eventManager'; 2 | /** 3 | * @ignore 4 | * 5 | * Not sure if it's the best way to hook into the app 6 | * so for now disable filters. 7 | */ 8 | export default class JoplinFilters { 9 | on(name: string, callback: FilterHandler): Promise; 10 | off(name: string, callback: FilterHandler): Promise; 11 | } 12 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import joplin from 'api'; 2 | import { ContentScriptType } from "api/types"; 3 | 4 | joplin.plugins.register({ 5 | onStart: async function() { 6 | await joplin.contentScripts.register( 7 | ContentScriptType.MarkdownItPlugin, 8 | 'lifeMdRule', 9 | './lifeMdRule.js' 10 | ); 11 | await joplin.contentScripts.register( 12 | ContentScriptType.CodeMirrorPlugin, 13 | 'lifeMdCtrl', 14 | './lifeMdCtrl.js' 15 | ); 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 1, 3 | "id": "com.hieuthi.joplin.life-calendar", 4 | "app_min_version": "2.2", 5 | "version": "1.5.1", 6 | "name": "Life Calendar", 7 | "description": "Life Calendar Plugin for Joplin", 8 | "author": "Hieu-Thi Luong", 9 | "homepage_url": "https://github.com/hieuthi/joplin-plugin-life-calendar", 10 | "repository_url": "https://github.com/hieuthi/joplin-plugin-life-calendar", 11 | "keywords": ["joplin-plugin", "life calendar", "week calendar", "life", "timeline"], 12 | "platforms": ["desktop", "mobile"] 13 | } 14 | -------------------------------------------------------------------------------- /src/lifeMdCtrl.js: -------------------------------------------------------------------------------- 1 | function plugin(CodeMirror) { 2 | // CodeMirror.defineMode("life", function(conf) { 3 | // var lifeMode = { 4 | // token: function(stream, state) { while (stream.next() != null ) {}; return null; } 5 | // } 6 | // return CodeMirror.overlayMode(CodeMirror.getMode(conf, "text/x-yaml"), lifeMode); 7 | // }); 8 | CodeMirror.defineMode("life", function(conf) { return CodeMirror.getMode(conf, "text/x-yaml"); }); 9 | } 10 | module.exports = { 11 | default: function(_context) { 12 | return { 13 | plugin: plugin, 14 | codeMirrorResources: [ 'addon/mode/overlay' ], 15 | codeMirrorOptions: {} 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /api/JoplinViewsToolbarButtons.d.ts: -------------------------------------------------------------------------------- 1 | import { ToolbarButtonLocation } from './types'; 2 | import Plugin from '../Plugin'; 3 | /** 4 | * Allows creating and managing toolbar buttons. 5 | * 6 | * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/register_command) 7 | */ 8 | export default class JoplinViewsToolbarButtons { 9 | private store; 10 | private plugin; 11 | constructor(plugin: Plugin, store: any); 12 | /** 13 | * Creates a new toolbar button and associate it with the given command. 14 | */ 15 | create(id: string, commandName: string, location: ToolbarButtonLocation): Promise; 16 | } 17 | -------------------------------------------------------------------------------- /api/JoplinViewsMenuItems.d.ts: -------------------------------------------------------------------------------- 1 | import { CreateMenuItemOptions, MenuItemLocation } from './types'; 2 | import Plugin from '../Plugin'; 3 | /** 4 | * Allows creating and managing menu items. 5 | * 6 | * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/register_command) 7 | * 8 | * desktop 9 | */ 10 | export default class JoplinViewsMenuItems { 11 | private store; 12 | private plugin; 13 | constructor(plugin: Plugin, store: any); 14 | /** 15 | * Creates a new menu item and associate it with the given command. You can specify under which menu the item should appear using the `location` parameter. 16 | */ 17 | create(id: string, commandName: string, location?: MenuItemLocation, options?: CreateMenuItemOptions): Promise; 18 | } 19 | -------------------------------------------------------------------------------- /api/JoplinViewsMenus.d.ts: -------------------------------------------------------------------------------- 1 | import { MenuItem, MenuItemLocation } from './types'; 2 | import Plugin from '../Plugin'; 3 | /** 4 | * Allows creating menus. 5 | * 6 | * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/menu) 7 | * 8 | * desktop 9 | */ 10 | export default class JoplinViewsMenus { 11 | private store; 12 | private plugin; 13 | constructor(plugin: Plugin, store: any); 14 | private registerCommandAccelerators; 15 | /** 16 | * Creates a new menu from the provided menu items and place it at the given location. As of now, it is only possible to place the 17 | * menu as a sub-menu of the application build-in menus. 18 | */ 19 | create(id: string, label: string, menuItems: MenuItem[], location?: MenuItemLocation): Promise; 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Hieu-Thi Luong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /api/JoplinWindow.d.ts: -------------------------------------------------------------------------------- 1 | import Plugin from '../Plugin'; 2 | export default class JoplinWindow { 3 | private store_; 4 | constructor(_plugin: Plugin, store: any); 5 | /** 6 | * Loads a chrome CSS file. It will apply to the window UI elements, except 7 | * for the note viewer. It is the same as the "Custom stylesheet for 8 | * Joplin-wide app styles" setting. See the [Load CSS Demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/load_css) 9 | * for an example. 10 | * 11 | * desktop 12 | */ 13 | loadChromeCssFile(filePath: string): Promise; 14 | /** 15 | * Loads a note CSS file. It will apply to the note viewer, as well as any 16 | * exported or printed note. It is the same as the "Custom stylesheet for 17 | * rendered Markdown" setting. See the [Load CSS Demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/load_css) 18 | * for an example. 19 | * 20 | * desktop 21 | */ 22 | loadNoteCssFile(filePath: string): Promise; 23 | } 24 | -------------------------------------------------------------------------------- /api/JoplinInterop.d.ts: -------------------------------------------------------------------------------- 1 | import { ExportModule, ImportModule } from './types'; 2 | /** 3 | * Provides a way to create modules to import external data into Joplin or to export notes into any arbitrary format. 4 | * 5 | * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/json_export) 6 | * 7 | * To implement an import or export module, you would simply define an object with various event handlers that are called 8 | * by the application during the import/export process. 9 | * 10 | * See the documentation of the [[ExportModule]] and [[ImportModule]] for more information. 11 | * 12 | * You may also want to refer to the Joplin API documentation to see the list of properties for each item (note, notebook, etc.) - https://joplinapp.org/help/api/references/rest_api 13 | * 14 | * desktop: While it is possible to register import and export 15 | * modules on mobile, there is no GUI to activate them. 16 | */ 17 | export default class JoplinInterop { 18 | registerExportModule(module: ExportModule): Promise; 19 | registerImportModule(module: ImportModule): Promise; 20 | } 21 | -------------------------------------------------------------------------------- /src/lifeMdRule.js: -------------------------------------------------------------------------------- 1 | const YAML = require('yaml'); 2 | 3 | module.exports = { 4 | default: function(context) { 5 | return { 6 | plugin: function (markdownIt, _options) { 7 | const defaultRender = markdownIt.renderer.rules.fence || function(tokens, idx, options, env, self) { 8 | return self.renderToken(tokens, idx, options, env, self); 9 | }; 10 | markdownIt.renderer.rules.fence = function(tokens, idx, options, env, self) { 11 | const token = tokens[idx]; 12 | if (token.info !== 'life') return defaultRender(tokens, idx, options, env, self); 13 | try { 14 | var contentHtml = JSON.stringify(YAML.parse(markdownIt.utils.escapeHtml(token.content))); 15 | } catch (e) { 16 | var contentHtml = {}; 17 | } 18 | return ` 19 |
20 |
${contentHtml}
21 |
22 | `; 23 | }; 24 | }, 25 | assets: function () { 26 | return [ 27 | { name: 'life-calendar.js' }, 28 | { name: 'life-calendar.css'},] 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "joplin-plugin-life-calendar", 3 | "version": "1.5.1", 4 | "scripts": { 5 | "dist": "webpack --env joplin-plugin-config=buildMain && webpack --env joplin-plugin-config=buildExtraScripts && webpack --env joplin-plugin-config=createArchive", 6 | "prepare": "npm run dist", 7 | "update": "npm install -g generator-joplin && yo joplin --node-package-manager npm --update --force", 8 | "updateVersion": "webpack --env joplin-plugin-config=updateVersion" 9 | }, 10 | "license": "MIT", 11 | "keywords": [ 12 | "joplin-plugin", 13 | "life calendar", 14 | "week calendar", 15 | "life", 16 | "timeline" 17 | ], 18 | "devDependencies": { 19 | "@types/node": "^18.7.13", 20 | "chalk": "^4.1.0", 21 | "copy-webpack-plugin": "^11.0.0", 22 | "fs-extra": "^10.1.0", 23 | "glob": "^8.0.3", 24 | "on-build-webpack": "^0.1.0", 25 | "tar": "^6.1.11", 26 | "ts-loader": "^9.3.1", 27 | "typescript": "^4.8.2", 28 | "webpack": "^5.74.0", 29 | "webpack-cli": "^4.10.0", 30 | "yargs": "^16.2.0" 31 | }, 32 | "dependencies": { 33 | "yaml": "^1.10.2" 34 | }, 35 | "files": [ 36 | "publish" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /api/JoplinClipboard.d.ts: -------------------------------------------------------------------------------- 1 | export default class JoplinClipboard { 2 | private electronClipboard_; 3 | private electronNativeImage_; 4 | constructor(electronClipboard: any, electronNativeImage: any); 5 | readText(): Promise; 6 | writeText(text: string): Promise; 7 | /** desktop */ 8 | readHtml(): Promise; 9 | /** desktop */ 10 | writeHtml(html: string): Promise; 11 | /** 12 | * Returns the image in [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) format. 13 | * 14 | * desktop 15 | */ 16 | readImage(): Promise; 17 | /** 18 | * Takes an image in [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) format. 19 | * 20 | * desktop 21 | */ 22 | writeImage(dataUrl: string): Promise; 23 | /** 24 | * Returns the list available formats (mime types). 25 | * 26 | * For example [ 'text/plain', 'text/html' ] 27 | */ 28 | availableFormats(): Promise; 29 | } 30 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v1.4.1] - 2023-02-05 4 | ### Added 5 | - Add icon support for period @shubhamjain0594 6 | 7 | ## [v1.4.0] - 2022-12-23 8 | ### Changed 9 | - Change period end date intepretation to inclusive 10 | - Strengthen datetime handling 11 | - Change CSS 12 | 13 | ### Added 14 | - Add className support for period 15 | - Add z-index (priority) support for event 16 | 17 | 18 | ## [v1.3.0] - 2022-12-16 19 | ### Changed 20 | - Adjust CSS 21 | 22 | ### Added 23 | - Support day-base, month-based, and year-based 24 | - Support pin down event window 25 | - Add CodeMirror Mode for life 26 | 27 | ## [v1.2.1] - 2022-02-14 28 | ### Changed 29 | - Change theme 30 | 31 | ### Added 32 | - Support periods 33 | 34 | ## [v1.1.0] - 2022-02-06 35 | ### Changed 36 | - Render asynchronously 37 | 38 | ### Added 39 | - Support className 40 | 41 | ## [v1.0.0] - 2022-02-05 42 | Initial release, fully functional 43 | 44 | [v1.4.1]: https://github.com/hieuthi/joplin-plugin-life-calendar/compare/v1.4.0...v1.4.1 45 | [v1.4.0]: https://github.com/hieuthi/joplin-plugin-life-calendar/compare/v1.3.0...v1.4.0 46 | [v1.3.0]: https://github.com/hieuthi/joplin-plugin-life-calendar/compare/v1.2.1...v1.3.0 47 | [v1.2.1]: https://github.com/hieuthi/joplin-plugin-life-calendar/compare/v1.1.0...v1.2.1 48 | [v1.1.0]: https://github.com/hieuthi/joplin-plugin-life-calendar/compare/v1.0.0...v1.1.0 49 | [v1.0.0]: https://github.com/hieuthi/joplin-plugin-life-calendar/releases/tag/v1.0.0 -------------------------------------------------------------------------------- /api/JoplinViews.d.ts: -------------------------------------------------------------------------------- 1 | import Plugin from '../Plugin'; 2 | import JoplinViewsDialogs from './JoplinViewsDialogs'; 3 | import JoplinViewsMenuItems from './JoplinViewsMenuItems'; 4 | import JoplinViewsMenus from './JoplinViewsMenus'; 5 | import JoplinViewsToolbarButtons from './JoplinViewsToolbarButtons'; 6 | import JoplinViewsPanels from './JoplinViewsPanels'; 7 | import JoplinViewsNoteList from './JoplinViewsNoteList'; 8 | import JoplinViewsEditors from './JoplinViewsEditor'; 9 | /** 10 | * This namespace provides access to view-related services. 11 | * 12 | * All view services provide a `create()` method which you would use to create the view object, whether it's a dialog, a toolbar button or a menu item. 13 | * In some cases, the `create()` method will return a [[ViewHandle]], which you would use to act on the view, for example to set certain properties or call some methods. 14 | */ 15 | export default class JoplinViews { 16 | private store; 17 | private plugin; 18 | private panels_; 19 | private menuItems_; 20 | private menus_; 21 | private toolbarButtons_; 22 | private dialogs_; 23 | private editors_; 24 | private noteList_; 25 | private implementation_; 26 | constructor(implementation: any, plugin: Plugin, store: any); 27 | get dialogs(): JoplinViewsDialogs; 28 | get panels(): JoplinViewsPanels; 29 | get editors(): JoplinViewsEditors; 30 | get menuItems(): JoplinViewsMenuItems; 31 | get menus(): JoplinViewsMenus; 32 | get toolbarButtons(): JoplinViewsToolbarButtons; 33 | get noteList(): JoplinViewsNoteList; 34 | } 35 | -------------------------------------------------------------------------------- /api/JoplinPlugins.d.ts: -------------------------------------------------------------------------------- 1 | import Plugin from '../Plugin'; 2 | import { ContentScriptType, Script } from './types'; 3 | /** 4 | * This class provides access to plugin-related features. 5 | */ 6 | export default class JoplinPlugins { 7 | private plugin; 8 | constructor(plugin: Plugin); 9 | /** 10 | * Registers a new plugin. This is the entry point when creating a plugin. You should pass a simple object with an `onStart` method to it. 11 | * That `onStart` method will be executed as soon as the plugin is loaded. 12 | * 13 | * ```typescript 14 | * joplin.plugins.register({ 15 | * onStart: async function() { 16 | * // Run your plugin code here 17 | * } 18 | * }); 19 | * ``` 20 | */ 21 | register(script: Script): Promise; 22 | /** 23 | * @deprecated Use joplin.contentScripts.register() 24 | */ 25 | registerContentScript(type: ContentScriptType, id: string, scriptPath: string): Promise; 26 | /** 27 | * Gets the plugin own data directory path. Use this to store any 28 | * plugin-related data. Unlike [[installationDir]], any data stored here 29 | * will be persisted. 30 | */ 31 | dataDir(): Promise; 32 | /** 33 | * Gets the plugin installation directory. This can be used to access any 34 | * asset that was packaged with the plugin. This directory should be 35 | * considered read-only because any data you store here might be deleted or 36 | * re-created at any time. To store new persistent data, use [[dataDir]]. 37 | */ 38 | installationDir(): Promise; 39 | /** 40 | * @deprecated Use joplin.require() 41 | */ 42 | require(_path: string): any; 43 | } 44 | -------------------------------------------------------------------------------- /src/life-calendar.css: -------------------------------------------------------------------------------- 1 | .life-calendar{ 2 | flex-wrap: wrap; 3 | line-height: 1em; 4 | } 5 | .life-calendar div.life-item { 6 | display: inline-block; 7 | padding: 1px; 8 | line-height: 12px; 9 | margin-bottom: 2px; 10 | } 11 | .life-calendar div.life-item span { 12 | display: inline-block; 13 | width: 12px; 14 | height: 12px; 15 | overflow: hidden; 16 | padding: 1px; 17 | font-size: 10px; 18 | border-radius: 4px; 19 | text-align: center; 20 | white-space: nowrap; 21 | } 22 | 23 | .life-calendar div.life-item.past span { 24 | background-color: rgb(170,170,170,0.5); 25 | color: #fff; 26 | } 27 | .life-calendar div.life-item.present span { 28 | background-color: rgb(247, 210, 110); 29 | color: #000; 30 | } 31 | .life-calendar div.life-item.future span { 32 | box-shadow:inset 0px 0px 0px 1px rgb(170,170,170,0.25); 33 | } 34 | .life-calendar div.life-item:hover span { 35 | box-shadow:inset 0px 0px 0px 1px rgb(247, 210, 110); 36 | } 37 | 38 | .life-calendar div.info { 39 | visibility: hidden; 40 | width: 224px; 41 | max-height: 40%; 42 | background-color: #555; 43 | padding: 5px 0; 44 | border-radius: 4px; 45 | font-size: 12px; 46 | line-height: 1.2em; 47 | color: #fefefe; 48 | overflow-y: overlay; 49 | 50 | /* Position the tooltip text */ 51 | position: fixed; 52 | z-index: 1; 53 | top: 12px; 54 | right: 12px; 55 | 56 | /* Fade in tooltip */ 57 | opacity: 0; 58 | transition: opacity 0.3s; 59 | } 60 | .life-calendar div.info.visible{ 61 | visibility: visible; 62 | opacity: 1; 63 | } 64 | 65 | div.info div { 66 | padding: 4px; 67 | } 68 | div.info .icon { 69 | min-width: 14px; 70 | text-align: center; 71 | display: inline-block; 72 | } 73 | div.info div.info-meta { 74 | color: #999; 75 | font-size: 9px; 76 | line-height: 9px; 77 | } 78 | div.info div.info-period { 79 | font-size: 9px; 80 | line-height: 9px; 81 | } 82 | -------------------------------------------------------------------------------- /api/JoplinViewsNoteList.d.ts: -------------------------------------------------------------------------------- 1 | import { Store } from 'redux'; 2 | import Plugin from '../Plugin'; 3 | import { ListRenderer } from './noteListType'; 4 | /** 5 | * This API allows you to customise how each note in the note list is rendered. 6 | * The renderer you implement follows a unidirectional data flow. 7 | * 8 | * The app provides the required dependencies whenever a note is updated - you 9 | * process these dependencies, and return some props, which are then passed to 10 | * your template and rendered. See [[ListRenderer]] for a detailed description 11 | * of each property of the renderer. 12 | * 13 | * ## Reference 14 | * 15 | * * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/note_list_renderer) 16 | * 17 | * * [Default simple renderer](https://github.com/laurent22/joplin/tree/dev/packages/lib/services/noteList/defaultListRenderer.ts) 18 | * 19 | * * [Default detailed renderer](https://github.com/laurent22/joplin/tree/dev/packages/lib/services/noteList/defaultMultiColumnsRenderer.ts) 20 | * 21 | * ## Screenshots: 22 | * 23 | * ### Top to bottom with title, date and body 24 | * 25 | * 26 | * 27 | * ### Left to right with thumbnails 28 | * 29 | * 30 | * 31 | * ### Top to bottom with editable title 32 | * 33 | * 34 | * 35 | * desktop 36 | */ 37 | export default class JoplinViewsNoteList { 38 | private plugin_; 39 | private store_; 40 | constructor(plugin: Plugin, store: Store); 41 | registerRenderer(renderer: ListRenderer): Promise; 42 | } 43 | -------------------------------------------------------------------------------- /api/JoplinContentScripts.d.ts: -------------------------------------------------------------------------------- 1 | import Plugin from '../Plugin'; 2 | import { ContentScriptType } from './types'; 3 | export default class JoplinContentScripts { 4 | private plugin; 5 | constructor(plugin: Plugin); 6 | /** 7 | * Registers a new content script. Unlike regular plugin code, which runs in 8 | * a separate process, content scripts run within the main process code and 9 | * thus allow improved performances and more customisations in specific 10 | * cases. It can be used for example to load a Markdown or editor plugin. 11 | * 12 | * Note that registering a content script in itself will do nothing - it 13 | * will only be loaded in specific cases by the relevant app modules (eg. 14 | * the Markdown renderer or the code editor). So it is not a way to inject 15 | * and run arbitrary code in the app, which for safety and performance 16 | * reasons is not supported. 17 | * 18 | * The plugin generator provides a way to build any content script you might 19 | * want to package as well as its dependencies. See the [Plugin Generator 20 | * doc](https://github.com/laurent22/joplin/blob/dev/packages/generator-joplin/README.md) 21 | * for more information. 22 | * 23 | * * [View the renderer demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script) 24 | * * [View the editor plugin tutorial](https://joplinapp.org/help/api/tutorials/cm6_plugin) 25 | * * [View the legacy editor demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/codemirror_content_script) 26 | * 27 | * See also the [postMessage demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages) 28 | * 29 | * @param type Defines how the script will be used. See the type definition for more information about each supported type. 30 | * @param id A unique ID for the content script. 31 | * @param scriptPath Must be a path relative to the plugin main script. For example, if your file content_script.js is next to your index.ts file, you would set `scriptPath` to `"./content_script.js`. 32 | */ 33 | register(type: ContentScriptType, id: string, scriptPath: string): Promise; 34 | /** 35 | * Listens to a messages sent from the content script using postMessage(). 36 | * See {@link ContentScriptType} for more information as well as the 37 | * [postMessage 38 | * demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages) 39 | */ 40 | onMessage(contentScriptId: string, callback: any): Promise; 41 | } 42 | -------------------------------------------------------------------------------- /api/Joplin.d.ts: -------------------------------------------------------------------------------- 1 | import Plugin from '../Plugin'; 2 | import JoplinData from './JoplinData'; 3 | import JoplinPlugins from './JoplinPlugins'; 4 | import JoplinWorkspace from './JoplinWorkspace'; 5 | import JoplinFilters from './JoplinFilters'; 6 | import JoplinCommands from './JoplinCommands'; 7 | import JoplinViews from './JoplinViews'; 8 | import JoplinInterop from './JoplinInterop'; 9 | import JoplinSettings from './JoplinSettings'; 10 | import JoplinContentScripts from './JoplinContentScripts'; 11 | import JoplinClipboard from './JoplinClipboard'; 12 | import JoplinWindow from './JoplinWindow'; 13 | import BasePlatformImplementation from '../BasePlatformImplementation'; 14 | import JoplinImaging from './JoplinImaging'; 15 | /** 16 | * This is the main entry point to the Joplin API. You can access various services using the provided accessors. 17 | * 18 | * The API is now relatively stable and in general maintaining backward compatibility is a top priority, so you shouldn't except much breakages. 19 | * 20 | * If a breaking change ever becomes needed, best effort will be done to: 21 | * 22 | * - Deprecate features instead of removing them, so as to give you time to fix the issue; 23 | * - Document breaking changes in the changelog; 24 | * 25 | * So if you are developing a plugin, please keep an eye on the changelog as everything will be in there with information about how to update your code. 26 | */ 27 | export default class Joplin { 28 | private data_; 29 | private plugins_; 30 | private imaging_; 31 | private workspace_; 32 | private filters_; 33 | private commands_; 34 | private views_; 35 | private interop_; 36 | private settings_; 37 | private contentScripts_; 38 | private clipboard_; 39 | private window_; 40 | private implementation_; 41 | constructor(implementation: BasePlatformImplementation, plugin: Plugin, store: any); 42 | get data(): JoplinData; 43 | get clipboard(): JoplinClipboard; 44 | get imaging(): JoplinImaging; 45 | get window(): JoplinWindow; 46 | get plugins(): JoplinPlugins; 47 | get workspace(): JoplinWorkspace; 48 | get contentScripts(): JoplinContentScripts; 49 | /** 50 | * @ignore 51 | * 52 | * Not sure if it's the best way to hook into the app 53 | * so for now disable filters. 54 | */ 55 | get filters(): JoplinFilters; 56 | get commands(): JoplinCommands; 57 | get views(): JoplinViews; 58 | get interop(): JoplinInterop; 59 | get settings(): JoplinSettings; 60 | /** 61 | * It is not possible to bundle native packages with a plugin, because they 62 | * need to work cross-platforms. Instead access to certain useful native 63 | * packages is provided using this function. 64 | * 65 | * Currently these packages are available: 66 | * 67 | * - [sqlite3](https://www.npmjs.com/package/sqlite3) 68 | * - [fs-extra](https://www.npmjs.com/package/fs-extra) 69 | * 70 | * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/nativeModule) 71 | * 72 | * desktop 73 | */ 74 | require(_path: string): any; 75 | versionInfo(): Promise; 76 | } 77 | -------------------------------------------------------------------------------- /api/JoplinViewsDialogs.d.ts: -------------------------------------------------------------------------------- 1 | import Plugin from '../Plugin'; 2 | import { ButtonSpec, ViewHandle, DialogResult } from './types'; 3 | /** 4 | * Allows creating and managing dialogs. A dialog is modal window that 5 | * contains a webview and a row of buttons. You can update the 6 | * webview using the `setHtml` method. Dialogs are hidden by default and 7 | * you need to call `open()` to open them. Once the user clicks on a 8 | * button, the `open` call will return an object indicating what button was 9 | * clicked on. 10 | * 11 | * ## Retrieving form values 12 | * 13 | * If your HTML content included one or more forms, a `formData` object 14 | * will also be included with the key/value for each form. 15 | * 16 | * ## Special button IDs 17 | * 18 | * The following buttons IDs have a special meaning: 19 | * 20 | * - `ok`, `yes`, `submit`, `confirm`: They are considered "submit" buttons 21 | * - `cancel`, `no`, `reject`: They are considered "dismiss" buttons 22 | * 23 | * This information is used by the application to determine what action 24 | * should be done when the user presses "Enter" or "Escape" within the 25 | * dialog. If they press "Enter", the first "submit" button will be 26 | * automatically clicked. If they press "Escape" the first "dismiss" button 27 | * will be automatically clicked. 28 | * 29 | * [View the demo 30 | * plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/dialog) 31 | */ 32 | export default class JoplinViewsDialogs { 33 | private store; 34 | private plugin; 35 | private implementation_; 36 | constructor(implementation: any, plugin: Plugin, store: any); 37 | private controller; 38 | /** 39 | * Creates a new dialog 40 | */ 41 | create(id: string): Promise; 42 | /** 43 | * Displays a message box with OK/Cancel buttons. Returns the button index that was clicked - "0" for OK and "1" for "Cancel" 44 | */ 45 | showMessageBox(message: string): Promise; 46 | /** 47 | * Displays a dialog to select a file or a directory. Same options and 48 | * output as 49 | * https://www.electronjs.org/docs/latest/api/dialog#dialogshowopendialogbrowserwindow-options 50 | * 51 | * desktop 52 | */ 53 | showOpenDialog(options: any): Promise; 54 | /** 55 | * Sets the dialog HTML content 56 | */ 57 | setHtml(handle: ViewHandle, html: string): Promise; 58 | /** 59 | * Adds and loads a new JS or CSS files into the dialog. 60 | */ 61 | addScript(handle: ViewHandle, scriptPath: string): Promise; 62 | /** 63 | * Sets the dialog buttons. 64 | */ 65 | setButtons(handle: ViewHandle, buttons: ButtonSpec[]): Promise; 66 | /** 67 | * Opens the dialog. 68 | * 69 | * On desktop, this closes any copies of the dialog open in different windows. 70 | */ 71 | open(handle: ViewHandle): Promise; 72 | /** 73 | * Toggle on whether to fit the dialog size to the content or not. 74 | * When set to false, the dialog is set to 90vw and 80vh 75 | * @default true 76 | */ 77 | setFitToContent(handle: ViewHandle, status: boolean): Promise; 78 | } 79 | -------------------------------------------------------------------------------- /api/JoplinSettings.d.ts: -------------------------------------------------------------------------------- 1 | import Plugin from '../Plugin'; 2 | import { SettingItem, SettingSection } from './types'; 3 | export interface ChangeEvent { 4 | /** 5 | * Setting keys that have been changed 6 | */ 7 | keys: string[]; 8 | } 9 | export type ChangeHandler = (event: ChangeEvent) => void; 10 | /** 11 | * This API allows registering new settings and setting sections, as well as getting and setting settings. Once a setting has been registered it will appear in the config screen and be editable by the user. 12 | * 13 | * Settings are essentially key/value pairs. 14 | * 15 | * Note: Currently this API does **not** provide access to Joplin's built-in settings. This is by design as plugins that modify user settings could give unexpected results 16 | * 17 | * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/settings) 18 | */ 19 | export default class JoplinSettings { 20 | private plugin_; 21 | constructor(plugin: Plugin); 22 | /** 23 | * Registers new settings. 24 | * Note that registering a setting item is dynamic and will be gone next time Joplin starts. 25 | * What it means is that you need to register the setting every time the plugin starts (for example in the onStart event). 26 | * The setting value however will be preserved from one launch to the next so there is no risk that it will be lost even if for some 27 | * reason the plugin fails to start at some point. 28 | */ 29 | registerSettings(settings: Record): Promise; 30 | /** 31 | * @deprecated Use joplin.settings.registerSettings() 32 | * 33 | * Registers a new setting. 34 | */ 35 | registerSetting(key: string, settingItem: SettingItem): Promise; 36 | /** 37 | * Registers a new setting section. Like for registerSetting, it is dynamic and needs to be done every time the plugin starts. 38 | */ 39 | registerSection(name: string, section: SettingSection): Promise; 40 | /** 41 | * Gets setting values (only applies to setting you registered from your plugin) 42 | */ 43 | values(keys: string[] | string): Promise>; 44 | /** 45 | * @deprecated Use joplin.settings.values() 46 | * 47 | * Gets a setting value (only applies to setting you registered from your plugin) 48 | */ 49 | value(key: string): Promise; 50 | /** 51 | * Sets a setting value (only applies to setting you registered from your plugin) 52 | */ 53 | setValue(key: string, value: any): Promise; 54 | /** 55 | * Gets a global setting value, including app-specific settings and those set by other plugins. 56 | * 57 | * The list of available settings is not documented yet, but can be found by looking at the source code: 58 | * 59 | * https://github.com/laurent22/joplin/blob/dev/packages/lib/models/Setting.ts#L142 60 | */ 61 | globalValue(key: string): Promise; 62 | /** 63 | * Called when one or multiple settings of your plugin have been changed. 64 | * - For performance reasons, this event is triggered with a delay. 65 | * - You will only get events for your own plugin settings. 66 | */ 67 | onChange(handler: ChangeHandler): Promise; 68 | } 69 | -------------------------------------------------------------------------------- /api/JoplinViewsPanels.d.ts: -------------------------------------------------------------------------------- 1 | import Plugin from '../Plugin'; 2 | import { ViewHandle } from './types'; 3 | /** 4 | * Allows creating and managing view panels. View panels allow displaying any HTML 5 | * content (within a webview) and updating it in real-time. For example it 6 | * could be used to display a table of content for the active note, or 7 | * display various metadata or graph. 8 | * 9 | * On desktop, view panels currently are displayed at the right of the sidebar, though can 10 | * be moved with "View" > "Change application layout". 11 | * 12 | * On mobile, view panels are shown in a tabbed dialog that can be opened using a 13 | * toolbar button. 14 | * 15 | * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/toc) 16 | */ 17 | export default class JoplinViewsPanels { 18 | private store; 19 | private plugin; 20 | constructor(plugin: Plugin, store: any); 21 | private controller; 22 | /** 23 | * Creates a new panel 24 | */ 25 | create(id: string): Promise; 26 | /** 27 | * Sets the panel webview HTML 28 | */ 29 | setHtml(handle: ViewHandle, html: string): Promise; 30 | /** 31 | * Adds and loads a new JS or CSS files into the panel. 32 | */ 33 | addScript(handle: ViewHandle, scriptPath: string): Promise; 34 | /** 35 | * Called when a message is sent from the webview (using postMessage). 36 | * 37 | * To post a message from the webview to the plugin use: 38 | * 39 | * ```javascript 40 | * const response = await webviewApi.postMessage(message); 41 | * ``` 42 | * 43 | * - `message` can be any JavaScript object, string or number 44 | * - `response` is whatever was returned by the `onMessage` handler 45 | * 46 | * Using this mechanism, you can have two-way communication between the 47 | * plugin and webview. 48 | * 49 | * See the [postMessage 50 | * demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages) for more details. 51 | * 52 | */ 53 | onMessage(handle: ViewHandle, callback: Function): Promise; 54 | /** 55 | * Sends a message to the webview. 56 | * 57 | * The webview must have registered a message handler prior, otherwise the message is ignored. Use; 58 | * 59 | * ```javascript 60 | * webviewApi.onMessage((message) => { ... }); 61 | * ``` 62 | * 63 | * - `message` can be any JavaScript object, string or number 64 | * 65 | * The view API may have only one onMessage handler defined. 66 | * This method is fire and forget so no response is returned. 67 | * 68 | * It is particularly useful when the webview needs to react to events emitted by the plugin or the joplin api. 69 | */ 70 | postMessage(handle: ViewHandle, message: any): void; 71 | /** 72 | * Shows the panel 73 | */ 74 | show(handle: ViewHandle, show?: boolean): Promise; 75 | /** 76 | * Hides the panel 77 | */ 78 | hide(handle: ViewHandle): Promise; 79 | /** 80 | * Tells whether the panel is visible or not 81 | */ 82 | visible(handle: ViewHandle): Promise; 83 | isActive(handle: ViewHandle): Promise; 84 | } 85 | -------------------------------------------------------------------------------- /api/JoplinImaging.d.ts: -------------------------------------------------------------------------------- 1 | import { Rectangle } from './types'; 2 | export interface CreateFromBufferOptions { 3 | width?: number; 4 | height?: number; 5 | scaleFactor?: number; 6 | } 7 | export interface CreateFromPdfOptions { 8 | /** 9 | * The first page to export. Defaults to `1`, the first page in 10 | * the document. 11 | */ 12 | minPage?: number; 13 | /** 14 | * The number of the last page to convert. Defaults to the last page 15 | * if not given. 16 | * 17 | * If `maxPage` is greater than the number of pages in the PDF, all pages 18 | * in the PDF will be converted to images. 19 | */ 20 | maxPage?: number; 21 | scaleFactor?: number; 22 | } 23 | export interface PdfInfo { 24 | pageCount: number; 25 | } 26 | export interface Implementation { 27 | createFromPath: (path: string) => Promise; 28 | createFromPdf: (path: string, options: CreateFromPdfOptions) => Promise; 29 | getPdfInfo: (path: string) => Promise; 30 | } 31 | export interface ResizeOptions { 32 | width?: number; 33 | height?: number; 34 | quality?: 'good' | 'better' | 'best'; 35 | } 36 | export type Handle = string; 37 | /** 38 | * Provides imaging functions to resize or process images. You create an image 39 | * using one of the `createFrom` functions, then use the other functions to 40 | * process the image. 41 | * 42 | * Images are associated with a handle which is what will be available to the 43 | * plugin. Once you are done with an image, free it using the `free()` function. 44 | * 45 | * [View the 46 | * example](https://github.com/laurent22/joplin/blob/dev/packages/app-cli/tests/support/plugins/imaging/src/index.ts) 47 | * 48 | * desktop 49 | */ 50 | export default class JoplinImaging { 51 | private implementation_; 52 | private images_; 53 | constructor(implementation: Implementation); 54 | private createImageHandle; 55 | private imageByHandle; 56 | private cacheImage; 57 | /** 58 | * Creates an image from the provided path. Note that images and PDFs are supported. If you 59 | * provide a URL instead of a local path, the file will be downloaded first then converted to an 60 | * image. 61 | */ 62 | createFromPath(filePath: string): Promise; 63 | createFromResource(resourceId: string): Promise; 64 | createFromPdfPath(path: string, options?: CreateFromPdfOptions): Promise; 65 | createFromPdfResource(resourceId: string, options?: CreateFromPdfOptions): Promise; 66 | getPdfInfoFromPath(path: string): Promise; 67 | getPdfInfoFromResource(resourceId: string): Promise; 68 | getSize(handle: Handle): Promise; 69 | resize(handle: Handle, options?: ResizeOptions): Promise; 70 | crop(handle: Handle, rectangle: Rectangle): Promise; 71 | toPngFile(handle: Handle, filePath: string): Promise; 72 | /** 73 | * Quality is between 0 and 100 74 | */ 75 | toJpgFile(handle: Handle, filePath: string, quality?: number): Promise; 76 | private tempFilePath; 77 | /** 78 | * Creates a new Joplin resource from the image data. The image will be 79 | * first converted to a JPEG. 80 | */ 81 | toJpgResource(handle: Handle, resourceProps: any, quality?: number): Promise; 82 | /** 83 | * Creates a new Joplin resource from the image data. The image will be 84 | * first converted to a PNG. 85 | */ 86 | toPngResource(handle: Handle, resourceProps: any): Promise; 87 | /** 88 | * Image data is not automatically deleted by Joplin so make sure you call 89 | * this method on the handle once you are done. 90 | */ 91 | free(handles: Handle[] | Handle): Promise; 92 | } 93 | -------------------------------------------------------------------------------- /api/JoplinWorkspace.d.ts: -------------------------------------------------------------------------------- 1 | import Plugin from '../Plugin'; 2 | import { FolderEntity } from '../../database/types'; 3 | import { Disposable, EditContextMenuFilterObject, FilterHandler } from './types'; 4 | declare enum ItemChangeEventType { 5 | Create = 1, 6 | Update = 2, 7 | Delete = 3 8 | } 9 | interface ItemChangeEvent { 10 | id: string; 11 | event: ItemChangeEventType; 12 | } 13 | interface ResourceChangeEvent { 14 | id: string; 15 | } 16 | interface NoteContentChangeEvent { 17 | note: any; 18 | } 19 | interface NoteSelectionChangeEvent { 20 | value: string[]; 21 | } 22 | interface NoteAlarmTriggerEvent { 23 | noteId: string; 24 | } 25 | interface SyncCompleteEvent { 26 | withErrors: boolean; 27 | } 28 | type WorkspaceEventHandler = (event: EventType) => void; 29 | type ItemChangeHandler = WorkspaceEventHandler; 30 | type SyncStartHandler = () => void; 31 | type ResourceChangeHandler = WorkspaceEventHandler; 32 | /** 33 | * The workspace service provides access to all the parts of Joplin that 34 | * are being worked on - i.e. the currently selected notes or notebooks as 35 | * well as various related events, such as when a new note is selected, or 36 | * when the note content changes. 37 | * 38 | * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins) 39 | */ 40 | export default class JoplinWorkspace { 41 | private store; 42 | private plugin; 43 | constructor(plugin: Plugin, store: any); 44 | /** 45 | * Called when a new note or notes are selected. 46 | */ 47 | onNoteSelectionChange(callback: WorkspaceEventHandler): Promise; 48 | /** 49 | * Called when the content of a note changes. 50 | * @deprecated Use `onNoteChange()` instead, which is reliably triggered whenever the note content, or any note property changes. 51 | */ 52 | onNoteContentChange(callback: WorkspaceEventHandler): Promise; 53 | /** 54 | * Called when the content of the current note changes. 55 | */ 56 | onNoteChange(handler: ItemChangeHandler): Promise; 57 | /** 58 | * Called when a resource is changed. Currently this handled will not be 59 | * called when a resource is added or deleted. 60 | */ 61 | onResourceChange(handler: ResourceChangeHandler): Promise; 62 | /** 63 | * Called when an alarm associated with a to-do is triggered. 64 | */ 65 | onNoteAlarmTrigger(handler: WorkspaceEventHandler): Promise; 66 | /** 67 | * Called when the synchronisation process is starting. 68 | */ 69 | onSyncStart(handler: SyncStartHandler): Promise; 70 | /** 71 | * Called when the synchronisation process has finished. 72 | */ 73 | onSyncComplete(callback: WorkspaceEventHandler): Promise; 74 | /** 75 | * Called just before the editor context menu is about to open. Allows 76 | * adding items to it. 77 | * 78 | * desktop 79 | */ 80 | filterEditorContextMenu(handler: FilterHandler): void; 81 | /** 82 | * Gets the currently selected note. Will be `null` if no note is selected. 83 | */ 84 | selectedNote(): Promise; 85 | /** 86 | * Gets the currently selected folder. In some cases, for example during 87 | * search or when viewing a tag, no folder is actually selected in the user 88 | * interface. In that case, that function would return the last selected 89 | * folder. 90 | */ 91 | selectedFolder(): Promise; 92 | /** 93 | * Gets the IDs of the selected notes (can be zero, one, or many). Use the data API to retrieve information about these notes. 94 | */ 95 | selectedNoteIds(): Promise; 96 | } 97 | export {}; 98 | -------------------------------------------------------------------------------- /api/JoplinCommands.d.ts: -------------------------------------------------------------------------------- 1 | import { Command } from './types'; 2 | import Plugin from '../Plugin'; 3 | /** 4 | * This class allows executing or registering new Joplin commands. Commands 5 | * can be executed or associated with 6 | * {@link JoplinViewsToolbarButtons | toolbar buttons} or 7 | * {@link JoplinViewsMenuItems | menu items}. 8 | * 9 | * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/register_command) 10 | * 11 | * ## Executing Joplin's internal commands 12 | * 13 | * It is also possible to execute internal Joplin's commands which, as of 14 | * now, are not well documented. You can find the list directly on GitHub 15 | * though at the following locations: 16 | * 17 | * * [Main screen commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/gui/MainScreen/commands) 18 | * * [Global commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/commands) 19 | * * [Editor commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts) 20 | * 21 | * To view what arguments are supported, you can open any of these files 22 | * and look at the `execute()` command. 23 | * 24 | * Note that many of these commands only work on desktop. The more limited list of mobile 25 | * commands can be found in these places: 26 | * 27 | * * [Global commands](https://github.com/laurent22/joplin/tree/dev/packages/app-mobile/commands) 28 | * * [Editor commands](https://github.com/laurent22/joplin/blob/dev/packages/app-mobile/components/NoteEditor/commandDeclarations.ts) 29 | * 30 | * ## Executing editor commands 31 | * 32 | * There might be a situation where you want to invoke editor commands 33 | * without using a {@link JoplinContentScripts | contentScript}. For this 34 | * reason Joplin provides the built in `editor.execCommand` command. 35 | * 36 | * `editor.execCommand` should work with any core command in both the 37 | * [CodeMirror](https://codemirror.net/doc/manual.html#execCommand) and 38 | * [TinyMCE](https://www.tiny.cloud/docs/api/tinymce/tinymce.editorcommands/#execcommand) editors, 39 | * as well as most functions calls directly on a CodeMirror editor object (extensions). 40 | * 41 | * * [CodeMirror commands](https://codemirror.net/doc/manual.html#commands) 42 | * * [TinyMCE core editor commands](https://www.tiny.cloud/docs/advanced/editor-command-identifiers/#coreeditorcommands) 43 | * 44 | * `editor.execCommand` supports adding arguments for the commands. 45 | * 46 | * ```typescript 47 | * await joplin.commands.execute('editor.execCommand', { 48 | * name: 'madeUpCommand', // CodeMirror and TinyMCE 49 | * args: [], // CodeMirror and TinyMCE 50 | * ui: false, // TinyMCE only 51 | * value: '', // TinyMCE only 52 | * }); 53 | * ``` 54 | * 55 | * [View the example using the CodeMirror editor](https://github.com/laurent22/joplin/blob/dev/packages/app-cli/tests/support/plugins/codemirror_content_script/src/index.ts) 56 | * 57 | */ 58 | export default class JoplinCommands { 59 | private plugin_; 60 | constructor(plugin_: Plugin); 61 | /** 62 | * Executes the given command. 63 | * 64 | * The command can take any number of arguments, and the supported 65 | * arguments will vary based on the command. For custom commands, this 66 | * is the `args` passed to the `execute()` function. For built-in 67 | * commands, you can find the supported arguments by checking the links 68 | * above. 69 | * 70 | * ```typescript 71 | * // Create a new note in the current notebook: 72 | * await joplin.commands.execute('newNote'); 73 | * 74 | * // Create a new sub-notebook under the provided notebook 75 | * // Note: internally, notebooks are called "folders". 76 | * await joplin.commands.execute('newFolder', "SOME_FOLDER_ID"); 77 | * ``` 78 | */ 79 | execute(commandName: string, ...args: any[]): Promise; 80 | /** 81 | * Registers a new command. 82 | * 83 | * ```typescript 84 | * // Register a new commmand called "testCommand1" 85 | * 86 | * await joplin.commands.register({ 87 | * name: 'testCommand1', 88 | * label: 'My Test Command 1', 89 | * iconName: 'fas fa-music', 90 | * execute: () => { 91 | * alert('Testing plugin command 1'); 92 | * }, 93 | * }); 94 | * ``` 95 | */ 96 | register(command: Command): Promise; 97 | } 98 | -------------------------------------------------------------------------------- /api/JoplinViewsEditor.d.ts: -------------------------------------------------------------------------------- 1 | import Plugin from '../Plugin'; 2 | import { ActivationCheckCallback, ViewHandle, UpdateCallback } from './types'; 3 | /** 4 | * Allows creating alternative note editors. You can create a view to handle loading and saving the 5 | * note, and do your own rendering. 6 | * 7 | * Although it may be used to implement an alternative text editor, the more common use case may be 8 | * to render the note in a different, graphical way - for example displaying a graph, and 9 | * saving/loading the graph data in the associated note. In that case, you would detect whether the 10 | * current note contains graph data and, in this case, you'd display your viewer. 11 | * 12 | * Terminology: An editor is **active** when it can be used to edit the current note. Note that it 13 | * doesn't necessarily mean that your editor is visible - it just means that the user has the option 14 | * to switch to it (via the "toggle editor" button). A **visible** editor is active and is currently 15 | * being displayed. 16 | * 17 | * To implement an editor you need to listen to two events: 18 | * 19 | * - `onActivationCheck`: This is a way for the app to know whether your editor should be active or 20 | * not. Return `true` from this handler to activate your editor. 21 | * 22 | * - `onUpdate`: When this is called you should update your editor based on the current note 23 | * content. Call `joplin.workspace.selectedNote()` to get the current note. 24 | * 25 | * - `showEditorPlugin` and `toggleEditorPlugin` commands. Additionally you can use these commands 26 | * to display your editor via `joplin.commands.execute('showEditorPlugin')`. This is not always 27 | * necessary since the user can switch to your editor using the "toggle editor" button, however 28 | * you may want to programmatically display the editor in some cases - for example when creating a 29 | * new note specific to your editor. 30 | * 31 | * Note that only one editor view can be active at a time. This is why it is important not to 32 | * activate your view if it's not relevant to the current note. If more than one is active, it is 33 | * undefined which editor is going to be used to display the note. 34 | * 35 | * For an example of editor plugin, see the [YesYouKan 36 | * plugin](https://github.com/joplin/plugin-yesyoukan/blob/master/src/index.ts). In particular, 37 | * check the logic around `onActivationCheck` and `onUpdate` since this is the entry points for 38 | * using this API. 39 | */ 40 | export default class JoplinViewsEditors { 41 | private store; 42 | private plugin; 43 | private activationCheckHandlers_; 44 | constructor(plugin: Plugin, store: any); 45 | private controller; 46 | /** 47 | * Creates a new editor view 48 | */ 49 | create(id: string): Promise; 50 | /** 51 | * Sets the editor HTML content 52 | */ 53 | setHtml(handle: ViewHandle, html: string): Promise; 54 | /** 55 | * Adds and loads a new JS or CSS file into the panel. 56 | */ 57 | addScript(handle: ViewHandle, scriptPath: string): Promise; 58 | /** 59 | * See [[JoplinViewPanels]] 60 | */ 61 | onMessage(handle: ViewHandle, callback: Function): Promise; 62 | /** 63 | * Emitted when the editor can potentially be activated - this for example when the current note 64 | * is changed, or when the application is opened. At that point should can check the current 65 | * note and decide whether your editor should be activated or not. If it should return `true`, 66 | * otherwise return `false`. 67 | */ 68 | onActivationCheck(handle: ViewHandle, callback: ActivationCheckCallback): Promise; 69 | /** 70 | * Emitted when the editor content should be updated. This for example when the currently 71 | * selected note changes, or when the user makes the editor visible. 72 | */ 73 | onUpdate(handle: ViewHandle, callback: UpdateCallback): Promise; 74 | /** 75 | * See [[JoplinViewPanels]] 76 | */ 77 | postMessage(handle: ViewHandle, message: any): void; 78 | /** 79 | * Tells whether the editor is active or not. 80 | */ 81 | isActive(handle: ViewHandle): Promise; 82 | /** 83 | * Tells whether the editor is effectively visible or not. If the editor is inactive, this will 84 | * return `false`. If the editor is active and the user has switched to it, it will return 85 | * `true`. Otherwise it will return `false`. 86 | */ 87 | isVisible(handle: ViewHandle): Promise; 88 | } 89 | -------------------------------------------------------------------------------- /api/JoplinData.d.ts: -------------------------------------------------------------------------------- 1 | import { ModelType } from '../../../BaseModel'; 2 | import Plugin from '../Plugin'; 3 | import { Path } from './types'; 4 | /** 5 | * This module provides access to the Joplin data API: https://joplinapp.org/help/api/references/rest_api 6 | * This is the main way to retrieve data, such as notes, notebooks, tags, etc. 7 | * or to update them or delete them. 8 | * 9 | * This is also what you would use to search notes, via the `search` endpoint. 10 | * 11 | * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/simple) 12 | * 13 | * In general you would use the methods in this class as if you were using a REST API. There are four methods that map to GET, POST, PUT and DELETE calls. 14 | * And each method takes these parameters: 15 | * 16 | * * `path`: This is an array that represents the path to the resource in the form `["resourceName", "resourceId", "resourceLink"]` (eg. ["tags", ":id", "notes"]). The "resources" segment is the name of the resources you want to access (eg. "notes", "folders", etc.). If not followed by anything, it will refer to all the resources in that collection. The optional "resourceId" points to a particular resources within the collection. Finally, an optional "link" can be present, which links the resource to a collection of resources. This can be used in the API for example to retrieve all the notes associated with a tag. 17 | * * `query`: (Optional) The query parameters. In a URL, this is the part after the question mark "?". In this case, it should be an object with key/value pairs. 18 | * * `data`: (Optional) Applies to PUT and POST calls only. The request body contains the data you want to create or modify, for example the content of a note or folder. 19 | * * `files`: (Optional) Used to create new resources and associate them with files. 20 | * 21 | * Please refer to the [Joplin API documentation](https://joplinapp.org/help/api/references/rest_api) for complete details about each call. As the plugin runs within the Joplin application **you do not need an authorisation token** to use this API. 22 | * 23 | * For example: 24 | * 25 | * ```typescript 26 | * // Get a note ID, title and body 27 | * const noteId = 'some_note_id'; 28 | * const note = await joplin.data.get(['notes', noteId], { fields: ['id', 'title', 'body'] }); 29 | * 30 | * // Get all folders 31 | * const folders = await joplin.data.get(['folders']); 32 | * 33 | * // Set the note body 34 | * await joplin.data.put(['notes', noteId], null, { body: "New note body" }); 35 | * 36 | * // Create a new note under one of the folders 37 | * await joplin.data.post(['notes'], null, { body: "my new note", title: "some title", parent_id: folders[0].id }); 38 | * ``` 39 | */ 40 | export default class JoplinData { 41 | private api_; 42 | private pathSegmentRegex_; 43 | private plugin; 44 | constructor(plugin: Plugin); 45 | private serializeApiBody; 46 | private pathToString; 47 | get(path: Path, query?: any): Promise; 48 | post(path: Path, query?: any, body?: any, files?: any[]): Promise; 49 | put(path: Path, query?: any, body?: any, files?: any[]): Promise; 50 | delete(path: Path, query?: any): Promise; 51 | itemType(itemId: string): Promise; 52 | resourcePath(resourceId: string): Promise; 53 | /** 54 | * Gets an item user data. User data are key/value pairs. The `key` can be any 55 | * arbitrary string, while the `value` can be of any type supported by 56 | * [JSON.stringify](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#description) 57 | * 58 | * User data is synchronised across devices, and each value wil be merged based on their timestamp: 59 | * 60 | * - If value is modified by client 1, then modified by client 2, it will take the value from client 2 61 | * - If value is modified by client 1, then deleted by client 2, the value will be deleted after merge 62 | * - If value is deleted by client 1, then updated by client 2, the value will be restored and set to the value from client 2 after merge 63 | */ 64 | userDataGet(itemType: ModelType, itemId: string, key: string): Promise; 65 | /** 66 | * Sets a note user data. See {@link JoplinData.userDataGet} for more details. 67 | */ 68 | userDataSet(itemType: ModelType, itemId: string, key: string, value: T): Promise; 69 | /** 70 | * Deletes a note user data. See {@link JoplinData.userDataGet} for more details. 71 | */ 72 | userDataDelete(itemType: ModelType, itemId: string, key: string): Promise; 73 | } 74 | -------------------------------------------------------------------------------- /GENERATOR_DOC.md: -------------------------------------------------------------------------------- 1 | # Plugin development 2 | 3 | This documentation describes how to create a plugin, and how to work with the plugin builder framework and API. 4 | 5 | ## Installation 6 | 7 | First, install [Yeoman](http://yeoman.io) and generator-joplin using [npm](https://www.npmjs.com/) (we assume you have pre-installed [node.js](https://nodejs.org/)). 8 | 9 | ```bash 10 | npm install -g yo@4.3.1 11 | npm install -g generator-joplin 12 | ``` 13 | 14 | Then generate your new project: 15 | 16 | ```bash 17 | yo --node-package-manager npm joplin 18 | ``` 19 | 20 | ## Structure 21 | 22 | The main two files you will want to look at are: 23 | 24 | - `/src/index.ts`, which contains the entry point for the plugin source code. 25 | - `/src/manifest.json`, which is the plugin manifest. It contains information such as the plugin a name, version, etc. 26 | 27 | The file `/plugin.config.json` could also be useful if you intend to use [external scripts](#external-script-files), such as content scripts or webview scripts. 28 | 29 | ## Building the plugin 30 | 31 | The plugin is built using Webpack, which creates the compiled code in `/dist`. A JPL archive will also be created at the root, which can use to distribute the plugin. 32 | 33 | To build the plugin, simply run `npm run dist`. 34 | 35 | The project is setup to use TypeScript, although you can change the configuration to use plain JavaScript. 36 | 37 | ## Updating the manifest version number 38 | 39 | You can run `npm run updateVersion` to bump the patch part of the version number, so for example 1.0.3 will become 1.0.4. This script will update both the package.json and manifest.json version numbers so as to keep them in sync. 40 | 41 | ## Publishing the plugin 42 | 43 | To publish the plugin, add it to npmjs.com by running `npm publish`. Later on, a script will pick up your plugin and add it automatically to the Joplin plugin repository as long as the package satisfies these conditions: 44 | 45 | - In `package.json`, the name starts with "joplin-plugin-". For example, "joplin-plugin-toc". 46 | - In `package.json`, the keywords include "joplin-plugin". 47 | - In the `publish/` directory, there should be a .jpl and .json file (which are built by `npm run dist`) 48 | 49 | In general all this is done automatically by the plugin generator, which will set the name and keywords of package.json, and will put the right files in the "publish" directory. But if something doesn't work and your plugin doesn't appear in the repository, double-check the above conditions. 50 | 51 | ## Updating the plugin framework 52 | 53 | To update the plugin framework, run `npm run update`. 54 | 55 | In general this command tries to do the right thing - in particular it's going to merge the changes in package.json and .gitignore instead of overwriting. It will also leave "/src" as well as README.md untouched. 56 | 57 | The file that may cause problem is "webpack.config.js" because it's going to be overwritten. For that reason, if you want to change it, consider creating a separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file. 58 | 59 | ## External script files 60 | 61 | By default, the compiler (webpack) is going to compile `src/index.ts` only (as well as any file it imports), and any other file will simply be copied to the plugin package. In some cases this is sufficient, however if you have [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplincontentscripts.html) or [webview scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinviewspanels.html#addscript) you might want to compile them too, in particular in these two cases: 62 | 63 | - The script is a TypeScript file - in which case it has to be compiled to JavaScript. 64 | 65 | - The script requires modules you've added to package.json. In that case, the script, whether JS or TS, must be compiled so that the dependencies are bundled with the JPL file. 66 | 67 | To get such an external script file to compile, you need to add it to the `extraScripts` array in `plugin.config.json`. The path you add should be relative to /src. For example, if you have a file in "/src/webviews/index.ts", the path should be set to "webviews/index.ts". Once compiled, the file will always be named with a .js extension. So you will get "webviews/index.js" in the plugin package, and that's the path you should use to reference the file. 68 | 69 | ## More information 70 | 71 | - [Joplin Plugin API](https://joplinapp.org/api/references/plugin_api/classes/joplin.html) 72 | - [Joplin Data API](https://joplinapp.org/help/api/references/rest_api) 73 | - [Joplin Plugin Manifest](https://joplinapp.org/api/references/plugin_manifest/) 74 | - Ask for help on the [forum](https://discourse.joplinapp.org/) or our [Discord channel](https://discord.gg/VSj7AFHvpq) 75 | 76 | ## License 77 | 78 | MIT © Laurent Cozic 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Life Calendar 2 | 3 | This plugin renders yaml-based fenced code into a Life Calendar inspired by Tim Urban's article "Your Life in Weeks". 4 | 5 | ![screenshot](https://raw.githubusercontent.com/hieuthi/joplin-plugin-life-calendar/main/docs/life-calendar-v1.2.0-screenshot.png) 6 | 7 | The purpose of Life Calendar is documenting the events in your life but there is nothing stopping you from using it for other purposes such as project management. The yaml-based syntax make it easy to export data to other format. 8 | 9 | ## Usage 10 | You need to create a fenced code with `life` as language to render the calendar then input the events using the the follow template: 11 | ``` 12 | - date : yyyy-mm-dd # (required) (interpretation DD/MM/YYYY 12:00 local time) 13 | title: # (required) event title 14 | icon : # (optional) event icon, use 1st character of title if icon is null 15 | z-index: # (optional) priority of the event, the one with the bigest value will be shown (default:0) 16 | className: # (optional) css class of the event (color, background-color, etc.) 17 | color: # (optional) overwrite color 18 | backgroundColor: # (optional) overwrite background-color 19 | 20 | ``` 21 | This plugin also support periods which is a span of time instead of a point in time. The template for period is as follow: 22 | ``` 23 | - start: yyyy-mm-dd # (required) (interpretation: DD/MM/YYYY 01:00 local time) 24 | end : yyyy-mm-dd # (required) (interpretation: DD/MM/YYYY 23:00 local time) 25 | title: # (required) event title 26 | className: # (optional) css class of the event (color, background-color, etc.) 27 | color: # (optional) overwrite color 28 | icon: # (optional) period icon 29 | backgroundColor: # (optional) overwrite background-color 30 | ``` 31 | 32 | The example below should demostrate all the supported features. It is recommended to intend the events block with spaces as yalm parser is quite sensitive. You may encounter parsing problems when typing with Joplin as it use tab for intending. 33 | 34 | From v1.3.0 forward, this plugin also support day-based, month-based, and year-based calendar beside week-based by writting down the desired type `type: month`. Moreover you can click on the a square to pin down the details window so you can read. Click again at any square to unpin it. 35 | 36 | `````markdown 37 | # 📆 Life Calendar 38 | 39 | 42 | 43 | - **Date of Birth**: 01/01/2020 44 | 45 | ```life 46 | type: week # [ day, week, month, year ] 47 | dob : 2020-01-01 # alias=startDate 48 | lifespan: 4 # alias=duration 49 | # dod: 2026-01-01 # alias=endDate 50 | ages: [1,2,3,4,5,6] 51 | events: 52 | - { date: 2022-04-19, title: Inline Event, icon: 🍿, backgroundColor: navy, color: white } 53 | - date : 2021-06-12 54 | title: Two events in the same week 55 | color: yellow 56 | - date : 2021-06-10 57 | title: Got a Gold Medal 58 | icon : 🥇 59 | z-index: 1 # shown icon of this event in calendar board instead of the other one 60 | backgroundColor: green 61 | - date : 2021-03-28 62 | title: Random Event 63 | icon : 🎤 64 | color: black 65 | backgroundColor: '#6495ED' 66 | - date : 2021-01-03 67 | title: Birthday Party 68 | icon : 🎂 69 | color: black 70 | backgroundColor: hotpink 71 | - date : 2020-08-19 72 | title: Obtain Magician License 73 | icon : 🎩 74 | className: education 75 | - date : 2020-03-19 76 | title: First character used as icon 77 | backgroundColor: orange 78 | color: black 79 | - date : 2020-03-27 80 | title: Used explicit icon 81 | icon : 'F' 82 | color: orange 83 | backgroundColor: black 84 | - date : 2020-01-01 85 | title: Date of Birth 86 | icon : 👶 87 | backgroundColor: transparent 88 | periods: 89 | - start: 2022-09-19 90 | end : 2022-11-05 91 | title: Sunny 92 | icon : 🌤 93 | - start: 2022-02-16 94 | end : 2022-08-16 95 | title: Advanced Magic Course 96 | className: education 97 | - start: 2020-12-15 98 | end: 2022-01-01 99 | title: High school 100 | color: black 101 | backgroundColor: lightblue 102 | - start: 2020-02-01 103 | end: 2020-12-15 104 | title: Preschool 105 | backgroundColor: yellow 106 | color: black 107 | ``` 108 | 109 | ````` 110 | 111 | You can further customize the apperance using `userstyle.css` 112 | 113 | ## Tips & Tricks 114 | ### Ages template 115 | Some pre-prepared templates for `ages` option, just copy it to your note instead of typing yourself: 116 | ``` 117 | ages: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99] 118 | ages: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98] 119 | ages: [3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60, 63, 66, 69, 72, 75, 78, 81, 84, 87, 90, 93, 96, 99] 120 | ages: [4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 68, 72, 76, 80, 84, 88, 92, 96] 121 | ages: [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95] 122 | ``` 123 | 124 | ## Acknowledgements 125 | - Tim Urban for popularizing the [Your Life in Weeks Concept](https://waitbutwhy.com/2014/05/life-weeks.html) 126 | - [Dylan N's Life Calendar Implementation](https://github.com/ngduc/life-calendar) which is a reference for this plugin. 127 | 128 | ## License 129 | [MIT](https://raw.githubusercontent.com/hieuthi/joplin-plugin-life-calendar/main/LICENSE) -------------------------------------------------------------------------------- /src/life-calendar.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('joplin-noteDidUpdate', buildCalendars ); 2 | 3 | var _life_pin = null; 4 | 5 | if (/WebKit/i.test(navigator.userAgent)) { // sniff 6 | var _timer_life = setInterval(function() { 7 | if (/loaded|complete/.test(document.readyState)) { 8 | buildCalendars() 9 | } 10 | }, 10); 11 | } 12 | 13 | // Helpers functions 14 | function dateToIsoString(date){ 15 | var month = (date.getMonth()+1).toString(); 16 | var day = (date.getDate()).toString() 17 | month = month.length == 1 ? '0' + month : month; 18 | day = day.length == 1 ? '0' + day : day; 19 | return `${date.getFullYear()}-${month}-${day}` 20 | } 21 | 22 | function isoStringToDate(iso) { 23 | if (iso==null) { return null } 24 | var [yyyy, mm, dd] = iso.split('-'); 25 | return new Date(yyyy, mm - 1, dd); 26 | } 27 | 28 | function buildCalendars() { 29 | if (_timer_life) clearInterval(_timer_life); 30 | _life_pin = null; 31 | 32 | const calendars = document.getElementsByClassName('life-calendar'); 33 | for (var i=0; i { 52 | var elem = document.createElement("div"); 53 | elem.className = 'info-event'; 54 | elem.innerHTML = `${event["date"]} ${event["icon"] || event["title"][0]} ${event["title"]}` 55 | if (event["className"]){ elem.className = elem.className + " " + event["className"]; } 56 | if (event["color"]){ elem.style.color = event["color"]; } 57 | if (event["backgroundColor"]){ elem.style.backgroundColor = event["backgroundColor"]; } 58 | infoElement.appendChild(elem); 59 | }); 60 | info["periods"].forEach(period => { 61 | var elem = document.createElement("div"); 62 | elem.className = 'info-period'; 63 | elem.innerHTML = `${period["sIso"]} ${period["eIso"]}`; 64 | if (period["icon"]){elem.innerHTML = elem.innerHTML + ` ${period["icon"]}`; } 65 | elem.innerHTML = elem.innerHTML + ` ${period["title"]}`; 66 | if (period["className"]){ elem.className = elem.className + " " + period["className"]; } 67 | if (period["color"]){ elem.style.color = period["color"]; } 68 | if (period["backgroundColor"]){ elem.style.backgroundColor = period["backgroundColor"]; } 69 | infoElement.appendChild(elem); 70 | }) 71 | } 72 | 73 | function itemMouseOut() { 74 | var firstInfo = this.parentElement.parentElement.firstChild; 75 | var secondInfo = firstInfo.nextSibling; 76 | secondInfo.classList.remove('visible') 77 | if (_life_pin !== null){ 78 | firstInfo.classList.add('visible') 79 | } 80 | } 81 | function itemMouseOver() { 82 | var firstInfo = this.parentElement.parentElement.firstChild; 83 | var secondInfo = firstInfo.nextSibling; 84 | secondInfo.innerHTML = ''; 85 | secondInfo.classList.add('visible') 86 | renderInfo(secondInfo, this.infoObj); 87 | if (_life_pin !== null){ 88 | firstInfo.classList.remove('visible'); 89 | } 90 | } 91 | function itemClick() { 92 | var firstInfo = this.parentElement.parentElement.firstChild; 93 | if (_life_pin == null){ 94 | _life_pin = this.infoObj; 95 | firstInfo.innerHTML = ''; 96 | firstInfo.classList.add('visible') 97 | renderInfo(firstInfo, this.infoObj); 98 | } else { 99 | firstInfo.classList.remove('visible'); 100 | _life_pin = null; 101 | } 102 | } 103 | 104 | const MSHOUR = 3.6e+6; 105 | const MSDAY = MSHOUR*24; 106 | const MSWEEK = MSDAY * 7; 107 | function getEventIndex(ctype, startDate, eventDate) { 108 | var idx = 0; 109 | switch (ctype) { 110 | case "day": 111 | idx = Math.floor((eventDate.getTime()-startDate.getTime()) / MSDAY); 112 | break; 113 | case "week": 114 | idx = Math.floor((eventDate.getTime()-startDate.getTime()) / MSWEEK); 115 | break; 116 | case "month": 117 | idx = (eventDate.getFullYear() - startDate.getFullYear()) * 12 + (eventDate.getMonth() - startDate.getMonth()); 118 | break; 119 | case "year": 120 | idx = eventDate.getFullYear() - startDate.getFullYear(); 121 | break; 122 | } 123 | return idx; 124 | } 125 | function getEventTime(ctype, startDate, index) { 126 | var date = startDate; 127 | switch (ctype) { 128 | case "day": 129 | date = new Date(startDate.getTime() + index*MSDAY); 130 | break; 131 | case "week": 132 | date = new Date(startDate.getTime() + index*MSWEEK); 133 | break; 134 | case "month": 135 | var month = startDate.getMonth() + index; 136 | date = new Date(startDate.getFullYear() + Math.floor(month/12), month % 12, startDate.getDate()); 137 | break; 138 | case "year": 139 | date = new Date(startDate.getFullYear() + index, startDate.getMonth(), startDate.getDate()); 140 | break; 141 | } 142 | return date; 143 | } 144 | 145 | function floorEventDate(ctype, date) { 146 | var ret = new Date(date.getFullYear(), date.getMonth(), date.getDate()); 147 | switch (ctype) { 148 | case "day": 149 | break; 150 | case "week": 151 | ret = new Date(ret.getTime() - ret.getDay()*MSDAY) 152 | break; 153 | case "month": 154 | ret = new Date(ret.getFullYear(), ret.getMonth(), 1); 155 | break; 156 | case "year": 157 | ret = new Date(ret.getFullYear(), 0, 1); 158 | break; 159 | } 160 | return ret; 161 | } 162 | 163 | function makeLifeCalendar(calendar, options) { 164 | calendar.innerHTML = ''; // Clear element content 165 | 166 | var firstInfo = document.createElement("div"); 167 | firstInfo.className = 'info'; 168 | calendar.appendChild(firstInfo); 169 | calendar.appendChild(firstInfo.cloneNode()); 170 | 171 | var ctype = options["type"] || "week"; 172 | ctype = !["day", "week", "month", "year"].includes(ctype) ? "week" : ctype; 173 | var itype = "W" 174 | switch (ctype) { 175 | case "day" : itype = "D"; break; 176 | case "week" : itype = "W"; break; 177 | case "month": itype = "M"; break; 178 | case "year" : itype = "Y"; break; 179 | } 180 | 181 | // Calculate time period 182 | 183 | var dob = options["dob"] ? isoStringToDate(options["dob"]) : isoStringToDate("1970-01-01"); 184 | dob = options["startDate"] ? isoStringToDate(options["startDate"]) : dob; 185 | var [ byear, bmonth, bdate ] = [ dob.getFullYear(), dob.getMonth(), dob.getDate() ]; 186 | var dodm = getEventTime(ctype, dob, 9999); 187 | 188 | var dod = options["lifespan"] ? new Date(byear+options["lifespan"],bmonth,bdate) : new Date(byear+5,bmonth,bdate); 189 | dod = options["duration"] ? new Date(byear+options["duration"],bmonth,bdate) : dod; 190 | dod = options["dod"] ? isoStringToDate(options["dod"]) : dod; 191 | dod = options["endDate"] ? isoStringToDate(options["endDate"]) : dod; 192 | dod = dod > dodm ? dodm : dod; 193 | 194 | const doba = floorEventDate(ctype, dob); 195 | const doda = floorEventDate(ctype, dod); 196 | const now = new Date(); 197 | 198 | // Prepare ages 199 | options["events"] = options["events"] || []; 200 | options["events"].push({ 201 | "date" : dateToIsoString(now), 202 | "title": `Today`, 203 | "icon" : `>` 204 | }); 205 | if (options["ages"]) { 206 | options["ages"].forEach(age => { 207 | var birthday = null; 208 | if (bmonth==1 && bdate==29 && age % 4 != 0){ 209 | birthday = new Date(byear+age,1,28); 210 | } else { 211 | birthday = new Date(byear+age,bmonth,bdate); 212 | } 213 | options["events"].push({ 214 | "date" : dateToIsoString(birthday), 215 | "title": `Turning ${age}`, 216 | "icon" : `${age}` 217 | }) 218 | }) 219 | } 220 | 221 | const events = {} 222 | // Prepare events 223 | if (options["events"]) { 224 | options["events"].sort( (a,b) => {return a["date"] > b["date"] ? -1 : 1; }) 225 | options["events"].forEach(item => { 226 | var edate = new Date(isoStringToDate(item["date"]).getTime()+MSHOUR*12); // Daylight Saving Time 227 | var idx = getEventIndex(ctype, doba, edate); 228 | if (events[idx]){ 229 | events[idx].push(item); 230 | } else { 231 | events[idx] = [item]; 232 | } 233 | }) 234 | } 235 | // Prepare periods 236 | const periods = [] 237 | if (options["periods"]){ 238 | options["periods"].sort( (a,b) => {return a["start"] > b["start"] ? -1 : 1}) 239 | options["periods"].forEach(item => { 240 | var sDate = new Date(isoStringToDate(item["start"]).getTime() + MSHOUR + 1); // Daylight Saving Time 241 | var eDate = new Date(isoStringToDate(item["end"]).getTime() + MSHOUR*23 - 1); // Daylight Saving Time 242 | if (sDate && eDate){ 243 | var title = item["title"]; 244 | var className = item["className"] || null; 245 | var color = item["color"] || null; 246 | var backgroundColor = item["backgroundColor"] || null; 247 | var icon = item["icon"] || null; 248 | periods.push({"start": sDate, "end": eDate, 249 | "sIso": dateToIsoString(sDate), "eIso": dateToIsoString(eDate), 250 | "title": title, "color": color, "backgroundColor": backgroundColor, 251 | "icon": icon, "className": className}) 252 | } 253 | }) 254 | } 255 | 256 | const children = []; 257 | 258 | const nowIdx = getEventIndex(ctype, doba, now); 259 | const nItems = getEventIndex(ctype, doba, doda) + 1; 260 | for (var i=0; i${itype}${i.toString().padStart(4,'0')} | ${dateToIsoString(dateStart)}~${dateToIsoString(dateEnd)} | Age: ${age}`}; 270 | 271 | var item = document.createElement("div"); 272 | var itemspan = document.createElement("span"); 273 | // Past, Present, and Future 274 | if ( i < nowIdx ) { 275 | item.className = 'life-item past'; 276 | } else if ( i == nowIdx) { 277 | item.className = 'life-item present'; 278 | } else { 279 | item.className = 'life-item future'; 280 | } 281 | // itemIcon is set by event if available, else by period 282 | var itemIcon = null; 283 | // Prepare info events 284 | if (events[i]){ 285 | var event = events[i][0]; 286 | events[i].forEach(item => { 287 | if ((item["z-index"] || 0) > (event["z-index"] ||0)) { 288 | event = item; 289 | } 290 | }) 291 | itemIcon = event["icon"] || event["title"][0]; 292 | if (event["className"]){ itemspan.className = event["className"]; } 293 | if (event["color"]){ itemspan.style.color = event["color"]; } 294 | if (event["backgroundColor"]){ itemspan.style.backgroundColor = event["backgroundColor"]; } 295 | } 296 | 297 | // Periods 298 | var bgcolor = null; 299 | var stclass = null; 300 | periods.forEach(period => { 301 | if ( !(dateEnd<=period["start"] || dateStart>=period["end"]) ) { 302 | info["periods"].push(period); 303 | if (period["icon"] && itemIcon===null){ itemIcon = period["icon"]; } 304 | if (period["backgroundColor"] && bgcolor===null){ bgcolor = period["backgroundColor"]; } 305 | if (period["className"] && stclass===null){ stclass = period["className"]; } 306 | } 307 | }); 308 | if (bgcolor){ item.style.backgroundColor = bgcolor; } 309 | if (stclass){ item.classList.add(stclass) } 310 | 311 | itemspan.innerHTML = itemIcon || ""; 312 | itemspan.infoObj = info; 313 | itemspan.onmouseover = itemMouseOver; 314 | itemspan.onmouseout = itemMouseOut; 315 | itemspan.onclick = itemClick; 316 | 317 | item.appendChild(itemspan); 318 | calendar.appendChild(item); 319 | } 320 | } -------------------------------------------------------------------------------- /api/noteListType.d.ts: -------------------------------------------------------------------------------- 1 | import { Size } from './types'; 2 | type ListRendererDatabaseDependency = 'folder.created_time' | 'folder.deleted_time' | 'folder.encryption_applied' | 'folder.encryption_cipher_text' | 'folder.icon' | 'folder.id' | 'folder.is_shared' | 'folder.master_key_id' | 'folder.parent_id' | 'folder.share_id' | 'folder.title' | 'folder.updated_time' | 'folder.user_created_time' | 'folder.user_data' | 'folder.user_updated_time' | 'folder.type_' | 'note.altitude' | 'note.application_data' | 'note.author' | 'note.body' | 'note.conflict_original_id' | 'note.created_time' | 'note.deleted_time' | 'note.encryption_applied' | 'note.encryption_cipher_text' | 'note.id' | 'note.is_conflict' | 'note.is_shared' | 'note.is_todo' | 'note.latitude' | 'note.longitude' | 'note.markup_language' | 'note.master_key_id' | 'note.order' | 'note.parent_id' | 'note.share_id' | 'note.source' | 'note.source_application' | 'note.source_url' | 'note.title' | 'note.todo_completed' | 'note.todo_due' | 'note.updated_time' | 'note.user_created_time' | 'note.user_data' | 'note.user_updated_time' | 'note.type_'; 3 | export declare enum ItemFlow { 4 | TopToBottom = "topToBottom", 5 | LeftToRight = "leftToRight" 6 | } 7 | export type RenderNoteView = Record; 8 | export interface OnChangeEvent { 9 | elementId: string; 10 | value: any; 11 | noteId: string; 12 | } 13 | export interface OnClickEvent { 14 | elementId: string; 15 | } 16 | export type OnRenderNoteHandler = (props: any) => Promise; 17 | export type OnChangeHandler = (event: OnChangeEvent) => Promise; 18 | export type OnClickHandler = (event: OnClickEvent) => Promise; 19 | /** 20 | * Most of these are the built-in note properties, such as `note.title`, `note.todo_completed`, etc. 21 | * complemented with special properties such as `note.isWatched`, to know if a note is currently 22 | * opened in the external editor, and `note.tags` to get the list tags associated with the note. 23 | * 24 | * The `note.todoStatusText` property is a localised description of the to-do status (e.g. 25 | * "to-do, incomplete"). If you include an `` for to-do items that would 26 | * otherwise be unlabelled, consider adding `note.todoStatusText` as the checkbox's `aria-label`. 27 | * 28 | * ## Item properties 29 | * 30 | * The `item.*` properties are specific to the rendered item. The most important being 31 | * `item.selected`, which you can use to display the selected note in a different way. 32 | */ 33 | export type ListRendererDependency = ListRendererDatabaseDependency | 'item.index' | 'item.selected' | 'item.size.height' | 'item.size.width' | 'note.folder.title' | 'note.isWatched' | 'note.tags' | 'note.todoStatusText' | 'note.titleHtml'; 34 | export type ListRendererItemValueTemplates = Record; 35 | export declare const columnNames: readonly ["note.folder.title", "note.is_todo", "note.latitude", "note.longitude", "note.source_url", "note.tags", "note.title", "note.todo_completed", "note.todo_due", "note.user_created_time", "note.user_updated_time"]; 36 | export type ColumnName = typeof columnNames[number]; 37 | export interface ListRenderer { 38 | /** 39 | * It must be unique to your plugin. 40 | */ 41 | id: string; 42 | /** 43 | * Can be top to bottom or left to right. Left to right gives you more 44 | * option to set the size of the items since you set both its width and 45 | * height. 46 | */ 47 | flow: ItemFlow; 48 | /** 49 | * Whether the renderer supports multiple columns. Applies only when `flow` 50 | * is `topToBottom`. Defaults to `false`. 51 | */ 52 | multiColumns?: boolean; 53 | /** 54 | * The size of each item must be specified in advance for performance 55 | * reasons, and cannot be changed afterwards. If the item flow is top to 56 | * bottom, you only need to specify the item height (the width will be 57 | * ignored). 58 | */ 59 | itemSize: Size; 60 | /** 61 | * The CSS is relative to the list item container. What will appear in the 62 | * page is essentially `.note-list-item { YOUR_CSS; }`. It means you can use 63 | * child combinator with guarantee it will only apply to your own items. In 64 | * this example, the styling will apply to `.note-list-item > .content`: 65 | * 66 | * ```css 67 | * > .content { 68 | * padding: 10px; 69 | * } 70 | * ``` 71 | * 72 | * In order to get syntax highlighting working here, it's recommended 73 | * installing an editor extension such as [es6-string-html VSCode 74 | * extension](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html) 75 | */ 76 | itemCss?: string; 77 | /** 78 | * List the dependencies that your plugin needs to render the note list 79 | * items. Only these will be passed to your `onRenderNote` handler. Ensure 80 | * that you do not add more than what you need since there is a performance 81 | * penalty for each property. 82 | */ 83 | dependencies?: ListRendererDependency[]; 84 | headerTemplate?: string; 85 | headerHeight?: number; 86 | onHeaderClick?: OnClickHandler; 87 | /** 88 | * This property is set differently depending on the `multiColumns` property. 89 | * 90 | * ## If `multiColumns` is `false` 91 | * 92 | * There is only one column and the template is used to render the entire row. 93 | * 94 | * This is the HTML template that will be used to render the note list item. This is a [Mustache 95 | * template](https://github.com/janl/mustache.js) and it will receive the variable you return 96 | * from `onRenderNote` as tags. For example, if you return a property named `formattedDate` from 97 | * `onRenderNote`, you can insert it in the template using `Created date: {{formattedDate}}` 98 | * 99 | * ## If `multiColumns` is `true` 100 | * 101 | * Since there is multiple columns, this template will be used to render each note property 102 | * within the row. For example if the current columns are the Updated and Title properties, this 103 | * template will be called once to render the updated time and a second time to render the 104 | * title. To display the current property, the generic `value` property is provided - it will be 105 | * replaced at runtime by the actual note property. To render something different depending on 106 | * the note property, use `itemValueTemplate`. A minimal example would be 107 | * `{{value}}` which will simply render the current property inside a span tag. 108 | * 109 | * In order to get syntax highlighting working here, it's recommended installing an editor 110 | * extension such as [es6-string-html VSCode 111 | * extension](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html) 112 | * 113 | * ## Default property rendering 114 | * 115 | * Certain properties are automatically rendered once inserted in the Mustache template. Those 116 | * are in particular all the date-related fields, such as `note.user_updated_time` or 117 | * `note.todo_completed`. Internally, those are timestamps in milliseconds, however when 118 | * rendered we display them as date/time strings using the user's preferred time format. Another 119 | * notable auto-rendered property is `note.title` which is going to include additional HTML, 120 | * such as the search markers. 121 | * 122 | * If you do not want this default rendering behaviour, for example if you want to display the 123 | * raw timestamps in milliseconds, you can simply return custom properties from 124 | * `onRenderNote()`. For example: 125 | * 126 | * ```typescript 127 | * onRenderNote: async (props: any) => { 128 | * return { 129 | * ...props, 130 | * // Return the property under a different name 131 | * updatedTimeMs: props.note.user_updated_time, 132 | * } 133 | * }, 134 | * 135 | * itemTemplate: // html 136 | * ` 137 | *
138 | * Raw timestamp: {{updatedTimeMs}} 140 | *
141 | * `, 142 | * 143 | * ``` 144 | * 145 | * See 146 | * `[https://github.com/laurent22/joplin/blob/dev/packages/lib/services/noteList/renderViewProps.ts](renderViewProps.ts)` 147 | * for the list of properties that have a default rendering. 148 | */ 149 | itemTemplate: string; 150 | /** 151 | * This property applies only when `multiColumns` is `true`. It is used to render something 152 | * different for each note property. 153 | * 154 | * This is a map of actual dependencies to templates - you only need to return something if the 155 | * default, as specified in `template`, is not enough. 156 | * 157 | * Again you need to return a Mustache template and it will be combined with the `template` 158 | * property to create the final template. For example if you return a property named 159 | * `formattedDate` from `onRenderNote`, you can insert it in the template using 160 | * `{{formattedDate}}`. This string will replace `{{value}}` in the `template` property. 161 | * 162 | * So if the template property is set to `{{value}}`, the final template will be 163 | * `{{formattedDate}}`. 164 | * 165 | * The property would be set as so: 166 | * 167 | * ```javascript 168 | * itemValueTemplates: { 169 | * 'note.user_updated_time': '{{formattedDate}}', 170 | * } 171 | * ``` 172 | */ 173 | itemValueTemplates?: ListRendererItemValueTemplates; 174 | /** 175 | * This user-facing text is used for example in the View menu, so that your 176 | * renderer can be selected. 177 | */ 178 | label: () => Promise; 179 | /** 180 | * This is where most of the real-time processing will happen. When a note 181 | * is rendered for the first time and every time it changes, this handler 182 | * receives the properties specified in the `dependencies` property. You can 183 | * then process them, load any additional data you need, and once done you 184 | * need to return the properties that are needed in the `itemTemplate` HTML. 185 | * Again, to use the formatted date example, you could have such a renderer: 186 | * 187 | * ```typescript 188 | * dependencies: [ 189 | * 'note.title', 190 | * 'note.created_time', 191 | * ], 192 | * 193 | * itemTemplate: // html 194 | * ` 195 | *
196 | * Title: {{note.title}}
197 | * Date: {{formattedDate}} 198 | *
199 | * `, 200 | * 201 | * onRenderNote: async (props: any) => { 202 | * const formattedDate = dayjs(props.note.created_time).format(); 203 | * return { 204 | * // Also return the props, so that note.title is available from the 205 | * // template 206 | * ...props, 207 | * formattedDate, 208 | * } 209 | * }, 210 | * ``` 211 | */ 212 | onRenderNote: OnRenderNoteHandler; 213 | /** 214 | * This handler allows adding some interactivity to the note renderer - whenever an input element 215 | * within the item is changed (for example, when a checkbox is clicked, or a text input is 216 | * changed), this `onChange` handler is going to be called. 217 | * 218 | * You can inspect `event.elementId` to know which element had some changes, and `event.value` 219 | * to know the new value. `event.noteId` also tells you what note is affected, so that you can 220 | * potentially apply changes to it. 221 | * 222 | * You specify the element ID, by setting a `data-id` attribute on the input. 223 | * 224 | * For example, if you have such a template: 225 | * 226 | * ```html 227 | *
228 | * 229 | *
230 | * ``` 231 | * 232 | * The event handler will receive an event with `elementId` set to `noteTitleInput`. 233 | * 234 | * ## Default event handlers 235 | * 236 | * Currently one click event is automatically handled: 237 | * 238 | * If there is a checkbox with a `data-id="todo-checkbox"` attribute is present, it is going to 239 | * automatically toggle the note to-do "completed" status. 240 | * 241 | * For example this is what is used in the default list renderer: 242 | * 243 | * `` 244 | */ 245 | onChange?: OnChangeHandler; 246 | } 247 | export interface NoteListColumn { 248 | name: ColumnName; 249 | width: number; 250 | } 251 | export type NoteListColumns = NoteListColumn[]; 252 | export declare const defaultWidth = 100; 253 | export declare const defaultListColumns: () => NoteListColumns; 254 | export {}; 255 | -------------------------------------------------------------------------------- /api/noteListType.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable multiline-comment-style */ 2 | 3 | import { Size } from './types'; 4 | 5 | // AUTO-GENERATED by generate-database-type 6 | type ListRendererDatabaseDependency = 'folder.created_time' | 'folder.deleted_time' | 'folder.encryption_applied' | 'folder.encryption_cipher_text' | 'folder.icon' | 'folder.id' | 'folder.is_shared' | 'folder.master_key_id' | 'folder.parent_id' | 'folder.share_id' | 'folder.title' | 'folder.updated_time' | 'folder.user_created_time' | 'folder.user_data' | 'folder.user_updated_time' | 'folder.type_' | 'note.altitude' | 'note.application_data' | 'note.author' | 'note.body' | 'note.conflict_original_id' | 'note.created_time' | 'note.deleted_time' | 'note.encryption_applied' | 'note.encryption_cipher_text' | 'note.id' | 'note.is_conflict' | 'note.is_shared' | 'note.is_todo' | 'note.latitude' | 'note.longitude' | 'note.markup_language' | 'note.master_key_id' | 'note.order' | 'note.parent_id' | 'note.share_id' | 'note.source' | 'note.source_application' | 'note.source_url' | 'note.title' | 'note.todo_completed' | 'note.todo_due' | 'note.updated_time' | 'note.user_created_time' | 'note.user_data' | 'note.user_updated_time' | 'note.type_'; 7 | // AUTO-GENERATED by generate-database-type 8 | 9 | export enum ItemFlow { 10 | TopToBottom = 'topToBottom', 11 | LeftToRight = 'leftToRight', 12 | } 13 | 14 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied 15 | export type RenderNoteView = Record; 16 | 17 | export interface OnChangeEvent { 18 | elementId: string; 19 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied 20 | value: any; 21 | noteId: string; 22 | } 23 | 24 | export interface OnClickEvent { 25 | elementId: string; 26 | } 27 | 28 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied 29 | export type OnRenderNoteHandler = (props: any)=> Promise; 30 | export type OnChangeHandler = (event: OnChangeEvent)=> Promise; 31 | export type OnClickHandler = (event: OnClickEvent)=> Promise; 32 | 33 | /** 34 | * Most of these are the built-in note properties, such as `note.title`, `note.todo_completed`, etc. 35 | * complemented with special properties such as `note.isWatched`, to know if a note is currently 36 | * opened in the external editor, and `note.tags` to get the list tags associated with the note. 37 | * 38 | * The `note.todoStatusText` property is a localised description of the to-do status (e.g. 39 | * "to-do, incomplete"). If you include an `` for to-do items that would 40 | * otherwise be unlabelled, consider adding `note.todoStatusText` as the checkbox's `aria-label`. 41 | * 42 | * ## Item properties 43 | * 44 | * The `item.*` properties are specific to the rendered item. The most important being 45 | * `item.selected`, which you can use to display the selected note in a different way. 46 | */ 47 | export type ListRendererDependency = 48 | ListRendererDatabaseDependency | 49 | 'item.index' | 50 | 'item.selected' | 51 | 'item.size.height' | 52 | 'item.size.width' | 53 | 'note.folder.title' | 54 | 'note.isWatched' | 55 | 'note.tags' | 56 | 'note.todoStatusText' | 57 | 'note.titleHtml'; 58 | 59 | export type ListRendererItemValueTemplates = Record; 60 | 61 | export const columnNames = [ 62 | 'note.folder.title', 63 | 'note.is_todo', 64 | 'note.latitude', 65 | 'note.longitude', 66 | 'note.source_url', 67 | 'note.tags', 68 | 'note.title', 69 | 'note.todo_completed', 70 | 'note.todo_due', 71 | 'note.user_created_time', 72 | 'note.user_updated_time', 73 | ] as const; 74 | 75 | export type ColumnName = typeof columnNames[number]; 76 | 77 | export interface ListRenderer { 78 | /** 79 | * It must be unique to your plugin. 80 | */ 81 | id: string; 82 | 83 | /** 84 | * Can be top to bottom or left to right. Left to right gives you more 85 | * option to set the size of the items since you set both its width and 86 | * height. 87 | */ 88 | flow: ItemFlow; 89 | 90 | /** 91 | * Whether the renderer supports multiple columns. Applies only when `flow` 92 | * is `topToBottom`. Defaults to `false`. 93 | */ 94 | multiColumns?: boolean; 95 | 96 | /** 97 | * The size of each item must be specified in advance for performance 98 | * reasons, and cannot be changed afterwards. If the item flow is top to 99 | * bottom, you only need to specify the item height (the width will be 100 | * ignored). 101 | */ 102 | itemSize: Size; 103 | 104 | /** 105 | * The CSS is relative to the list item container. What will appear in the 106 | * page is essentially `.note-list-item { YOUR_CSS; }`. It means you can use 107 | * child combinator with guarantee it will only apply to your own items. In 108 | * this example, the styling will apply to `.note-list-item > .content`: 109 | * 110 | * ```css 111 | * > .content { 112 | * padding: 10px; 113 | * } 114 | * ``` 115 | * 116 | * In order to get syntax highlighting working here, it's recommended 117 | * installing an editor extension such as [es6-string-html VSCode 118 | * extension](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html) 119 | */ 120 | itemCss?: string; 121 | 122 | /** 123 | * List the dependencies that your plugin needs to render the note list 124 | * items. Only these will be passed to your `onRenderNote` handler. Ensure 125 | * that you do not add more than what you need since there is a performance 126 | * penalty for each property. 127 | */ 128 | dependencies?: ListRendererDependency[]; 129 | 130 | headerTemplate?: string; 131 | headerHeight?: number; 132 | onHeaderClick?: OnClickHandler; 133 | 134 | /** 135 | * This property is set differently depending on the `multiColumns` property. 136 | * 137 | * ## If `multiColumns` is `false` 138 | * 139 | * There is only one column and the template is used to render the entire row. 140 | * 141 | * This is the HTML template that will be used to render the note list item. This is a [Mustache 142 | * template](https://github.com/janl/mustache.js) and it will receive the variable you return 143 | * from `onRenderNote` as tags. For example, if you return a property named `formattedDate` from 144 | * `onRenderNote`, you can insert it in the template using `Created date: {{formattedDate}}` 145 | * 146 | * ## If `multiColumns` is `true` 147 | * 148 | * Since there is multiple columns, this template will be used to render each note property 149 | * within the row. For example if the current columns are the Updated and Title properties, this 150 | * template will be called once to render the updated time and a second time to render the 151 | * title. To display the current property, the generic `value` property is provided - it will be 152 | * replaced at runtime by the actual note property. To render something different depending on 153 | * the note property, use `itemValueTemplate`. A minimal example would be 154 | * `{{value}}` which will simply render the current property inside a span tag. 155 | * 156 | * In order to get syntax highlighting working here, it's recommended installing an editor 157 | * extension such as [es6-string-html VSCode 158 | * extension](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html) 159 | * 160 | * ## Default property rendering 161 | * 162 | * Certain properties are automatically rendered once inserted in the Mustache template. Those 163 | * are in particular all the date-related fields, such as `note.user_updated_time` or 164 | * `note.todo_completed`. Internally, those are timestamps in milliseconds, however when 165 | * rendered we display them as date/time strings using the user's preferred time format. Another 166 | * notable auto-rendered property is `note.title` which is going to include additional HTML, 167 | * such as the search markers. 168 | * 169 | * If you do not want this default rendering behaviour, for example if you want to display the 170 | * raw timestamps in milliseconds, you can simply return custom properties from 171 | * `onRenderNote()`. For example: 172 | * 173 | * ```typescript 174 | * onRenderNote: async (props: any) => { 175 | * return { 176 | * ...props, 177 | * // Return the property under a different name 178 | * updatedTimeMs: props.note.user_updated_time, 179 | * } 180 | * }, 181 | * 182 | * itemTemplate: // html 183 | * ` 184 | *
185 | * Raw timestamp: {{updatedTimeMs}} 187 | *
188 | * `, 189 | * 190 | * ``` 191 | * 192 | * See 193 | * `[https://github.com/laurent22/joplin/blob/dev/packages/lib/services/noteList/renderViewProps.ts](renderViewProps.ts)` 194 | * for the list of properties that have a default rendering. 195 | */ 196 | itemTemplate: string; 197 | 198 | /** 199 | * This property applies only when `multiColumns` is `true`. It is used to render something 200 | * different for each note property. 201 | * 202 | * This is a map of actual dependencies to templates - you only need to return something if the 203 | * default, as specified in `template`, is not enough. 204 | * 205 | * Again you need to return a Mustache template and it will be combined with the `template` 206 | * property to create the final template. For example if you return a property named 207 | * `formattedDate` from `onRenderNote`, you can insert it in the template using 208 | * `{{formattedDate}}`. This string will replace `{{value}}` in the `template` property. 209 | * 210 | * So if the template property is set to `{{value}}`, the final template will be 211 | * `{{formattedDate}}`. 212 | * 213 | * The property would be set as so: 214 | * 215 | * ```javascript 216 | * itemValueTemplates: { 217 | * 'note.user_updated_time': '{{formattedDate}}', 218 | * } 219 | * ``` 220 | */ 221 | itemValueTemplates?: ListRendererItemValueTemplates; 222 | 223 | /** 224 | * This user-facing text is used for example in the View menu, so that your 225 | * renderer can be selected. 226 | */ 227 | label: ()=> Promise; 228 | 229 | /** 230 | * This is where most of the real-time processing will happen. When a note 231 | * is rendered for the first time and every time it changes, this handler 232 | * receives the properties specified in the `dependencies` property. You can 233 | * then process them, load any additional data you need, and once done you 234 | * need to return the properties that are needed in the `itemTemplate` HTML. 235 | * Again, to use the formatted date example, you could have such a renderer: 236 | * 237 | * ```typescript 238 | * dependencies: [ 239 | * 'note.title', 240 | * 'note.created_time', 241 | * ], 242 | * 243 | * itemTemplate: // html 244 | * ` 245 | *
246 | * Title: {{note.title}}
247 | * Date: {{formattedDate}} 248 | *
249 | * `, 250 | * 251 | * onRenderNote: async (props: any) => { 252 | * const formattedDate = dayjs(props.note.created_time).format(); 253 | * return { 254 | * // Also return the props, so that note.title is available from the 255 | * // template 256 | * ...props, 257 | * formattedDate, 258 | * } 259 | * }, 260 | * ``` 261 | */ 262 | onRenderNote: OnRenderNoteHandler; 263 | 264 | /** 265 | * This handler allows adding some interactivity to the note renderer - whenever an input element 266 | * within the item is changed (for example, when a checkbox is clicked, or a text input is 267 | * changed), this `onChange` handler is going to be called. 268 | * 269 | * You can inspect `event.elementId` to know which element had some changes, and `event.value` 270 | * to know the new value. `event.noteId` also tells you what note is affected, so that you can 271 | * potentially apply changes to it. 272 | * 273 | * You specify the element ID, by setting a `data-id` attribute on the input. 274 | * 275 | * For example, if you have such a template: 276 | * 277 | * ```html 278 | *
279 | * 280 | *
281 | * ``` 282 | * 283 | * The event handler will receive an event with `elementId` set to `noteTitleInput`. 284 | * 285 | * ## Default event handlers 286 | * 287 | * Currently one click event is automatically handled: 288 | * 289 | * If there is a checkbox with a `data-id="todo-checkbox"` attribute is present, it is going to 290 | * automatically toggle the note to-do "completed" status. 291 | * 292 | * For example this is what is used in the default list renderer: 293 | * 294 | * `` 295 | */ 296 | onChange?: OnChangeHandler; 297 | } 298 | 299 | export interface NoteListColumn { 300 | name: ColumnName; 301 | width: number; 302 | } 303 | 304 | export type NoteListColumns = NoteListColumn[]; 305 | 306 | export const defaultWidth = 100; 307 | 308 | export const defaultListColumns = () => { 309 | const columns: NoteListColumns = [ 310 | { 311 | name: 'note.is_todo', 312 | width: 30, 313 | }, 314 | { 315 | name: 'note.user_updated_time', 316 | width: defaultWidth, 317 | }, 318 | { 319 | name: 'note.title', 320 | width: 0, 321 | }, 322 | ]; 323 | 324 | return columns; 325 | }; 326 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // This file is used to build the plugin file (.jpl) and plugin info (.json). It 3 | // is recommended not to edit this file as it would be overwritten when updating 4 | // the plugin framework. If you do make some changes, consider using an external 5 | // JS file and requiring it here to minimize the changes. That way when you 6 | // update, you can easily restore the functionality you've added. 7 | // ----------------------------------------------------------------------------- 8 | 9 | /* eslint-disable no-console */ 10 | 11 | const path = require('path'); 12 | const crypto = require('crypto'); 13 | const fs = require('fs-extra'); 14 | const chalk = require('chalk'); 15 | const CopyPlugin = require('copy-webpack-plugin'); 16 | const tar = require('tar'); 17 | const glob = require('glob'); 18 | const execSync = require('child_process').execSync; 19 | 20 | // AUTO-GENERATED by updateCategories 21 | const allPossibleCategories = [{'name':'appearance'},{'name':'developer tools'},{'name':'productivity'},{'name':'themes'},{'name':'integrations'},{'name':'viewer'},{'name':'search'},{'name':'tags'},{'name':'editor'},{'name':'files'},{'name':'personal knowledge management'}]; 22 | // AUTO-GENERATED by updateCategories 23 | 24 | const rootDir = path.resolve(__dirname); 25 | const userConfigFilename = './plugin.config.json'; 26 | const userConfigPath = path.resolve(rootDir, userConfigFilename); 27 | const distDir = path.resolve(rootDir, 'dist'); 28 | const srcDir = path.resolve(rootDir, 'src'); 29 | const publishDir = path.resolve(rootDir, 'publish'); 30 | 31 | const userConfig = { 32 | extraScripts: [], 33 | ...(fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {}), 34 | }; 35 | 36 | const manifestPath = `${srcDir}/manifest.json`; 37 | const packageJsonPath = `${rootDir}/package.json`; 38 | const allPossibleScreenshotsType = ['jpg', 'jpeg', 'png', 'gif', 'webp']; 39 | const manifest = readManifest(manifestPath); 40 | const pluginArchiveFilePath = path.resolve(publishDir, `${manifest.id}.jpl`); 41 | const pluginInfoFilePath = path.resolve(publishDir, `${manifest.id}.json`); 42 | 43 | const { builtinModules } = require('node:module'); 44 | 45 | // Webpack5 doesn't polyfill by default and displays a warning when attempting to require() built-in 46 | // node modules. Set these to false to prevent Webpack from warning about not polyfilling these modules. 47 | // We don't need to polyfill because the plugins run in Electron's Node environment. 48 | const moduleFallback = {}; 49 | for (const moduleName of builtinModules) { 50 | moduleFallback[moduleName] = false; 51 | } 52 | 53 | const getPackageJson = () => { 54 | return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); 55 | }; 56 | 57 | function validatePackageJson() { 58 | const content = getPackageJson(); 59 | if (!content.name || content.name.indexOf('joplin-plugin-') !== 0) { 60 | console.warn(chalk.yellow(`WARNING: To publish the plugin, the package name should start with "joplin-plugin-" (found "${content.name}") in ${packageJsonPath}`)); 61 | } 62 | 63 | if (!content.keywords || content.keywords.indexOf('joplin-plugin') < 0) { 64 | console.warn(chalk.yellow(`WARNING: To publish the plugin, the package keywords should include "joplin-plugin" (found "${JSON.stringify(content.keywords)}") in ${packageJsonPath}`)); 65 | } 66 | 67 | if (content.scripts && content.scripts.postinstall) { 68 | console.warn(chalk.yellow(`WARNING: package.json contains a "postinstall" script. It is recommended to use a "prepare" script instead so that it is executed before publish. In ${packageJsonPath}`)); 69 | } 70 | } 71 | 72 | function fileSha256(filePath) { 73 | const content = fs.readFileSync(filePath); 74 | return crypto.createHash('sha256').update(content).digest('hex'); 75 | } 76 | 77 | function currentGitInfo() { 78 | try { 79 | let branch = execSync('git rev-parse --abbrev-ref HEAD', { stdio: 'pipe' }).toString().trim(); 80 | const commit = execSync('git rev-parse HEAD', { stdio: 'pipe' }).toString().trim(); 81 | if (branch === 'HEAD') branch = 'master'; 82 | return `${branch}:${commit}`; 83 | } catch (error) { 84 | const messages = error.message ? error.message.split('\n') : ['']; 85 | console.info(chalk.cyan('Could not get git commit (not a git repo?):', messages[0].trim())); 86 | console.info(chalk.cyan('Git information will not be stored in plugin info file')); 87 | return ''; 88 | } 89 | } 90 | 91 | function validateCategories(categories) { 92 | if (!categories) return null; 93 | if ((categories.length !== new Set(categories).size)) throw new Error('Repeated categories are not allowed'); 94 | // eslint-disable-next-line github/array-foreach -- Old code before rule was applied 95 | categories.forEach(category => { 96 | if (!allPossibleCategories.map(category => { return category.name; }).includes(category)) throw new Error(`${category} is not a valid category. Please make sure that the category name is lowercase. Valid categories are: \n${allPossibleCategories.map(category => { return category.name; })}\n`); 97 | }); 98 | } 99 | 100 | function validateScreenshots(screenshots) { 101 | if (!screenshots) return null; 102 | for (const screenshot of screenshots) { 103 | if (!screenshot.src) throw new Error('You must specify a src for each screenshot'); 104 | 105 | // Avoid attempting to download and verify URL screenshots. 106 | if (screenshot.src.startsWith('https://') || screenshot.src.startsWith('http://')) { 107 | continue; 108 | } 109 | 110 | const screenshotType = screenshot.src.split('.').pop(); 111 | if (!allPossibleScreenshotsType.includes(screenshotType)) throw new Error(`${screenshotType} is not a valid screenshot type. Valid types are: \n${allPossibleScreenshotsType}\n`); 112 | 113 | const screenshotPath = path.resolve(rootDir, screenshot.src); 114 | 115 | // Max file size is 1MB 116 | const fileMaxSize = 1024; 117 | const fileSize = fs.statSync(screenshotPath).size / 1024; 118 | if (fileSize > fileMaxSize) throw new Error(`Max screenshot file size is ${fileMaxSize}KB. ${screenshotPath} is ${fileSize}KB`); 119 | } 120 | } 121 | 122 | function readManifest(manifestPath) { 123 | const content = fs.readFileSync(manifestPath, 'utf8'); 124 | const output = JSON.parse(content); 125 | if (!output.id) throw new Error(`Manifest plugin ID is not set in ${manifestPath}`); 126 | validateCategories(output.categories); 127 | validateScreenshots(output.screenshots); 128 | return output; 129 | } 130 | 131 | function createPluginArchive(sourceDir, destPath) { 132 | const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true, windowsPathsNoEscape: true }) 133 | .map(f => f.substr(sourceDir.length + 1)); 134 | 135 | if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty'); 136 | fs.removeSync(destPath); 137 | 138 | tar.create( 139 | { 140 | strict: true, 141 | portable: true, 142 | file: destPath, 143 | cwd: sourceDir, 144 | sync: true, 145 | }, 146 | distFiles, 147 | ); 148 | 149 | console.info(chalk.cyan(`Plugin archive has been created in ${destPath}`)); 150 | } 151 | 152 | const writeManifest = (manifestPath, content) => { 153 | fs.writeFileSync(manifestPath, JSON.stringify(content, null, '\t'), 'utf8'); 154 | }; 155 | 156 | function createPluginInfo(manifestPath, destPath, jplFilePath) { 157 | const contentText = fs.readFileSync(manifestPath, 'utf8'); 158 | const content = JSON.parse(contentText); 159 | content._publish_hash = `sha256:${fileSha256(jplFilePath)}`; 160 | content._publish_commit = currentGitInfo(); 161 | writeManifest(destPath, content); 162 | } 163 | 164 | function onBuildCompleted() { 165 | try { 166 | fs.removeSync(path.resolve(publishDir, 'index.js')); 167 | createPluginArchive(distDir, pluginArchiveFilePath); 168 | createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath); 169 | validatePackageJson(); 170 | } catch (error) { 171 | console.error(chalk.red(error.message)); 172 | } 173 | } 174 | 175 | const baseConfig = { 176 | mode: 'production', 177 | target: 'node', 178 | stats: 'errors-only', 179 | module: { 180 | rules: [ 181 | { 182 | test: /\.tsx?$/, 183 | use: 'ts-loader', 184 | exclude: /node_modules/, 185 | }, 186 | ], 187 | }, 188 | ...userConfig.webpackOverrides, 189 | }; 190 | 191 | const pluginConfig = { ...baseConfig, entry: './src/index.ts', 192 | resolve: { 193 | alias: { 194 | api: path.resolve(__dirname, 'api'), 195 | }, 196 | fallback: moduleFallback, 197 | // JSON files can also be required from scripts so we include this. 198 | // https://github.com/joplin/plugin-bibtex/pull/2 199 | extensions: ['.js', '.tsx', '.ts', '.json'], 200 | }, 201 | output: { 202 | filename: 'index.js', 203 | path: distDir, 204 | }, 205 | plugins: [ 206 | new CopyPlugin({ 207 | patterns: [ 208 | { 209 | from: '**/*', 210 | context: path.resolve(__dirname, 'src'), 211 | to: path.resolve(__dirname, 'dist'), 212 | globOptions: { 213 | ignore: [ 214 | // All TypeScript files are compiled to JS and 215 | // already copied into /dist so we don't copy them. 216 | '**/*.ts', 217 | '**/*.tsx', 218 | ], 219 | }, 220 | }, 221 | ], 222 | }), 223 | ] }; 224 | 225 | 226 | // These libraries can be included with require(...) or 227 | // joplin.require(...) from content scripts. 228 | const externalContentScriptLibraries = [ 229 | '@codemirror/view', 230 | '@codemirror/state', 231 | '@codemirror/search', 232 | '@codemirror/language', 233 | '@codemirror/autocomplete', 234 | '@codemirror/commands', 235 | '@codemirror/highlight', 236 | '@codemirror/lint', 237 | '@codemirror/lang-html', 238 | '@codemirror/lang-markdown', 239 | '@codemirror/language-data', 240 | '@lezer/common', 241 | '@lezer/markdown', 242 | '@lezer/highlight', 243 | ]; 244 | 245 | const extraScriptExternals = {}; 246 | for (const library of externalContentScriptLibraries) { 247 | extraScriptExternals[library] = { commonjs: library }; 248 | } 249 | 250 | const extraScriptConfig = { 251 | ...baseConfig, 252 | resolve: { 253 | alias: { 254 | api: path.resolve(__dirname, 'api'), 255 | }, 256 | fallback: moduleFallback, 257 | extensions: ['.js', '.tsx', '.ts', '.json'], 258 | }, 259 | 260 | // We support requiring @codemirror/... libraries through require('@codemirror/...') 261 | externalsType: 'commonjs', 262 | externals: extraScriptExternals, 263 | }; 264 | 265 | const createArchiveConfig = { 266 | stats: 'errors-only', 267 | entry: './dist/index.js', 268 | resolve: { 269 | fallback: moduleFallback, 270 | }, 271 | output: { 272 | filename: 'index.js', 273 | path: publishDir, 274 | }, 275 | plugins: [{ 276 | apply(compiler) { 277 | compiler.hooks.done.tap('archiveOnBuildListener', onBuildCompleted); 278 | }, 279 | }], 280 | }; 281 | 282 | function resolveExtraScriptPath(name) { 283 | const relativePath = `./src/${name}`; 284 | 285 | const fullPath = path.resolve(`${rootDir}/${relativePath}`); 286 | if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`); 287 | 288 | const s = name.split('.'); 289 | s.pop(); 290 | const nameNoExt = s.join('.'); 291 | 292 | return { 293 | entry: relativePath, 294 | output: { 295 | filename: `${nameNoExt}.js`, 296 | path: distDir, 297 | library: 'default', 298 | libraryTarget: 'commonjs', 299 | libraryExport: 'default', 300 | }, 301 | }; 302 | } 303 | 304 | function buildExtraScriptConfigs(userConfig) { 305 | if (!userConfig.extraScripts.length) return []; 306 | 307 | const output = []; 308 | 309 | for (const scriptName of userConfig.extraScripts) { 310 | const scriptPaths = resolveExtraScriptPath(scriptName); 311 | output.push({ ...extraScriptConfig, entry: scriptPaths.entry, 312 | output: scriptPaths.output }); 313 | } 314 | 315 | return output; 316 | } 317 | 318 | const increaseVersion = version => { 319 | try { 320 | const s = version.split('.'); 321 | const d = Number(s[s.length - 1]) + 1; 322 | s[s.length - 1] = `${d}`; 323 | return s.join('.'); 324 | } catch (error) { 325 | error.message = `Could not parse version number: ${version}: ${error.message}`; 326 | throw error; 327 | } 328 | }; 329 | 330 | const updateVersion = () => { 331 | const packageJson = getPackageJson(); 332 | packageJson.version = increaseVersion(packageJson.version); 333 | fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, 'utf8'); 334 | 335 | const manifest = readManifest(manifestPath); 336 | manifest.version = increaseVersion(manifest.version); 337 | writeManifest(manifestPath, manifest); 338 | 339 | if (packageJson.version !== manifest.version) { 340 | console.warn(chalk.yellow(`Version numbers have been updated but they do not match: package.json (${packageJson.version}), manifest.json (${manifest.version}). Set them to the required values to get them in sync.`)); 341 | } 342 | }; 343 | 344 | function main(environ) { 345 | const configName = environ['joplin-plugin-config']; 346 | if (!configName) throw new Error('A config file must be specified via the --joplin-plugin-config flag'); 347 | 348 | // Webpack configurations run in parallel, while we need them to run in 349 | // sequence, and to do that it seems the only way is to run webpack multiple 350 | // times, with different config each time. 351 | 352 | const configs = { 353 | // Builds the main src/index.ts and copy the extra content from /src to 354 | // /dist including scripts, CSS and any other asset. 355 | buildMain: [pluginConfig], 356 | 357 | // Builds the extra scripts as defined in plugin.config.json. When doing 358 | // so, some JavaScript files that were copied in the previous might be 359 | // overwritten here by the compiled version. This is by design. The 360 | // result is that JS files that don't need compilation, are simply 361 | // copied to /dist, while those that do need it are correctly compiled. 362 | buildExtraScripts: buildExtraScriptConfigs(userConfig), 363 | 364 | // Ths config is for creating the .jpl, which is done via the plugin, so 365 | // it doesn't actually need an entry and output, however webpack won't 366 | // run without this. So we give it an entry that we know is going to 367 | // exist and output in the publish dir. Then the plugin will delete this 368 | // temporary file before packaging the plugin. 369 | createArchive: [createArchiveConfig], 370 | }; 371 | 372 | // If we are running the first config step, we clean up and create the build 373 | // directories. 374 | if (configName === 'buildMain') { 375 | fs.removeSync(distDir); 376 | fs.removeSync(publishDir); 377 | fs.mkdirpSync(publishDir); 378 | } 379 | 380 | if (configName === 'updateVersion') { 381 | updateVersion(); 382 | return []; 383 | } 384 | 385 | return configs[configName]; 386 | } 387 | 388 | 389 | module.exports = (env) => { 390 | let exportedConfigs = []; 391 | 392 | try { 393 | exportedConfigs = main(env); 394 | } catch (error) { 395 | console.error(error.message); 396 | process.exit(1); 397 | } 398 | 399 | if (!exportedConfigs.length) { 400 | // Nothing to do - for example where there are no external scripts to 401 | // compile. 402 | process.exit(0); 403 | } 404 | 405 | return exportedConfigs; 406 | }; 407 | -------------------------------------------------------------------------------- /api/types.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable multiline-comment-style */ 2 | 3 | // ================================================================= 4 | // Command API types 5 | // ================================================================= 6 | 7 | export interface Command { 8 | /** 9 | * Name of command - must be globally unique 10 | */ 11 | name: string; 12 | 13 | /** 14 | * Label to be displayed on menu items or keyboard shortcut editor for example. 15 | * If it is missing, it's assumed it's a private command, to be called programmatically only. 16 | * In that case the command will not appear in the shortcut editor or command panel, and logically 17 | * should not be used as a menu item. 18 | */ 19 | label?: string; 20 | 21 | /** 22 | * Icon to be used on toolbar buttons for example 23 | */ 24 | iconName?: string; 25 | 26 | /** 27 | * Code to be ran when the command is executed. It may return a result. 28 | */ 29 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied 30 | execute(...args: any[]): Promise; 31 | 32 | /** 33 | * Defines whether the command should be enabled or disabled, which in turns 34 | * affects the enabled state of any associated button or menu item. 35 | * 36 | * The condition should be expressed as a "when-clause" (as in Visual Studio 37 | * Code). It's a simple boolean expression that evaluates to `true` or 38 | * `false`. It supports the following operators: 39 | * 40 | * Operator | Symbol | Example 41 | * -- | -- | -- 42 | * Equality | == | "editorType == markdown" 43 | * Inequality | != | "currentScreen != config" 44 | * Or | \|\| | "noteIsTodo \|\| noteTodoCompleted" 45 | * And | && | "oneNoteSelected && !inConflictFolder" 46 | * 47 | * Joplin, unlike VSCode, also supports parentheses, which allows creating 48 | * more complex expressions such as `cond1 || (cond2 && cond3)`. Only one 49 | * level of parentheses is possible (nested ones aren't supported). 50 | * 51 | * Currently the supported context variables aren't documented, but you can 52 | * find the list below: 53 | * 54 | * - [Global When Clauses](https://github.com/laurent22/joplin/blob/dev/packages/lib/services/commands/stateToWhenClauseContext.ts) 55 | * - [Desktop app When Clauses](https://github.com/laurent22/joplin/blob/dev/packages/app-desktop/services/commands/stateToWhenClauseContext.ts) 56 | * 57 | * Note: Commands are enabled by default unless you use this property. 58 | */ 59 | enabledCondition?: string; 60 | } 61 | 62 | // ================================================================= 63 | // Interop API types 64 | // ================================================================= 65 | 66 | export enum FileSystemItem { 67 | File = 'file', 68 | Directory = 'directory', 69 | } 70 | 71 | export enum ImportModuleOutputFormat { 72 | Markdown = 'md', 73 | Html = 'html', 74 | } 75 | 76 | /** 77 | * Used to implement a module to export data from Joplin. [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/json_export) for an example. 78 | * 79 | * In general, all the event handlers you'll need to implement take a `context` object as a first argument. This object will contain the export or import path as well as various optional properties, such as which notes or notebooks need to be exported. 80 | * 81 | * To get a better sense of what it will contain it can be useful to print it using `console.info(context)`. 82 | */ 83 | export interface ExportModule { 84 | /** 85 | * The format to be exported, eg "enex", "jex", "json", etc. 86 | */ 87 | format: string; 88 | 89 | /** 90 | * The description that will appear in the UI, for example in the menu item. 91 | */ 92 | description: string; 93 | 94 | /** 95 | * Whether the module will export a single file or multiple files in a directory. It affects the open dialog that will be presented to the user when using your exporter. 96 | */ 97 | target: FileSystemItem; 98 | 99 | /** 100 | * Only applies to single file exporters or importers 101 | * It tells whether the format can package multiple notes into one file. 102 | * For example JEX or ENEX can, but HTML cannot. 103 | */ 104 | isNoteArchive: boolean; 105 | 106 | /** 107 | * The extensions of the files exported by your module. For example, it is `["htm", "html"]` for the HTML module, and just `["jex"]` for the JEX module. 108 | */ 109 | fileExtensions?: string[]; 110 | 111 | /** 112 | * Called when the export process starts. 113 | */ 114 | onInit(context: ExportContext): Promise; 115 | 116 | /** 117 | * Called when an item needs to be processed. An "item" can be any Joplin object, such as a note, a folder, a notebook, etc. 118 | */ 119 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied 120 | onProcessItem(context: ExportContext, itemType: number, item: any): Promise; 121 | 122 | /** 123 | * Called when a resource file needs to be exported. 124 | */ 125 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied 126 | onProcessResource(context: ExportContext, resource: any, filePath: string): Promise; 127 | 128 | /** 129 | * Called when the export process is done. 130 | */ 131 | onClose(context: ExportContext): Promise; 132 | } 133 | 134 | export interface ImportModule { 135 | /** 136 | * The format to be exported, eg "enex", "jex", "json", etc. 137 | */ 138 | format: string; 139 | 140 | /** 141 | * The description that will appear in the UI, for example in the menu item. 142 | */ 143 | description: string; 144 | 145 | /** 146 | * Only applies to single file exporters or importers 147 | * It tells whether the format can package multiple notes into one file. 148 | * For example JEX or ENEX can, but HTML cannot. 149 | */ 150 | isNoteArchive: boolean; 151 | 152 | /** 153 | * The type of sources that are supported by the module. Tells whether the module can import files or directories or both. 154 | */ 155 | sources: FileSystemItem[]; 156 | 157 | /** 158 | * Tells the file extensions of the exported files. 159 | */ 160 | fileExtensions?: string[]; 161 | 162 | /** 163 | * Tells the type of notes that will be generated, either HTML or Markdown (default). 164 | */ 165 | outputFormat?: ImportModuleOutputFormat; 166 | 167 | /** 168 | * Called when the import process starts. There is only one event handler within which you should import the complete data. 169 | */ 170 | onExec(context: ImportContext): Promise; 171 | } 172 | 173 | export interface ExportOptions { 174 | format?: string; 175 | path?: string; 176 | sourceFolderIds?: string[]; 177 | sourceNoteIds?: string[]; 178 | // modulePath?: string; 179 | target?: FileSystemItem; 180 | } 181 | 182 | export interface ExportContext { 183 | destPath: string; 184 | options: ExportOptions; 185 | 186 | /** 187 | * You can attach your own custom data using this property - it will then be passed to each event handler, allowing you to keep state from one event to the next. 188 | */ 189 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied 190 | userData?: any; 191 | } 192 | 193 | export interface ImportContext { 194 | sourcePath: string; 195 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied 196 | options: any; 197 | warnings: string[]; 198 | } 199 | 200 | // ================================================================= 201 | // Misc types 202 | // ================================================================= 203 | 204 | export interface Script { 205 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied 206 | onStart?(event: any): Promise; 207 | } 208 | 209 | export interface Disposable { 210 | // dispose():void; 211 | } 212 | 213 | export enum ModelType { 214 | Note = 1, 215 | Folder = 2, 216 | Setting = 3, 217 | Resource = 4, 218 | Tag = 5, 219 | NoteTag = 6, 220 | Search = 7, 221 | Alarm = 8, 222 | MasterKey = 9, 223 | ItemChange = 10, 224 | NoteResource = 11, 225 | ResourceLocalState = 12, 226 | Revision = 13, 227 | Migration = 14, 228 | SmartFilter = 15, 229 | Command = 16, 230 | } 231 | 232 | export interface VersionInfo { 233 | version: string; 234 | profileVersion: number; 235 | syncVersion: number; 236 | 237 | platform: 'desktop'|'mobile'; 238 | } 239 | 240 | // ================================================================= 241 | // Menu types 242 | // ================================================================= 243 | 244 | export interface CreateMenuItemOptions { 245 | accelerator: string; 246 | } 247 | 248 | export enum MenuItemLocation { 249 | File = 'file', 250 | Edit = 'edit', 251 | View = 'view', 252 | Note = 'note', 253 | Tools = 'tools', 254 | Help = 'help', 255 | 256 | /** 257 | * @deprecated Do not use - same as NoteListContextMenu 258 | */ 259 | Context = 'context', 260 | 261 | // If adding an item here, don't forget to update isContextMenuItemLocation() 262 | 263 | /** 264 | * When a command is called from the note list context menu, the 265 | * command will receive the following arguments: 266 | * 267 | * - `noteIds:string[]`: IDs of the notes that were right-clicked on. 268 | */ 269 | NoteListContextMenu = 'noteListContextMenu', 270 | 271 | EditorContextMenu = 'editorContextMenu', 272 | 273 | /** 274 | * When a command is called from a folder context menu, the 275 | * command will receive the following arguments: 276 | * 277 | * - `folderId:string`: ID of the folder that was right-clicked on 278 | */ 279 | FolderContextMenu = 'folderContextMenu', 280 | 281 | /** 282 | * When a command is called from a tag context menu, the 283 | * command will receive the following arguments: 284 | * 285 | * - `tagId:string`: ID of the tag that was right-clicked on 286 | */ 287 | TagContextMenu = 'tagContextMenu', 288 | } 289 | 290 | export function isContextMenuItemLocation(location: MenuItemLocation): boolean { 291 | return [ 292 | MenuItemLocation.Context, 293 | MenuItemLocation.NoteListContextMenu, 294 | MenuItemLocation.EditorContextMenu, 295 | MenuItemLocation.FolderContextMenu, 296 | MenuItemLocation.TagContextMenu, 297 | ].includes(location); 298 | } 299 | 300 | export interface MenuItem { 301 | /** 302 | * Command that should be associated with the menu item. All menu item should 303 | * have a command associated with them unless they are a sub-menu. 304 | */ 305 | commandName?: string; 306 | 307 | /** 308 | * Arguments that should be passed to the command. They will be as rest 309 | * parameters. 310 | */ 311 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied 312 | commandArgs?: any[]; 313 | 314 | /** 315 | * Set to "separator" to create a divider line 316 | */ 317 | type?: ('normal' | 'separator' | 'submenu' | 'checkbox' | 'radio'); 318 | 319 | /** 320 | * Accelerator associated with the menu item 321 | */ 322 | accelerator?: string; 323 | 324 | /** 325 | * Menu items that should appear below this menu item. Allows creating a menu tree. 326 | */ 327 | submenu?: MenuItem[]; 328 | 329 | /** 330 | * Menu item label. If not specified, the command label will be used instead. 331 | */ 332 | label?: string; 333 | } 334 | 335 | // ================================================================= 336 | // View API types 337 | // ================================================================= 338 | 339 | export interface ButtonSpec { 340 | id: ButtonId; 341 | title?: string; 342 | onClick?(): void; 343 | } 344 | 345 | export type ButtonId = string; 346 | 347 | export enum ToolbarButtonLocation { 348 | /** 349 | * This toolbar in the top right corner of the application. It applies to the note as a whole, including its metadata. 350 | * 351 | * desktop 352 | */ 353 | NoteToolbar = 'noteToolbar', 354 | 355 | /** 356 | * This toolbar is right above the text editor. It applies to the note body only. 357 | */ 358 | EditorToolbar = 'editorToolbar', 359 | } 360 | 361 | export type ViewHandle = string; 362 | 363 | export interface EditorCommand { 364 | name: string; 365 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied 366 | value?: any; 367 | } 368 | 369 | export interface DialogResult { 370 | id: ButtonId; 371 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied 372 | formData?: any; 373 | } 374 | 375 | export interface Size { 376 | width?: number; 377 | height?: number; 378 | } 379 | 380 | export interface Rectangle { 381 | x?: number; 382 | y?: number; 383 | width?: number; 384 | height?: number; 385 | } 386 | 387 | export type ActivationCheckCallback = ()=> Promise; 388 | 389 | export type UpdateCallback = ()=> Promise; 390 | 391 | export type VisibleHandler = ()=> Promise; 392 | 393 | export interface EditContextMenuFilterObject { 394 | items: MenuItem[]; 395 | } 396 | 397 | export interface EditorActivationCheckFilterObject { 398 | activatedEditors: { 399 | pluginId: string; 400 | viewId: string; 401 | isActive: boolean; 402 | }[]; 403 | } 404 | 405 | export type FilterHandler = (object: T)=> Promise; 406 | 407 | // ================================================================= 408 | // Settings types 409 | // ================================================================= 410 | 411 | export enum SettingItemType { 412 | Int = 1, 413 | String = 2, 414 | Bool = 3, 415 | Array = 4, 416 | Object = 5, 417 | Button = 6, 418 | } 419 | 420 | export enum SettingItemSubType { 421 | FilePathAndArgs = 'file_path_and_args', 422 | FilePath = 'file_path', // Not supported on mobile! 423 | DirectoryPath = 'directory_path', // Not supported on mobile! 424 | } 425 | 426 | export enum AppType { 427 | Desktop = 'desktop', 428 | Mobile = 'mobile', 429 | Cli = 'cli', 430 | } 431 | 432 | export enum SettingStorage { 433 | Database = 1, 434 | File = 2, 435 | } 436 | 437 | // Redefine a simplified interface to mask internal details 438 | // and to remove function calls as they would have to be async. 439 | export interface SettingItem { 440 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied 441 | value: any; 442 | type: SettingItemType; 443 | 444 | /** 445 | * Currently only used to display a file or directory selector. Always set 446 | * `type` to `SettingItemType.String` when using this property. 447 | */ 448 | subType?: SettingItemSubType; 449 | 450 | label: string; 451 | description?: string; 452 | 453 | /** 454 | * A public setting will appear in the Configuration screen and will be 455 | * modifiable by the user. A private setting however will not appear there, 456 | * and can only be changed programmatically. You may use this to store some 457 | * values that you do not want to directly expose. 458 | */ 459 | public: boolean; 460 | 461 | /** 462 | * You would usually set this to a section you would have created 463 | * specifically for the plugin. 464 | */ 465 | section?: string; 466 | 467 | /** 468 | * To create a setting with multiple options, set this property to `true`. 469 | * That setting will render as a dropdown list in the configuration screen. 470 | */ 471 | isEnum?: boolean; 472 | 473 | /** 474 | * This property is required when `isEnum` is `true`. In which case, it 475 | * should contain a map of value => label. 476 | */ 477 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied 478 | options?: Record; 479 | 480 | /** 481 | * Reserved property. Not used at the moment. 482 | */ 483 | appTypes?: AppType[]; 484 | 485 | /** 486 | * Set this to `true` to store secure data, such as passwords. Any such 487 | * setting will be stored in the system keychain if one is available. 488 | */ 489 | secure?: boolean; 490 | 491 | /** 492 | * An advanced setting will be moved under the "Advanced" button in the 493 | * config screen. 494 | */ 495 | advanced?: boolean; 496 | 497 | /** 498 | * Set the min, max and step values if you want to restrict an int setting 499 | * to a particular range. 500 | */ 501 | minimum?: number; 502 | maximum?: number; 503 | step?: number; 504 | 505 | /** 506 | * Either store the setting in the database or in settings.json. Defaults to database. 507 | */ 508 | storage?: SettingStorage; 509 | } 510 | 511 | export interface SettingSection { 512 | label: string; 513 | iconName?: string; 514 | description?: string; 515 | name?: string; 516 | } 517 | 518 | // ================================================================= 519 | // Data API types 520 | // ================================================================= 521 | 522 | /** 523 | * An array of at least one element and at most three elements. 524 | * 525 | * - **[0]**: Resource name (eg. "notes", "folders", "tags", etc.) 526 | * - **[1]**: (Optional) Resource ID. 527 | * - **[2]**: (Optional) Resource link. 528 | */ 529 | export type Path = string[]; 530 | 531 | // ================================================================= 532 | // Content Script types 533 | // ================================================================= 534 | 535 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied 536 | export type PostMessageHandler = (message: any)=> Promise; 537 | 538 | /** 539 | * When a content script is initialised, it receives a `context` object. 540 | */ 541 | export interface ContentScriptContext { 542 | /** 543 | * The plugin ID that registered this content script 544 | */ 545 | pluginId: string; 546 | 547 | /** 548 | * The content script ID, which may be necessary to post messages 549 | */ 550 | contentScriptId: string; 551 | 552 | /** 553 | * Can be used by CodeMirror content scripts to post a message to the plugin 554 | */ 555 | postMessage: PostMessageHandler; 556 | } 557 | 558 | export interface ContentScriptModuleLoadedEvent { 559 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied 560 | userData?: any; 561 | } 562 | 563 | export interface ContentScriptModule { 564 | onLoaded?: (event: ContentScriptModuleLoadedEvent)=> void; 565 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied 566 | plugin: ()=> any; 567 | assets?: ()=> void; 568 | } 569 | 570 | export interface MarkdownItContentScriptModule extends Omit { 571 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied 572 | plugin: (markdownIt: any, options: any)=> any; 573 | } 574 | 575 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied 576 | type EditorCommandCallback = (...args: any[])=> any; 577 | 578 | export interface CodeMirrorControl { 579 | /** Points to a CodeMirror 6 EditorView instance. */ 580 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied 581 | editor: any; 582 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied 583 | cm6: any; 584 | 585 | /** `extension` should be a [CodeMirror 6 extension](https://codemirror.net/docs/ref/#state.Extension). */ 586 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied 587 | addExtension(extension: any|any[]): void; 588 | 589 | supportsCommand(name: string): boolean; 590 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied 591 | execCommand(name: string, ...args: any[]): any; 592 | registerCommand(name: string, callback: EditorCommandCallback): void; 593 | 594 | joplinExtensions: { 595 | /** 596 | * Returns a [CodeMirror 6 extension](https://codemirror.net/docs/ref/#state.Extension) that 597 | * registers the given [CompletionSource](https://codemirror.net/docs/ref/#autocomplete.CompletionSource). 598 | * 599 | * Use this extension rather than the built-in CodeMirror [`autocompletion`](https://codemirror.net/docs/ref/#autocomplete.autocompletion) 600 | * if you don't want to use [languageData-based autocompletion](https://codemirror.net/docs/ref/#autocomplete.autocompletion^config.override). 601 | * 602 | * Using `autocompletion({ override: [ ... ]})` causes errors when done by multiple plugins. 603 | */ 604 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied 605 | completionSource(completionSource: any): any; 606 | 607 | /** 608 | * Creates an extension that enables or disables [`languageData`-based autocompletion](https://codemirror.net/docs/ref/#autocomplete.autocompletion^config.override). 609 | */ 610 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied 611 | enableLanguageDataAutocomplete: { of: (enabled: boolean)=> any }; 612 | }; 613 | } 614 | 615 | export interface MarkdownEditorContentScriptModule extends Omit { 616 | plugin: (editorControl: CodeMirrorControl)=> void; 617 | } 618 | 619 | export enum ContentScriptType { 620 | /** 621 | * Registers a new Markdown-It plugin, which should follow the template 622 | * below. 623 | * 624 | * ```javascript 625 | * module.exports = { 626 | * default: function(context) { 627 | * return { 628 | * plugin: function(markdownIt, pluginOptions) { 629 | * // ... 630 | * }, 631 | * assets: { 632 | * // ... 633 | * }, 634 | * } 635 | * } 636 | * } 637 | * ``` 638 | * 639 | * See [the 640 | * demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script) 641 | * for a simple Markdown-it plugin example. 642 | * 643 | * ## Exported members 644 | * 645 | * - The `context` argument is currently unused but could be used later on 646 | * to provide access to your own plugin so that the content script and 647 | * plugin can communicate. 648 | * 649 | * - The **required** `plugin` key is the actual Markdown-It plugin - check 650 | * the [official doc](https://github.com/markdown-it/markdown-it) for more 651 | * information. 652 | * 653 | * - Using the **optional** `assets` key you may specify assets such as JS 654 | * or CSS that should be loaded in the rendered HTML document. Check for 655 | * example the Joplin [Mermaid 656 | * plugin](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml/rules/mermaid.ts) 657 | * to see how the data should be structured. 658 | * 659 | * ## Supporting the Rich Text Editor 660 | * 661 | * Joplin's Rich Text Editor works with rendered HTML, which is converted back 662 | * to markdown when saving. To prevent the original markdown for your plugin from 663 | * being lost, Joplin needs additional metadata. 664 | * 665 | * To provide this, 666 | * 1. Wrap the HTML generated by your plugin in an element with class `joplin-editable`. 667 | * For example, 668 | * ```html 669 | *
670 | * ...your html... 671 | *
672 | * ``` 673 | * 2. Add a child with class `joplin-source` that contains the original markdown that 674 | * was rendered by your plugin. Include `data-joplin-source-open`, `data-joplin-source-close`, 675 | * and `data-joplin-language` attributes. 676 | * For example, if your plugin rendered the following code block, 677 | * ```` 678 | * ```foo 679 | * ... original source here ... 680 | * ``` 681 | * ```` 682 | * then it should render to 683 | * ```html 684 | *
685 | *
 ... original source here ... 
691 | * ... rendered HTML here ... 692 | *
693 | * ``` 694 | * 695 | * See [the demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script) 696 | * for a complete example. 697 | * 698 | * ## Getting the settings from the renderer 699 | * 700 | * You can access your plugin settings from the renderer by calling 701 | * `pluginOptions.settingValue("your-setting-key')`. 702 | * 703 | * ## Posting messages from the content script to your plugin 704 | * 705 | * The application provides the following function to allow executing 706 | * commands from the rendered HTML code: 707 | * 708 | * ```javascript 709 | * const response = await webviewApi.postMessage(contentScriptId, message); 710 | * ``` 711 | * 712 | * - `contentScriptId` is the ID you've defined when you registered the 713 | * content script. You can retrieve it from the 714 | * {@link ContentScriptContext | context}. 715 | * - `message` can be any basic JavaScript type (number, string, plain 716 | * object), but it cannot be a function or class instance. 717 | * 718 | * When you post a message, the plugin can send back a `response` thus 719 | * allowing two-way communication: 720 | * 721 | * ```javascript 722 | * await joplin.contentScripts.onMessage(contentScriptId, (message) => { 723 | * // Process message 724 | * return response; // Can be any object, string or number 725 | * }); 726 | * ``` 727 | * 728 | * See {@link JoplinContentScripts.onMessage} for more details, as well as 729 | * the [postMessage 730 | * demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages). 731 | * 732 | * ## Registering an existing Markdown-it plugin 733 | * 734 | * To include a regular Markdown-It plugin, that doesn't make use of any 735 | * Joplin-specific features, you would simply create a file such as this: 736 | * 737 | * ```javascript 738 | * module.exports = { 739 | * default: function(context) { 740 | * return { 741 | * plugin: require('markdown-it-toc-done-right'); 742 | * } 743 | * } 744 | * } 745 | * ``` 746 | */ 747 | MarkdownItPlugin = 'markdownItPlugin', 748 | 749 | /** 750 | * Registers a new CodeMirror plugin, which should follow the template 751 | * below. 752 | * 753 | * ```javascript 754 | * module.exports = { 755 | * default: function(context) { 756 | * return { 757 | * plugin: function(CodeMirror) { 758 | * // ... 759 | * }, 760 | * codeMirrorResources: [], 761 | * codeMirrorOptions: { 762 | * // ... 763 | * }, 764 | * assets: { 765 | * // ... 766 | * }, 767 | * } 768 | * } 769 | * } 770 | * ``` 771 | * 772 | * - The `context` argument allows communicating with other parts of 773 | * your plugin (see below). 774 | * 775 | * - The `plugin` key is your CodeMirror plugin. This is where you can 776 | * register new commands with CodeMirror or interact with the CodeMirror 777 | * instance as needed. 778 | * 779 | * - **CodeMirror 5 only**: The `codeMirrorResources` key is an array of CodeMirror resources that 780 | * will be loaded and attached to the CodeMirror module. These are made up 781 | * of addons, keymaps, and modes. For example, for a plugin that want's to 782 | * enable clojure highlighting in code blocks. `codeMirrorResources` would 783 | * be set to `['mode/clojure/clojure']`. 784 | * This field is ignored on mobile and when the desktop beta editor is enabled. 785 | * 786 | * - **CodeMirror 5 only**: The `codeMirrorOptions` key contains all the 787 | * [CodeMirror](https://codemirror.net/doc/manual.html#config) options 788 | * that will be set or changed by this plugin. New options can alse be 789 | * declared via 790 | * [`CodeMirror.defineOption`](https://codemirror.net/doc/manual.html#defineOption), 791 | * and then have their value set here. For example, a plugin that enables 792 | * line numbers would set `codeMirrorOptions` to `{'lineNumbers': true}`. 793 | * 794 | * - Using the **optional** `assets` key you may specify **only** CSS assets 795 | * that should be loaded in the rendered HTML document. Check for example 796 | * the Joplin [Mermaid 797 | * plugin](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml/rules/mermaid.ts) 798 | * to see how the data should be structured. 799 | * 800 | * One of the `plugin`, `codeMirrorResources`, or `codeMirrorOptions` keys 801 | * must be provided for the plugin to be valid. Having multiple or all 802 | * provided is also okay. 803 | * 804 | * See also: 805 | * - The [demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/codemirror_content_script) 806 | * for an example of all these keys being used in one plugin. 807 | * - See [the editor plugin tutorial](https://joplinapp.org/help/api/tutorials/cm6_plugin) 808 | * for how to develop a plugin for the mobile editor and the desktop beta markdown editor. 809 | * 810 | * ## Posting messages from the content script to your plugin 811 | * 812 | * In order to post messages to the plugin, you can use the postMessage 813 | * function passed to the {@link ContentScriptContext | context}. 814 | * 815 | * ```javascript 816 | * const response = await context.postMessage('messageFromCodeMirrorContentScript'); 817 | * ``` 818 | * 819 | * When you post a message, the plugin can send back a `response` thus 820 | * allowing two-way communication: 821 | * 822 | * ```javascript 823 | * await joplin.contentScripts.onMessage(contentScriptId, (message) => { 824 | * // Process message 825 | * return response; // Can be any object, string or number 826 | * }); 827 | * ``` 828 | * 829 | * See {@link JoplinContentScripts.onMessage} for more details, as well as 830 | * the [postMessage 831 | * demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages). 832 | * 833 | */ 834 | CodeMirrorPlugin = 'codeMirrorPlugin', 835 | } 836 | --------------------------------------------------------------------------------