Merge branch 'dev' into plugin_editor_context_menu

plugin_editor_context_menu
Laurent Cozic 2020-11-13 23:40:28 +00:00
commit 46c40ce9fa
129 changed files with 6884 additions and 741 deletions

View File

@ -433,6 +433,9 @@ packages/app-desktop/gui/MainScreen/commands/showSpellCheckerMenu.js.map
packages/app-desktop/gui/MainScreen/commands/toggleEditors.d.ts
packages/app-desktop/gui/MainScreen/commands/toggleEditors.js
packages/app-desktop/gui/MainScreen/commands/toggleEditors.js.map
packages/app-desktop/gui/MainScreen/commands/toggleLayoutMoveMode.d.ts
packages/app-desktop/gui/MainScreen/commands/toggleLayoutMoveMode.js
packages/app-desktop/gui/MainScreen/commands/toggleLayoutMoveMode.js.map
packages/app-desktop/gui/MainScreen/commands/toggleNoteList.d.ts
packages/app-desktop/gui/MainScreen/commands/toggleNoteList.js
packages/app-desktop/gui/MainScreen/commands/toggleNoteList.js.map
@ -583,6 +586,9 @@ packages/app-desktop/gui/NoteListControls/commands/focusSearch.js.map
packages/app-desktop/gui/NoteListItem.d.ts
packages/app-desktop/gui/NoteListItem.js
packages/app-desktop/gui/NoteListItem.js.map
packages/app-desktop/gui/NoteListWrapper/NoteListWrapper.d.ts
packages/app-desktop/gui/NoteListWrapper/NoteListWrapper.js
packages/app-desktop/gui/NoteListWrapper/NoteListWrapper.js.map
packages/app-desktop/gui/NoteTextViewer.d.ts
packages/app-desktop/gui/NoteTextViewer.js
packages/app-desktop/gui/NoteTextViewer.js.map
@ -592,15 +598,60 @@ packages/app-desktop/gui/NoteToolbar/NoteToolbar.js.map
packages/app-desktop/gui/OneDriveLoginScreen.d.ts
packages/app-desktop/gui/OneDriveLoginScreen.js
packages/app-desktop/gui/OneDriveLoginScreen.js.map
packages/app-desktop/gui/ResizableLayout/MoveButtons.d.ts
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
packages/app-desktop/gui/ResizableLayout/MoveButtons.js.map
packages/app-desktop/gui/ResizableLayout/ResizableLayout.d.ts
packages/app-desktop/gui/ResizableLayout/ResizableLayout.js
packages/app-desktop/gui/ResizableLayout/ResizableLayout.js.map
packages/app-desktop/gui/ResizableLayout/hooks/useLayoutItemSizes.d.ts
packages/app-desktop/gui/ResizableLayout/hooks/useLayoutItemSizes.js
packages/app-desktop/gui/ResizableLayout/hooks/useLayoutItemSizes.js.map
packages/app-desktop/gui/ResizableLayout/hooks/useWindowResizeEvent.d.ts
packages/app-desktop/gui/ResizableLayout/hooks/useWindowResizeEvent.js
packages/app-desktop/gui/ResizableLayout/hooks/useWindowResizeEvent.js.map
packages/app-desktop/gui/ResizableLayout/utils/findItemByKey.d.ts
packages/app-desktop/gui/ResizableLayout/utils/findItemByKey.js
packages/app-desktop/gui/ResizableLayout/utils/findItemByKey.js.map
packages/app-desktop/gui/ResizableLayout/utils/isTempContainer.d.ts
packages/app-desktop/gui/ResizableLayout/utils/isTempContainer.js
packages/app-desktop/gui/ResizableLayout/utils/isTempContainer.js.map
packages/app-desktop/gui/ResizableLayout/utils/iterateItems.d.ts
packages/app-desktop/gui/ResizableLayout/utils/iterateItems.js
packages/app-desktop/gui/ResizableLayout/utils/iterateItems.js.map
packages/app-desktop/gui/ResizableLayout/utils/layoutItemProp.d.ts
packages/app-desktop/gui/ResizableLayout/utils/layoutItemProp.js
packages/app-desktop/gui/ResizableLayout/utils/layoutItemProp.js.map
packages/app-desktop/gui/ResizableLayout/utils/movements.d.ts
packages/app-desktop/gui/ResizableLayout/utils/movements.js
packages/app-desktop/gui/ResizableLayout/utils/movements.js.map
packages/app-desktop/gui/ResizableLayout/utils/movements.test.d.ts
packages/app-desktop/gui/ResizableLayout/utils/movements.test.js
packages/app-desktop/gui/ResizableLayout/utils/movements.test.js.map
packages/app-desktop/gui/ResizableLayout/utils/persist.d.ts
packages/app-desktop/gui/ResizableLayout/utils/persist.js
packages/app-desktop/gui/ResizableLayout/utils/persist.js.map
packages/app-desktop/gui/ResizableLayout/utils/persist.test.d.ts
packages/app-desktop/gui/ResizableLayout/utils/persist.test.js
packages/app-desktop/gui/ResizableLayout/utils/persist.test.js.map
packages/app-desktop/gui/ResizableLayout/utils/removeItem.d.ts
packages/app-desktop/gui/ResizableLayout/utils/removeItem.js
packages/app-desktop/gui/ResizableLayout/utils/removeItem.js.map
packages/app-desktop/gui/ResizableLayout/utils/setLayoutItemProps.d.ts
packages/app-desktop/gui/ResizableLayout/utils/setLayoutItemProps.js
packages/app-desktop/gui/ResizableLayout/utils/setLayoutItemProps.js.map
packages/app-desktop/gui/ResizableLayout/utils/style.d.ts
packages/app-desktop/gui/ResizableLayout/utils/style.js
packages/app-desktop/gui/ResizableLayout/utils/style.js.map
packages/app-desktop/gui/ResizableLayout/utils/types.d.ts
packages/app-desktop/gui/ResizableLayout/utils/types.js
packages/app-desktop/gui/ResizableLayout/utils/types.js.map
packages/app-desktop/gui/ResizableLayout/utils/useLayoutItemSizes.d.ts
packages/app-desktop/gui/ResizableLayout/utils/useLayoutItemSizes.js
packages/app-desktop/gui/ResizableLayout/utils/useLayoutItemSizes.js.map
packages/app-desktop/gui/ResizableLayout/utils/useLayoutItemSizes.test.d.ts
packages/app-desktop/gui/ResizableLayout/utils/useLayoutItemSizes.test.js
packages/app-desktop/gui/ResizableLayout/utils/useLayoutItemSizes.test.js.map
packages/app-desktop/gui/ResizableLayout/utils/useWindowResizeEvent.d.ts
packages/app-desktop/gui/ResizableLayout/utils/useWindowResizeEvent.js
packages/app-desktop/gui/ResizableLayout/utils/useWindowResizeEvent.js.map
packages/app-desktop/gui/ResizableLayout/utils/validateLayout.d.ts
packages/app-desktop/gui/ResizableLayout/utils/validateLayout.js
packages/app-desktop/gui/ResizableLayout/utils/validateLayout.js.map
packages/app-desktop/gui/ResourceScreen.d.ts
packages/app-desktop/gui/ResourceScreen.js
packages/app-desktop/gui/ResourceScreen.js.map
@ -679,6 +730,9 @@ packages/app-desktop/plugins/GotoAnything.js.map
packages/app-desktop/services/bridge.d.ts
packages/app-desktop/services/bridge.js
packages/app-desktop/services/bridge.js.map
packages/app-desktop/services/commands/stateToWhenClauseContext.d.ts
packages/app-desktop/services/commands/stateToWhenClauseContext.js
packages/app-desktop/services/commands/stateToWhenClauseContext.js.map
packages/app-desktop/services/commands/types.d.ts
packages/app-desktop/services/commands/types.js
packages/app-desktop/services/commands/types.js.map

View File

@ -25,6 +25,9 @@ module.exports = {
'afterEach': 'readonly',
'jasmine': 'readonly',
// Jest variables
'test': 'readonly',
// React Native variables
'__DEV__': 'readonly',

66
.gitignore vendored
View File

@ -425,6 +425,9 @@ packages/app-desktop/gui/MainScreen/commands/showSpellCheckerMenu.js.map
packages/app-desktop/gui/MainScreen/commands/toggleEditors.d.ts
packages/app-desktop/gui/MainScreen/commands/toggleEditors.js
packages/app-desktop/gui/MainScreen/commands/toggleEditors.js.map
packages/app-desktop/gui/MainScreen/commands/toggleLayoutMoveMode.d.ts
packages/app-desktop/gui/MainScreen/commands/toggleLayoutMoveMode.js
packages/app-desktop/gui/MainScreen/commands/toggleLayoutMoveMode.js.map
packages/app-desktop/gui/MainScreen/commands/toggleNoteList.d.ts
packages/app-desktop/gui/MainScreen/commands/toggleNoteList.js
packages/app-desktop/gui/MainScreen/commands/toggleNoteList.js.map
@ -575,6 +578,9 @@ packages/app-desktop/gui/NoteListControls/commands/focusSearch.js.map
packages/app-desktop/gui/NoteListItem.d.ts
packages/app-desktop/gui/NoteListItem.js
packages/app-desktop/gui/NoteListItem.js.map
packages/app-desktop/gui/NoteListWrapper/NoteListWrapper.d.ts
packages/app-desktop/gui/NoteListWrapper/NoteListWrapper.js
packages/app-desktop/gui/NoteListWrapper/NoteListWrapper.js.map
packages/app-desktop/gui/NoteTextViewer.d.ts
packages/app-desktop/gui/NoteTextViewer.js
packages/app-desktop/gui/NoteTextViewer.js.map
@ -584,15 +590,60 @@ packages/app-desktop/gui/NoteToolbar/NoteToolbar.js.map
packages/app-desktop/gui/OneDriveLoginScreen.d.ts
packages/app-desktop/gui/OneDriveLoginScreen.js
packages/app-desktop/gui/OneDriveLoginScreen.js.map
packages/app-desktop/gui/ResizableLayout/MoveButtons.d.ts
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
packages/app-desktop/gui/ResizableLayout/MoveButtons.js.map
packages/app-desktop/gui/ResizableLayout/ResizableLayout.d.ts
packages/app-desktop/gui/ResizableLayout/ResizableLayout.js
packages/app-desktop/gui/ResizableLayout/ResizableLayout.js.map
packages/app-desktop/gui/ResizableLayout/hooks/useLayoutItemSizes.d.ts
packages/app-desktop/gui/ResizableLayout/hooks/useLayoutItemSizes.js
packages/app-desktop/gui/ResizableLayout/hooks/useLayoutItemSizes.js.map
packages/app-desktop/gui/ResizableLayout/hooks/useWindowResizeEvent.d.ts
packages/app-desktop/gui/ResizableLayout/hooks/useWindowResizeEvent.js
packages/app-desktop/gui/ResizableLayout/hooks/useWindowResizeEvent.js.map
packages/app-desktop/gui/ResizableLayout/utils/findItemByKey.d.ts
packages/app-desktop/gui/ResizableLayout/utils/findItemByKey.js
packages/app-desktop/gui/ResizableLayout/utils/findItemByKey.js.map
packages/app-desktop/gui/ResizableLayout/utils/isTempContainer.d.ts
packages/app-desktop/gui/ResizableLayout/utils/isTempContainer.js
packages/app-desktop/gui/ResizableLayout/utils/isTempContainer.js.map
packages/app-desktop/gui/ResizableLayout/utils/iterateItems.d.ts
packages/app-desktop/gui/ResizableLayout/utils/iterateItems.js
packages/app-desktop/gui/ResizableLayout/utils/iterateItems.js.map
packages/app-desktop/gui/ResizableLayout/utils/layoutItemProp.d.ts
packages/app-desktop/gui/ResizableLayout/utils/layoutItemProp.js
packages/app-desktop/gui/ResizableLayout/utils/layoutItemProp.js.map
packages/app-desktop/gui/ResizableLayout/utils/movements.d.ts
packages/app-desktop/gui/ResizableLayout/utils/movements.js
packages/app-desktop/gui/ResizableLayout/utils/movements.js.map
packages/app-desktop/gui/ResizableLayout/utils/movements.test.d.ts
packages/app-desktop/gui/ResizableLayout/utils/movements.test.js
packages/app-desktop/gui/ResizableLayout/utils/movements.test.js.map
packages/app-desktop/gui/ResizableLayout/utils/persist.d.ts
packages/app-desktop/gui/ResizableLayout/utils/persist.js
packages/app-desktop/gui/ResizableLayout/utils/persist.js.map
packages/app-desktop/gui/ResizableLayout/utils/persist.test.d.ts
packages/app-desktop/gui/ResizableLayout/utils/persist.test.js
packages/app-desktop/gui/ResizableLayout/utils/persist.test.js.map
packages/app-desktop/gui/ResizableLayout/utils/removeItem.d.ts
packages/app-desktop/gui/ResizableLayout/utils/removeItem.js
packages/app-desktop/gui/ResizableLayout/utils/removeItem.js.map
packages/app-desktop/gui/ResizableLayout/utils/setLayoutItemProps.d.ts
packages/app-desktop/gui/ResizableLayout/utils/setLayoutItemProps.js
packages/app-desktop/gui/ResizableLayout/utils/setLayoutItemProps.js.map
packages/app-desktop/gui/ResizableLayout/utils/style.d.ts
packages/app-desktop/gui/ResizableLayout/utils/style.js
packages/app-desktop/gui/ResizableLayout/utils/style.js.map
packages/app-desktop/gui/ResizableLayout/utils/types.d.ts
packages/app-desktop/gui/ResizableLayout/utils/types.js
packages/app-desktop/gui/ResizableLayout/utils/types.js.map
packages/app-desktop/gui/ResizableLayout/utils/useLayoutItemSizes.d.ts
packages/app-desktop/gui/ResizableLayout/utils/useLayoutItemSizes.js
packages/app-desktop/gui/ResizableLayout/utils/useLayoutItemSizes.js.map
packages/app-desktop/gui/ResizableLayout/utils/useLayoutItemSizes.test.d.ts
packages/app-desktop/gui/ResizableLayout/utils/useLayoutItemSizes.test.js
packages/app-desktop/gui/ResizableLayout/utils/useLayoutItemSizes.test.js.map
packages/app-desktop/gui/ResizableLayout/utils/useWindowResizeEvent.d.ts
packages/app-desktop/gui/ResizableLayout/utils/useWindowResizeEvent.js
packages/app-desktop/gui/ResizableLayout/utils/useWindowResizeEvent.js.map
packages/app-desktop/gui/ResizableLayout/utils/validateLayout.d.ts
packages/app-desktop/gui/ResizableLayout/utils/validateLayout.js
packages/app-desktop/gui/ResizableLayout/utils/validateLayout.js.map
packages/app-desktop/gui/ResourceScreen.d.ts
packages/app-desktop/gui/ResourceScreen.js
packages/app-desktop/gui/ResourceScreen.js.map
@ -671,6 +722,9 @@ packages/app-desktop/plugins/GotoAnything.js.map
packages/app-desktop/services/bridge.d.ts
packages/app-desktop/services/bridge.js
packages/app-desktop/services/bridge.js.map
packages/app-desktop/services/commands/stateToWhenClauseContext.d.ts
packages/app-desktop/services/commands/stateToWhenClauseContext.js
packages/app-desktop/services/commands/stateToWhenClauseContext.js.map
packages/app-desktop/services/commands/types.d.ts
packages/app-desktop/services/commands/types.js
packages/app-desktop/services/commands/types.js.map

View File

@ -8,6 +8,7 @@
"files.exclude": {
"lerna-debug.log": true,
"_mydocs/mdtest/": true,
"./packages/lib/plugin_types": true,
"_releases/": true,
"_vieux/": true,
".gitignore": true,

View File

@ -1,6 +1,7 @@
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
import ToolbarButtonUtils from '@joplin/lib/services/commands/ToolbarButtonUtils';
import CommandService, { CommandDeclaration, CommandRuntime } from '@joplin/lib/services/CommandService';
import stateToWhenClauseContext from '@joplin/lib/services/commands/stateToWhenClauseContext';
import KeymapService from '@joplin/lib/services/KeymapService';
const { asyncTest, setupDatabaseAndSynchronizer, switchClient, expectThrow, expectNotThrow } = require('./test-utils.js');
@ -17,7 +18,7 @@ function newService(): CommandService {
return {};
},
};
service.initialize(mockStore, true);
service.initialize(mockStore, true, stateToWhenClauseContext);
return service;
}

