From a323692ebd9b1df97acf38fd7ff68da3c84c3fbe Mon Sep 17 00:00:00 2001 From: benji300 <54955848+benji300@users.noreply.github.com> Date: Fri, 5 Feb 2021 08:25:26 +0100 Subject: [PATCH] Release v1.2.0 (#33) * Prepare version v1.2.0 * Merge latest changes from favorites plugin * Update to latest plugin generator version * Fixed breadcrumbs + Add option to specify min width of breadcrumb * Add command to toggle panel visibility (#16) * Improved scrollbar style * Merge file structure with latest one from favorites plugin * Improve reading and updating values from plugin settings (#24) * Improve overflow behavior for tabs and breadcrumbs * Add option to specify font size for the panel * Improve handling of click events * Add option to specify hover background + improve drag&drop implementation * Add support to drag notes from note list to tabs (#31) * Add support to drag&drop favorite notes onto tabs and vice versa * Add option to display navigation buttons below the tabs * Release v1.2.0 --- CHANGELOG.md | 17 ++ README.md | 133 +++++---- api/Joplin.d.ts | 3 + api/JoplinContentScripts.d.ts | 40 +++ api/JoplinPlugins.d.ts | 23 +- api/JoplinSettings.d.ts | 14 + api/JoplinViewsPanels.d.ts | 16 ++ api/types.ts | 151 +++++++--- package-lock.json | 191 ++++++++++--- package.json | 9 +- src/helpers.ts | 238 ---------------- src/index.ts | 509 ++++++---------------------------- src/lastActiveNote.ts | 34 +++ src/manifest.json | 15 +- src/noteTabs.ts | 163 +++++++++++ src/panel.ts | 257 +++++++++++++++++ src/settings.ts | 393 ++++++++++++++++++++++++++ src/webview.css | 56 ++-- src/webview.js | 181 ++++++------ webpack.config.js | 78 ++++-- 20 files changed, 1555 insertions(+), 966 deletions(-) create mode 100644 api/JoplinContentScripts.d.ts delete mode 100644 src/helpers.ts create mode 100644 src/lastActiveNote.ts create mode 100644 src/noteTabs.ts create mode 100644 src/panel.ts create mode 100644 src/settings.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 997a4ce..12b7c65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - None +## [1.2.0] - 2021-02-05 + +### Added + +- User option to specify minimum width for a single breadcrumb entry ([#30](https://github.com/benji300/joplin-note-tabs/issues/30)) +- User option to specify the font size for the panel +- User option to specify the background color when hovering over tabs +- User option to display navigation buttons (backward/forward) below the tabs +- Command to toggle panel visibility ([#16](https://github.com/benji300/joplin-note-tabs/issues/16)) +- Support to drag & drop notes from note list to pin as tabs ([#31](https://github.com/benji300/joplin-note-tabs/issues/31)) +- Support to drag & drop notes from [favorites plugin](https://github.com/benji300/joplin-favorites) to pin as tabs + +### Changed + +- Breadcrumbs scroll independently from tabs ([#29](https://github.com/benji300/joplin-note-tabs/issues/29)) +- Improved reading and updating values from plugin settings ([#24](https://github.com/benji300/joplin-note-tabs/issues/24)) + ## [1.1.1] - 2021-01-12 ### Changed diff --git a/README.md b/README.md index 157bfeb..881822f 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,22 @@ # Joplin Note Tabs -Joplin Note Tabs is a plugin to extend the UX and UI of [Joplin's](https://joplinapp.org/) desktop application. +Note Tabs is a plugin to extend the UX and UI of [Joplin's](https://joplinapp.org/) desktop application. It allows to open several notes at once in tabs and pin them to be kept open. -> :warning: **CAUTION** - Requires Joplin **v1.5.7** or newer +> :warning: **CAUTION** - Requires Joplin **v1.6.7** or newer ## Table of contents - [Features](#features) - [Screenshots](#screenshots) -- [Commands](#Commands) +- [Installation](#installation) +- [Usage](#usage) + - [Place the panel](#place-the-panel) +- [Commands](#commands) + - [Keyboard shortcuts](#keyboard-shortcuts) - [User options](#user-options) - [UI Tweaks](#ui-tweaks) -- [Installation](#installation) -- [Uninstallation](#uninstallation) - [Feedback](#feedback) - [Support](#support) - [Development](#development) @@ -24,15 +26,18 @@ It allows to open several notes at once in tabs and pin them to be kept open. ## Features - Display selected note as tab -- Display full breadcrumbs for selected note below tabs +- Additional display options below the tabs + - Full breadcrumbs for selected note + - Navigation buttons (`historyBackward/Forward`) - Pin note(s) to the tabs + - Either via command or drag & drop from the note list - Save pinned tabs permanently - Stored in database (not synced with other devices!) - Remember last opened and unpinned note - Change position of tabs within the panel - Either via drag & drop or keyboard shortcuts (which have been assigned to the corresponding commands) -- Toggle to-do state from tabs - - Automatically unpin completed to-dos ([configurable](#user-options)) +- Toggle to-do state directly on the tabs + - Optionally unpin completed to-dos automatically - [Configurable](#user-options) style attributes - Support horizontal and vertical layout @@ -56,24 +61,64 @@ It allows to open several notes at once in tabs and pin them to be kept open. > **NOTE** - The used UI theme on this screenshot can be downloaded [here](https://github.com/benji300/joplin-milford-ui). +## Installation + +### Automatic (Joplin v1.6.4 and newer) + +- Open Joplin and navigate to `Tools > Options > Plugins` +- Search for `tabs` and press install +- Restart Joplin to enable the plugin +- By default the panel will appear on the right side of the screen, see how to [place the panel](#place-the-panel) + +### Manual + +- Download the latest released JPL package (`*.jpl`) from [here](https://github.com/benji300/joplin-note-tabs/releases) +- Open Joplin and navigate to `Tools > Options > Plugins` +- Press `Install plugin` and select the previously downloaded `jpl` file +- Confirm selection +- Restart Joplin to enable the plugin +- By default the panel will appear on the right side of the screen, see how to [place the panel](#place-the-panel) + +### Uninstall + +- Open Joplin and navigate to `Tools > Options > Plugins` +- Search for the `Note Tabs` plugin +- Press `Delete` to remove the plugin completely + - Alternatively you can also disable the plugin by clicking on the toggle button +- Restart Joplin + +## Usage + +### Place the panel + +By default the panel will be on the right side of the screen, this can be adjusted by: + +- `View > Change application layout` +- Use the arrow keys (the displayed ones, not keyboard keys) to move the panel at the desired position +- Move the splitter to reach the desired height/width of the panel + - As soon as the width of the panel goes below `400px`, it automatically switches from horizontal to vertical layout +- Press `ESC` to save the layout and return to normal mode + ## Commands This plugin provides additional commands as described in the following table. -| Command Label | Command ID | Description | Menu contexts | -| ------------------------------- | ---------------------- | --------------------------------------------------------------------------------------------- | ----------------------------------------------- | -| Tabs: Pin note | `tabsPinNote` | Pin selected note(s) to the tabs. | `Tools>Tabs`, `NoteListContext`,`EditorContext` | -| Tabs: Unpin note | `tabsUnpinNote` | Unpin selected note(s) from the tabs. | `Tools>Tabs` | -| Tabs: Switch to last active tab | `tabsSwitchLastActive` | Switch to the last active tab, i.e. to previous selected note. | `Tools>Tabs` | -| Tabs: Switch to left tab | `tabsSwitchLeft` | Switch to the left tab next to the active, i.e. select the left note. | `Tools>Tabs` | -| Tabs: Switch to right tab | `tabsSwitchRight` | Switch to the right tab next to the active, i.e. select the right note. | `Tools>Tabs` | -| Tabs: Move tab left | `tabsMoveLeft` | Move active tab one position to the left. | `Tools>Tabs` | -| Tabs: Move tab right | `tabsMoveRight` | Move active tab one position to the right. | `Tools>Tabs` | -| Tabs: Remove all pinned tabs | `tabsClear` | Remove all pinned tabs. In case no note is selected, the tabs list might be empty afterwards. | `Tools>Tabs` | +| Command Label | Command ID | Description | Menu contexts | +| ------------------------------- | ---------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | +| Tabs: Pin note | `tabsPinNote` | Pin selected note(s) to the tabs. | `Tools>Tabs`, `NoteListContext`, `EditorContext`, `Command palette` | +| Tabs: Unpin note | `tabsUnpinNote` | Unpin selected note(s) from the tabs. | `Tools>Tabs`, `Command palette` | +| Tabs: Switch to last active tab | `tabsSwitchLastActive` | Switch to the last active tab, i.e. to previous selected note. | `Tools>Tabs`, `Command palette` | +| Tabs: Switch to left tab | `tabsSwitchLeft` | Switch to the left tab next to the active, i.e. select the left note. | `Tools>Tabs`, `Command palette` | +| Tabs: Switch to right tab | `tabsSwitchRight` | Switch to the right tab next to the active, i.e. select the right note. | `Tools>Tabs`, `Command palette` | +| Tabs: Move tab left | `tabsMoveLeft` | Move active tab one position to the left. | `Tools>Tabs`, `Command palette` | +| Tabs: Move tab right | `tabsMoveRight` | Move active tab one position to the right. | `Tools>Tabs`, `Command palette` | +| Tabs: Remove all pinned tabs | `tabsClear` | Remove all pinned tabs. In case no note is selected, the tabs list might be empty afterwards. | `Tools>Tabs`, `Command palette` | +| Tabs: Toggle visibility | `tabsToggleVisibility` | Toggle panel visibility. | `Tools>Tabs`, `Command palette` | -> **NOTE** - Keyboard shortcuts can be assigned in user options via `Tools > Options > Keyboard Shortcuts`. Search for the command label where shortcuts shall be added. +### Keyboard shortcuts -> **NOTE** - All commands can also be accessed via the `Command palette`. +Keyboard shortcuts can be assigned in user options via `Tools > Options > Keyboard Shortcuts` to all [commands](#commands) which are assigned to the `Tools>Favorites` menu context. +In the keyboard shortcut editor, search for the command label where shortcuts shall be added. ## User options @@ -107,40 +152,6 @@ Follow these steps to hide it via the `userchrome.css` stylesheet: > **NOTE** - Since there is currently no unique attribute for the breadcrumbs, this can only be done using the workaround from above. However, the behavior may change with each version and it may happen that it no longer works. -## Installation - -### Joplin v1.6.4 and newer - -- Open Joplin and navigate to `Tools > Options > Plugins` -- Search for `tabs` and press install -- Restart Joplin to enable the plugin - -### Joplin v1.6.2 and previous - -- Download the latest released JPL package (`joplin.plugin.note.tabs.jpl`) from [here](https://github.com/benji300/joplin-note-tabs/releases) -- Open Joplin and navigate to `Tools > Options > Plugins` -- Press `Install plugin` and select the previously downloaded `jpl` file -- Confirm selection -- Restart Joplin to enable the plugin - -### Place tabs - -By default the tabs will be on the right side of the screen, this can be adjusted by: - -- `View > Change application layout` -- Use the arrow keys (the displayed ones, not keyboard keys) to move the panel at the desired position -- Move the splitter (between content and tabs panel) up to reach the desired height of the panel -- Press `ESC` to save the layout and return to normal mode - -## Uninstallation - -- Open Joplin -- Navigate to `Tools > Options > Plugins` -- Search for the `Note Tabs` plugin -- Press `Delete` to remove the plugin from the user profile directory - - Alternatively you can also disable the plugin by clicking on the toggle button -- Restart Joplin - ## Feedback - :question: Need help? @@ -152,19 +163,21 @@ By default the tabs will be on the right side of the screen, this can be adjuste ## Support -You like this plugin as much as I do and it helps you in your daily work with Joplin? +You like this plugin as much as I do and it improves your daily work with Joplin? -Then I would be very happy if you would buy me a beer via [PayPal](https://www.paypal.com/donate?hosted_button_id=6FHDGK3PTNU22) :wink::beer: +Then I would be very happy if you buy me a beer via [PayPal](https://www.paypal.com/donate?hosted_button_id=6FHDGK3PTNU22) :wink::beer: ## Development +The npm package of the plugin can be found [here](https://www.npmjs.com/package/joplin-plugin-note-tabs). + ### Building the plugin -If you want to build the plugin by your own simply run: +If you want to build the plugin by your own simply run `npm run dist`. -``` -npm run dist -``` +### Updating the plugin framework + +To update the plugin framework, run `npm run update`. ## Changes diff --git a/api/Joplin.d.ts b/api/Joplin.d.ts index fc09aee..2d5ee2c 100644 --- a/api/Joplin.d.ts +++ b/api/Joplin.d.ts @@ -7,6 +7,7 @@ import JoplinCommands from './JoplinCommands'; import JoplinViews from './JoplinViews'; import JoplinInterop from './JoplinInterop'; import JoplinSettings from './JoplinSettings'; +import JoplinContentScripts from './JoplinContentScripts'; /** * This is the main entry point to the Joplin API. You can access various services using the provided accessors. * @@ -31,10 +32,12 @@ export default class Joplin { private views_; private interop_; private settings_; + private contentScripts_; constructor(implementation: any, plugin: Plugin, store: any); get data(): JoplinData; get plugins(): JoplinPlugins; get workspace(): JoplinWorkspace; + get contentScripts(): JoplinContentScripts; /** * @ignore * diff --git a/api/JoplinContentScripts.d.ts b/api/JoplinContentScripts.d.ts new file mode 100644 index 0000000..145e9d1 --- /dev/null +++ b/api/JoplinContentScripts.d.ts @@ -0,0 +1,40 @@ +import Plugin from '../Plugin'; +import { ContentScriptType } from './types'; +export default class JoplinContentScripts { + private plugin; + constructor(plugin: Plugin); + /** + * Registers a new content script. Unlike regular plugin code, which runs in + * a separate process, content scripts run within the main process code and + * thus allow improved performances and more customisations in specific + * cases. It can be used for example to load a Markdown or editor plugin. + * + * Note that registering a content script in itself will do nothing - it + * will only be loaded in specific cases by the relevant app modules (eg. + * the Markdown renderer or the code editor). So it is not a way to inject + * and run arbitrary code in the app, which for safety and performance + * reasons is not supported. + * + * The plugin generator provides a way to build any content script you might + * want to package as well as its dependencies. See the [Plugin Generator + * doc](https://github.com/laurent22/joplin/blob/dev/packages/generator-joplin/README.md) + * for more information. + * + * * [View the renderer demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script) + * * [View the editor demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/codemirror_content_script) + * + * See also the [postMessage demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages) + * + * @param type Defines how the script will be used. See the type definition for more information about each supported type. + * @param id A unique ID for the content script. + * @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`. + */ + register(type: ContentScriptType, id: string, scriptPath: string): Promise; + /** + * Listens to a messages sent from the content script using postMessage(). + * See {@link ContentScriptType} for more information as well as the + * [postMessage + * demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages) + */ + onMessage(contentScriptId: string, callback: any): Promise; +} diff --git a/api/JoplinPlugins.d.ts b/api/JoplinPlugins.d.ts index 72cbe07..56e7e8d 100644 --- a/api/JoplinPlugins.d.ts +++ b/api/JoplinPlugins.d.ts @@ -20,28 +20,7 @@ export default class JoplinPlugins { */ register(script: Script): Promise; /** - * Registers a new content script. Unlike regular plugin code, which runs in - * a separate process, content scripts run within the main process code and - * thus allow improved performances and more customisations in specific - * cases. It can be used for example to load a Markdown or editor plugin. - * - * Note that registering a content script in itself will do nothing - it - * will only be loaded in specific cases by the relevant app modules (eg. - * the Markdown renderer or the code editor). So it is not a way to inject - * and run arbitrary code in the app, which for safety and performance - * reasons is not supported. - * - * The plugin generator provides a way to build any content script you might - * want to package as well as its dependencies. See the [Plugin Generator - * doc](https://github.com/laurent22/joplin/blob/dev/packages/generator-joplin/README.md) - * for more information. - * - * * [View the renderer demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script) - * * [View the editor demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/codemirror_content_script) - * - * @param type Defines how the script will be used. See the type definition for more information about each supported type. - * @param id A unique ID for the content script. - * @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`. + * @deprecated Use joplin.contentScripts.register() */ registerContentScript(type: ContentScriptType, id: string, scriptPath: string): Promise; } diff --git a/api/JoplinSettings.d.ts b/api/JoplinSettings.d.ts index d725ea4..82e4ab3 100644 --- a/api/JoplinSettings.d.ts +++ b/api/JoplinSettings.d.ts @@ -1,5 +1,12 @@ import Plugin from '../Plugin'; import { SettingItem, SettingSection } from './types'; +export interface ChangeEvent { + /** + * Setting keys that have been changed + */ + keys: string[]; +} +export declare type ChangeHandler = (event: ChangeEvent)=> void; /** * 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,6 +19,7 @@ import { SettingItem, SettingSection } from './types'; export default class JoplinSettings { private plugin_; constructor(plugin: Plugin); + private get keyPrefix(); private namespacedKey; /** * Registers a new setting. Note that registering a setting item is dynamic and will be gone next time Joplin starts. @@ -40,4 +48,10 @@ export default class JoplinSettings { * https://github.com/laurent22/joplin/blob/dev/packages/lib/models/Setting.ts#L142 */ globalValue(key: string): Promise; + /** + * Called when one or multiple settings of your plugin have been changed. + * - For performance reasons, this event is triggered with a delay. + * - You will only get events for your own plugin settings. + */ + onChange(handler: ChangeHandler): Promise; } diff --git a/api/JoplinViewsPanels.d.ts b/api/JoplinViewsPanels.d.ts index 956ddb7..20d3e12 100644 --- a/api/JoplinViewsPanels.d.ts +++ b/api/JoplinViewsPanels.d.ts @@ -28,6 +28,22 @@ export default class JoplinViewsPanels { addScript(handle: ViewHandle, scriptPath: string): Promise; /** * Called when a message is sent from the webview (using postMessage). + * + * To post a message from the webview to the plugin use: + * + * ```javascript + * const response = await webviewApi.postMessage(message); + * ``` + * + * - `message` can be any JavaScript object, string or number + * - `response` is whatever was returned by the `onMessage` handler + * + * Using this mechanism, you can have two-way communication between the + * plugin and webview. + * + * See the [postMessage + * demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages) for more details. + * */ onMessage(handle: ViewHandle, callback: Function): Promise; /** diff --git a/api/types.ts b/api/types.ts index d14c63b..c8110fb 100644 --- a/api/types.ts +++ b/api/types.ts @@ -366,9 +366,31 @@ export interface SettingSection { export type Path = string[]; // ================================================================= -// Plugins type +// Content Script types // ================================================================= +export type PostMessageHandler = (id: string, message: any)=> Promise; + +/** + * When a content script is initialised, it receives a `context` object. + */ +export interface ContentScriptContext { + /** + * The plugin ID that registered this content script + */ + pluginId: string; + + /** + * The content script ID, which may be necessary to post messages + */ + contentScriptId: string; + + /** + * Can be used by CodeMirror content scripts to post a message to the plugin + */ + postMessage: PostMessageHandler; +} + export enum ContentScriptType { /** * Registers a new Markdown-It plugin, which should follow the template @@ -394,43 +416,56 @@ export enum ContentScriptType { * * ## Exported members * - * - The `context` argument is currently unused but could be used later - * on to provide access to your own plugin so that the content script - * and plugin can communicate. + * - The `context` argument is currently unused but could be used later on + * to provide access to your own plugin so that the content script and + * plugin can communicate. * - * - The **required** `plugin` key is the actual Markdown-It plugin - - * check the [official - * doc](https://github.com/markdown-it/markdown-it) for more + * - The **required** `plugin` key is the actual Markdown-It plugin - check + * the [official doc](https://github.com/markdown-it/markdown-it) for more * information. The `options` parameter is of type * [RuleOptions](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml.ts), - * which contains a number of options, mostly useful for Joplin's - * internal code. + * which contains a number of options, mostly useful for Joplin's internal + * code. * - * - Using the **optional** `assets` key you may specify assets such as - * JS or CSS that should be loaded in the rendered HTML document. - * Check for example the Joplin [Mermaid + * - Using the **optional** `assets` key you may specify assets such as JS + * or CSS that should be loaded in the rendered HTML document. Check for + * example the Joplin [Mermaid * plugin](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml/rules/mermaid.ts) * to see how the data should be structured. * - * ## Passing messages from the content script to your plugin + * ## Posting messages from the content script to your plugin * * The application provides the following function to allow executing * commands from the rendered HTML code: * - * `webviewApi.executeCommand(commandName, ...args)` + * ```javascript + * const response = await webviewApi.postMessage(contentScriptId, message); + * ``` * - * So you can use this mechanism to pass messages from the note viewer - * to your own plugin. To do so you would define a command, using - * `joplin.commands.register`, then you would call this command using - * the `webviewApi` object. See again [the - * demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script) - * to see how this can be done. + * - `contentScriptId` is the ID you've defined when you registered the + * content script. You can retrieve it from the + * {@link ContentScriptContext | context}. + * - `message` can be any basic JavaScript type (number, string, plain + * object), but it cannot be a function or class instance. + * + * When you post a message, the plugin can send back a `response` thus + * allowing two-way communication: + * + * ```javascript + * await joplin.contentScripts.onMessage(contentScriptId, (message) => { + * // Process message + * return response; // Can be any object, string or number + * }); + * ``` + * + * See {@link JoplinContentScripts.onMessage} for more details, as well as + * the [postMessage + * demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages). * * ## Registering an existing Markdown-it plugin * - * To include a regular Markdown-It plugin, that doesn't make use of - * any Joplin-specific features, you would simply create a file such as - * this: + * To include a regular Markdown-It plugin, that doesn't make use of any + * Joplin-specific features, you would simply create a file such as this: * * ```javascript * module.exports = { @@ -443,6 +478,7 @@ export enum ContentScriptType { * ``` */ MarkdownItPlugin = 'markdownItPlugin', + /** * Registers a new CodeMirror plugin, which should follow the template * below. @@ -466,42 +502,65 @@ export enum ContentScriptType { * } * ``` * - * - The `context` argument is currently unused but could be used later - * on to provide access to your own plugin so that the content script - * and plugin can communicate. + * - The `context` argument is currently unused but could be used later on + * to provide access to your own plugin so that the content script and + * plugin can communicate. * * - The `plugin` key is your CodeMirror plugin. This is where you can - * register new commands with CodeMirror or interact with the - * CodeMirror instance as needed. + * register new commands with CodeMirror or interact with the CodeMirror + * instance as needed. * - * - The `codeMirrorResources` key is an array of CodeMirror resources - * that will be loaded and attached to the CodeMirror module. These - * are made up of addons, keymaps, and modes. For example, for a - * plugin that want's to enable clojure highlighting in code blocks. - * `codeMirrorResources` would be set to `['mode/clojure/clojure']`. + * - The `codeMirrorResources` key is an array of CodeMirror resources that + * will be loaded and attached to the CodeMirror module. These are made up + * of addons, keymaps, and modes. For example, for a plugin that want's to + * enable clojure highlighting in code blocks. `codeMirrorResources` would + * be set to `['mode/clojure/clojure']`. * * - The `codeMirrorOptions` key contains all the - * [CodeMirror](https://codemirror.net/doc/manual.html#config) - * options that will be set or changed by this plugin. New options - * can alse be declared via + * [CodeMirror](https://codemirror.net/doc/manual.html#config) options + * that will be set or changed by this plugin. New options can alse be + * declared via * [`CodeMirror.defineOption`](https://codemirror.net/doc/manual.html#defineOption), - * and then have their value set here. For example, a plugin that - * enables line numbers would set `codeMirrorOptions` to - * `{'lineNumbers': true}`. + * and then have their value set here. For example, a plugin that enables + * line numbers would set `codeMirrorOptions` to `{'lineNumbers': true}`. * - * - Using the **optional** `assets` key you may specify **only** CSS - * assets that should be loaded in the rendered HTML document. Check - * for example the Joplin [Mermaid + * - Using the **optional** `assets` key you may specify **only** CSS assets + * that should be loaded in the rendered HTML document. Check for example + * the Joplin [Mermaid * plugin](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml/rules/mermaid.ts) * to see how the data should be structured. * - * One of the `plugin`, `codeMirrorResources`, or `codeMirrorOptions` - * keys must be provided for the plugin to be valid. Having multiple or - * all provided is also okay. + * One of the `plugin`, `codeMirrorResources`, or `codeMirrorOptions` keys + * must be provided for the plugin to be valid. Having multiple or all + * provided is also okay. * - * See the [demo + * See also the [demo * plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/codemirror_content_script) * for an example of all these keys being used in one plugin. + * + * ## Posting messages from the content script to your plugin + * + * In order to post messages to the plugin, you can use the postMessage + * function passed to the {@link ContentScriptContext | context}. + * + * ```javascript + * const response = await context.postMessage('messageFromCodeMirrorContentScript'); + * ``` + * + * When you post a message, the plugin can send back a `response` thus + * allowing two-way communication: + * + * ```javascript + * await joplin.contentScripts.onMessage(contentScriptId, (message) => { + * // Process message + * return response; // Can be any object, string or number + * }); + * ``` + * + * See {@link JoplinContentScripts.onMessage} for more details, as well as + * the [postMessage + * demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages). + * */ CodeMirrorPlugin = 'codeMirrorPlugin', } diff --git a/package-lock.json b/package-lock.json index b8dc7cc..0306353 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "joplin-plugin-note-tabs", - "version": "1.1.1", + "version": "1.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1227,6 +1227,12 @@ "prr": "~1.0.1" } }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -4462,6 +4468,15 @@ } } }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, "json5": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", @@ -4482,6 +4497,40 @@ "json5": "^1.0.1" } }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, "supports-color": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", @@ -4490,6 +4539,24 @@ "requires": { "has-flag": "^3.0.0" } + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } } } }, @@ -4563,64 +4630,114 @@ "dev": true }, "yargs": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", - "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.2" + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" }, "dependencies": { - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "locate-path": "^3.0.0" + "color-convert": "^2.0.1" } }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" } }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "requires": { - "p-try": "^2.0.0" + "color-name": "~1.1.4" } }, - "p-locate": { + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "is-fullwidth-code-point": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", "dev": true, "requires": { - "p-limit": "^2.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" } }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz", + "integrity": "sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg==", + "dev": true + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", "dev": true } } diff --git a/package.json b/package.json index b92b238..654c508 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "joplin-plugin-note-tabs", - "version": "1.1.1", - "description": "Plugin for Joplin which allows to open several notes at once in tabs and pin them.", + "version": "1.2.0", + "description": "Allows to open several notes at once in tabs and pin them.", "author": "Benji300", "homepage": "https://github.com/benji300/joplin-note-tabs", "bugs": { @@ -12,7 +12,7 @@ "url": "git+https://github.com/benji300/joplin-note-tabs.git" }, "scripts": { - "dist": "webpack", + "dist": "webpack --joplin-plugin-config buildMain && webpack --joplin-plugin-config buildExtraScripts && webpack --joplin-plugin-config createArchive", "prepare": "npm run dist", "update": "npm install -g generator-joplin && yo joplin --update" }, @@ -31,6 +31,7 @@ "ts-loader": "^7.0.5", "typescript": "^3.9.3", "webpack": "^4.43.0", - "webpack-cli": "^3.3.11" + "webpack-cli": "^3.3.11", + "yargs": "^16.2.0" } } \ No newline at end of file diff --git a/src/helpers.ts b/src/helpers.ts deleted file mode 100644 index 0726b98..0000000 --- a/src/helpers.ts +++ /dev/null @@ -1,238 +0,0 @@ -import joplin from 'api'; - -/** - * Advanced style setting default values. - * Used when setting is set to 'default'. - */ -export enum SettingDefaults { - Default = 'default', - Font = 'Roboto', - Background = 'var(--joplin-background-color3)', - Foreground = 'var(--joplin-color-faded)', - ActiveBackground = 'var(--joplin-background-color)', - ActiveForeground = 'var(--joplin-color)', - DividerColor = 'var(--joplin-divider-color)' -} - -/** - * Tab type definition. - * Open state is currently not used. - */ -export enum NoteTabType { - Temporary = 1, - Open = 2, - Pinned = 3 -} - -/** - * Storage of note tabs. - */ -export class NoteTabs { - // [ - // { - // "id": "note id", - // "type": NoteTabType - // } - // ] - private _tabs: any[]; - - constructor() { - this._tabs = new Array(); - } - - /** - * Reads the noteTabs settings array. - */ - async read() { - this._tabs = await joplin.settings.value('noteTabs'); - } - - /** - * Writes the temporay tabs store back to the settings array. - */ - private async store() { - await joplin.settings.setValue('noteTabs', this._tabs); - } - - /** - * Inserts handled tab at specified index. - */ - private async insertAtIndex(index: number, tab: any) { - if (index < 0 || tab == null) return; - - this._tabs.splice(index, 0, tab); - await this.store(); - } - - /** - * Replaces tab at specified index with handled one. - */ - private async replaceAtIndex(index: number, tab: any) { - if (index < 0 || tab == null) return; - - this._tabs.splice(index, 1, tab); - await this.store(); - } - - /** - * Gets the number of tabs. - */ - length(): number { - return this._tabs.length; - } - - /** - * Gets all tabs. - */ - getAll(): any[] { - return this._tabs; - } - - /** - * Gets the tab for the handled note. - */ - get(index: number): any { - if (index < 0 || index >= this.length()) return; - - return this._tabs[index]; - } - - /** - * Gets index of tab for note with handled id. -1 if not exist. - */ - indexOf(noteId: string): number { - if (noteId == null) return; - - for (let i: number = 0; i < this.length(); i++) { - if (this._tabs[i]['id'] === noteId) return i; - } - return -1; - } - - /** - * Gets index of the temporary tab. -1 if not exist. - */ - indexOfTemp(): number { - for (let i: number = 0; i < this.length(); i++) { - if (this._tabs[i]['type'] === NoteTabType.Temporary) return i; - } - return -1; - } - - /** - * Gets a value whether the handled note has already a tab or not. - */ - hasTab(noteId: string): boolean { - if (noteId == null) return; - - return this.indexOf(noteId) < 0 ? false : true; - } - - /** - * Adds note as new tab at the end. - */ - async add(noteId: string, noteType: NoteTabType) { - if (noteId == null) return; - - this._tabs.push({ id: noteId, type: noteType }); - await this.store(); - } - - /** - * Moves the tab on source index to the target index. - */ - async moveWithIndex(sourceIdx: number, targetIdx: number) { - if (sourceIdx < 0 || sourceIdx >= this.length()) return; - if (targetIdx < 0 || targetIdx >= this.length()) return; - - // console.log(`moveWithIndex: ${sourceIdx} to ${targetIdx} with length = ${this.length()}`); - - const tab: any = this._tabs[sourceIdx]; - await this.delete(this.get(sourceIdx).id); - await this.insertAtIndex((targetIdx == 0 ? 0 : targetIdx), tab); - await this.store(); - } - - /** - * Moves the tab of source note to the index of the target note. - */ - async moveWithId(sourceId: string, targetId: string) { - if (targetId == null || sourceId == null) return; - - await this.moveWithIndex(this.indexOf(sourceId), this.indexOf(targetId)); - } - - /** - * Changes type of the tab for the handled note. - */ - async changeType(noteId: string, newType: NoteTabType) { - if (!this.hasTab(noteId)) return; - - this._tabs[this.indexOf(noteId)].type = newType; - await this.store(); - } - - /** - * Replaces tab at specified index with handled one. - */ - async replaceTemp(noteId: string) { - if (noteId == null) return; - - const tempIdx: number = this.indexOfTemp(); - if (tempIdx >= 0) { - this._tabs[tempIdx].id = noteId; - await this.store(); - } - } - - /** - * Removes tab on handled index. - */ - async delete(noteId: string) { - if (noteId == null) return; - - if (this.hasTab(noteId)) { - this._tabs.splice(this.indexOf(noteId), 1); - } - await this.store(); - } - - /** - * Clears the stored tabs array. - */ - async clearAll() { - this._tabs = []; - await this.store(); - } -} - -/** - * Queue to store last active note id. - * Contains maximum two entries - current (index=1) and last active (index=0). - */ -export class LastActiveNoteQueue { - // stores the ids of the notes - private _store: string[] = new Array(); - - push(id: string) { - // if already two entries exist - remove first one - if (this._store.length == 2) { - // return if id is already second entry - if (this._store[1] == id) return; - - this._store.shift(); - } - // add handled note id at last - this._store.push(id); - - // console.log(`push: ${JSON.stringify(this._store)}`); - } - - pop(): string | undefined { - return this._store.shift(); - } - - length(): number { - return this._store.length; - } -} diff --git a/src/index.ts b/src/index.ts index 701311e..4a7c339 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,196 +1,37 @@ import joplin from 'api'; -import { MenuItem, MenuItemLocation, SettingItemType } from 'api/types'; -import { NoteTabType, SettingDefaults, NoteTabs, LastActiveNoteQueue } from './helpers'; +import { MenuItem, MenuItemLocation } from 'api/types'; +import { ChangeEvent } from 'api/JoplinSettings'; +import { NoteTabType, NoteTabs } from './noteTabs'; +import { LastActiveNote } from './lastActiveNote'; +import { Settings } from './settings'; +import { Panel } from './panel'; joplin.plugins.register({ onStart: async function () { const COMMANDS = joplin.commands; const DATA = joplin.data; const DIALOGS = joplin.views.dialogs; - const PANELS = joplin.views.panels; const SETTINGS = joplin.settings; const WORKSPACE = joplin.workspace; - //#region USER OPTIONS + const settings: Settings = new Settings(); + await settings.register(); - await SETTINGS.registerSection('note.tabs.settings', { - label: 'Note Tabs', - iconName: 'fas fa-window-maximize', - description: 'Changes are applied after selecting another note.' - }); - - await SETTINGS.registerSetting('noteTabs', { - value: [], - type: SettingItemType.Array, - section: 'note.tabs.settings', - public: false, - label: 'Note tabs' - }); - - // General settings - await SETTINGS.registerSetting('enableDragAndDrop', { - value: true, - type: SettingItemType.Bool, - section: 'note.tabs.settings', - public: true, - label: 'Enable drag & drop of tabs', - description: 'If disabled, position of tabs can be change via commands or move buttons.' - }); - await SETTINGS.registerSetting('showTodoCheckboxes', { - value: true, - type: SettingItemType.Bool, - section: 'note.tabs.settings', - public: true, - label: 'Show to-do checkboxes on tabs', - description: 'If enabled, to-dos can be completed directly on the tabs.' - }); - await SETTINGS.registerSetting('showBreadcrumbs', { - value: false, - type: SettingItemType.Bool, - section: 'note.tabs.settings', - public: true, - label: 'Show breadcrumbs below tabs', - description: 'Display full breadcrumbs for selected note below tabs. Only available in horizontal layout.' - }); - await SETTINGS.registerSetting('pinEditedNotes', { - value: false, - type: SettingItemType.Bool, - section: 'note.tabs.settings', - public: true, - label: 'Automatically pin notes when edited', - description: 'Pin notes automatically as soon as the title, content or any other attribute changes.' - }); - await SETTINGS.registerSetting('unpinCompletedTodos', { - value: false, - type: SettingItemType.Bool, - section: 'note.tabs.settings', - public: true, - label: 'Automatically unpin completed to-dos', - description: 'Unpin notes automatically as soon as the to-do status changes to completed. Removes the tab completely unless it is the selected note.' - }); - await SETTINGS.registerSetting('tabHeight', { - value: "40", - type: SettingItemType.Int, - section: 'note.tabs.settings', - public: true, - label: 'Note Tabs height (px)', - description: "Height of the tabs. Row height in vertical layout." - }); - await SETTINGS.registerSetting('minTabWidth', { - value: "50", - type: SettingItemType.Int, - section: 'note.tabs.settings', - public: true, - label: 'Minimum Tab width (px)' - }); - await SETTINGS.registerSetting('maxTabWidth', { - value: "150", - type: SettingItemType.Int, - section: 'note.tabs.settings', - public: true, - label: 'Maximum Tab width (px)' - }); - await SETTINGS.registerSetting('breadcrumbsMaxWidth', { - value: "100", - type: SettingItemType.Int, - section: 'note.tabs.settings', - public: true, - label: 'Maximum breadcrumb width (px)' - }); - - // Advanced settings - await SETTINGS.registerSetting('fontFamily', { - value: SettingDefaults.Default, - type: SettingItemType.String, - section: 'note.tabs.settings', - public: true, - advanced: true, - label: 'Font family', - description: "Font family used in the panel. Font families other than 'default' must be installed on the system. If the font is incorrect or empty, it might default to a generic sans-serif font. (default: Roboto)" - }); - await SETTINGS.registerSetting('mainBackground', { - value: SettingDefaults.Default, - type: SettingItemType.String, - section: 'note.tabs.settings', - public: true, - advanced: true, - label: 'Background color', - description: "Main background color of the panel. (default: Note list background color)" - }); - await SETTINGS.registerSetting('activeBackground', { - value: SettingDefaults.Default, - type: SettingItemType.String, - section: 'note.tabs.settings', - public: true, - advanced: true, - label: 'Active background color', - description: "Background color of the current active tab. (default: Editor background color)" - }); - await SETTINGS.registerSetting('breadcrumbsBackground', { - value: SettingDefaults.Default, - type: SettingItemType.String, - section: 'note.tabs.settings', - public: true, - advanced: true, - label: 'Breadcrumbs background color', - description: "Background color of the breadcrumbs. (default: Editor background color)" - }); - await SETTINGS.registerSetting('mainForeground', { - value: SettingDefaults.Default, - type: SettingItemType.String, - section: 'note.tabs.settings', - public: true, - advanced: true, - label: 'Foreground color', - description: "Default foreground color used for text and icons. (default: App faded color)" - }); - await SETTINGS.registerSetting('activeForeground', { - value: SettingDefaults.Default, - type: SettingItemType.String, - section: 'note.tabs.settings', - public: true, - advanced: true, - label: 'Active foreground color', - description: "Foreground color of the current active tab. (default: Editor font color)" - }); - await SETTINGS.registerSetting('dividerColor', { - value: SettingDefaults.Default, - type: SettingItemType.String, - section: 'note.tabs.settings', - public: true, - advanced: true, - label: 'Divider color', - description: "Color of the divider between the tabs. (default: App divider/border color)" - }); - - //#endregion + const tabs = new NoteTabs(settings.noteTabs); + const lastActiveNote = new LastActiveNote(); - //#region INITIALIZATION + const panel = new Panel(tabs, settings); + await panel.register(); - let lastActiveNoteQueue = new LastActiveNoteQueue(); - let tabs = new NoteTabs(); - await tabs.read(); + //#region HELPERS - //#endregion - - //#region COMMANDS - - async function getSettingOrDefault(setting: string, defaultValue: string): Promise { - const value: string = await SETTINGS.value(setting); - if (value.match(new RegExp(SettingDefaults.Default, "i"))) { - return defaultValue; - } else { - return value; - } - } - - /** - * Add note as temporary tab, if not already has one. - */ + /** + * Add note as temporary tab, if not already has one. + */ async function addTab(noteId: string) { if (tabs.hasTab(noteId)) return; - if (tabs.indexOfTemp() >= 0) { + if (tabs.indexOfTemp >= 0) { // replace existing temporary tab... tabs.replaceTemp(noteId); } else { @@ -199,73 +40,40 @@ joplin.plugins.register({ } } - /** - * Add new or pin tab for handled note. - */ - async function pinTab(note: any, addAsNew: boolean) { + /** + * Add new or pin tab for handled note. Optionally at the specified index of targetId. + */ + async function pinTab(note: any, addAsNew: boolean, targetId?: string) { // do not pin completed todos if auto unpin is enabled - const unpinCompletedTodos: boolean = await SETTINGS.value('unpinCompletedTodos'); - if (unpinCompletedTodos && note.is_todo && note.todo_completed) return; + if (settings.unpinCompletedTodos && note.is_todo && note.todo_completed) return; if (tabs.hasTab(note.id)) { // if note has already a tab, change type to pinned await tabs.changeType(note.id, NoteTabType.Pinned); } else { - // otherwise add as new one at the end - if (addAsNew) await tabs.add(note.id, NoteTabType.Pinned); + // otherwise add as new one + if (addAsNew) await tabs.add(note.id, NoteTabType.Pinned, targetId); } } - /** - * Remove or unpin note with handled id. - */ + /** + * Remove or unpin note with handled id. + */ async function removeTab(noteId: string) { const selectedNote: any = await WORKSPACE.selectedNote(); // remove tab completely await tabs.delete(noteId); - // if note is the selected note + // if note is the selected note, add as temp tab or replace existing one if (selectedNote && selectedNote.id == noteId) { - // add as temp tab or replace existing one await addTab(noteId); } } - /** - * Toggle state of handled todo. - */ - async function toggleTodo(noteId: string, checked: any) { - try { - const note: any = await DATA.get(['notes', noteId], { fields: ['id', 'is_todo', 'todo_completed'] }); - if (note.is_todo && checked) { - await DATA.put(['notes', note.id], null, { todo_completed: Date.now() }); - } else { - await DATA.put(['notes', note.id], null, { todo_completed: 0 }); - } - // updatePanelView() is called from onNoteChange event - } catch (error) { - return; - } - } - - /** - * Gets an array of all parents starting from the handled parent_id. - * Consider first entry is handled parent. - */ - async function getNoteParents(parent_id: string): Promise { - const parents: any[] = new Array(); - let last_id: string = parent_id; - - while (last_id) { - const parent: any = await DATA.get(['folders', last_id], { fields: ['id', 'title', 'parent_id'] }); - if (!parent) break; + //#endregion - last_id = parent.parent_id; - parents.push(parent); - } - return parents; - } + //#region COMMANDS // Command: tabsPinNote // Desc: Pin the selected note(s) to the tabs @@ -274,18 +82,24 @@ joplin.plugins.register({ label: 'Tabs: Pin note', iconName: 'fas fa-thumbtack', enabledCondition: "someNotesSelected", - execute: async (noteIds: string[]) => { + execute: async (noteIds: string[], targetId?: string) => { // get selected note ids and return if empty let selectedNoteIds = noteIds; if (!selectedNoteIds) selectedNoteIds = await WORKSPACE.selectedNoteIds(); if (!selectedNoteIds) return; - // pin all handled notes and update panel + // Add all handled note ids as pinned tabs. Optionally at the specified index of targetId. for (const noteId of selectedNoteIds) { - const note: any = await DATA.get(['notes', noteId], { fields: ['id', 'is_todo', 'todo_completed'] }); - await pinTab(note, true); + try { + const note: any = await DATA.get(['notes', noteId], { fields: ['id', 'is_todo', 'todo_completed'] }); + if (note) { + pinTab(note, true, targetId); + } + } catch (error) { + continue; + } } - await updatePanelView(); + await panel.updateWebview(); } }); @@ -306,7 +120,7 @@ joplin.plugins.register({ for (const noteId of selectedNoteIds) { await removeTab(noteId); } - await updatePanelView(); + await panel.updateWebview(); } }); @@ -324,7 +138,7 @@ joplin.plugins.register({ // change index of tab and update panel const index: number = tabs.indexOf(selectedNote.id); await tabs.moveWithIndex(index, index - 1); - await updatePanelView(); + await panel.updateWebview(); } }); @@ -342,7 +156,7 @@ joplin.plugins.register({ // change index of tab and update panel const index: number = tabs.indexOf(selectedNote.id); await tabs.moveWithIndex(index, index + 1); - await updatePanelView(); + await panel.updateWebview(); } }); @@ -354,10 +168,10 @@ joplin.plugins.register({ iconName: 'fas fa-step-backward', enabledCondition: "oneNoteSelected", execute: async () => { - if (lastActiveNoteQueue.length() < 2) return; + if (lastActiveNote.length < 2) return; // get the last active note from the queue - const lastActiveNoteId = lastActiveNoteQueue.pop(); + const lastActiveNoteId = lastActiveNote.id; // select note with stored id await COMMANDS.execute('openNote', lastActiveNoteId); @@ -400,7 +214,7 @@ joplin.plugins.register({ // check if note is not already last, otherwise exit const index: number = tabs.indexOf(selectedNote.id); if (index < 0) return; - if (index == tabs.length() - 1) return; + if (index == tabs.length - 1) return; // get id of right note and select it await COMMANDS.execute('openNote', tabs.get(index + 1).id); @@ -427,13 +241,24 @@ joplin.plugins.register({ await COMMANDS.execute('openNote', selectedNoteIds[0]); // updatePanelView() is called from onNoteSelectionChange event } else { - await updatePanelView(); + await panel.updateWebview(); } } }); - // prepare Tools > Tabs menu - const tabsCommandsSubMenu: MenuItem[] = [ + // Command: tabsToggleVisibility + // Desc: Toggle panel visibility + await COMMANDS.register({ + name: 'tabsToggleVisibility', + label: 'Tabs: Toggle visibility', + iconName: 'fas fa-eye-slash', + execute: async () => { + await panel.toggleVisibility(); + } + }); + + // prepare commands menu + const commandsSubMenu: MenuItem[] = [ { commandName: "tabsPinNote", label: 'Pin note' @@ -465,194 +290,29 @@ joplin.plugins.register({ { commandName: "tabsClear", label: 'Remove all pinned tabs' + }, + { + commandName: "tabsToggleVisibility", + label: 'Toggle panel visibility' } ]; - await joplin.views.menus.create('toolsTabs', 'Tabs', tabsCommandsSubMenu, MenuItemLocation.Tools); + await joplin.views.menus.create('toolsTabs', 'Tabs', commandsSubMenu, MenuItemLocation.Tools); - // add commands to note list context menu - await joplin.views.menuItems.create('noteListContextMenuPinToTabs', 'tabsPinNote', MenuItemLocation.NoteListContextMenu); + // add commands to notes context menu + await joplin.views.menuItems.create('notesContextMenuPinToTabs', 'tabsPinNote', MenuItemLocation.NoteListContextMenu); // add commands to editor context menu await joplin.views.menuItems.create('editorContextMenuPinNote', 'tabsPinNote', MenuItemLocation.EditorContextMenu); //#endregion - //#region PANEL VIEW + //#region EVENTS - // prepare panel object - const panel = await PANELS.create("note.tabs.panel"); - await PANELS.addScript(panel, './assets/fontawesome/css/all.min.css'); - await PANELS.addScript(panel, './webview.css'); - await PANELS.addScript(panel, './webview.js'); - await PANELS.onMessage(panel, async (message: any) => { - if (message.name === 'tabsOpenFolder') { - await COMMANDS.execute('openFolder', message.id); - } - if (message.name === 'tabsOpen') { - await COMMANDS.execute('openNote', message.id); - } - if (message.name === 'tabsPinNote') { - let id: string[] = [message.id]; - await COMMANDS.execute('tabsPinNote', id); - } - if (message.name === 'tabsUnpinNote') { - let id: string[] = [message.id]; - await COMMANDS.execute('tabsUnpinNote', id); - } - if (message.name === 'tabsToggleTodo') { - await toggleTodo(message.id, message.checked); - // updatePanelView() is called from onNoteChange event - } - if (message.name === 'tabsMoveLeft') { - await COMMANDS.execute('tabsMoveLeft'); - } - if (message.name === 'tabsMoveRight') { - await COMMANDS.execute('tabsMoveRight'); - } - if (message.name === 'tabsDrag') { - await tabs.moveWithId(message.sourceId, message.targetId); - await updatePanelView(); - } + SETTINGS.onChange(async (event: ChangeEvent) => { + await settings.read(event); + await panel.updateWebview(); }); - // set init message - const font: string = await getSettingOrDefault('fontFamily', SettingDefaults.Font); - const mainBg: string = await getSettingOrDefault('mainBackground', SettingDefaults.Background); - await PANELS.setHtml(panel, ` -
-
-

Loading tabs...

-
-
- `); - - // update HTML content - async function updatePanelView() { - const noteTabsHtml: any = []; - const selectedNote: any = await WORKSPACE.selectedNote(); - - // get style values from settings - const enableDragAndDrop: boolean = await SETTINGS.value('enableDragAndDrop'); - const showCheckboxes: boolean = await SETTINGS.value('showTodoCheckboxes'); - const showBreadcrumbs: boolean = await SETTINGS.value('showBreadcrumbs'); - const showCompletedTodos: boolean = await SETTINGS.globalValue('showCompletedTodos'); - const tabHeight: number = await SETTINGS.value('tabHeight'); - const minWidth: number = await SETTINGS.value('minTabWidth'); - const maxWidth: number = await SETTINGS.value('maxTabWidth'); - const font: string = await getSettingOrDefault('fontFamily', SettingDefaults.Font); - const mainBg: string = await getSettingOrDefault('mainBackground', SettingDefaults.Background); - const mainFg: string = await getSettingOrDefault('mainForeground', SettingDefaults.Foreground); - const activeBg: string = await getSettingOrDefault('activeBackground', SettingDefaults.ActiveBackground); - const activeFg: string = await getSettingOrDefault('activeForeground', SettingDefaults.ActiveForeground); - const dividerColor: string = await getSettingOrDefault('dividerColor', SettingDefaults.DividerColor); - const breadcrumbsBg: string = await getSettingOrDefault('breadcrumbsBackground', SettingDefaults.ActiveBackground); - const breadcrumbsMaxWidth: number = await SETTINGS.value('breadcrumbsMaxWidth'); - - // create HTML for each tab - for (const noteTab of tabs.getAll()) { - let note: any = null; - - // get real note from database, if no longer exists remove tab and continue with next one - try { - note = await DATA.get(['notes', noteTab.id], { fields: ['id', 'title', 'is_todo', 'todo_completed'] }); - } catch (error) { - await tabs.delete(noteTab.id); - continue; - } - - if (note) { - // continue with next tab if completed todos shall not be shown - if ((!showCompletedTodos) && note.todo_completed) continue; - - // prepare tab style attributes - const background: string = (selectedNote && note.id == selectedNote.id) ? activeBg : mainBg; - const foreground: string = (selectedNote && note.id == selectedNote.id) ? activeFg : mainFg; - const newTab: string = (noteTab.type == NoteTabType.Temporary) ? " new" : ""; - const icon: string = (noteTab.type == NoteTabType.Pinned) ? "fa-times" : "fa-thumbtack"; - const iconTitle: string = (noteTab.type == NoteTabType.Pinned) ? "Unpin" : "Pin"; - const textDecoration: string = (note.is_todo && note.todo_completed) ? 'line-through' : ''; - - // prepare checkbox for todo - let checkboxHtml: string = ''; - if (showCheckboxes && note.is_todo) { - checkboxHtml = ``; - } - - noteTabsHtml.push(` - - `); - } - } - - // prepare control buttons, if drag&drop is disabled - let controlsHtml: string = ''; - if (!enableDragAndDrop) { - controlsHtml = ` -
- - -
- `; - } - - // prepare breadcrumbs, if enabled - let breadcrumbsHtml: string = ''; - if (showBreadcrumbs && selectedNote) { - let parentsHtml: any[] = new Array(); - let parents: any[] = await getNoteParents(selectedNote.parent_id); - - // collect all parent folders and prepare html container for each - while (parents) { - const parent: any = parents.pop(); - if (!parent) break; - - parentsHtml.push(` - - `); - } - - // setup breadcrumbs container html - breadcrumbsHtml = ` - - `; - } - - // add tabs to container and push to panel - await PANELS.setHtml(panel, ` -
-
- ${noteTabsHtml.join('\n')} - ${controlsHtml} -
- ${breadcrumbsHtml} -
- `); - } - - //#endregion - - //#region MAP EVENTS - WORKSPACE.onNoteSelectionChange(async () => { try { const selectedNote: any = await WORKSPACE.selectedNote(); @@ -662,10 +322,10 @@ joplin.plugins.register({ await addTab(selectedNote.id); // add selected note id to last active queue - lastActiveNoteQueue.push(selectedNote.id); + lastActiveNote.id = selectedNote.id; } - await updatePanelView(); + await panel.updateWebview(); } catch (error) { console.error(`onNoteSelectionChange: ${error}`); } @@ -677,46 +337,37 @@ joplin.plugins.register({ if (ev) { // note was updated (ItemChangeEventType.Update) if (ev.event == 2) { - // console.log(`onNoteChange: note '${ev.id}' was updated`); // get handled note and return if null const note: any = await DATA.get(['notes', ev.id], { fields: ['id', 'is_todo', 'todo_completed'] }); if (note == null) return; // if auto pin is enabled and handled, pin to tabs - const pinEditedNotes: boolean = await SETTINGS.value('pinEditedNotes'); - if (pinEditedNotes) { + if (settings.pinEditedNotes) await pinTab(note, false); - } // if auto unpin is enabled and handled note is a completed todo... - const unpinCompletedTodos: boolean = await SETTINGS.value('unpinCompletedTodos'); - if (unpinCompletedTodos && note.is_todo && note.todo_completed) { + if (settings.unpinCompletedTodos && note.is_todo && note.todo_completed) await removeTab(note.id); - } } - // note was deleted (ItemChangeEventType.Delete) + // note was deleted (ItemChangeEventType.Delete) - remove tab if (ev.event == 3) { - // console.log(`onNoteChange: note '${ev.id}' was deleted`); - - // if note was deleted, remove tab await tabs.delete(ev.id); } } - await updatePanelView(); + await panel.updateWebview(); } catch (error) { console.error(`onNoteChange: ${error}`); } }); WORKSPACE.onSyncComplete(async () => { - await updatePanelView(); + await panel.updateWebview(); }); //#endregion - await updatePanelView(); - }, + } }); diff --git a/src/lastActiveNote.ts b/src/lastActiveNote.ts new file mode 100644 index 0000000..34b3810 --- /dev/null +++ b/src/lastActiveNote.ts @@ -0,0 +1,34 @@ +/** + * Queue to store last active note id. + * Contains maximum two entries - current (index=1) and last active (index=0). + */ +export class LastActiveNote { + // stores the ids of the notes + private _store: string[]; + + constructor() { + this._store = new Array(); + } + + get id(): string | undefined { + return this._store.shift(); + } + + set id(id: string) { + // if already two entries exist - remove first one + if (this._store.length == 2) { + // return if id is already second entry + if (this._store[1] == id) return; + + this._store.shift(); + } + // add handled note id at last + this._store.push(id); + + // console.log(`push: ${JSON.stringify(this._store)}`); + } + + get length(): number { + return this._store.length; + } +} diff --git a/src/manifest.json b/src/manifest.json index 57be239..2bd7c0b 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -1,11 +1,18 @@ { "manifest_version": 1, "id": "joplin.plugin.note.tabs", - "app_min_version": "1.5", - "version": "1.1.1", + "app_min_version": "1.6.7", + "version": "1.2.0", "name": "Note Tabs", - "description": "Allows to open several notes at once in tabs and pin them. (v1.1.1)", + "description": "Allows to open several notes at once in tabs and pin them.", "author": "Benji300", "homepage_url": "https://github.com/benji300/joplin-note-tabs", - "repository_url": "https://github.com/benji300/joplin-note-tabs" + "repository_url": "https://github.com/benji300/joplin-note-tabs", + "keywords": [ + "notes", + "panel", + "pin", + "tab", + "view" + ] } \ No newline at end of file diff --git a/src/noteTabs.ts b/src/noteTabs.ts new file mode 100644 index 0000000..04d6d23 --- /dev/null +++ b/src/noteTabs.ts @@ -0,0 +1,163 @@ +/** + * Tab type definitions. + */ +export enum NoteTabType { + Temporary = 1, + Open = 2, // currently not used + Pinned = 3 +} + +/** + * Helper class to work with note tabs array. + * - Read settings array once at startup. + * - Then work on this._tabs array. + */ +export class NoteTabs { + + /** + * Temporary array to work with tabs. + * + * Definition of one tab entry: + * [{ + * "id": "note id", + * "type": NoteTabType + * }] + */ + private _tabs: any[]; + + /** + * Init with noteTabs settings array. + */ + constructor(noteTabs: any[]) { + this._tabs = noteTabs; + } + + //#region GETTER + + /** + * All note tabs. + */ + get all(): any[] { + return this._tabs; + } + + /** + * Number of tabs. + */ + get length(): number { + return this._tabs.length; + } + + /** + * Index of the temporary tab. -1 if not exist. + */ + get indexOfTemp(): number { + return this._tabs.findIndex(x => x.type === NoteTabType.Temporary); + } + + //#endregion + + /** + * Inserts handled tab at specified index. + */ + private async insertAtIndex(index: number, tab: any) { + if (index < 0 || tab === undefined) return; + + this._tabs.splice(index, 0, tab); + } + + /** + * Gets the tab for the handled note. + */ + get(index: number): any { + if (index < 0 || index >= this.length) return; + + return this._tabs[index]; + } + + /** + * Gets index of tab for note with handled id. -1 if not exist. + */ + indexOf(noteId: string): number { + return this._tabs.findIndex(x => x.id === noteId); + } + + /** + * Gets a value whether the handled note has already a tab or not. + */ + hasTab(noteId: string): boolean { + return (this._tabs.find(x => x.id === noteId) !== undefined); + } + + /** + * Adds note as new tab at the end. + */ + async add(noteId: string, noteType: NoteTabType, targetId?: string) { + if (noteId === undefined || noteType === undefined) return; + + const newTab: any = { id: noteId, type: noteType }; + if (targetId) + await this.insertAtIndex(this.indexOf(targetId), newTab); + else + this._tabs.push(newTab); + } + + /** + * Moves the tab on source index to the target index. + */ + async moveWithIndex(sourceIdx: number, targetIdx: number) { + if (sourceIdx < 0 || sourceIdx >= this.length) return; + if (targetIdx < 0 || targetIdx >= this.length) return; + + const tab: any = this._tabs[sourceIdx]; + await this.delete(this.get(sourceIdx).id); + await this.insertAtIndex((targetIdx == 0 ? 0 : targetIdx), tab); + } + + /** + * Moves the tab of source note to the index of the target note. + */ + async moveWithId(sourceId: string, targetId: string) { + const targetIdx: number = (targetId) ? this.indexOf(targetId) : (this.length - 1); + await this.moveWithIndex(this.indexOf(sourceId), targetIdx); + } + + /** + * Changes type of the tab for the handled note. + */ + async changeType(noteId: string, newType: NoteTabType) { + const index = this.indexOf(noteId); + if (index >= 0) { + this._tabs[index].type = newType; + } + } + + /** + * Replaces tab at specified index with handled one. + */ + async replaceTemp(noteId: string) { + if (noteId === undefined) return; + + const tempIdx: number = this.indexOfTemp; + if (tempIdx >= 0) { + this._tabs[tempIdx].id = noteId; + } + } + + /** + * Removes tab on handled index. + */ + async delete(noteId: string) { + const index = this.indexOf(noteId); + if (index >= 0) { + this._tabs.splice(index, 1); + } + } + + /** + * Clears the stored tabs array. + */ + async clearAll() { + this._tabs = []; + } +} diff --git a/src/panel.ts b/src/panel.ts new file mode 100644 index 0000000..3c7edc3 --- /dev/null +++ b/src/panel.ts @@ -0,0 +1,257 @@ +import joplin from 'api'; +import { NoteTabType, NoteTabs } from './noteTabs'; +import { Settings } from './settings'; + +export class Panel { + + private _panel: any; + private _tabs: NoteTabs; + private _settings: Settings; + + constructor(tabs: NoteTabs, settings: Settings) { + this._tabs = tabs; + this._settings = settings; + } + + private async toggleTodoState(noteId: string, checked: any) { + try { + const note: any = await joplin.data.get(['notes', noteId], { fields: ['id', 'is_todo', 'todo_completed'] }); + if (note.is_todo && checked) { + await joplin.data.put(['notes', note.id], null, { todo_completed: Date.now() }); + } else { + await joplin.data.put(['notes', note.id], null, { todo_completed: 0 }); + } + // updateWebview() is called from onNoteChange event + } catch (error) { + return; + } + } + + /** + * Gets an array of all parents starting from the handled parent_id. + * Consider first entry is handled parent. + */ + private async getNoteParents(parent_id: string): Promise { + const parents: any[] = new Array(); + let last_id: string = parent_id; + + while (last_id) { + const parent: any = await joplin.data.get(['folders', last_id], { fields: ['id', 'title', 'parent_id'] }); + if (!parent) break; + last_id = parent.parent_id; + parents.push(parent); + } + return parents; + } + + // create HTML for each tab + private async getNoteTabsHtml(selectedNote: any): Promise { + const showCompletedTodos: boolean = await this._settings.showCompletedTodos; + const noteTabsHtml: any = []; + + for (const noteTab of this._tabs.all) { + let note: any = null; + + // get real note from database, if no longer exists remove tab and continue with next one + try { + note = await joplin.data.get(['notes', noteTab.id], { fields: ['id', 'title', 'is_todo', 'todo_completed'] }); + // console.log(`add note: ${JSON.stringify(note)}`); + } catch (error) { + // console.log(`delete note: ${noteTab.id}`); + await this._tabs.delete(noteTab.id); + continue; + } + + if (note) { + // continue with next tab if completed todos shall not be shown + if ((!showCompletedTodos) && note.todo_completed) continue; + + // prepare tab style attributes + const bg: string = (selectedNote && note.id == selectedNote.id) ? this._settings.actBackground : this._settings.background; + const fg: string = (selectedNote && note.id == selectedNote.id) ? this._settings.actForeground : this._settings.foreground; + const newTab: string = (noteTab.type == NoteTabType.Temporary) ? ' new' : ''; + const icon: string = (noteTab.type == NoteTabType.Pinned) ? 'fa-times' : 'fa-thumbtack'; + const iconTitle: string = (noteTab.type == NoteTabType.Pinned) ? 'Unpin' : 'Pin'; + const textDecoration: string = (note.is_todo && note.todo_completed) ? 'line-through' : ''; + + // prepare checkbox for todo + let checkboxHtml: string = ''; + if (this._settings.showTodoCheckboxes && note.is_todo) { + checkboxHtml = ``; + } + + noteTabsHtml.push(` + + `); + } + } + + return noteTabsHtml.join('\n'); + } + + // prepare control buttons, if drag&drop is disabled + private getControlsHtml(): string { + let controlsHtml: string = ''; + + if (!this._settings.enableDragAndDrop) { + controlsHtml = ` +
+ + +
+ `; + } + return controlsHtml; + } + + // prepare navigation buttons, if enabled + // prepare breadcrumbs, if enabled + private async getBreadcrumbsHtml(selectedNote: any): Promise { + let breadcrumbsHtml: string = ''; + + if (this._settings.showBreadcrumbs && selectedNote) { + let navigationHtml: string = ''; + if (this._settings.showNavigationButtons) { + navigationHtml = ` + + `; + } + + let parentsHtml: any[] = new Array(); + let parents: any[] = await this.getNoteParents(selectedNote.parent_id); + + // collect all parent folders and prepare html container for each + while (parents) { + const parent: any = parents.pop(); + if (!parent) break; + + parentsHtml.push(` + + `); + } + + // setup breadcrumbs container html + breadcrumbsHtml = ` + + `; + } + return breadcrumbsHtml; + } + + /** + * Register plugin panel and update webview for the first time. + */ + async register() { + this._panel = await joplin.views.panels.create('note.tabs.panel'); + await joplin.views.panels.addScript(this._panel, './assets/fontawesome/css/all.min.css'); + await joplin.views.panels.addScript(this._panel, './webview.css'); + await joplin.views.panels.addScript(this._panel, './webview.js'); + await joplin.views.panels.onMessage(this._panel, async (message: any) => { + if (message.name === 'tabsOpenFolder') { + await joplin.commands.execute('openFolder', message.id); + } + if (message.name === 'tabsOpen') { + await joplin.commands.execute('openNote', message.id); + } + if (message.name === 'tabsPinNote') { + let id: string[] = [message.id]; + await joplin.commands.execute('tabsPinNote', id); + } + if (message.name === 'tabsUnpinNote') { + let id: string[] = [message.id]; + await joplin.commands.execute('tabsUnpinNote', id); + } + if (message.name === 'tabsToggleTodo') { + await this.toggleTodoState(message.id, message.checked); + } + if (message.name === 'tabsMoveLeft') { + await joplin.commands.execute('tabsMoveLeft'); + } + if (message.name === 'tabsMoveRight') { + await joplin.commands.execute('tabsMoveRight'); + } + if (message.name === 'tabsBack') { + await joplin.commands.execute('historyBackward'); + } + if (message.name === 'tabsForward') { + await joplin.commands.execute('historyForward'); + } + if (message.name === 'tabsDrag') { + await this._tabs.moveWithId(message.sourceId, message.targetId); + await this.updateWebview(); + } + if (message.name === 'tabsDragNotes') { + await joplin.commands.execute('tabsPinNote', message.noteIds, message.targetId); + } + }); + + // set init message + await joplin.views.panels.setHtml(this._panel, ` +
+
+

Loading panel...

+
+
+ `); + + await this.updateWebview(); + } + + async updateWebview() { + const selectedNote: any = await joplin.workspace.selectedNote(); + const noteTabsHtml: string = await this.getNoteTabsHtml(selectedNote); + const controlsHtml: string = this.getControlsHtml(); + const breadcrumbsHtml: string = await this.getBreadcrumbsHtml(selectedNote); + + // add entries to container and push to panel + await joplin.views.panels.setHtml(this._panel, ` +
+
+ ${noteTabsHtml} + ${controlsHtml} +
+ ${breadcrumbsHtml} +
+ `); + + // store the current note tabs array back to the settings + // - Currently there's no "event" to call store() only on App closing + // - Which would be preferred + this._settings.storeTabs(this._tabs.all); + } + + /** + * Toggle visibility of the panel. + */ + async toggleVisibility() { + const isVisible: boolean = await joplin.views.panels.visible(this._panel); + await joplin.views.panels.show(this._panel, (!isVisible)); + } +} \ No newline at end of file diff --git a/src/settings.ts b/src/settings.ts new file mode 100644 index 0000000..8a9cb4c --- /dev/null +++ b/src/settings.ts @@ -0,0 +1,393 @@ +import joplin from 'api'; +import { SettingItemType } from 'api/types'; +import { ChangeEvent } from 'api/JoplinSettings'; + +/** + * Advanced style setting default values. + * Used when setting is set to 'default'. + */ +enum SettingDefaults { + Default = 'default', + FontFamily = 'Roboto', + FontSize = 'var(--joplin-font-size)', + Background = 'var(--joplin-background-color3)', + HoverBackground = 'var(--joplin-background-color-hover3)', // var(--joplin-background-hover) + Foreground = 'var(--joplin-color-faded)', + ActiveBackground = 'var(--joplin-background-color)', + ActiveForeground = 'var(--joplin-color)', + DividerColor = 'var(--joplin-divider-color)' +} + +/** + * Definitions of plugin joplin.settings. + */ +export class Settings { + // private settings + private _noteTabs: any[] = new Array(); + // general settings + private _enableDragAndDrop: boolean = true; + private _showTodoCheckboxes: boolean = false; + private _showBreadcrumbs: boolean = false; + private _showNavigationButtons: boolean = false; + private _pinEditedNotes: boolean = false; + private _unpinCompletedTodos: boolean = false; + private _tabHeight: number = 35; + private _minTabWidth: number = 50; + private _maxTabWidth: number = 150; + private _breadcrumbsMinWidth: number = 10; + private _breadcrumbsMaxWidth: number = 100; + // advanced settings + private _fontFamily: string = SettingDefaults.Default; + private _fontSize: string = SettingDefaults.Default; + private _background: string = SettingDefaults.Default; + private _hoverBackground: string = SettingDefaults.Default; + private _actBackground: string = SettingDefaults.Default; + private _breadcrumbsBackground: string = SettingDefaults.Default; + private _foreground: string = SettingDefaults.Default; + private _actForeground: string = SettingDefaults.Default; + private _dividerColor: string = SettingDefaults.Default; + // internals + private _defaultRegExp: RegExp = new RegExp(SettingDefaults.Default, "i"); + + constructor() { + } + + //#region GETTER + + get noteTabs(): any[] { + return this._noteTabs; + } + + get enableDragAndDrop(): boolean { + return this._enableDragAndDrop; + } + + get showTodoCheckboxes(): boolean { + return this._showTodoCheckboxes; + } + + get showBreadcrumbs(): boolean { + return this._showBreadcrumbs; + } + + get showNavigationButtons(): boolean { + return this._showNavigationButtons; + } + + get pinEditedNotes(): boolean { + return this._pinEditedNotes; + } + + get unpinCompletedTodos(): boolean { + return this._unpinCompletedTodos; + } + + get tabHeight(): number { + return this._tabHeight; + } + + get minTabWidth(): number { + return this._minTabWidth; + } + + get maxTabWidth(): number { + return this._maxTabWidth; + } + + get breadcrumbsMinWidth(): number { + return this._breadcrumbsMinWidth; + } + + get breadcrumbsMaxWidth(): number { + return this._breadcrumbsMaxWidth; + } + + get fontFamily(): string { + return this._fontFamily; + } + + get fontSize(): string { + return this._fontSize; + } + + get background(): string { + return this._background; + } + + get hoverBackground(): string { + return this._hoverBackground; + } + + get actBackground(): string { + return this._actBackground; + } + + get breadcrumbsBackground(): string { + return this._breadcrumbsBackground; + } + + get foreground(): string { + return this._foreground; + } + + get actForeground(): string { + return this._actForeground; + } + + get dividerColor(): string { + return this._dividerColor; + } + + //#endregion + + //#region GLOBAL VALUES + + get showCompletedTodos(): Promise { + return joplin.settings.globalValue('showCompletedTodos'); + } + + //#endregion + + /** + * Register settings section with all options and intially read them at the end. + */ + async register() { + // settings section + await joplin.settings.registerSection('note.tabs.settings', { + label: 'Note Tabs', + iconName: 'fas fa-window-maximize' + }); + + // private settings + await joplin.settings.registerSetting('noteTabs', { + value: [], + type: SettingItemType.Array, + section: 'note.tabs.settings', + public: false, + label: 'Note tabs' + }); + this._noteTabs = await joplin.settings.value('noteTabs'); + + // general settings + await joplin.settings.registerSetting('enableDragAndDrop', { + value: this._enableDragAndDrop, + type: SettingItemType.Bool, + section: 'note.tabs.settings', + public: true, + label: 'Enable drag & drop of tabs', + description: 'If disabled, position of tabs can be change via commands or move buttons.' + }); + await joplin.settings.registerSetting('showTodoCheckboxes', { + value: this._showTodoCheckboxes, + type: SettingItemType.Bool, + section: 'note.tabs.settings', + public: true, + label: 'Show to-do checkboxes on tabs', + description: 'If enabled, to-dos can be completed directly on the tabs.' + }); + await joplin.settings.registerSetting('showBreadcrumbs', { + value: this._showBreadcrumbs, + type: SettingItemType.Bool, + section: 'note.tabs.settings', + public: true, + label: 'Show breadcrumbs below tabs', + description: 'Display full breadcrumbs for selected note below tabs. Only available in horizontal layout.' + }); + await joplin.settings.registerSetting('showNavigationButtons', { + value: this._showNavigationButtons, + type: SettingItemType.Bool, + section: 'note.tabs.settings', + public: true, + label: 'Show navigation buttons below tabs', + description: 'Display history backward and forward buttons before the breadcrumds. Only visible if breadcrumbs are also enabled and visible.' + }); + await joplin.settings.registerSetting('pinEditedNotes', { + value: this._pinEditedNotes, + type: SettingItemType.Bool, + section: 'note.tabs.settings', + public: true, + label: 'Automatically pin notes when edited', + description: 'Pin notes automatically as soon as the title, content or any other attribute changes.' + }); + await joplin.settings.registerSetting('unpinCompletedTodos', { + value: this._unpinCompletedTodos, + type: SettingItemType.Bool, + section: 'note.tabs.settings', + public: true, + label: 'Automatically unpin completed to-dos', + description: 'Unpin notes automatically as soon as the to-do status changes to completed. Removes the tab completely unless it is the selected note.' + }); + await joplin.settings.registerSetting('tabHeight', { + value: this._tabHeight, + type: SettingItemType.Int, + section: 'note.tabs.settings', + public: true, + label: 'Note Tabs height (px)', + description: 'Height of the tabs. Row height in vertical layout.' + }); + await joplin.settings.registerSetting('minTabWidth', { + value: this._minTabWidth, + type: SettingItemType.Int, + section: 'note.tabs.settings', + public: true, + label: 'Minimum Tab width (px)', + description: 'Minimum width of one tab in pixel.' + }); + await joplin.settings.registerSetting('maxTabWidth', { + value: this._maxTabWidth, + type: SettingItemType.Int, + section: 'note.tabs.settings', + public: true, + label: 'Maximum Tab width (px)', + description: 'Maximum width of one tab in pixel.' + }); + await joplin.settings.registerSetting('breadcrumbsMinWidth', { + value: this._breadcrumbsMinWidth, + type: SettingItemType.Int, + section: 'note.tabs.settings', + public: true, + label: 'Minimum breadcrumb width (px)', + description: 'Minimum width of one breadcrumb in pixel.' + }); + await joplin.settings.registerSetting('breadcrumbsMaxWidth', { + value: this._breadcrumbsMaxWidth, + type: SettingItemType.Int, + section: 'note.tabs.settings', + public: true, + label: 'Maximum breadcrumb width (px)', + description: 'Maximum width of one breadcrumb in pixel.' + }); + + // advanced settings + await joplin.settings.registerSetting('fontFamily', { + value: this._fontFamily, + type: SettingItemType.String, + section: 'note.tabs.settings', + public: true, + advanced: true, + label: 'Font family', + description: "Font family used in the panel. Font families other than 'default' must be installed on the system. If the font is incorrect or empty, it might default to a generic sans-serif font. (default: Roboto)" + }); + await joplin.settings.registerSetting('fontSize', { + value: this._fontSize, + type: SettingItemType.String, + section: 'note.tabs.settings', + public: true, + advanced: true, + label: 'Font size', + description: "Font size used in the panel. Values other than 'default' must be specified in valid CSS syntax, e.g. '13px'. (default: App default font size)" + }); + await joplin.settings.registerSetting('mainBackground', { + value: this._background, + type: SettingItemType.String, + section: 'note.tabs.settings', + public: true, + advanced: true, + label: 'Background color', + description: 'Main background color of the panel. (default: Note list background color)' + }); + await joplin.settings.registerSetting('hoverBackground', { + value: this._hoverBackground, + type: SettingItemType.String, + section: 'note.tabs.settings', + public: true, + advanced: true, + label: 'Hover Background color', + description: 'Background color used when hovering a favorite. (default: Note list hover color)' + }); + await joplin.settings.registerSetting('activeBackground', { + value: this._actBackground, + type: SettingItemType.String, + section: 'note.tabs.settings', + public: true, + advanced: true, + label: 'Active background color', + description: 'Background color of the current active tab. (default: Editor background color)' + }); + await joplin.settings.registerSetting('breadcrumbsBackground', { + value: this._breadcrumbsBackground, + type: SettingItemType.String, + section: 'note.tabs.settings', + public: true, + advanced: true, + label: 'Breadcrumbs background color', + description: 'Background color of the breadcrumbs. (default: Editor background color)' + }); + await joplin.settings.registerSetting('mainForeground', { + value: this._foreground, + type: SettingItemType.String, + section: 'note.tabs.settings', + public: true, + advanced: true, + label: 'Foreground color', + description: 'Foreground color used for text and icons. (default: App faded color)' + }); + await joplin.settings.registerSetting('activeForeground', { + value: this._actForeground, + type: SettingItemType.String, + section: 'note.tabs.settings', + public: true, + advanced: true, + label: 'Active foreground color', + description: 'Foreground color of the current active tab. (default: Editor font color)' + }); + await joplin.settings.registerSetting('dividerColor', { + value: this._dividerColor, + type: SettingItemType.String, + section: 'note.tabs.settings', + public: true, + advanced: true, + label: 'Divider color', + description: 'Color of the divider between the tabs. (default: App default border color)' + }); + + // initially read settings + await this.read(); + } + + private async getOrDefault(event: ChangeEvent, localVar: any, setting: string, defaultValue?: string): Promise { + const read: boolean = (!event || event.keys.includes(setting)); + if (read) { + const value: string = await joplin.settings.value(setting); + if (defaultValue && value.match(this._defaultRegExp)) { + return defaultValue; + } else { + return value; + } + } + return localVar; + } + + /** + * Update settings. Either all or only changed ones. + */ + async read(event?: ChangeEvent) { + this._enableDragAndDrop = await this.getOrDefault(event, this._enableDragAndDrop, 'enableDragAndDrop'); + this._showTodoCheckboxes = await this.getOrDefault(event, this._showTodoCheckboxes, 'showTodoCheckboxes'); + this._showBreadcrumbs = await this.getOrDefault(event, this._showBreadcrumbs, 'showBreadcrumbs'); + this._showNavigationButtons = await this.getOrDefault(event, this._showNavigationButtons, 'showNavigationButtons'); + this._pinEditedNotes = await this.getOrDefault(event, this._pinEditedNotes, 'pinEditedNotes'); + this._unpinCompletedTodos = await this.getOrDefault(event, this._unpinCompletedTodos, 'unpinCompletedTodos'); + this._tabHeight = await this.getOrDefault(event, this._tabHeight, 'tabHeight'); + this._minTabWidth = await this.getOrDefault(event, this._minTabWidth, 'minTabWidth'); + this._maxTabWidth = await this.getOrDefault(event, this._maxTabWidth, 'maxTabWidth'); + this._breadcrumbsMinWidth = await this.getOrDefault(event, this._breadcrumbsMinWidth, 'breadcrumbsMinWidth'); + this._breadcrumbsMaxWidth = await this.getOrDefault(event, this._breadcrumbsMaxWidth, 'breadcrumbsMaxWidth'); + this._fontFamily = await this.getOrDefault(event, this._fontFamily, 'fontFamily', SettingDefaults.FontFamily); + this._fontSize = await this.getOrDefault(event, this._fontSize, 'fontSize', SettingDefaults.FontSize); + this._background = await this.getOrDefault(event, this._background, 'mainBackground', SettingDefaults.Background); + this._hoverBackground = await this.getOrDefault(event, this._hoverBackground, 'hoverBackground', SettingDefaults.HoverBackground); + this._actBackground = await this.getOrDefault(event, this._actBackground, 'activeBackground', SettingDefaults.ActiveBackground); + this._breadcrumbsBackground = await this.getOrDefault(event, this._breadcrumbsBackground, 'breadcrumbsBackground', SettingDefaults.ActiveBackground); + this._foreground = await this.getOrDefault(event, this._foreground, 'mainForeground', SettingDefaults.Foreground); + this._actForeground = await this.getOrDefault(event, this._actForeground, 'activeForeground', SettingDefaults.ActiveForeground); + this._dividerColor = await this.getOrDefault(event, this._dividerColor, 'dividerColor', SettingDefaults.DividerColor); + } + + /** + * Store the handled tabs array back to the settings. + */ + async storeTabs(noteTabs: any[]) { + await joplin.settings.setValue('noteTabs', noteTabs); + } +} diff --git a/src/webview.css b/src/webview.css index 6d3560d..1da879c 100644 --- a/src/webview.css +++ b/src/webview.css @@ -12,7 +12,6 @@ html, body, body > div, div#joplin-plugin-content { - font-size: var(--joplin-font-size); height: 100%; } a { @@ -22,26 +21,18 @@ span { cursor: default; } -::-webkit-scrollbar { - height: 4px; - width: 4px; -} -::-webkit-scrollbar-thumb { - background: rgba(0, 0, 0, 0.2); - border-radius: 10px; -} - /* HORIZONTAL LAYOUT */ #container { height: 100%; - overflow-x: auto; - overflow-y: hidden; + overflow-x: hidden; + overflow-y: overlay; width: 100%; } #tabs-container { display: flex; float: left; + overflow-x: overlay; width: 100%; } #tab { @@ -106,17 +97,34 @@ span { #breadcrumbs-container { display: flex; float: left; + overflow: overlay; width: 100%; } +#breadcrumbs-container .fas { + font-size: 1.1em; +} + +.navigation-icons { + border-style: solid; + border-width: 0; + border-right-width: 1px; + height: 100%; + margin: auto 0; + padding: 0 4px; +} +.navigation-icons .fas { + padding: 0 4px; +} + .breadcrumbs-icon { height: 100%; margin: auto 0; padding-left: 8px; } + .breadcrumb { display: flex; height: 25px; - min-width: 50px; } .breadcrumb:last-of-type .fas { display: none; @@ -133,8 +141,14 @@ span { .breadcrumb-title { overflow: hidden; padding: 0 4px; + text-align: center; text-overflow: ellipsis; white-space: nowrap; + width: 100%; +} +.breadcrumb-inner .fas { + margin-left: auto; + margin-right: 0px; } /* DRAG AND DROP */ @@ -145,20 +159,18 @@ span { -webkit-user-select: none; -ms-user-select: none; } -.dragging { - opacity: 0.4; + +::-webkit-scrollbar { + height: 4px; + width: 7px; } -.dragover { - background-color: var(--joplin-background-color-hover3) !important; +::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.2); + border-radius: 5px; } /* VERCTICAL LAYOUT OVERWRITES */ @media screen and (max-width: 400px) { - #container { - overflow-x: hidden; - overflow-y: auto; - } - #tabs-container { display: block; width: 100%; diff --git a/src/webview.js b/src/webview.js index bb31af7..084bc3d 100644 --- a/src/webview.js +++ b/src/webview.js @@ -1,64 +1,47 @@ -/* DOUBLE CLICK EVENT */ -document.addEventListener('dblclick', event => { - const element = event.target; - - if (element.id === 'tab' || element.className === 'tab-inner' || element.className === 'tab-title') { - webviewApi.postMessage({ - name: 'tabsPinNote', - id: element.dataset.id, - }); +function getDataId(event) { + if (event.currentTarget.id === 'tab' || event.currentTarget.className === 'breadcrumb') { + return event.currentTarget.dataset.id; + } else { + return; } -}) +} /* CLICK EVENTS */ -document.addEventListener('click', event => { - const element = event.target; - - if (element.className === 'breadcrumb-title') { - webviewApi.postMessage({ - name: 'tabsOpenFolder', - id: element.dataset.id, - }); - } - if (element.id === 'tab' || element.className === 'tab-inner' || element.className === 'tab-title') { - webviewApi.postMessage({ - name: 'tabsOpen', - id: element.dataset.id, - }); - } - if (element.id === 'Pin') { - webviewApi.postMessage({ - name: 'tabsPinNote', - id: element.dataset.id, - }); - } - if (element.id === 'Unpin') { - webviewApi.postMessage({ - name: 'tabsUnpinNote', - id: element.dataset.id, - }); - } - if (element.id === 'check') { - webviewApi.postMessage({ - name: 'tabsToggleTodo', - id: element.dataset.id, - checked: element.checked - }); +function message(message) { + webviewApi.postMessage({ name: message }); +} + +function openFolder(event) { + const dataId = getDataId(event); + if (dataId) { + webviewApi.postMessage({ name: 'tabsOpenFolder', id: dataId }); } - if (element.id === 'moveTabLeft') { - webviewApi.postMessage({ - name: 'tabsMoveLeft' - }); +} + +function pinNote(event) { + const dataId = getDataId(event); + if (dataId) { + webviewApi.postMessage({ name: 'tabsPinNote', id: dataId }); } - if (element.id === 'moveTabRight') { - webviewApi.postMessage({ - name: 'tabsMoveRight' - }); +} + +function tabClick(event) { + const dataId = getDataId(event); + if (dataId) { + if (event.target.id === 'Pin') { + pinNote(event); + } else if (event.target.id === 'Unpin') { + webviewApi.postMessage({ name: 'tabsUnpinNote', id: dataId }); + } else if (event.target.id === 'check') { + webviewApi.postMessage({ name: 'tabsToggleTodo', id: dataId, checked: event.target.checked }); + } else { + webviewApi.postMessage({ name: 'tabsOpen', id: dataId }); + } } -}) +} /* DRAG AND DROP */ -let sourceNoteId = ""; +let sourceId = ''; function cancelDefault(event) { event.preventDefault(); @@ -66,38 +49,44 @@ function cancelDefault(event) { return false; } +function setBackground(event, background) { + event.currentTarget.style.background = background; +} + +function resetBackground(element) { + if (element.dataset.bg) { + element.style.background = element.dataset.bg; + } +} + +function resetTabBackgrounds() { + document.querySelectorAll('#tab').forEach(x => { resetBackground(x); }); + + tabsContainer = document.querySelector('#tabs-container'); + if (tabsContainer) { + tabsContainer.style.background = 'none'; + } +} + function dragStart(event) { - const element = event.target; - element.classList.add("dragging"); - event.dataTransfer.setData("text/plain", element.dataset.id); - sourceNoteId = element.dataset.id + const dataId = getDataId(event); + if (dataId) { + event.dataTransfer.setData('text/x-plugin-note-tabs-id', dataId); + sourceId = dataId; + } } function dragEnd(event) { + resetTabBackgrounds(); cancelDefault(event); - const element = event.target; - element.classList.remove("dragging"); - sourceNoteId = ""; + sourceId = ''; } -function dragOver(event) { +function dragOver(event, hoverColor) { + resetTabBackgrounds(); cancelDefault(event); - const element = event.target; - - document.querySelectorAll('#tab').forEach(tab => { - if (tab.dataset.id !== element.dataset.id) { - tab.classList.remove("dragover"); - } - }); - - if (element.dataset.id !== sourceNoteId) { - if (element.id === 'tab') { - element.classList.add("dragover"); - } else if (element.parentElement.id === 'tab') { - element.parentElement.classList.add("dragover"); - } else if (element.parentElement.parentElement.id === 'tab') { - element.parentElement.parentElement.classList.add("dragover"); - } + if (sourceId !== getDataId(event)) { + setBackground(event, hoverColor); } } @@ -107,17 +96,33 @@ function dragLeave(event) { function drop(event) { cancelDefault(event); - const targetElement = event.target; - const sourceId = event.dataTransfer.getData("text/plain"); - - if (targetElement && sourceId) { - if (targetElement.dataset.id !== sourceNoteId) { - webviewApi.postMessage({ - name: 'tabsDrag', - targetId: targetElement.dataset.id, - sourceId: sourceId - }); - targetElement.classList.remove("dragover"); + const dataTargetId = getDataId(event); + + // check whether plugin tab was dragged - trigger tabsDrag message + const noteTabsId = event.dataTransfer.getData('text/x-plugin-note-tabs-id'); + if (noteTabsId) { + if (dataTargetId !== sourceId) { + webviewApi.postMessage({ name: 'tabsDrag', targetId: dataTargetId, sourceId: noteTabsId }); + return; + } + } + + // check whether note was dragged from app onto the panel - add new tab at dropped index + const joplinNoteIds = event.dataTransfer.getData('text/x-jop-note-ids'); + if (joplinNoteIds) { + const noteIds = new Array(); + for (const noteId of JSON.parse(joplinNoteIds)) { + noteIds.push(noteId); } + webviewApi.postMessage({ name: 'tabsDragNotes', noteIds: noteIds, targetId: dataTargetId }); + return; + } + + // check whether favorite (from joplin.plugin.benji.favorites plugin) was dragged onto the panel - add new tab at dropped index + const favoritesId = event.dataTransfer.getData('text/x-plugin-favorites-id'); + if (favoritesId) { + const noteIds = new Array(favoritesId); + webviewApi.postMessage({ name: 'tabsDragNotes', noteIds: noteIds, targetId: dataTargetId }); + return; } } diff --git a/webpack.config.js b/webpack.config.js index 35538a5..47eafec 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -33,10 +33,6 @@ const manifest = readManifest(manifestPath); const pluginArchiveFilePath = path.resolve(publishDir, `${manifest.id}.jpl`); const pluginInfoFilePath = path.resolve(publishDir, `${manifest.id}.json`); -fs.removeSync(distDir); -fs.removeSync(publishDir); -fs.mkdirpSync(publishDir); - function validatePackageJson() { const content = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); if (!content.name || content.name.indexOf('joplin-plugin-') !== 0) { @@ -109,6 +105,7 @@ function createPluginInfo(manifestPath, destPath, jplFilePath) { function onBuildCompleted() { try { + fs.removeSync(path.resolve(publishDir, 'index.js')); createPluginArchive(distDir, pluginArchiveFilePath); createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath); validatePackageJson(); @@ -174,6 +171,16 @@ const extraScriptConfig = Object.assign({}, baseConfig, { }, }); +const createArchiveConfig = { + stats: 'errors-only', + entry: './dist/index.js', + output: { + filename: 'index.js', + path: publishDir, + }, + plugins: [new WebpackOnBuildPlugin(onBuildCompleted)], +}; + function resolveExtraScriptPath(name) { const relativePath = `./src/${name}`; @@ -196,8 +203,8 @@ function resolveExtraScriptPath(name) { }; } -function addExtraScriptConfigs(baseConfig, userConfig) { - if (!userConfig.extraScripts.length) return baseConfig; +function buildExtraScriptConfigs(userConfig) { + if (!userConfig.extraScripts.length) return []; const output = []; @@ -209,25 +216,64 @@ function addExtraScriptConfigs(baseConfig, userConfig) { })); } - return baseConfig.concat(output); + return output; } -function addLastConfigStep(config) { - const lastConfig = config[config.length - 1]; - if (!lastConfig.plugins) lastConfig.plugins = []; - lastConfig.plugins.push(new WebpackOnBuildPlugin(onBuildCompleted)); - config[config.length - 1] = lastConfig; - return config; +function main(processArgv) { + const yargs = require('yargs/yargs'); + const argv = yargs(processArgv).argv; + + const configName = argv['joplin-plugin-config']; + if (!configName) throw new Error('A config file must be specified via the --joplin-plugin-config flag'); + + // Webpack configurations run in parallel, while we need them to run in + // sequence, and to do that it seems the only way is to run webpack multiple + // times, with different config each time. + + const configs = { + // Builds the main src/index.ts and copy the extra content from /src to + // /dist including scripts, CSS and any other asset. + buildMain: [pluginConfig], + + // Builds the extra scripts as defined in plugin.config.json. When doing + // so, some JavaScript files that were copied in the previous might be + // overwritten here by the compiled version. This is by design. The + // result is that JS files that don't need compilation, are simply + // copied to /dist, while those that do need it are correctly compiled. + buildExtraScripts: buildExtraScriptConfigs(userConfig), + + // Ths config is for creating the .jpl, which is done via the plugin, so + // it doesn't actually need an entry and output, however webpack won't + // run without this. So we give it an entry that we know is going to + // exist and output in the publish dir. Then the plugin will delete this + // temporary file before packaging the plugin. + createArchive: [createArchiveConfig], + }; + + // If we are running the first config step, we clean up and create the build + // directories. + if (configName === 'buildMain') { + fs.removeSync(distDir); + fs.removeSync(publishDir); + fs.mkdirpSync(publishDir); + } + + return configs[configName]; } -let exportedConfigs = [pluginConfig]; +let exportedConfigs = []; try { - exportedConfigs = addExtraScriptConfigs(exportedConfigs, userConfig); - exportedConfigs = addLastConfigStep(exportedConfigs); + exportedConfigs = main(process.argv); } catch (error) { console.error(chalk.red(error.message)); process.exit(1); } +if (!exportedConfigs.length) { + // Nothing to do - for example where there are no external scripts to + // compile. + process.exit(0); +} + module.exports = exportedConfigs;