View File

@ -17,7 +17,7 @@ export default class JoplinViewsDialogs {
/**
* Creates a new dialog
*/
create(): Promise<ViewHandle>;
create(id: string): Promise<ViewHandle>;
/**
* Displays a message box with OK/Cancel buttons. Returns the button index that was clicked - "0" for OK and "1" for "Cancel"
*/

View File

@ -12,5 +12,5 @@ export default class JoplinViewsMenuItems {
/**
* Creates a new menu item and associate it with the given command. You can specify under which menu the item should appear using the `location` parameter.
*/
create(commandName: string, location?: MenuItemLocation, options?: CreateMenuItemOptions): Promise<void>;
create(id: string, commandName: string, location?: MenuItemLocation, options?: CreateMenuItemOptions): Promise<void>;
}

View File

@ -14,5 +14,5 @@ export default class JoplinViewsMenus {
* Creates a new menu from the provided menu items and place it at the given location. As of now, it is only possible to place the
* menu as a sub-menu of the application build-in menus.
*/
create(label: string, menuItems: MenuItem[], location?: MenuItemLocation): Promise<void>;
create(id: string, label: string, menuItems: MenuItem[], location?: MenuItemLocation): Promise<void>;
}

View File

@ -1,8 +1,11 @@
import Plugin from '../Plugin';
import { ViewHandle } from './types';
/**
* Allows creating and managing view panels. View panels currently are displayed at the right of the sidebar and allows displaying any HTML content (within a webview) and update it in real-time. For example
* it could be used to display a table of content for the active note, or display various metadata or graph.
* Allows creating and managing view panels. View panels currently are
* displayed at the right of the sidebar and allows displaying any HTML
* content (within a webview) and update it in real-time. For example it
* could be used to display a table of content for the active note, or
* display various metadata or graph.
*
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/toc)
*/
@ -14,7 +17,7 @@ export default class JoplinViewsPanels {
/**
* Creates a new panel
*/
create(): Promise<ViewHandle>;
create(id: string): Promise<ViewHandle>;
/**
* Sets the panel webview HTML
*/

View File

@ -12,5 +12,5 @@ export default class JoplinViewsToolbarButtons {
/**
* Creates a new toolbar button and associate it with the given command.
*/
create(commandName: string, location: ToolbarButtonLocation): Promise<void>;
create(id: string, commandName: string, location: ToolbarButtonLocation): Promise<void>;
}

View File

@ -17,7 +17,7 @@ export default class JoplinViewsDialogs {
/**
* Creates a new dialog
*/
create(): Promise<ViewHandle>;
create(id: string): Promise<ViewHandle>;
/**
* Displays a message box with OK/Cancel buttons. Returns the button index that was clicked - "0" for OK and "1" for "Cancel"
*/

View File

@ -12,5 +12,5 @@ export default class JoplinViewsMenuItems {
/**
* Creates a new menu item and associate it with the given command. You can specify under which menu the item should appear using the `location` parameter.
*/
create(commandName: string, location?: MenuItemLocation, options?: CreateMenuItemOptions): Promise<void>;
create(id: string, commandName: string, location?: MenuItemLocation, options?: CreateMenuItemOptions): Promise<void>;
}

View File

@ -14,5 +14,5 @@ export default class JoplinViewsMenus {
* Creates a new menu from the provided menu items and place it at the given location. As of now, it is only possible to place the
* menu as a sub-menu of the application build-in menus.
*/
create(label: string, menuItems: MenuItem[], location?: MenuItemLocation): Promise<void>;
create(id: string, label: string, menuItems: MenuItem[], location?: MenuItemLocation): Promise<void>;
}

View File

@ -1,8 +1,11 @@
import Plugin from '../Plugin';
import { ViewHandle } from './types';
/**
* Allows creating and managing view panels. View panels currently are displayed at the right of the sidebar and allows displaying any HTML content (within a webview) and update it in real-time. For example
* it could be used to display a table of content for the active note, or display various metadata or graph.
* Allows creating and managing view panels. View panels currently are
* displayed at the right of the sidebar and allows displaying any HTML
* content (within a webview) and update it in real-time. For example it
* could be used to display a table of content for the active note, or
* display various metadata or graph.
*
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/toc)
*/
@ -14,7 +17,7 @@ export default class JoplinViewsPanels {
/**
* Creates a new panel
*/
create(): Promise<ViewHandle>;
create(id: string): Promise<ViewHandle>;
/**
* Sets the panel webview HTML
*/

View File

@ -12,5 +12,5 @@ export default class JoplinViewsToolbarButtons {
/**
* Creates a new toolbar button and associate it with the given command.
*/
create(commandName: string, location: ToolbarButtonLocation): Promise<void>;
create(id: string, commandName: string, location: ToolbarButtonLocation): Promise<void>;
}

View File

@ -4,12 +4,12 @@ joplin.plugins.register({
onStart: async function() {
const dialogs = joplin.views.dialogs;
const handle = await dialogs.create();
const handle = await dialogs.create('myDialog1');
await dialogs.setHtml(handle, '<p>Testing dialog with default buttons</p><p>Second line</p><p>Third line</p>');
const result = await dialogs.open(handle);
alert('Got result: ' + JSON.stringify(result));
const handle2 = await dialogs.create();
const handle2 = await dialogs.create('myDialog2');
await dialogs.setHtml(handle2, '<p>Testing dialog with custom buttons</p><p>Second line</p><p>Third line</p>');
await dialogs.setButtons(handle2, [
{

View File

@ -17,7 +17,7 @@ export default class JoplinViewsDialogs {
/**
* Creates a new dialog
*/
create(): Promise<ViewHandle>;
create(id: string): Promise<ViewHandle>;
/**
* Displays a message box with OK/Cancel buttons. Returns the button index that was clicked - "0" for OK and "1" for "Cancel"
*/

View File

@ -12,5 +12,5 @@ export default class JoplinViewsMenuItems {
/**
* Creates a new menu item and associate it with the given command. You can specify under which menu the item should appear using the `location` parameter.
*/
create(commandName: string, location?: MenuItemLocation, options?: CreateMenuItemOptions): Promise<void>;
create(id: string, commandName: string, location?: MenuItemLocation, options?: CreateMenuItemOptions): Promise<void>;
}

View File

@ -14,5 +14,5 @@ export default class JoplinViewsMenus {
* Creates a new menu from the provided menu items and place it at the given location. As of now, it is only possible to place the
* menu as a sub-menu of the application build-in menus.
*/
create(label: string, menuItems: MenuItem[], location?: MenuItemLocation): Promise<void>;
create(id: string, label: string, menuItems: MenuItem[], location?: MenuItemLocation): Promise<void>;
}

View File

@ -1,8 +1,11 @@
import Plugin from '../Plugin';
import { ViewHandle } from './types';
/**
* Allows creating and managing view panels. View panels currently are displayed at the right of the sidebar and allows displaying any HTML content (within a webview) and update it in real-time. For example
* it could be used to display a table of content for the active note, or display various metadata or graph.
* Allows creating and managing view panels. View panels currently are
* displayed at the right of the sidebar and allows displaying any HTML
* content (within a webview) and update it in real-time. For example it
* could be used to display a table of content for the active note, or
* display various metadata or graph.
*
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/toc)
*/
@ -14,7 +17,7 @@ export default class JoplinViewsPanels {
/**
* Creates a new panel
*/
create(): Promise<ViewHandle>;
create(id: string): Promise<ViewHandle>;
/**
* Sets the panel webview HTML
*/

View File

@ -12,5 +12,5 @@ export default class JoplinViewsToolbarButtons {
/**
* Creates a new toolbar button and associate it with the given command.
*/
create(commandName: string, location: ToolbarButtonLocation): Promise<void>;
create(id: string, commandName: string, location: ToolbarButtonLocation): Promise<void>;
}

View File

@ -17,7 +17,7 @@ export default class JoplinViewsDialogs {
/**
* Creates a new dialog
*/
create(): Promise<ViewHandle>;
create(id: string): Promise<ViewHandle>;
/**
* Displays a message box with OK/Cancel buttons. Returns the button index that was clicked - "0" for OK and "1" for "Cancel"
*/

View File

@ -12,5 +12,5 @@ export default class JoplinViewsMenuItems {
/**
* Creates a new menu item and associate it with the given command. You can specify under which menu the item should appear using the `location` parameter.
*/
create(commandName: string, location?: MenuItemLocation, options?: CreateMenuItemOptions): Promise<void>;
create(id: string, commandName: string, location?: MenuItemLocation, options?: CreateMenuItemOptions): Promise<void>;
}

View File

@ -14,5 +14,5 @@ export default class JoplinViewsMenus {
* Creates a new menu from the provided menu items and place it at the given location. As of now, it is only possible to place the
* menu as a sub-menu of the application build-in menus.
*/
create(label: string, menuItems: MenuItem[], location?: MenuItemLocation): Promise<void>;
create(id: string, label: string, menuItems: MenuItem[], location?: MenuItemLocation): Promise<void>;
}

View File

@ -1,8 +1,11 @@
import Plugin from '../Plugin';
import { ViewHandle } from './types';
/**
* Allows creating and managing view panels. View panels currently are displayed at the right of the sidebar and allows displaying any HTML content (within a webview) and update it in real-time. For example
* it could be used to display a table of content for the active note, or display various metadata or graph.
* Allows creating and managing view panels. View panels currently are
* displayed at the right of the sidebar and allows displaying any HTML
* content (within a webview) and update it in real-time. For example it
* could be used to display a table of content for the active note, or
* display various metadata or graph.
*
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/toc)
*/
@ -14,7 +17,7 @@ export default class JoplinViewsPanels {
/**
* Creates a new panel
*/
create(): Promise<ViewHandle>;
create(id: string): Promise<ViewHandle>;
/**
* Sets the panel webview HTML
*/

View File

@ -12,5 +12,5 @@ export default class JoplinViewsToolbarButtons {
/**
* Creates a new toolbar button and associate it with the given command.
*/
create(commandName: string, location: ToolbarButtonLocation): Promise<void>;
create(id: string, commandName: string, location: ToolbarButtonLocation): Promise<void>;
}

View File

@ -1,6 +1,6 @@
import Plugin from '../Plugin';
import Joplin from './Joplin';
import Logger from 'lib/Logger';
import Logger from '../../../Logger';
/**
* @ignore
*/

View File

@ -7,7 +7,7 @@ import JoplinCommands from './JoplinCommands';
import JoplinViews from './JoplinViews';
import JoplinInterop from './JoplinInterop';
import JoplinSettings from './JoplinSettings';
import Logger from 'lib/Logger';
import Logger from '../../../Logger';
/**
* This is the main entry point to the Joplin API. You can access various services using the provided accessors.
*/

View File

@ -1,25 +1,35 @@
import { Command } from './types';
/**
* This class allows executing or registering new Joplin commands. Commands can be executed or associated with
* {@link JoplinViewsToolbarButtons | toolbar buttons} or {@link JoplinViewsMenuItems | menu items}.
* This class allows executing or registering new Joplin commands. Commands
* can be executed or associated with
* {@link JoplinViewsToolbarButtons | toolbar buttons} or
* {@link JoplinViewsMenuItems | menu items}.
*
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/CliClient/tests/support/plugins/register_command)
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/register_command)
*
* ## Executing Joplin's internal commands
*
* It is also possible to execute internal Joplin's commands which, as of now, are not well documented.
* You can find the list directly on GitHub though at the following locations:
* It is also possible to execute internal Joplin's commands which, as of
* now, are not well documented. You can find the list directly on GitHub
* though at the following locations:
*
* https://github.com/laurent22/joplin/tree/dev/ElectronClient/gui/MainScreen/commands
* https://github.com/laurent22/joplin/tree/dev/ElectronClient/commands
* https://github.com/laurent22/joplin/tree/dev/ElectronClient/gui/NoteEditor/commands/editorCommandDeclarations.ts
* * [Main screen commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/gui/MainScreen/commands)
* * [Global commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/commands)
* * [Editor commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.ts)
*
* To view what arguments are supported, you can open any of these files and look at the `execute()` command.
* To view what arguments are supported, you can open any of these files
* and look at the `execute()` command.
*/
export default class JoplinCommands {
/**
* <span class="platform-desktop">desktop</span> Executes the given command.
* The `props` are the arguments passed to the command, and they vary based on the command
* <span class="platform-desktop">desktop</span> Executes the given
* command.
*
* The command can take any number of arguments, and the supported
* arguments will vary based on the command. For custom commands, this
* is the `args` passed to the `execute()` function. For built-in
* commands, you can find the supported arguments by checking the links
* above.
*
* ```typescript
* // Create a new note in the current notebook:
@ -27,10 +37,10 @@ export default class JoplinCommands {
*
* // Create a new sub-notebook under the provided notebook
* // Note: internally, notebooks are called "folders".
* await joplin.commands.execute('newFolder', { parent_id: "SOME_FOLDER_ID" });
* await joplin.commands.execute('newFolder', "SOME_FOLDER_ID");
* ```
*/
execute(commandName: string, props?: any): Promise<any>;
execute(commandName: string, ...args: any[]): Promise<any | void>;
/**
* <span class="platform-desktop">desktop</span> Registers a new command.
*

View File

@ -6,7 +6,7 @@ import { Path } from './types';
*
* This is also what you would use to search notes, via the `search` endpoint.
*
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/CliClient/tests/support/plugins/simple)
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/simple)
*
* In general you would use the methods in this class as if you were using a REST API. There are four methods that map to GET, POST, PUT and DELETE calls.
* And each method takes these parameters:

View File

@ -2,7 +2,7 @@ import { ExportModule, ImportModule } from './types';
/**
* Provides a way to create modules to import external data into Joplin or to export notes into any arbitrary format.
*
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/CliClient/tests/support/plugins/json_export)
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/json_export)
*
* To implement an import or export module, you would simply define an object with various event handlers that are called
* by the application during the import/export process.

View File

@ -1,6 +1,6 @@
import Plugin from '../Plugin';
import Logger from 'lib/Logger';
import { Script } from './types';
import Logger from '../../../Logger';
import { ContentScriptType, Script } from './types';
/**
* This class provides access to plugin-related features.
*/
@ -21,4 +21,18 @@ export default class JoplinPlugins {
* ```
*/
register(script: Script): Promise<void>;
/**
* 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.
*
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/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`.
*/
registerContentScript(type: ContentScriptType, id: string, scriptPath: string): Promise<void>;
}

View File

@ -7,7 +7,7 @@ import { SettingItem, SettingSection } from './types';
*
* Note: Currently this API does **not** provide access to Joplin's built-in settings. This is by design as plugins that modify user settings could give unexpected results
*
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/CliClient/tests/support/plugins/settings)
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/settings)
*/
export default class JoplinSettings {
private plugin_;
@ -37,7 +37,7 @@ export default class JoplinSettings {
*
* The list of available settings is not documented yet, but can be found by looking at the source code:
*
* https://github.com/laurent22/joplin/blob/3539a452a359162c461d2849829d2d42973eab50/ReactNativeClient/lib/models/Setting.ts#L142
* https://github.com/laurent22/joplin/blob/3539a452a359162c461d2849829d2d42973eab50/packages/app-mobile/lib/models/Setting.ts#L142
*/
globalValue(key: string): Promise<any>;
}

View File

@ -5,7 +5,7 @@ import { ButtonSpec, ViewHandle, ButtonId } from './types';
* Dialogs are hidden by default and you need to call `open()` to open them. Once the user clicks on a button, the `open` call will return and provide the button ID that was
* clicked on. There is currently no "close" method since the dialog should be thought as a modal one and thus can only be closed by clicking on one of the buttons.
*
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/CliClient/tests/support/plugins/dialog)
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/dialog)
*/
export default class JoplinViewsDialogs {
private store;
@ -16,7 +16,7 @@ export default class JoplinViewsDialogs {
/**
* Creates a new dialog
*/
create(): Promise<ViewHandle>;
create(id: string): Promise<ViewHandle>;
/**
* Displays a message box with OK/Cancel buttons. Returns the button index that was clicked - "0" for OK and "1" for "Cancel"
*/

View File

@ -3,7 +3,7 @@ import Plugin from '../Plugin';
/**
* Allows creating and managing menu items.
*
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/CliClient/tests/support/plugins/register_command)
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/register_command)
*/
export default class JoplinViewsMenuItems {
private store;
@ -12,5 +12,5 @@ export default class JoplinViewsMenuItems {
/**
* Creates a new menu item and associate it with the given command. You can specify under which menu the item should appear using the `location` parameter.
*/
create(commandName: string, location?: MenuItemLocation, options?: CreateMenuItemOptions): Promise<void>;
create(id: string, commandName: string, location?: MenuItemLocation, options?: CreateMenuItemOptions): Promise<void>;
}

View File

@ -3,7 +3,7 @@ import Plugin from '../Plugin';
/**
* Allows creating menus.
*
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/CliClient/tests/support/plugins/menu)
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/menu)
*/
export default class JoplinViewsMenus {
private store;
@ -14,5 +14,5 @@ export default class JoplinViewsMenus {
* Creates a new menu from the provided menu items and place it at the given location. As of now, it is only possible to place the
* menu as a sub-menu of the application build-in menus.
*/
create(label: string, menuItems: MenuItem[], location?: MenuItemLocation): Promise<void>;
create(id: string, label: string, menuItems: MenuItem[], location?: MenuItemLocation): Promise<void>;
}

View File

@ -1,10 +1,13 @@
import Plugin from '../Plugin';
import { ViewHandle } from './types';
/**
* Allows creating and managing view panels. View panels currently are displayed at the right of the sidebar and allows displaying any HTML content (within a webview) and update it in real-time. For example
* it could be used to display a table of content for the active note, or display various metadata or graph.
* Allows creating and managing view panels. View panels currently are
* displayed at the right of the sidebar and allows displaying any HTML
* content (within a webview) and update it in real-time. For example it
* could be used to display a table of content for the active note, or
* display various metadata or graph.
*
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/CliClient/tests/support/plugins/toc)
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/toc)
*/
export default class JoplinViewsPanels {
private store;
@ -14,7 +17,7 @@ export default class JoplinViewsPanels {
/**
* Creates a new panel
*/
create(): Promise<ViewHandle>;
create(id: string): Promise<ViewHandle>;
/**
* Sets the panel webview HTML
*/

View File

@ -3,7 +3,7 @@ import Plugin from '../Plugin';
/**
* Allows creating and managing toolbar buttons.
*
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/CliClient/tests/support/plugins/register_command)
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/register_command)
*/
export default class JoplinViewsToolbarButtons {
private store;
@ -12,5 +12,5 @@ export default class JoplinViewsToolbarButtons {
/**
* Creates a new toolbar button and associate it with the given command.
*/
create(commandName: string, location: ToolbarButtonLocation): Promise<void>;
create(id: string, commandName: string, location: ToolbarButtonLocation): Promise<void>;
}

View File

@ -2,7 +2,7 @@
* The workspace service provides access to all the parts of Joplin that are being worked on - i.e. the currently selected notes or notebooks as well
* as various related events, such as when a new note is selected, or when the note content changes.
*
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/CliClient/tests/support/plugins)
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins)
*/
export default class JoplinWorkspace {
private store;

View File

@ -3,12 +3,48 @@
// =================================================================
export interface Command {
name: string
label: string
iconName?: string,
execute(props:any):Promise<any>
isEnabled?(props:any):boolean
mapStateToProps?(state:any):any
/**
* Name of command - must be globally unique
*/
name: string;
/**
* Label to be displayed on menu items or keyboard shortcut editor for example.
* If it is missing, it's assumed it's a private command, to be called programmatically only.
* In that case the command will not appear in the shortcut editor or command panel, and logically
* should not be used as a menu item.
*/
label?: string;
/**
* Icon to be used on toolbar buttons for example
*/
iconName?: string;
/**
* Code to be ran when the command is executed. It may return a result.
*/
execute(...args: any[]): Promise<any | void>;
/**
* Defines whether the command should be enabled or disabled, which in turns affects
* the enabled state of any associated button or menu item.
*
* The condition should be expressed as a "when-clause" (as in Visual Studio Code). It's a simple boolean expression that evaluates to
* `true` or `false`. It supports the following operators:
*
* Operator | Symbol | Example
* -- | -- | --
* Equality | == | "editorType == markdown"
* Inequality | != | "currentScreen != config"
* Or | \|\| | "noteIsTodo \|\| noteTodoCompleted"
* And | && | "oneNoteSelected && !inConflictFolder"
*
* Currently the supported context variables aren't documented, but you can [find the list here](https://github.com/laurent22/joplin/blob/dev/packages/app-mobile/lib/services/commands/stateToWhenClauseContext.ts).
*
* Note: Commands are enabled by default unless you use this property.
*/
enabledCondition?: string;
}
// =================================================================
@ -26,7 +62,7 @@ export enum ImportModuleOutputFormat {
}
/**
* Used to implement a module to export data from Joplin. [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/CliClient/tests/support/plugins/json_export) for an example.
* Used to implement a module to export data from Joplin. [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/json_export) for an example.
*
* In general, all the event handlers you'll need to implement take a `context` object as a first argument. This object will contain the export or import path as well as various optional properties, such as which notes or notebooks need to be exported.
*
@ -36,113 +72,113 @@ export interface ExportModule {
/**
* The format to be exported, eg "enex", "jex", "json", etc.
*/
format: string,
format: string;
/**
* The description that will appear in the UI, for example in the menu item.
*/
description: string,
description: string;
/**
* Whether the module will export a single file or multiple files in a directory. It affects the open dialog that will be presented to the user when using your exporter.
*/
target: FileSystemItem,
target: FileSystemItem;
/**
* Only applies to single file exporters or importers
* It tells whether the format can package multiple notes into one file.
* For example JEX or ENEX can, but HTML cannot.
*/
isNoteArchive: boolean,
isNoteArchive: boolean;
/**
* The extensions of the files exported by your module. For example, it is `["htm", "html"]` for the HTML module, and just `["jex"]` for the JEX module.
*/
fileExtensions?: string[],
fileExtensions?: string[];
/**
* Called when the export process starts.
*/
onInit(context:ExportContext): Promise<void>;
onInit(context: ExportContext): Promise<void>;
/**
* Called when an item needs to be processed. An "item" can be any Joplin object, such as a note, a folder, a notebook, etc.
*/
onProcessItem(context:ExportContext, itemType:number, item:any):Promise<void>;
onProcessItem(context: ExportContext, itemType: number, item: any): Promise<void>;
/**
* Called when a resource file needs to be exported.
*/
onProcessResource(context:ExportContext, resource:any, filePath:string):Promise<void>;
onProcessResource(context: ExportContext, resource: any, filePath: string): Promise<void>;
/**
* Called when the export process is done.
*/
onClose(context:ExportContext):Promise<void>;
onClose(context: ExportContext): Promise<void>;
}
export interface ImportModule {
/**
* The format to be exported, eg "enex", "jex", "json", etc.
*/
format: string,
format: string;
/**
* The description that will appear in the UI, for example in the menu item.
*/
description: string,
description: string;
/**
* Only applies to single file exporters or importers
* It tells whether the format can package multiple notes into one file.
* For example JEX or ENEX can, but HTML cannot.
*/
isNoteArchive: boolean,
isNoteArchive: boolean;
/**
* The type of sources that are supported by the module. Tells whether the module can import files or directories or both.
*/
sources: FileSystemItem[],
sources: FileSystemItem[];
/**
* Tells the file extensions of the exported files.
*/
fileExtensions?: string[],
fileExtensions?: string[];
/**
* Tells the type of notes that will be generated, either HTML or Markdown (default).
*/
outputFormat?: ImportModuleOutputFormat,
outputFormat?: ImportModuleOutputFormat;
/**
* Called when the import process starts. There is only one event handler within which you should import the complete data.
*/
onExec(context:ImportContext): Promise<void>;
onExec(context: ImportContext): Promise<void>;
}
export interface ExportOptions {
format?: string,
path?:string,
sourceFolderIds?: string[],
sourceNoteIds?: string[],
modulePath?:string,
target?:FileSystemItem,
format?: string;
path?: string;
sourceFolderIds?: string[];
sourceNoteIds?: string[];
modulePath?: string;
target?: FileSystemItem;
}
export interface ExportContext {
destPath: string,
options: ExportOptions,
destPath: string;
options: ExportOptions;
/**
* You can attach your own custom data using this propery - it will then be passed to each event handler, allowing you to keep state from one event to the next.
*/
userData?: any,
userData?: any;
}
export interface ImportContext {
sourcePath: string,
options: any,
warnings: string[],
sourcePath: string;
options: any;
warnings: string[];
}
// =================================================================
@ -150,7 +186,7 @@ export interface ImportContext {
// =================================================================
export interface Script {
onStart?(event:any):Promise<void>,
onStart?(event: any): Promise<void>;
}
// =================================================================
@ -158,7 +194,7 @@ export interface Script {
// =================================================================
export interface CreateMenuItemOptions {
accelerator: string,
accelerator: string;
}
export enum MenuItemLocation {
@ -172,10 +208,26 @@ export enum MenuItemLocation {
}
export interface MenuItem {
commandName?: string,
accelerator?: string,
submenu?: MenuItem[],
label?: string,
/**
* Command that should be associated with the menu item. All menu item should
* have a command associated with them unless they are a sub-menu.
*/
commandName?: string;
/**
* Accelerator associated with the menu item
*/
accelerator?: string;
/**
* Menu items that should appear below this menu item. Allows creating a menu tree.
*/
submenu?: MenuItem[];
/**
* Menu item label. If not specified, the command label will be used instead.
*/
label?: string;
}
// =================================================================
@ -183,9 +235,9 @@ export interface MenuItem {
// =================================================================
export interface ButtonSpec {
id: ButtonId,
title?: string,
onClick?():void,
id: ButtonId;
title?: string;
onClick?(): void;
}
export type ButtonId = string;
@ -225,28 +277,28 @@ export enum SettingItemType {
// Redefine a simplified interface to mask internal details
// and to remove function calls as they would have to be async.
export interface SettingItem {
value: any,
type: SettingItemType,
public: boolean,
label:string,
value: any;
type: SettingItemType;
public: boolean;
label: string;
description?:string,
isEnum?: boolean,
section?: string,
options?:any,
appTypes?:string[],
secure?: boolean,
advanced?: boolean,
minimum?: number,
maximum?: number,
step?: number,
description?: string;
isEnum?: boolean;
section?: string;
options?: any;
appTypes?: string[];
secure?: boolean;
advanced?: boolean;
minimum?: number;
maximum?: number;
step?: number;
}
export interface SettingSection {
label: string,
iconName?: string,
description?: string,
name?: string,
label: string;
iconName?: string;
description?: string;
name?: string;
}
// =================================================================
@ -261,3 +313,48 @@ export interface SettingSection {
* [2]: (Optional) Resource link.
*/
export type Path = string[];
// =================================================================
// Plugins type
// =================================================================
export enum ContentScriptType {
/**
* Registers a new Markdown-It plugin, which should follow the template below.
*
* ```javascript
* module.exports = {
* default: function(context) {
* return {
* plugin: function(markdownIt, options) {
* // ...
* },
* assets: {
* // ...
* },
* }
* }
* }
* ```
*
* - 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 information. The `options` parameter is of type [RuleOptions](https://github.com/laurent22/joplin/blob/dev/packages/app-mobile/lib/joplin-renderer/MdToHtml.ts), 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 plugin](https://github.com/laurent22/joplin/blob/dev/packages/app-mobile/lib/joplin-renderer/MdToHtml/rules/mermaid.ts) to see how the data should be structured.
*
* 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 = {
* default: function(context) {
* return {
* plugin: require('markdown-it-toc-done-right');
* }
* }
* }
* ```
*/
MarkdownItPlugin = 'markdownItPlugin',
CodeMirrorPlugin = 'codeMirrorPlugin',
}

View File

@ -2,7 +2,7 @@ import joplin from 'api';
joplin.plugins.register({
onStart: async function() {
await joplin.views.menus.create('My Menu', [
await joplin.views.menus.create('myMenu', 'My Menu', [
{
commandName: "newNote",
},

View File

@ -17,7 +17,7 @@ export default class JoplinViewsDialogs {
/**
* Creates a new dialog
*/
create(): Promise<ViewHandle>;
create(id: string): Promise<ViewHandle>;
/**
* Displays a message box with OK/Cancel buttons. Returns the button index that was clicked - "0" for OK and "1" for "Cancel"
*/

View File

@ -12,5 +12,5 @@ export default class JoplinViewsMenuItems {
/**
* Creates a new menu item and associate it with the given command. You can specify under which menu the item should appear using the `location` parameter.
*/
create(commandName: string, location?: MenuItemLocation, options?: CreateMenuItemOptions): Promise<void>;
create(id: string, commandName: string, location?: MenuItemLocation, options?: CreateMenuItemOptions): Promise<void>;
}

View File

@ -14,5 +14,5 @@ export default class JoplinViewsMenus {
* Creates a new menu from the provided menu items and place it at the given location. As of now, it is only possible to place the
* menu as a sub-menu of the application build-in menus.
*/
create(label: string, menuItems: MenuItem[], location?: MenuItemLocation): Promise<void>;
create(id: string, label: string, menuItems: MenuItem[], location?: MenuItemLocation): Promise<void>;
}

View File

@ -1,8 +1,11 @@
import Plugin from '../Plugin';
import { ViewHandle } from './types';
/**
* Allows creating and managing view panels. View panels currently are displayed at the right of the sidebar and allows displaying any HTML content (within a webview) and update it in real-time. For example
* it could be used to display a table of content for the active note, or display various metadata or graph.
* Allows creating and managing view panels. View panels currently are
* displayed at the right of the sidebar and allows displaying any HTML
* content (within a webview) and update it in real-time. For example it
* could be used to display a table of content for the active note, or
* display various metadata or graph.
*
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/toc)
*/
@ -14,7 +17,7 @@ export default class JoplinViewsPanels {
/**
* Creates a new panel
*/
create(): Promise<ViewHandle>;
create(id: string): Promise<ViewHandle>;
/**
* Sets the panel webview HTML
*/

View File

@ -12,5 +12,5 @@ export default class JoplinViewsToolbarButtons {
/**
* Creates a new toolbar button and associate it with the given command.
*/
create(commandName: string, location: ToolbarButtonLocation): Promise<void>;
create(id: string, commandName: string, location: ToolbarButtonLocation): Promise<void>;
}

View File

@ -17,7 +17,7 @@ export default class JoplinViewsDialogs {
/**
* Creates a new dialog
*/
create(): Promise<ViewHandle>;
create(id: string): Promise<ViewHandle>;
/**
* Displays a message box with OK/Cancel buttons. Returns the button index that was clicked - "0" for OK and "1" for "Cancel"
*/

View File

@ -12,5 +12,5 @@ export default class JoplinViewsMenuItems {
/**
* Creates a new menu item and associate it with the given command. You can specify under which menu the item should appear using the `location` parameter.
*/
create(commandName: string, location?: MenuItemLocation, options?: CreateMenuItemOptions): Promise<void>;
create(id: string, commandName: string, location?: MenuItemLocation, options?: CreateMenuItemOptions): Promise<void>;
}

View File

@ -14,5 +14,5 @@ export default class JoplinViewsMenus {
* Creates a new menu from the provided menu items and place it at the given location. As of now, it is only possible to place the
* menu as a sub-menu of the application build-in menus.
*/
create(label: string, menuItems: MenuItem[], location?: MenuItemLocation): Promise<void>;
create(id: string, label: string, menuItems: MenuItem[], location?: MenuItemLocation): Promise<void>;
}

View File

@ -1,8 +1,11 @@
import Plugin from '../Plugin';
import { ViewHandle } from './types';
/**
* Allows creating and managing view panels. View panels currently are displayed at the right of the sidebar and allows displaying any HTML content (within a webview) and update it in real-time. For example
* it could be used to display a table of content for the active note, or display various metadata or graph.
* Allows creating and managing view panels. View panels currently are
* displayed at the right of the sidebar and allows displaying any HTML
* content (within a webview) and update it in real-time. For example it
* could be used to display a table of content for the active note, or
* display various metadata or graph.
*
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/toc)
*/
@ -14,7 +17,7 @@ export default class JoplinViewsPanels {
/**
* Creates a new panel
*/
create(): Promise<ViewHandle>;
create(id: string): Promise<ViewHandle>;
/**
* Sets the panel webview HTML
*/

View File

@ -12,5 +12,5 @@ export default class JoplinViewsToolbarButtons {
/**
* Creates a new toolbar button and associate it with the given command.
*/
create(commandName: string, location: ToolbarButtonLocation): Promise<void>;
create(id: string, commandName: string, location: ToolbarButtonLocation): Promise<void>;
}

View File

@ -31,14 +31,14 @@ joplin.plugins.register({
});
// Add the first command to the note toolbar
await joplin.views.toolbarButtons.create('testCommand1', ToolbarButtonLocation.NoteToolbar);
await joplin.views.toolbarButtons.create('myButton1', 'testCommand1', ToolbarButtonLocation.NoteToolbar);
// Add the second command to the editor toolbar
await joplin.views.toolbarButtons.create('testCommand2', ToolbarButtonLocation.EditorToolbar);
await joplin.views.toolbarButtons.create('myButton2', 'testCommand2', ToolbarButtonLocation.EditorToolbar);
// Also add the commands to the menu
await joplin.views.menuItems.create('testCommand1', MenuItemLocation.Tools, { accelerator: 'CmdOrCtrl+Alt+Shift+B' });
await joplin.views.menuItems.create('testCommand2', MenuItemLocation.Tools);
await joplin.views.menuItems.create('myMenuItem1', 'testCommand1', MenuItemLocation.Tools, { accelerator: 'CmdOrCtrl+Alt+Shift+B' });
await joplin.views.menuItems.create('myMenuItem2', 'testCommand2', MenuItemLocation.Tools);
console.info('Running command with arguments...');
const result = await joplin.commands.execute('commandWithResult', 'abcd', 123);

View File

@ -17,7 +17,7 @@ export default class JoplinViewsDialogs {
/**
* Creates a new dialog
*/
create(): Promise<ViewHandle>;
create(id: string): Promise<ViewHandle>;
/**
* Displays a message box with OK/Cancel buttons. Returns the button index that was clicked - "0" for OK and "1" for "Cancel"
*/

View File

@ -12,5 +12,5 @@ export default class JoplinViewsMenuItems {
/**
* Creates a new menu item and associate it with the given command. You can specify under which menu the item should appear using the `location` parameter.
*/
create(commandName: string, location?: MenuItemLocation, options?: CreateMenuItemOptions): Promise<void>;
create(id: string, commandName: string, location?: MenuItemLocation, options?: CreateMenuItemOptions): Promise<void>;
}

View File

@ -14,5 +14,5 @@ export default class JoplinViewsMenus {
* Creates a new menu from the provided menu items and place it at the given location. As of now, it is only possible to place the
* menu as a sub-menu of the application build-in menus.
*/
create(label: string, menuItems: MenuItem[], location?: MenuItemLocation): Promise<void>;
create(id: string, label: string, menuItems: MenuItem[], location?: MenuItemLocation): Promise<void>;
}

View File

@ -1,8 +1,11 @@
import Plugin from '../Plugin';
import { ViewHandle } from './types';
/**
* Allows creating and managing view panels. View panels currently are displayed at the right of the sidebar and allows displaying any HTML content (within a webview) and update it in real-time. For example
* it could be used to display a table of content for the active note, or display various metadata or graph.
* Allows creating and managing view panels. View panels currently are
* displayed at the right of the sidebar and allows displaying any HTML
* content (within a webview) and update it in real-time. For example it
* could be used to display a table of content for the active note, or
* display various metadata or graph.
*
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/toc)
*/
@ -14,7 +17,7 @@ export default class JoplinViewsPanels {
/**
* Creates a new panel
*/
create(): Promise<ViewHandle>;
create(id: string): Promise<ViewHandle>;
/**
* Sets the panel webview HTML
*/

View File

@ -12,5 +12,5 @@ export default class JoplinViewsToolbarButtons {
/**
* Creates a new toolbar button and associate it with the given command.
*/
create(commandName: string, location: ToolbarButtonLocation): Promise<void>;
create(id: string, commandName: string, location: ToolbarButtonLocation): Promise<void>;
}

View File

@ -17,7 +17,7 @@ export default class JoplinViewsDialogs {
/**
* Creates a new dialog
*/
create(): Promise<ViewHandle>;
create(id: string): Promise<ViewHandle>;
/**
* Displays a message box with OK/Cancel buttons. Returns the button index that was clicked - "0" for OK and "1" for "Cancel"
*/

View File

@ -12,5 +12,5 @@ export default class JoplinViewsMenuItems {
/**
* Creates a new menu item and associate it with the given command. You can specify under which menu the item should appear using the `location` parameter.
*/
create(commandName: string, location?: MenuItemLocation, options?: CreateMenuItemOptions): Promise<void>;
create(id: string, commandName: string, location?: MenuItemLocation, options?: CreateMenuItemOptions): Promise<void>;
}

View File

@ -14,5 +14,5 @@ export default class JoplinViewsMenus {
* Creates a new menu from the provided menu items and place it at the given location. As of now, it is only possible to place the
* menu as a sub-menu of the application build-in menus.
*/
create(label: string, menuItems: MenuItem[], location?: MenuItemLocation): Promise<void>;
create(id: string, label: string, menuItems: MenuItem[], location?: MenuItemLocation): Promise<void>;
}

View File

@ -1,8 +1,11 @@
import Plugin from '../Plugin';
import { ViewHandle } from './types';
/**
* Allows creating and managing view panels. View panels currently are displayed at the right of the sidebar and allows displaying any HTML content (within a webview) and update it in real-time. For example
* it could be used to display a table of content for the active note, or display various metadata or graph.
* Allows creating and managing view panels. View panels currently are
* displayed at the right of the sidebar and allows displaying any HTML
* content (within a webview) and update it in real-time. For example it
* could be used to display a table of content for the active note, or
* display various metadata or graph.
*
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/toc)
*/
@ -14,7 +17,7 @@ export default class JoplinViewsPanels {
/**
* Creates a new panel
*/
create(): Promise<ViewHandle>;
create(id: string): Promise<ViewHandle>;
/**
* Sets the panel webview HTML
*/

View File

@ -12,5 +12,5 @@ export default class JoplinViewsToolbarButtons {
/**
* Creates a new toolbar button and associate it with the given command.
*/
create(commandName: string, location: ToolbarButtonLocation): Promise<void>;
create(id: string, commandName: string, location: ToolbarButtonLocation): Promise<void>;
}

View File

@ -17,7 +17,7 @@ export default class JoplinViewsDialogs {
/**
* Creates a new dialog
*/
create(): Promise<ViewHandle>;
create(id: string): Promise<ViewHandle>;
/**
* Displays a message box with OK/Cancel buttons. Returns the button index that was clicked - "0" for OK and "1" for "Cancel"
*/

View File

@ -12,5 +12,5 @@ export default class JoplinViewsMenuItems {
/**
* Creates a new menu item and associate it with the given command. You can specify under which menu the item should appear using the `location` parameter.
*/
create(commandName: string, location?: MenuItemLocation, options?: CreateMenuItemOptions): Promise<void>;
create(id: string, commandName: string, location?: MenuItemLocation, options?: CreateMenuItemOptions): Promise<void>;
}

View File

@ -14,5 +14,5 @@ export default class JoplinViewsMenus {
* Creates a new menu from the provided menu items and place it at the given location. As of now, it is only possible to place the
* menu as a sub-menu of the application build-in menus.
*/
create(label: string, menuItems: MenuItem[], location?: MenuItemLocation): Promise<void>;
create(id: string, label: string, menuItems: MenuItem[], location?: MenuItemLocation): Promise<void>;
}

View File

@ -1,8 +1,11 @@
import Plugin from '../Plugin';
import { ViewHandle } from './types';
/**
* Allows creating and managing view panels. View panels currently are displayed at the right of the sidebar and allows displaying any HTML content (within a webview) and update it in real-time. For example
* it could be used to display a table of content for the active note, or display various metadata or graph.
* Allows creating and managing view panels. View panels currently are
* displayed at the right of the sidebar and allows displaying any HTML
* content (within a webview) and update it in real-time. For example it
* could be used to display a table of content for the active note, or
* display various metadata or graph.
*
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/toc)
*/
@ -14,7 +17,7 @@ export default class JoplinViewsPanels {
/**
* Creates a new panel
*/
create(): Promise<ViewHandle>;
create(id: string): Promise<ViewHandle>;
/**
* Sets the panel webview HTML
*/

View File

@ -12,5 +12,5 @@ export default class JoplinViewsToolbarButtons {
/**
* Creates a new toolbar button and associate it with the given command.
*/
create(commandName: string, location: ToolbarButtonLocation): Promise<void>;
create(id: string, commandName: string, location: ToolbarButtonLocation): Promise<void>;
}

View File

@ -41,7 +41,7 @@ joplin.plugins.register({
onStart: async function() {
const panels = joplin.views.panels;
const view = await panels.create();
const view = await (panels as any).create();
await panels.setHtml(view, 'Loading...');
await panels.addScript(view, './webview.js');

View File

@ -11,6 +11,7 @@ yo joplin
rsync -a --delete --exclude "src/" --exclude "package.json" --exclude "package-lock.json" --exclude "node_modules/" --exclude "dist/" "$TEMP_DIR/" "$SCRIPT_DIR/dialog/"
rsync -a --delete --exclude "src/" --exclude "package.json" --exclude "package-lock.json" --exclude "node_modules/" --exclude "dist/" "$TEMP_DIR/" "$SCRIPT_DIR/events/"
rsync -a --delete --exclude "src/" --exclude "package.json" --exclude "package-lock.json" --exclude "node_modules/" --exclude "dist/" "$TEMP_DIR/" "$SCRIPT_DIR/json_export/"
rsync -a --delete --exclude "src/" --exclude "package.json" --exclude "package-lock.json" --exclude "node_modules/" --exclude "dist/" "$TEMP_DIR/" "$SCRIPT_DIR/menu/"
rsync -a --delete --exclude "src/" --exclude "package.json" --exclude "package-lock.json" --exclude "node_modules/" --exclude "dist/" "$TEMP_DIR/" "$SCRIPT_DIR/multi_selection/"
rsync -a --delete --exclude "src/" --exclude "package.json" --exclude "package-lock.json" --exclude "node_modules/" --exclude "dist/" "$TEMP_DIR/" "$SCRIPT_DIR/register_command/"
rsync -a --delete --exclude "src/" --exclude "package.json" --exclude "package-lock.json" --exclude "node_modules/" --exclude "dist/" "$TEMP_DIR/" "$SCRIPT_DIR/selected_text/"

View File

@ -17,7 +17,7 @@ export default class JoplinViewsDialogs {
/**
* Creates a new dialog
*/
create(): Promise<ViewHandle>;
create(id: string): Promise<ViewHandle>;
/**
* Displays a message box with OK/Cancel buttons. Returns the button index that was clicked - "0" for OK and "1" for "Cancel"
*/

View File

@ -12,5 +12,5 @@ export default class JoplinViewsMenuItems {
/**
* Creates a new menu item and associate it with the given command. You can specify under which menu the item should appear using the `location` parameter.
*/
create(commandName: string, location?: MenuItemLocation, options?: CreateMenuItemOptions): Promise<void>;
create(id: string, commandName: string, location?: MenuItemLocation, options?: CreateMenuItemOptions): Promise<void>;
}

View File

@ -14,5 +14,5 @@ export default class JoplinViewsMenus {
* Creates a new menu from the provided menu items and place it at the given location. As of now, it is only possible to place the
* menu as a sub-menu of the application build-in menus.
*/
create(label: string, menuItems: MenuItem[], location?: MenuItemLocation): Promise<void>;
create(id: string, label: string, menuItems: MenuItem[], location?: MenuItemLocation): Promise<void>;
}

View File

@ -1,8 +1,11 @@
import Plugin from '../Plugin';
import { ViewHandle } from './types';
/**
* Allows creating and managing view panels. View panels currently are displayed at the right of the sidebar and allows displaying any HTML content (within a webview) and update it in real-time. For example
* it could be used to display a table of content for the active note, or display various metadata or graph.
* Allows creating and managing view panels. View panels currently are
* displayed at the right of the sidebar and allows displaying any HTML
* content (within a webview) and update it in real-time. For example it
* could be used to display a table of content for the active note, or
* display various metadata or graph.
*
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/toc)
*/
@ -14,7 +17,7 @@ export default class JoplinViewsPanels {
/**
* Creates a new panel
*/
create(): Promise<ViewHandle>;
create(id: string): Promise<ViewHandle>;
/**
* Sets the panel webview HTML
*/

View File

@ -12,5 +12,5 @@ export default class JoplinViewsToolbarButtons {
/**
* Creates a new toolbar button and associate it with the given command.
*/
create(commandName: string, location: ToolbarButtonLocation): Promise<void>;
create(id: string, commandName: string, location: ToolbarButtonLocation): Promise<void>;
}

View File

@ -18,6 +18,9 @@ import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerS
import SpellCheckerServiceDriverNative from './services/spellChecker/SpellCheckerServiceDriverNative';
import bridge from './services/bridge';
import menuCommandNames from './gui/menuCommandNames';
import { LayoutItem } from './gui/ResizableLayout/utils/types';
import stateToWhenClauseContext from './services/commands/stateToWhenClauseContext';
import ResourceService from '@joplin/lib/services/ResourceService';
const { FoldersScreenUtils } = require('@joplin/lib/folders-screen-utils.js');
const MasterKey = require('@joplin/lib/models/MasterKey');
@ -27,7 +30,6 @@ const Tag = require('@joplin/lib/models/Tag.js');
const { reg } = require('@joplin/lib/registry.js');
const packageInfo = require('./packageInfo.js');
const DecryptionWorker = require('@joplin/lib/services/DecryptionWorker');
const ResourceService = require('@joplin/lib/services/ResourceService').default;
const ClipperServer = require('@joplin/lib/ClipperServer');
const ExternalEditWatcher = require('@joplin/lib/services/ExternalEditWatcher');
const { webFrame } = require('electron');
@ -67,6 +69,7 @@ const commands = [
require('./gui/MainScreen/commands/openNote'),
require('./gui/MainScreen/commands/openFolder'),
require('./gui/MainScreen/commands/openTag'),
require('./gui/MainScreen/commands/toggleLayoutMoveMode'),
require('./gui/NoteEditor/commands/focusElementNoteBody'),
require('./gui/NoteEditor/commands/focusElementNoteTitle'),
require('./gui/NoteEditor/commands/showLocalSearch'),
@ -105,17 +108,17 @@ export interface AppState extends State {
route: AppStateRoute;
navHistory: any[];
noteVisiblePanes: string[];
sidebarVisibility: boolean;
noteListVisibility: boolean;
windowContentSize: any;
watchedNoteFiles: string[];
lastEditorScrollPercents: any;
devToolsVisible: boolean;
visibleDialogs: any; // empty object if no dialog is visible. Otherwise contains the list of visible dialogs.
focusedField: string;
layoutMoveMode: boolean;
// Extra reducer keys go here
watchedResources: any;
mainLayout: LayoutItem;
}
const appDefaultState: AppState = {
@ -127,14 +130,14 @@ const appDefaultState: AppState = {
},
navHistory: [],
noteVisiblePanes: ['editor', 'viewer'],
sidebarVisibility: true,
noteListVisibility: true,
windowContentSize: bridge().windowContentSize(),
watchedNoteFiles: [],
lastEditorScrollPercents: {},
devToolsVisible: false,
visibleDialogs: {}, // empty object if no dialog is visible. Otherwise contains the list of visible dialogs.
focusedField: null,
layoutMoveMode: false,
mainLayout: null,
...resourceEditWatcherDefaultState,
};
@ -234,25 +237,12 @@ class Application extends BaseApplication {
newState.noteVisiblePanes = action.panes;
break;
case 'SIDEBAR_VISIBILITY_TOGGLE':
case 'MAIN_LAYOUT_SET':
newState = Object.assign({}, state);
newState.sidebarVisibility = !state.sidebarVisibility;
break;
case 'SIDEBAR_VISIBILITY_SET':
newState = Object.assign({}, state);
newState.sidebarVisibility = action.visibility;
break;
case 'NOTELIST_VISIBILITY_TOGGLE':
newState = Object.assign({}, state);
newState.noteListVisibility = !state.noteListVisibility;
break;
case 'NOTELIST_VISIBILITY_SET':
newState = Object.assign({}, state);
newState.noteListVisibility = action.visibility;
newState = {
...state,
mainLayout: action.value,
};
break;
case 'NOTE_FILE_WATCHER_ADD':
@ -333,6 +323,14 @@ class Application extends BaseApplication {
}
break;
case 'LAYOUT_MOVE_MODE_SET':
newState = {
...state,
layoutMoveMode: action.value,
};
break;
}
} catch (error) {
error.message = `In reducer: ${error.message} Action: ${JSON.stringify(action)}`;
@ -384,14 +382,6 @@ class Application extends BaseApplication {
Setting.setValue('noteVisiblePanes', newState.noteVisiblePanes);
}
if (['SIDEBAR_VISIBILITY_TOGGLE', 'SIDEBAR_VISIBILITY_SET'].indexOf(action.type) >= 0) {
Setting.setValue('sidebarVisibility', newState.sidebarVisibility);
}
if (['NOTELIST_VISIBILITY_TOGGLE', 'NOTELIST_VISIBILITY_SET'].indexOf(action.type) >= 0) {
Setting.setValue('noteListVisibility', newState.noteListVisibility);
}
if (['NOTE_DEVTOOLS_TOGGLE', 'NOTE_DEVTOOLS_SET'].indexOf(action.type) >= 0) {
this.toggleDevTools(newState.devToolsVisible);
}
@ -497,6 +487,37 @@ class Application extends BaseApplication {
return cssString;
}
private async initPluginService() {
const pluginLogger = new Logger();
pluginLogger.addTarget(TargetType.File, { path: `${Setting.value('profileDir')}/log-plugins.txt` });
pluginLogger.addTarget(TargetType.Console, { prefix: 'Plugin Service:' });
pluginLogger.setLevel(Setting.value('env') == 'dev' ? Logger.LEVEL_DEBUG : Logger.LEVEL_INFO);
const pluginRunner = new PluginRunner();
PluginService.instance().setLogger(pluginLogger);
PluginService.instance().initialize(PlatformImplementation.instance(), pluginRunner, this.store());
try {
if (await shim.fsDriver().exists(Setting.value('pluginDir'))) await PluginService.instance().loadAndRunPlugins(Setting.value('pluginDir'));
} catch (error) {
this.logger().error(`There was an error loading plugins from ${Setting.value('pluginDir')}:`, error);
}
try {
if (Setting.value('plugins.devPluginPaths')) {
const paths = Setting.value('plugins.devPluginPaths').split(',').map((p: string) => p.trim());
await PluginService.instance().loadAndRunPlugins(paths);
}
// Also load dev plugins that have passed via command line arguments
if (Setting.value('startupDevPlugins')) {
await PluginService.instance().loadAndRunPlugins(Setting.value('startupDevPlugins'));
}
} catch (error) {
this.logger().error(`There was an error loading plugins from ${Setting.value('plugins.devPluginPaths')}:`, error);
}
}
async start(argv: string[]): Promise<any> {
const electronIsDev = require('electron-is-dev');
@ -539,7 +560,7 @@ class Application extends BaseApplication {
this.initRedux();
CommandService.instance().initialize(this.store(), Setting.value('env') == 'dev');
CommandService.instance().initialize(this.store(), Setting.value('env') == 'dev', stateToWhenClauseContext);
for (const command of commands) {
CommandService.instance().registerDeclaration(command.declaration);
@ -688,34 +709,7 @@ class Application extends BaseApplication {
bridge().addEventListener('nativeThemeUpdated', this.bridge_nativeThemeUpdated);
const pluginLogger = new Logger();
pluginLogger.addTarget(TargetType.File, { path: `${Setting.value('profileDir')}/log-plugins.txt` });
pluginLogger.addTarget(TargetType.Console, { prefix: 'Plugin Service:' });
pluginLogger.setLevel(Setting.value('env') == 'dev' ? Logger.LEVEL_DEBUG : Logger.LEVEL_INFO);
const pluginRunner = new PluginRunner();
PluginService.instance().setLogger(pluginLogger);
PluginService.instance().initialize(PlatformImplementation.instance(), pluginRunner, this.store());
try {
if (await shim.fsDriver().exists(Setting.value('pluginDir'))) await PluginService.instance().loadAndRunPlugins(Setting.value('pluginDir'));
} catch (error) {
this.logger().error(`There was an error loading plugins from ${Setting.value('pluginDir')}:`, error);
}
try {
if (Setting.value('plugins.devPluginPaths')) {
const paths = Setting.value('plugins.devPluginPaths').split(',').map((p: string) => p.trim());
await PluginService.instance().loadAndRunPlugins(paths);
}
// Also load dev plugins that have passed via command line arguments
if (Setting.value('startupDevPlugins')) {
await PluginService.instance().loadAndRunPlugins(Setting.value('startupDevPlugins'));
}
} catch (error) {
this.logger().error(`There was an error loading plugins from ${Setting.value('plugins.devPluginPaths')}:`, error);
}
await this.initPluginService();
this.setupContextMenu();

View File

@ -14,7 +14,7 @@ interface Props {
iconName?: string;
level?: ButtonLevel;
className?: string;
onClick: Function;
onClick?: Function;
color?: string;
iconAnimation?: string;
tooltip?: string;
@ -57,12 +57,14 @@ const StyledButtonPrimary = styled(StyledButtonBase)`
border: none;
background-color: ${(props: any) => props.theme.backgroundColor5};
&:hover {
background-color: ${(props: any) => props.theme.backgroundColorHover5};
}
${(props: any) => props.disabled} {
&:hover {
background-color: ${(props: any) => props.theme.backgroundColorHover5};
}
&:active {
background-color: ${(props: any) => props.theme.backgroundColorActive5};
&:active {
background-color: ${(props: any) => props.theme.backgroundColorActive5};
}
}
${StyledIcon} {
@ -78,12 +80,14 @@ const StyledButtonSecondary = styled(StyledButtonBase)`
border: 1px solid ${(props: any) => props.theme.borderColor4};
background-color: ${(props: any) => props.theme.backgroundColor4};
&:hover {
background-color: ${(props: any) => props.theme.backgroundColorHover4};
}
${(props: any) => props.disabled} {
&:hover {
background-color: ${(props: any) => props.theme.backgroundColorHover4};
}
&:active {
background-color: ${(props: any) => props.theme.backgroundColorActive4};
&:active {
background-color: ${(props: any) => props.theme.backgroundColorActive4};
}
}
${StyledIcon} {

View File

@ -1,13 +1,15 @@
import * as React from 'react';
import ResizableLayout, { findItemByKey, LayoutItem, LayoutItemDirection, allDynamicSizes } from '../ResizableLayout/ResizableLayout';
import NoteList from '../NoteList/NoteList';
import ResizableLayout from '../ResizableLayout/ResizableLayout';
import findItemByKey from '../ResizableLayout/utils/findItemByKey';
import { MoveButtonClickEvent } from '../ResizableLayout/MoveButtons';
import { move } from '../ResizableLayout/utils/movements';
import { LayoutItem } from '../ResizableLayout/utils/types';
import NoteEditor from '../NoteEditor/NoteEditor';
import NoteContentPropertiesDialog from '../NoteContentPropertiesDialog';
import ShareNoteDialog from '../ShareNoteDialog';
import NoteListControls from '../NoteListControls/NoteListControls';
import CommandService from '@joplin/lib/services/CommandService';
import PluginService from '@joplin/lib/services/plugins/PluginService';
import { utils as pluginUtils } from '@joplin/lib/services/plugins/reducer';
import { PluginStates, utils as pluginUtils } from '@joplin/lib/services/plugins/reducer';
import SideBar from '../SideBar/SideBar';
import UserWebview from '../../services/plugins/UserWebview';
import UserWebviewDialog from '../../services/plugins/UserWebviewDialog';
@ -15,20 +17,61 @@ import { ContainerType } from '@joplin/lib/services/plugins/WebviewController';
import { stateUtils } from '@joplin/lib/reducer';
import InteropServiceHelper from '../../InteropServiceHelper';
import { _ } from '@joplin/lib/locale';
import NoteListWrapper from '../NoteListWrapper/NoteListWrapper';
import { AppState } from '../../app';
import { saveLayout, loadLayout } from '../ResizableLayout/utils/persist';
import Setting from '@joplin/lib/models/Setting';
import produce from 'immer';
import shim from '@joplin/lib/shim';
import bridge from '../../services/bridge';
import time from '@joplin/lib/time';
import styled from 'styled-components';
import { themeStyle } from '@joplin/lib/theme';
import validateLayout from '../ResizableLayout/utils/validateLayout';
import iterateItems from '../ResizableLayout/utils/iterateItems';
import removeItem from '../ResizableLayout/utils/removeItem';
const produce = require('immer').default;
const { connect } = require('react-redux');
const { PromptDialog } = require('../PromptDialog.min.js');
const NotePropertiesDialog = require('../NotePropertiesDialog.min.js');
const Setting = require('@joplin/lib/models/Setting').default;
const shim = require('@joplin/lib/shim').default;
const { themeStyle } = require('@joplin/lib/theme.js');
const bridge = require('electron').remote.require('./bridge').default;
const PluginManager = require('@joplin/lib/services/PluginManager');
const EncryptionService = require('@joplin/lib/services/EncryptionService');
const ipcRenderer = require('electron').ipcRenderer;
const time = require('@joplin/lib/time').default;
const styled = require('styled-components').default;
interface LayerModalState {
visible: boolean;
message: string;
}
interface Props {
plugins: PluginStates;
pluginsLoaded: boolean;
hasNotesBeingSaved: boolean;
dispatch: Function;
mainLayout: LayoutItem;
style: any;
layoutMoveMode: boolean;
editorNoteStatuses: any;
customCss: string;
shouldUpgradeSyncTarget: boolean;
hasDisabledSyncItems: boolean;
hasDisabledEncryptionItems: boolean;
showMissingMasterKeyMessage: boolean;
showNeedUpgradingMasterKeyMessage: boolean;
showShouldReencryptMessage: boolean;
focusedField: string;
themeId: number;
settingEditorCodeView: boolean;
pluginsLegacy: any;
}
interface State {
promptOptions: any;
modalLayer: LayerModalState;
notePropertiesDialogOptions: any;
noteContentPropertiesDialogOptions: any;
shareNoteDialogOptions: any;
}
const StyledUserWebviewDialogContainer = styled.div`
display: flex;
@ -41,6 +84,15 @@ const StyledUserWebviewDialogContainer = styled.div`
box-sizing: border-box;
`;
const defaultLayout: LayoutItem = {
key: 'root',
children: [
{ key: 'sideBar', width: 250 },
{ key: 'noteList', width: 250 },
{ key: 'editor' },
],
};
const commands = [
require('./commands/editAlarm'),
require('./commands/exportPdf'),
@ -65,20 +117,21 @@ const commands = [
require('./commands/toggleNoteList'),
require('./commands/toggleSideBar'),
require('./commands/toggleVisiblePanes'),
require('./commands/toggleLayoutMoveMode'),
require('./commands/openNote'),
require('./commands/openFolder'),
require('./commands/openTag'),
];
class MainScreenComponent extends React.Component<any, any> {
class MainScreenComponent extends React.Component<Props, State> {
waitForNotesSavedIID_: any;
isPrinting_: boolean;
styleKey_: string;
styles_: any;
promptOnClose_: Function;
private waitForNotesSavedIID_: any;
private isPrinting_: boolean;
private styleKey_: string;
private styles_: any;
private promptOnClose_: Function;
constructor(props: any) {
constructor(props: Props) {
super(props);
this.state = {
@ -90,9 +143,10 @@ class MainScreenComponent extends React.Component<any, any> {
notePropertiesDialogOptions: {},
noteContentPropertiesDialogOptions: {},
shareNoteDialogOptions: {},
layout: this.buildLayout(props.plugins),
};
this.updateMainLayout(this.buildLayout(props.plugins));
this.registerCommands();
this.setupAppCloseHandling();
@ -103,117 +157,72 @@ class MainScreenComponent extends React.Component<any, any> {
this.userWebview_message = this.userWebview_message.bind(this);
this.resizableLayout_resize = this.resizableLayout_resize.bind(this);
this.resizableLayout_renderItem = this.resizableLayout_renderItem.bind(this);
this.resizableLayout_moveButtonClick = this.resizableLayout_moveButtonClick.bind(this);
this.window_resize = this.window_resize.bind(this);
this.rowHeight = this.rowHeight.bind(this);
this.layoutModeListenerKeyDown = this.layoutModeListenerKeyDown.bind(this);
window.addEventListener('resize', this.window_resize);
}
buildLayout(plugins: any): LayoutItem {
const rootLayoutSize = this.rootLayoutSize();
const theme = themeStyle(this.props.themeId);
const sideBarMinWidth = 200;
const sizes = {
sideBarColumn: {
width: 150,
},
noteListColumn: {
width: 150,
},
pluginColumn: {
width: 150,
},
...Setting.value('ui.layout'),
};
for (const k in sizes) {
if (sizes[k].width < sideBarMinWidth) sizes[k].width = sideBarMinWidth;
}
const pluginColumnChildren: LayoutItem[] = [];
private updateLayoutPluginViews(layout: LayoutItem, plugins: PluginStates) {
const infos = pluginUtils.viewInfosByType(plugins, 'webview');
for (const info of infos) {
if (info.view.containerType !== ContainerType.Panel) continue;
let newLayout = produce(layout, (draftLayout: LayoutItem) => {
for (const info of infos) {
if (info.view.containerType !== ContainerType.Panel) continue;
// For now it's assumed all views go in the "pluginColumn" so they are
// resizable vertically. But horizontally they stretch 100%
const viewId = info.view.id;
const viewId = info.view.id;
const existingItem = findItemByKey(draftLayout, viewId);
const size = {
...(sizes[viewId] ? sizes[viewId] : null),
width: '100%',
};
if (!existingItem) {
draftLayout.children.push({
key: viewId,
context: {
pluginId: info.plugin.id,
},
});
}
}
});
pluginColumnChildren.push({
key: viewId,
resizableBottom: true,
context: {
plugin: info.plugin,
control: info.view,
},
...size,
});
// Remove layout items that belong to plugins that are no longer
// active.
const pluginIds = Object.keys(plugins);
const itemsToRemove: string[] = [];
iterateItems(newLayout, (_itemIndex: number, item: LayoutItem, _parent: LayoutItem) => {
if (item.context && item.context.pluginId && !pluginIds.includes(item.context.pluginId)) {
itemsToRemove.push(item.key);
}
return true;
});
for (const itemKey of itemsToRemove) {
newLayout = removeItem(newLayout, itemKey);
}
return {
key: 'root',
direction: LayoutItemDirection.Row,
width: rootLayoutSize.width,
height: rootLayoutSize.height,
children: [
{
key: 'sideBarColumn',
direction: LayoutItemDirection.Column,
resizableRight: true,
width: sizes.sideBarColumn.width,
visible: Setting.value('sidebarVisibility'),
minWidth: sideBarMinWidth,
children: [
{
key: 'sideBar',
},
],
},
{
key: 'noteListColumn',
direction: LayoutItemDirection.Column,
resizableRight: true,
width: sizes.noteListColumn.width,
visible: Setting.value('noteListVisibility'),
minWidth: sideBarMinWidth,
children: [
{
height: theme.topRowHeight,
key: 'noteListControls',
},
{
key: 'noteList',
},
],
},
{
key: 'pluginColumn',
direction: LayoutItemDirection.Column,
resizableRight: true,
width: sizes.pluginColumn.width,
visible: !!pluginColumnChildren.length,
minWidth: sideBarMinWidth,
children: pluginColumnChildren,
},
{
key: 'editorColumn',
direction: LayoutItemDirection.Column,
children: [
{
key: 'editor',
},
],
},
],
};
return newLayout !== layout ? validateLayout(newLayout) : layout;
}
private buildLayout(plugins: PluginStates): LayoutItem {
const rootLayoutSize = this.rootLayoutSize();
const userLayout = Setting.value('ui.layout');
let output = null;
try {
output = loadLayout(userLayout, defaultLayout, rootLayoutSize);
if (!findItemByKey(output, 'sideBar') || !findItemByKey(output, 'noteList') || !findItemByKey(output, 'editor')) {
throw new Error('"sideBar", "noteList" and "editor" must be present in the layout');
}
} catch (error) {
console.warn('Could not load layout - restoring default layout:', error);
console.warn('Layout was:', userLayout);
output = loadLayout(null, defaultLayout, rootLayoutSize);
}
return this.updateLayoutPluginViews(output, plugins);
}
window_resize() {
@ -263,25 +272,22 @@ class MainScreenComponent extends React.Component<any, any> {
this.setState({ shareNoteDialogOptions: {} });
}
updateMainLayout(layout: LayoutItem) {
this.props.dispatch({
type: 'MAIN_LAYOUT_SET',
value: layout,
});
}
updateRootLayoutSize() {
this.setState({ layout: produce(this.state.layout, (draft: any) => {
this.updateMainLayout(produce(this.props.mainLayout, (draft: any) => {
const s = this.rootLayoutSize();
draft.width = s.width;
draft.height = s.height;
}) });
}));
}
componentDidUpdate(prevProps: any, prevState: any) {
if (this.props.noteListVisibility !== prevProps.noteListVisibility || this.props.sidebarVisibility !== prevProps.sidebarVisibility) {
this.setState({ layout: produce(this.state.layout, (draft: any) => {
const noteListColumn = findItemByKey(draft, 'noteListColumn');
noteListColumn.visible = this.props.noteListVisibility;
const sideBarColumn = findItemByKey(draft, 'sideBarColumn');
sideBarColumn.visible = this.props.sidebarVisibility;
}) });
}
componentDidUpdate(prevProps: Props, prevState: State) {
if (prevProps.style.width !== this.props.style.width ||
prevProps.style.height !== this.props.style.height ||
this.messageBoxVisible(prevProps) !== this.messageBoxVisible(this.props)
@ -290,7 +296,8 @@ class MainScreenComponent extends React.Component<any, any> {
}
if (prevProps.plugins !== this.props.plugins) {
this.setState({ layout: this.buildLayout(this.props.plugins) });
this.updateMainLayout(this.updateLayoutPluginViews(this.props.mainLayout, this.props.plugins));
// this.setState({ layout: this.buildLayout(this.props.plugins) });
}
if (this.state.notePropertiesDialogOptions !== prevState.notePropertiesDialogOptions) {
@ -313,28 +320,28 @@ class MainScreenComponent extends React.Component<any, any> {
name: 'shareNote',
});
}
if (this.props.mainLayout !== prevProps.mainLayout) {
const toSave = saveLayout(this.props.mainLayout);
Setting.setValue('ui.layout', toSave);
}
}
layoutModeListenerKeyDown(event: any) {
if (event.key !== 'Escape') return;
if (!this.props.layoutMoveMode) return;
CommandService.instance().execute('toggleLayoutMoveMode');
}
componentDidMount() {
this.updateRootLayoutSize();
window.addEventListener('keydown', this.layoutModeListenerKeyDown);
}
componentWillUnmount() {
this.unregisterCommands();
window.removeEventListener('resize', this.window_resize);
}
toggleSideBar() {
this.props.dispatch({
type: 'SIDEBAR_VISIBILITY_TOGGLE',
});
}
toggleNoteList() {
this.props.dispatch({
type: 'NOTELIST_VISIBILITY_TOGGLE',
});
window.removeEventListener('keydown', this.layoutModeListenerKeyDown);
}
async waitForNoteToSaved(noteId: string) {
@ -399,8 +406,8 @@ class MainScreenComponent extends React.Component<any, any> {
return 50;
}
styles(themeId: number, width: number, height: number, messageBoxVisible: boolean, isSidebarVisible: any, isNoteListVisible: any) {
const styleKey = [themeId, width, height, messageBoxVisible, +isSidebarVisible, +isNoteListVisible].join('_');
styles(themeId: number, width: number, height: number, messageBoxVisible: boolean) {
const styleKey = [themeId, width, height, messageBoxVisible].join('_');
if (styleKey === this.styleKey_) return this.styles_;
const theme = themeStyle(themeId);
@ -561,30 +568,57 @@ class MainScreenComponent extends React.Component<any, any> {
}
resizableLayout_resize(event: any) {
this.setState({ layout: event.layout });
Setting.setValue('ui.layout', allDynamicSizes(event.layout));
this.updateMainLayout(event.layout);
}
resizableLayout_moveButtonClick(event: MoveButtonClickEvent) {
const newLayout = move(this.props.mainLayout, event.itemKey, event.direction);
this.updateMainLayout(newLayout);
}
resizableLayout_renderItem(key: string, event: any) {
const eventEmitter = event.eventEmitter;
if (key === 'sideBar') {
return <SideBar key={key} />;
} else if (key === 'noteList') {
return <NoteList key={key} resizableLayoutEventEmitter={eventEmitter} size={event.size} visible={event.visible}/>;
} else if (key === 'editor') {
const bodyEditor = this.props.settingEditorCodeView ? 'CodeMirror' : 'TinyMCE';
return <NoteEditor key={key} bodyEditor={bodyEditor} />;
} else if (key === 'noteListControls') {
return <NoteListControls key={key} showNewNoteButtons={this.props.focusedField !== 'globalSearch'} />;
} else if (key.indexOf('plugin-view') === 0) {
const { control, plugin } = event.item.context;
const components: any = {
sideBar: () => {
return <SideBar key={key} />;
},
noteList: () => {
return <NoteListWrapper
key={key}
resizableLayoutEventEmitter={eventEmitter}
visible={event.visible}
focusedField={this.props.focusedField}
size={event.size}
themeId={this.props.themeId}
/>;
},
editor: () => {
const bodyEditor = this.props.settingEditorCodeView ? 'CodeMirror' : 'TinyMCE';
return <NoteEditor key={key} bodyEditor={bodyEditor} />;
},
};
if (components[key]) return components[key]();
if (key.indexOf('plugin-view') === 0) {
const viewInfo = pluginUtils.viewInfoByViewId(this.props.plugins, event.item.key);
if (!viewInfo) {
console.warn(`Could not find plugin associated with view: ${event.item.key}`);
return null;
}
const { view, plugin } = viewInfo;
return <UserWebview
key={control.id}
viewId={control.id}
key={view.id}
viewId={view.id}
themeId={this.props.themeId}
html={control.html}
scripts={control.scripts}
html={view.html}
scripts={view.scripts}
pluginId={plugin.id}
onMessage={this.userWebview_message}
borderBottom={true}
@ -635,9 +669,7 @@ class MainScreenComponent extends React.Component<any, any> {
this.props.style
);
const promptOptions = this.state.promptOptions;
const sidebarVisibility = this.props.sidebarVisibility;
const noteListVisibility = this.props.noteListVisibility;
const styles = this.styles(this.props.themeId, style.width, style.height, this.messageBoxVisible(), sidebarVisibility, noteListVisibility);
const styles = this.styles(this.props.themeId, style.width, style.height, this.messageBoxVisible());
if (!this.promptOnClose_) {
this.promptOnClose_ = (answer: any, buttonType: any) => {
@ -656,6 +688,18 @@ class MainScreenComponent extends React.Component<any, any> {
const noteContentPropertiesDialogOptions = this.state.noteContentPropertiesDialogOptions;
const shareNoteDialogOptions = this.state.shareNoteDialogOptions;
const layoutComp = this.props.mainLayout ? (
<ResizableLayout
height={styles.rowHeight}
layout={this.props.mainLayout}
onResize={this.resizableLayout_resize}
onMoveButtonClick={this.resizableLayout_moveButtonClick}
renderItem={this.resizableLayout_renderItem}
moveMode={this.props.layoutMoveMode}
moveModeMessage={_('Use the arrows to move the layout items. Press "Escape" to exit.')}
/>
) : null;
return (
<div style={style}>
<div style={modalLayerStyle}>{this.state.modalLayer.message}</div>
@ -667,25 +711,17 @@ class MainScreenComponent extends React.Component<any, any> {
<PromptDialog autocomplete={promptOptions && 'autocomplete' in promptOptions ? promptOptions.autocomplete : null} defaultValue={promptOptions && promptOptions.value ? promptOptions.value : ''} themeId={this.props.themeId} style={styles.prompt} onClose={this.promptOnClose_} label={promptOptions ? promptOptions.label : ''} description={promptOptions ? promptOptions.description : null} visible={!!this.state.promptOptions} buttons={promptOptions && 'buttons' in promptOptions ? promptOptions.buttons : null} inputType={promptOptions && 'inputType' in promptOptions ? promptOptions.inputType : null} />
{messageComp}
<ResizableLayout
width={this.state.width}
height={styles.rowHeight}
layout={this.state.layout}
onResize={this.resizableLayout_resize}
renderItem={this.resizableLayout_renderItem}
/>
{layoutComp}
{pluginDialog}
</div>
);
}
}
const mapStateToProps = (state: any) => {
const mapStateToProps = (state: AppState) => {
return {
themeId: state.settings.theme,
settingEditorCodeView: state.settings['editor.codeView'],
sidebarVisibility: state.sidebarVisibility,
noteListVisibility: state.noteListVisibility,
folders: state.folders,
notes: state.notes,
hasDisabledSyncItems: state.hasDisabledSyncItems,
@ -703,6 +739,8 @@ const mapStateToProps = (state: any) => {
editorNoteStatuses: state.editorNoteStatuses,
hasNotesBeingSaved: stateUtils.hasNotesBeingSaved(state),
focusedField: state.focusedField,
layoutMoveMode: state.layoutMoveMode,
mainLayout: state.mainLayout,
};
};

View File

@ -0,0 +1,20 @@
import { CommandDeclaration, CommandRuntime, CommandContext } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
import { AppState } from '../../../app';
export const declaration: CommandDeclaration = {
name: 'toggleLayoutMoveMode',
label: () => _('Change application layout'),
};
export const runtime = (): CommandRuntime => {
return {
execute: async (context: CommandContext, value: boolean = null) => {
const newValue = value !== null ? value : !(context.state as AppState).layoutMoveMode;
context.dispatch({
type: 'LAYOUT_MOVE_MODE_SET',
value: newValue,
});
},
};
};

View File

@ -1,5 +1,8 @@
import { CommandDeclaration, CommandRuntime } from '@joplin/lib/services/CommandService';
import { CommandContext, CommandDeclaration, CommandRuntime } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
import setLayoutItemProps from '../../ResizableLayout/utils/setLayoutItemProps';
import layoutItemProp from '../../ResizableLayout/utils/layoutItemProp';
import { AppState } from '../../../app';
export const declaration: CommandDeclaration = {
name: 'toggleNoteList',
@ -7,11 +10,18 @@ export const declaration: CommandDeclaration = {
iconName: 'fas fa-align-justify',
};
export const runtime = (comp: any): CommandRuntime => {
export const runtime = (): CommandRuntime => {
return {
execute: async () => {
comp.props.dispatch({
type: 'NOTELIST_VISIBILITY_TOGGLE',
execute: async (context: CommandContext) => {
const layout = (context.state as AppState).mainLayout;
const newLayout = setLayoutItemProps(layout, 'noteList', {
visible: !layoutItemProp(layout, 'noteList', 'visible'),
});
context.dispatch({
type: 'MAIN_LAYOUT_SET',
value: newLayout,
});
},
};

View File

@ -1,5 +1,8 @@
import { CommandDeclaration, CommandRuntime } from '@joplin/lib/services/CommandService';
import { CommandContext, CommandDeclaration, CommandRuntime } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
import setLayoutItemProps from '../../ResizableLayout/utils/setLayoutItemProps';
import layoutItemProp from '../../ResizableLayout/utils/layoutItemProp';
import { AppState } from '../../../app';
export const declaration: CommandDeclaration = {
name: 'toggleSideBar',
@ -7,11 +10,18 @@ export const declaration: CommandDeclaration = {
iconName: 'fas fa-bars',
};
export const runtime = (comp: any): CommandRuntime => {
export const runtime = (): CommandRuntime => {
return {
execute: async () => {
comp.props.dispatch({
type: 'SIDEBAR_VISIBILITY_TOGGLE',
execute: async (context: CommandContext) => {
const layout = (context.state as AppState).mainLayout;
const newLayout = setLayoutItemProps(layout, 'sideBar', {
visible: !layoutItemProp(layout, 'sideBar', 'visible'),
});
context.dispatch({
type: 'MAIN_LAYOUT_SET',
value: newLayout,
});
},
};

View File

@ -13,9 +13,9 @@ import { Module } from '@joplin/lib/services/interop/types';
import InteropServiceHelper from '../InteropServiceHelper';
import { _ } from '@joplin/lib/locale';
import { MenuItem, MenuItemLocation } from '@joplin/lib/services/plugins/api/types';
import stateToWhenClauseContext from '@joplin/lib/services/commands/stateToWhenClauseContext';
import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService';
import menuCommandNames from './menuCommandNames';
import stateToWhenClauseContext from '../services/commands/stateToWhenClauseContext';
const { connect } = require('react-redux');
const { reg } = require('@joplin/lib/registry.js');
@ -519,6 +519,8 @@ function useMenu(props: Props) {
view: {
label: _('&View'),
submenu: [
menuItemDic.toggleLayoutMoveMode,
separator(),
menuItemDic.toggleSideBar,
menuItemDic.toggleNoteList,
menuItemDic.toggleVisiblePanes,

View File

@ -5,7 +5,7 @@ import { utils as pluginUtils } from '@joplin/lib/services/plugins/reducer';
import { connect } from 'react-redux';
import { AppState } from '../../../../app';
import ToolbarButtonUtils, { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
import stateToWhenClauseContext from '@joplin/lib/services/commands/stateToWhenClauseContext';
import stateToWhenClauseContext from '../../../../services/commands/stateToWhenClauseContext';
const { buildStyle } = require('@joplin/lib/theme');
interface ToolbarProps {

View File

@ -23,12 +23,12 @@ import eventManager from '@joplin/lib/eventManager';
import { AppState } from '../../app';
import ToolbarButtonUtils from '@joplin/lib/services/commands/ToolbarButtonUtils';
import { _ } from '@joplin/lib/locale';
import stateToWhenClauseContext from '@joplin/lib/services/commands/stateToWhenClauseContext';
import TagList from '../TagList';
import NoteTitleBar from './NoteTitle/NoteTitleBar';
import markupLanguageUtils from '@joplin/lib/markupLanguageUtils';
import usePrevious from '../hooks/usePrevious';
import Setting from '@joplin/lib/models/Setting';
import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseContext';
const { themeStyle } = require('@joplin/lib/theme');
const { substrWithEllipsis } = require('@joplin/lib/string-utils');

View File

@ -8,10 +8,11 @@ const styled = require('styled-components').default;
interface Props {
showNewNoteButtons: boolean;
height: number;
}
const StyledRoot = styled.div`
width: 100%;
height: ${(props: any) => props.height}px;
display: flex;
flex-direction: row;
padding: ${(props: any) => props.theme.mainPadding}px;
@ -68,7 +69,7 @@ export default function NoteListControls(props: Props) {
}
return (
<StyledRoot>
<StyledRoot height={props.height}>
<SearchBar inputRef={searchBarRef}/>
{renderNewNoteButtons()}
</StyledRoot>

View File

@ -0,0 +1,39 @@
import { themeStyle } from '@joplin/lib/theme';
import * as React from 'react';
import { useMemo } from 'react';
import NoteList from '../NoteList/NoteList';
import NoteListControls from '../NoteListControls/NoteListControls';
import { Size } from '../ResizableLayout/utils/types';
import styled from 'styled-components';
interface Props {
resizableLayoutEventEmitter: any;
size: Size;
visible: boolean;
focusedField: string;
themeId: number;
}
const StyledRoot = styled.div`
display: flex;
flex-direction: column;
`;
export default function NoteListWrapper(props: Props) {
const theme = themeStyle(props.themeId);
const controlHeight = theme.topRowHeight;
const noteListSize = useMemo(() => {
return {
width: props.size.width,
height: props.size.height - controlHeight,
};
}, [props.size, controlHeight]);
return (
<StyledRoot>
<NoteListControls showNewNoteButtons={props.focusedField !== 'globalSearch'} height={controlHeight} />
<NoteList resizableLayoutEventEmitter={props.resizableLayoutEventEmitter} size={noteListSize} visible={props.visible}/>
</StyledRoot>
);
}

View File

@ -3,7 +3,7 @@ import CommandService from '@joplin/lib/services/CommandService';
import ToolbarBase from '../ToolbarBase';
import { utils as pluginUtils } from '@joplin/lib/services/plugins/reducer';
import ToolbarButtonUtils, { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
import stateToWhenClauseContext from '@joplin/lib/services/commands/stateToWhenClauseContext';
import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseContext';
const { connect } = require('react-redux');
const { buildStyle } = require('@joplin/lib/theme');

View File

@ -0,0 +1,80 @@
import * as React from 'react';
import { useCallback } from 'react';
import Button, { ButtonLevel } from '../Button/Button';
import { MoveDirection } from './utils/movements';
import styled from 'styled-components';
const StyledRoot = styled.div`
display: flex;
flex-direction: column;
padding: 5px;
background-color: ${props => props.theme.backgroundColor};
border-radius: 5px;
`;
const ButtonRow = styled.div`
display: flex;
flex-direction: row;
justify-content: center;
`;
const EmptyButton = styled(Button)`
visibility: hidden;
`;
const ArrowButton = styled(Button)`
opacity: ${props => props.disabled ? 0.2 : 1};
`;
export interface MoveButtonClickEvent {
direction: MoveDirection;
itemKey: string;
}
interface Props {
onClick(event: MoveButtonClickEvent): void;
itemKey: string;
canMoveLeft: boolean;
canMoveRight: boolean;
canMoveUp: boolean;
canMoveDown: boolean;
}
export default function MoveButtons(props: Props) {
const onButtonClick = useCallback((direction: MoveDirection) => {
props.onClick({ direction, itemKey: props.itemKey });
}, [props.onClick, props.itemKey]);
function canMove(dir: MoveDirection) {
if (dir === MoveDirection.Up) return props.canMoveUp;
if (dir === MoveDirection.Down) return props.canMoveDown;
if (dir === MoveDirection.Left) return props.canMoveLeft;
if (dir === MoveDirection.Right) return props.canMoveRight;
throw new Error('Unreachable');
}
function renderButton(dir: MoveDirection) {
return <ArrowButton
disabled={!canMove(dir)}
level={ButtonLevel.Primary}
iconName={`fas fa-arrow-${dir}`}
onClick={() => onButtonClick(dir)}
/>;
}
return (
<StyledRoot>
<ButtonRow>
{renderButton(MoveDirection.Up)}
</ButtonRow>
<ButtonRow>
{renderButton(MoveDirection.Left)}
<EmptyButton iconName="fas fa-arrow-down" disabled={true}/>
{renderButton(MoveDirection.Right)}
</ButtonRow>
<ButtonRow>
{renderButton(MoveDirection.Down)}
</ButtonRow>
</StyledRoot>
);
}

View File

@ -1,36 +1,18 @@
import * as React from 'react';
import { useRef, useState } from 'react';
import produce from 'immer';
import useWindowResizeEvent from './hooks/useWindowResizeEvent';
import useLayoutItemSizes, { LayoutItemSizes, itemSize } from './hooks/useLayoutItemSizes';
const { Resizable } = require('re-resizable');
import { useRef, useState, useEffect } from 'react';
import useWindowResizeEvent from './utils/useWindowResizeEvent';
import setLayoutItemProps from './utils/setLayoutItemProps';
import useLayoutItemSizes, { LayoutItemSizes, itemSize } from './utils/useLayoutItemSizes';
import validateLayout from './utils/validateLayout';
import { Size, LayoutItem } from './utils/types';
import { canMove, MoveDirection } from './utils/movements';
import MoveButtons, { MoveButtonClickEvent } from './MoveButtons';
import { StyledWrapperRoot, StyledMoveOverlay, MoveModeRootWrapper, MoveModeRootMessage } from './utils/style';
import { Resizable } from 're-resizable';
const EventEmitter = require('events');
export const dragBarThickness = 5;
export enum LayoutItemDirection {
Row = 'row',
Column = 'column',
}
export interface Size {
width: number;
height: number;
}
export interface LayoutItem {
key: string;
width?: number;
height?: number;
minWidth?: number;
minHeight?: number;
children?: LayoutItem[];
direction?: LayoutItemDirection;
resizableRight?: boolean;
resizableBottom?: boolean;
visible?: boolean;
context?: any;
}
const itemMinWidth = 20;
const itemMinHeight = 20;
interface onResizeEvent {
layout: LayoutItem;
@ -42,78 +24,24 @@ interface Props {
width?: number;
height?: number;
renderItem: Function;
onMoveButtonClick(event: MoveButtonClickEvent): void;
moveMode: boolean;
moveModeMessage: string;
}
export function allDynamicSizes(layout: LayoutItem): any {
const output: any = {};
function recurseProcess(item: LayoutItem) {
if (item.resizableBottom || item.resizableRight) {
if ('width' in item || 'height' in item) {
const size: any = {};
if ('width' in item) size.width = item.width;
if ('height' in item) size.height = item.height;
output[item.key] = size;
}
}
if (item.children) {
for (const child of item.children) {
recurseProcess(child);
}
}
}
recurseProcess(layout);
return output;
function itemVisible(item: LayoutItem, moveMode: boolean) {
if (moveMode) return true;
if (item.children && !item.children.length) return false;
return item.visible !== false;
}
export function findItemByKey(layout: LayoutItem, key: string): LayoutItem {
function recurseFind(item: LayoutItem): LayoutItem {
if (item.key === key) return item;
if (item.children) {
for (const child of item.children) {
const found = recurseFind(child);
if (found) return found;
}
}
return null;
}
const output = recurseFind(layout);
if (!output) throw new Error(`Invalid item key: ${key}`);
return output;
}
function updateLayoutItem(layout: LayoutItem, key: string, props: any) {
return produce(layout, (draftState: LayoutItem) => {
function recurseFind(item: LayoutItem) {
if (item.key === key) {
for (const n in props) {
(item as any)[n] = props[n];
}
} else {
if (item.children) {
for (const child of item.children) {
recurseFind(child);
}
}
}
}
recurseFind(draftState);
});
}
function renderContainer(item: LayoutItem, sizes: LayoutItemSizes, onResizeStart: Function, onResize: Function, onResizeStop: Function, children: any[], isLastChild: boolean): any {
function renderContainer(item: LayoutItem, parent: LayoutItem | null, sizes: LayoutItemSizes, onResizeStart: Function, onResize: Function, onResizeStop: Function, children: any[], isLastChild: boolean, moveMode: boolean): any {
const style: any = {
display: item.visible !== false ? 'flex' : 'none',
display: itemVisible(item, moveMode) ? 'flex' : 'none',
flexDirection: item.direction,
};
const size: Size = itemSize(item, sizes);
const size: Size = itemSize(item, parent, sizes, true);
const className = `resizableLayoutItem rli-${item.key}`;
if (item.resizableRight || item.resizableBottom) {
@ -128,21 +56,18 @@ function renderContainer(item: LayoutItem, sizes: LayoutItemSizes, onResizeStart
topLeft: false,
};
if (item.resizableRight) style.paddingRight = dragBarThickness;
if (item.resizableBottom) style.paddingBottom = dragBarThickness;
return (
<Resizable
key={item.key}
className={className}
style={style}
size={size}
onResizeStart={onResizeStart}
onResize={onResize}
onResizeStop={onResizeStop}
onResizeStart={onResizeStart as any}
onResize={onResize as any}
onResizeStop={onResizeStop as any}
enable={enable}
minWidth={item.minWidth}
minHeight={item.minHeight}
minWidth={'minWidth' in item ? item.minWidth : itemMinWidth}
minHeight={'minHeight' in item ? item.minHeight : itemMinHeight}
>
{children}
</Resizable>
@ -161,8 +86,29 @@ function ResizableLayout(props: Props) {
const [resizedItem, setResizedItem] = useState<any>(null);
function renderLayoutItem(item: LayoutItem, sizes: LayoutItemSizes, isVisible: boolean, isLastChild: boolean): any {
function renderItemWrapper(comp: any, item: LayoutItem, parent: LayoutItem | null, size: Size, moveMode: boolean) {
const moveOverlay = moveMode ? (
<StyledMoveOverlay>
<MoveButtons
itemKey={item.key}
onClick={props.onMoveButtonClick}
canMoveLeft={canMove(MoveDirection.Left, item, parent)}
canMoveRight={canMove(MoveDirection.Right, item, parent)}
canMoveUp={canMove(MoveDirection.Up, item, parent)}
canMoveDown={canMove(MoveDirection.Down, item, parent)}
/>
</StyledMoveOverlay>
) : null;
return (
<StyledWrapperRoot key={item.key} size={size}>
{moveOverlay}
{comp}
</StyledWrapperRoot>
);
}
function renderLayoutItem(item: LayoutItem, parent: LayoutItem | null, sizes: LayoutItemSizes, isVisible: boolean, isLastChild: boolean): any {
function onResizeStart() {
setResizedItem({
key: item.key,
@ -171,45 +117,79 @@ function ResizableLayout(props: Props) {
});
}
function onResize(_event: any, _direction: any, _refToElement: HTMLDivElement, delta: any) {
const newLayout = updateLayoutItem(props.layout, item.key, {
width: resizedItem.initialWidth + delta.width,
height: resizedItem.initialHeight + delta.height,
});
function onResize(_event: any, direction: string, _refToElement: any, delta: any) {
const newWidth = Math.max(itemMinWidth, resizedItem.initialWidth + delta.width);
const newHeight = Math.max(itemMinHeight, resizedItem.initialHeight + delta.height);
const newSize: any = {};
if (item.width) newSize.width = item.width;
if (item.height) newSize.height = item.height;
if (direction === 'bottom') {
newSize.height = newHeight;
} else {
newSize.width = newWidth;
}
const newLayout = setLayoutItemProps(props.layout, item.key, newSize);
props.onResize({ layout: newLayout });
eventEmitter.current.emit('resize');
}
function onResizeStop(_event: any, _direction: any, _refToElement: HTMLDivElement, delta: any) {
function onResizeStop(_event: any, _direction: any, _refToElement: any, delta: any) {
onResize(_event, _direction, _refToElement, delta);
setResizedItem(null);
}
if (!item.children) {
const size = itemSize(item, parent, sizes, false);
const comp = props.renderItem(item.key, {
item: item,
eventEmitter: eventEmitter.current,
size: sizes[item.key],
size: size,
visible: isVisible,
});
return renderContainer(item, sizes, onResizeStart, onResize, onResizeStop, [comp], isLastChild);
const wrapper = renderItemWrapper(comp, item, parent, size, props.moveMode);
return renderContainer(item, parent, sizes, onResizeStart, onResize, onResizeStop, [wrapper], isLastChild, props.moveMode);
} else {
const childrenComponents = [];
for (let i = 0; i < item.children.length; i++) {
const child = item.children[i];
childrenComponents.push(renderLayoutItem(child, sizes, isVisible && child.visible !== false, i === item.children.length - 1));
childrenComponents.push(renderLayoutItem(child, item, sizes, isVisible && itemVisible(child, props.moveMode), i === item.children.length - 1));
}
return renderContainer(item, sizes, onResizeStart, onResize, onResizeStop, childrenComponents, isLastChild);
return renderContainer(item, parent, sizes, onResizeStart, onResize, onResizeStop, childrenComponents, isLastChild, props.moveMode);
}
}
useWindowResizeEvent(eventEmitter);
const sizes = useLayoutItemSizes(props.layout);
useEffect(() => {
validateLayout(props.layout);
}, [props.layout]);
return renderLayoutItem(props.layout, sizes, props.layout.visible !== false, true);
useWindowResizeEvent(eventEmitter);
const sizes = useLayoutItemSizes(props.layout, props.moveMode);
function renderMoveModeBox(rootComp: any) {
return (
<MoveModeRootWrapper>
<MoveModeRootMessage>{props.moveModeMessage}</MoveModeRootMessage>
{rootComp}
</MoveModeRootWrapper>
);
}
const rootComp = renderLayoutItem(props.layout, null, sizes, itemVisible(props.layout, props.moveMode), true);
if (props.moveMode) {
return renderMoveModeBox(rootComp);
} else {
return rootComp;
}
}
export default ResizableLayout;

View File

@ -1,83 +0,0 @@
import { useMemo } from 'react';
import { LayoutItem, Size, dragBarThickness } from '../ResizableLayout';
export interface LayoutItemSizes {
[key: string]: Size;
}
export function itemSize(item: LayoutItem, sizes: LayoutItemSizes): Size {
return {
width: 'width' in item ? item.width : sizes[item.key].width,
height: 'height' in item ? item.height : sizes[item.key].height,
};
}
function calculateChildrenSizes(item: LayoutItem, sizes: LayoutItemSizes): LayoutItemSizes {
if (!item.children) return sizes;
const parentSize = itemSize(item, sizes);
const remainingSize: Size = {
width: parentSize.width,
height: parentSize.height,
};
const noWidthChildren: LayoutItem[] = [];
const noHeightChildren: LayoutItem[] = [];
for (const child of item.children) {
let w = 'width' in child ? child.width : null;
let h = 'height' in child ? child.height : null;
if (child.visible === false) {
w = 0;
h = 0;
}
if (item.resizableRight) w -= dragBarThickness;
if (item.resizableBottom) h -= dragBarThickness;
sizes[child.key] = { width: w, height: h };
if (w !== null) remainingSize.width -= w;
if (h !== null) remainingSize.height -= h;
if (w === null) noWidthChildren.push(child);
if (h === null) noHeightChildren.push(child);
}
if (noWidthChildren.length) {
const w = item.direction === 'row' ? remainingSize.width / noWidthChildren.length : parentSize.width;
for (const child of noWidthChildren) {
sizes[child.key].width = w;
}
}
if (noHeightChildren.length) {
const h = item.direction === 'column' ? remainingSize.height / noHeightChildren.length : parentSize.height;
for (const child of noHeightChildren) {
sizes[child.key].height = h;
}
}
for (const child of item.children) {
const childrenSizes = calculateChildrenSizes(child, sizes);
sizes = { ...sizes, ...childrenSizes };
}
return sizes;
}
export default function useLayoutItemSizes(layout: LayoutItem) {
return useMemo(() => {
let sizes: LayoutItemSizes = {};
if (!('width' in layout) || !('height' in layout)) throw new Error('width and height are required on layout root');
sizes[layout.key] = {
width: layout.width,
height: layout.height,
};
sizes = calculateChildrenSizes(layout, sizes);
return sizes;
}, [layout]);
}

View File

@ -0,0 +1,23 @@
import { LayoutItem } from './types';
export default function findItemByKey(layout: LayoutItem, key: string): LayoutItem {
if (!layout) throw new Error('Layout cannot be null');
function recurseFind(item: LayoutItem): LayoutItem {
if (item.key === key) return item;
if (item.children) {
for (const child of item.children) {
const found = recurseFind(child);
if (found) return found;
}
}
return null;
}
return recurseFind(layout);
// const output = recurseFind(layout);
// if (!output) throw new Error(`Could not find item "${key}"`);
// return output;
}

View File

@ -0,0 +1,5 @@
import { tempContainerPrefix } from './types';
export default function(itemKey: string): boolean {
return itemKey.indexOf(tempContainerPrefix) === 0;
}

View File

@ -0,0 +1,21 @@
import { LayoutItem } from './types';
type ItemItemCallback = (itemIndex: number, item: LayoutItem, parent: LayoutItem)=> boolean;
export default function iterateItems(layout: LayoutItem, callback: ItemItemCallback) {
const result = callback(0, layout, null);
if (result === false) return;
function recurseFind(item: LayoutItem, callback: Function): boolean {
if (item.children) {
for (let childIndex = 0; childIndex < item.children.length; childIndex++) {
const child = item.children[childIndex];
if (callback(childIndex, child, item) === false) return false;
if (recurseFind(child, callback) === false) return false;
}
}
return true;
}
if (recurseFind(layout, callback) === false) return;
}

View File

@ -0,0 +1,8 @@
import findItemByKey from './findItemByKey';
import { LayoutItem } from './types';
export default function layoutItemProp(layout: LayoutItem, key: string, propName: string) {
const item = findItemByKey(layout, key);
if (!item) throw new Error(`Could not find layout item: ${key}`);
return (item as any)[propName];
}

View File

@ -0,0 +1,228 @@
import { LayoutItem, LayoutItemDirection } from './types';
import validateLayout from './validateLayout';
import { canMove, MoveDirection, moveHorizontal, moveVertical } from './movements';
import findItemByKey from './findItemByKey';
describe('movements', () => {
test('should move items horizontally to the right', () => {
let layout: LayoutItem = validateLayout({
key: 'root',
width: 100,
height: 100,
direction: LayoutItemDirection.Row,
children: [
{
key: 'col1',
},
{
key: 'col2',
},
{
key: 'col3',
},
],
});
expect(() => moveHorizontal(layout, 'col1', -1)).toThrow();
layout = moveHorizontal(layout, 'col1', 1);
expect(layout.children[0].children[0].key).toBe('col2');
expect(layout.children[0].children[1].key).toBe('col1');
expect(layout.children[1].key).toBe('col3');
layout = moveHorizontal(layout, 'col1', 1);
expect(layout.children[0].key).toBe('col2');
expect(layout.children[1].key).toBe('col1');
expect(layout.children[2].key).toBe('col3');
layout = moveHorizontal(layout, 'col1', 1);
layout = moveHorizontal(layout, 'col1', 1);
expect(() => moveHorizontal(layout, 'col1', 1)).toThrow();
});
test('should move items horizontally to the left', () => {
let layout: LayoutItem = validateLayout({
key: 'root',
width: 100,
height: 100,
direction: LayoutItemDirection.Row,
children: [
{
key: 'col1',
direction: LayoutItemDirection.Column,
children: [
{ key: 'item1' },
{ key: 'item2' },
],
},
{
key: 'col2',
},
],
});
layout = moveHorizontal(layout, 'item2', -1);
expect(layout.children[0].key).toBe('item2');
expect(layout.children[1].key).toBe('item1');
expect(layout.children[2].key).toBe('col2');
});
test('should move items vertically', () => {
let layout: LayoutItem = validateLayout({
key: 'root',
width: 100,
height: 100,
direction: LayoutItemDirection.Row,
children: [
{
key: 'col1',
},
{
key: 'col2',
direction: LayoutItemDirection.Column,
children: [
{ key: 'row1' },
{ key: 'row2' },
{ key: 'row3' },
],
},
{
key: 'col3',
},
],
});
layout = moveVertical(layout, 'row3', -1);
expect(layout.children[1].children[0].key).toBe('row1');
expect(layout.children[1].children[1].key).toBe('row3');
expect(layout.children[1].children[2].key).toBe('row2');
});
test('should tell if item can be moved', () => {
const layout: LayoutItem = validateLayout({
key: 'root',
width: 200,
height: 100,
direction: LayoutItemDirection.Row,
children: [
{
key: 'col1',
resizableRight: true,
direction: LayoutItemDirection.Column,
children: [
{ key: 'row1' },
{ key: 'row2' },
],
},
{
key: 'col2',
},
],
});
expect(canMove(MoveDirection.Up, findItemByKey(layout, 'col1'), findItemByKey(layout, 'root'))).toBe(false);
expect(canMove(MoveDirection.Down, findItemByKey(layout, 'col1'), findItemByKey(layout, 'root'))).toBe(false);
expect(canMove(MoveDirection.Left, findItemByKey(layout, 'col1'), findItemByKey(layout, 'root'))).toBe(false);
expect(canMove(MoveDirection.Right, findItemByKey(layout, 'col1'), findItemByKey(layout, 'root'))).toBe(true);
expect(canMove(MoveDirection.Up, findItemByKey(layout, 'row1'), findItemByKey(layout, 'col1'))).toBe(false);
expect(canMove(MoveDirection.Down, findItemByKey(layout, 'row1'), findItemByKey(layout, 'col1'))).toBe(true);
expect(canMove(MoveDirection.Left, findItemByKey(layout, 'row1'), findItemByKey(layout, 'col1'))).toBe(true);
expect(canMove(MoveDirection.Right, findItemByKey(layout, 'row1'), findItemByKey(layout, 'col1'))).toBe(true);
expect(canMove(MoveDirection.Up, findItemByKey(layout, 'row2'), findItemByKey(layout, 'col1'))).toBe(true);
expect(canMove(MoveDirection.Down, findItemByKey(layout, 'row2'), findItemByKey(layout, 'col1'))).toBe(false);
expect(canMove(MoveDirection.Left, findItemByKey(layout, 'row2'), findItemByKey(layout, 'col1'))).toBe(true);
expect(canMove(MoveDirection.Right, findItemByKey(layout, 'row2'), findItemByKey(layout, 'col1'))).toBe(true);
expect(canMove(MoveDirection.Up, findItemByKey(layout, 'col2'), findItemByKey(layout, 'root'))).toBe(false);
expect(canMove(MoveDirection.Down, findItemByKey(layout, 'col2'), findItemByKey(layout, 'root'))).toBe(false);
expect(canMove(MoveDirection.Left, findItemByKey(layout, 'col2'), findItemByKey(layout, 'root'))).toBe(true);
expect(canMove(MoveDirection.Right, findItemByKey(layout, 'col2'), findItemByKey(layout, 'root'))).toBe(false);
});
test('Container with only one child should take the width of its parent', () => {
let layout: LayoutItem = validateLayout({
key: 'root',
width: 100,
height: 100,
direction: LayoutItemDirection.Row,
children: [
{
key: 'col1',
width: 50,
},
{
key: 'col2',
},
],
});
layout = moveHorizontal(layout, 'col2', -1);
expect(layout.children[0].children[0].key).toBe('col1');
expect(layout.children[0].children[0].width).toBe(undefined);
});
test('Temp container should take the width of the child it replaces', () => {
let layout: LayoutItem = validateLayout({
key: 'root',
width: 100,
height: 100,
direction: LayoutItemDirection.Row,
children: [
{
key: 'col1',
width: 20,
},
{
key: 'col2',
width: 80,
},
{
key: 'col3',
},
],
});
layout = moveHorizontal(layout, 'col2', -1);
expect(layout.children[0].width).toBe(20);
expect(layout.children[0].children[0].width).toBe(undefined);
expect(layout.children[0].children[1].width).toBe(undefined);
});
test('Last child should have flexible width if all siblings have fixed width', () => {
let layout: LayoutItem = validateLayout({
key: 'root',
width: 100,
height: 100,
direction: LayoutItemDirection.Row,
children: [
{
key: 'col1',
width: 20,
},
{
key: 'col2',
width: 20,
},
{
key: 'col3',
},
],
});
layout = moveHorizontal(layout, 'col3', -1);
expect(layout.children[0].width).toBe(20);
expect(layout.children[1].width).toBe(undefined);
});
});

View File

@ -0,0 +1,190 @@
import iterateItems from './iterateItems';
import { LayoutItem, LayoutItemDirection, tempContainerPrefix } from './types';
import produce from 'immer';
import uuid from '@joplin/lib/uuid';
import validateLayout from './validateLayout';
export enum MoveDirection {
Up = 'up',
Down = 'down',
Left = 'left',
Right = 'right',
}
enum MovementDirection {
Horizontal = 1,
Vertical = 2,
}
function array_move(arr: any[], old_index: number, new_index: number) {
arr = arr.slice();
if (new_index >= arr.length) {
let k = new_index - arr.length + 1;
while (k--) {
arr.push(undefined);
}
}
arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
return arr;
}
function findItemIndex(siblings: LayoutItem[], key: string) {
return siblings.findIndex((value: LayoutItem) => {
return value.key === key;
});
}
function isHorizontalMove(direction: MoveDirection) {
return direction === MoveDirection.Left || direction === MoveDirection.Right;
}
function resetItemSizes(items: LayoutItem[]) {
return items.map((item: LayoutItem) => {
const newItem = { ...item };
delete newItem.width;
delete newItem.height;
return newItem;
});
}
export function canMove(direction: MoveDirection, item: LayoutItem, parent: LayoutItem) {
if (!parent) return false;
if (isHorizontalMove(direction)) {
if (parent.isRoot) {
const idx = direction === MoveDirection.Left ? 0 : parent.children.length - 1;
return parent.children[idx] !== item;
} else if (parent.direction === LayoutItemDirection.Column) {
return true;
}
} else {
if (parent.isRoot) {
return false;
} else if (parent.direction === LayoutItemDirection.Column) {
const idx = direction === MoveDirection.Up ? 0 : parent.children.length - 1;
return parent.children[idx] !== item;
}
}
throw new Error('Unhandled case');
}
// For all movements we make the assumption that there's a root container,
// which is a row of multiple columns. Within each of these columns there
// can be multiple rows (one item per row). Items cannot be more deeply
// nested.
function moveItem(direction: MovementDirection, layout: LayoutItem, key: string, inc: number): LayoutItem {
const itemParents: Record<string, LayoutItem> = {};
const itemIsRoot = (item: LayoutItem) => {
return !itemParents[item.key];
};
const updatedLayout = produce(layout, (draft: any) => {
iterateItems(draft, (itemIndex: number, item: LayoutItem, parent: LayoutItem) => {
itemParents[item.key] = parent;
if (item.key !== key || !parent) return true;
// - "flow" means we are moving an item horizontally within a
// row
// - "contrary" means we are moving an item horizontally within
// a column. Sicen it can't move horizontally, it is moved
// out of its container. And vice-versa for vertical
// movements.
let moveType = null;
if (direction === MovementDirection.Horizontal && parent.direction === LayoutItemDirection.Row) moveType = 'flow';
if (direction === MovementDirection.Horizontal && parent.direction === LayoutItemDirection.Column) moveType = 'contrary';
if (direction === MovementDirection.Vertical && parent.direction === LayoutItemDirection.Column) moveType = 'flow';
if (direction === MovementDirection.Vertical && parent.direction === LayoutItemDirection.Row) moveType = 'contrary';
if (moveType === 'flow') {
const newIndex = itemIndex + inc;
if (newIndex >= parent.children.length || newIndex < 0) throw new Error(`Cannot move item "${key}" from position ${itemIndex} to ${newIndex}`);
// If the item next to it is a container (has children),
// move the item inside the container
if (parent.children[newIndex].children) {
const newParent = parent.children[newIndex];
parent.children.splice(itemIndex, 1);
newParent.children.push(item);
newParent.children = resetItemSizes(newParent.children);
} else {
// If the item is a child of the root container, create
// a new column at `newIndex` and move the item that
// was there, as well as the current item, in this
// container.
if (itemIsRoot(parent)) {
const targetChild = parent.children[newIndex];
// The new container takes the size of the item it
// replaces.
const newSize: any = {};
if (direction === MovementDirection.Horizontal) {
if ('width' in targetChild) newSize.width = targetChild.width;
} else {
if ('height' in targetChild) newSize.height = targetChild.height;
}
const newParent: LayoutItem = {
key: `${tempContainerPrefix}${uuid.createNano()}`,
direction: LayoutItemDirection.Column,
children: [
targetChild,
item,
],
...newSize,
};
parent.children[newIndex] = newParent;
parent.children.splice(itemIndex, 1);
newParent.children = resetItemSizes(newParent.children);
} else {
// Otherwise the default case is simply to move the
// item left/right
parent.children = array_move(parent.children, itemIndex, newIndex);
}
}
} else {
const parentParent = itemParents[parent.key];
const parentIndex = findItemIndex(parentParent.children, parent.key);
parent.children.splice(itemIndex, 1);
let newInc = inc;
if (parent.children.length <= 1) {
parentParent.children[parentIndex] = parent.children[0];
newInc = inc < 0 ? inc + 1 : inc;
}
const newItemIndex = parentIndex + newInc;
parentParent.children.splice(newItemIndex, 0, item);
parentParent.children = resetItemSizes(parentParent.children);
}
return false;
});
});
return validateLayout(updatedLayout);
}
export function moveHorizontal(layout: LayoutItem, key: string, inc: number): LayoutItem {
return moveItem(MovementDirection.Horizontal, layout, key, inc);
}
export function moveVertical(layout: LayoutItem, key: string, inc: number): LayoutItem {
return moveItem(MovementDirection.Vertical, layout, key, inc);
}
export function move(layout: LayoutItem, key: string, direction: MoveDirection): LayoutItem {
if (direction === MoveDirection.Up) return moveVertical(layout, key, -1);
if (direction === MoveDirection.Down) return moveVertical(layout, key, +1);
if (direction === MoveDirection.Left) return moveHorizontal(layout, key, -1);
if (direction === MoveDirection.Right) return moveHorizontal(layout, key, +1);
throw new Error('Unreachable');
}

View File

@ -0,0 +1,77 @@
import { loadLayout, saveLayout } from './persist';
import { LayoutItem, LayoutItemDirection } from './types';
import validateLayout from './validateLayout';
describe('persist', () => {
test('should save layout and filter out non-user properties', () => {
const layout: LayoutItem = validateLayout({
key: 'root',
width: 100,
height: 100,
direction: LayoutItemDirection.Row,
children: [
{
key: 'col1',
width: 50,
},
{
key: 'col2',
direction: LayoutItemDirection.Column,
children: [
{ key: 'item1', height: 20 },
{ key: 'item2' },
],
},
{
key: 'col3',
},
],
});
const toSave = saveLayout(layout);
expect(toSave.key).toBe('root');
expect(toSave.width).toBeUndefined();
expect(toSave.height).toBeUndefined();
expect(toSave.direction).toBeUndefined();
expect(toSave.children.length).toBe(3);
expect(toSave.children[1].key).toBe('col2');
expect(toSave.children[1].direction).toBeUndefined();
});
test('should load a layout', () => {
const layout: any = {
key: 'root',
children: [
{
key: 'col1',
width: 50,
},
{
key: 'col2',
children: [
{ key: 'item1', height: 20 },
{ key: 'item2' },
],
},
{
key: 'col3',
},
],
};
const loaded = loadLayout(layout, null, { width: 100, height: 200 });
expect(loaded.key).toBe('root');
expect(loaded.width).toBe(100);
expect(loaded.height).toBe(200);
expect(loaded.direction).toBe(LayoutItemDirection.Row);
expect(loaded.children.length).toBe(3);
expect(loaded.children[1].key).toBe('col2');
expect(loaded.children[1].direction).toBe(LayoutItemDirection.Column);
});
});

View File

@ -0,0 +1,41 @@
import { LayoutItem, Size } from './types';
import produce from 'immer';
import iterateItems from './iterateItems';
import validateLayout from './validateLayout';
export function saveLayout(layout: LayoutItem): any {
const propertyWhiteList = [
'visible',
'width',
'height',
'children',
'key',
'context',
];
return produce(layout, (draft: any) => {
delete draft.width;
delete draft.height;
iterateItems(draft, (_itemIndex: number, item: LayoutItem, _parent: LayoutItem) => {
for (const k of Object.keys(item)) {
if (!propertyWhiteList.includes(k)) delete (item as any)[k];
}
return true;
});
});
}
export function loadLayout(layout: any, defaultLayout: LayoutItem, rootSize: Size): LayoutItem {
let output: LayoutItem = null;
if (layout) {
output = { ...layout };
} else {
output = { ...defaultLayout };
}
output.width = rootSize.width;
output.height = rootSize.height;
return validateLayout(output);
}

View File

@ -0,0 +1,18 @@
import produce from 'immer';
import iterateItems from './iterateItems';
import { LayoutItem } from './types';
import validateLayout from './validateLayout';
export default function(layout: LayoutItem, itemKey: string): LayoutItem {
const output = produce(layout, (layoutDraft: LayoutItem) => {
iterateItems(layoutDraft, (itemIndex: number, item: LayoutItem, parent: LayoutItem) => {
if (item.key === itemKey) {
parent.children.splice(itemIndex, 1);
return false;
}
return true;
});
});
return output !== layout ? validateLayout(output) : layout;
}

View File

@ -0,0 +1,23 @@
import produce from 'immer';
import { LayoutItem } from './types';
import validateLayout from './validateLayout';
export default function setLayoutItemProps(layout: LayoutItem, key: string, props: any) {
return validateLayout(produce(layout, (draftState: LayoutItem) => {
function recurseFind(item: LayoutItem) {
if (item.key === key) {
for (const n in props) {
(item as any)[n] = props[n];
}
} else {
if (item.children) {
for (const child of item.children) {
recurseFind(child);
}
}
}
}
recurseFind(draftState);
}));
}

View File

@ -0,0 +1,38 @@
import { ThemeAppearance } from '@joplin/lib/themes/type';
import styled from 'styled-components';
export const StyledWrapperRoot = styled.div`
position: relative;
display: flex;
width: ${props => props.size.width}px;
height: ${props => props.size.height}px;
`;
export const StyledMoveOverlay = styled.div`
display: flex;
justify-content: center;
align-items: center;
z-index: 100;
background-color: ${props => props.theme.appearance === ThemeAppearance.Light ? 'rgba(0,0,0,0.5)' : 'rgba(255,255,255,0.5)'};
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
`;
export const MoveModeRootWrapper = styled.div`
position:relative;
display: flex;
align-items: center;
justify-content: center;
`;
export const MoveModeRootMessage = styled.div`
position:absolute;
bottom: 10px;
z-index:200;
background-color: ${props => props.theme.backgroundColor};
padding: 10px;
border-radius: 5;
`;

Some files were not shown because too many files have changed in this diff Show More