mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
Merge remote-tracking branch 'upstream/dev' into pr/sync/sync-status-message-improvement
This commit is contained in:
commit
d123f38a9e
@ -596,6 +596,16 @@ packages/app-mobile/components/DialogManager/types.js
|
||||
packages/app-mobile/components/DismissibleDialog.js
|
||||
packages/app-mobile/components/Dropdown.test.js
|
||||
packages/app-mobile/components/Dropdown.js
|
||||
packages/app-mobile/components/EditorToolbar/EditorToolbar.test.js
|
||||
packages/app-mobile/components/EditorToolbar/EditorToolbar.js
|
||||
packages/app-mobile/components/EditorToolbar/ToolbarButton.js
|
||||
packages/app-mobile/components/EditorToolbar/ToolbarEditorDialog.js
|
||||
packages/app-mobile/components/EditorToolbar/testing/mockCommandRuntimes.js
|
||||
packages/app-mobile/components/EditorToolbar/types.js
|
||||
packages/app-mobile/components/EditorToolbar/utils/allToolbarCommandNamesFromState.js
|
||||
packages/app-mobile/components/EditorToolbar/utils/isSelected.js
|
||||
packages/app-mobile/components/EditorToolbar/utils/selectedCommandNamesFromState.js
|
||||
packages/app-mobile/components/EditorToolbar/utils/toolbarButtonsFromState.js
|
||||
packages/app-mobile/components/ExtendedWebView/index.jest.js
|
||||
packages/app-mobile/components/ExtendedWebView/index.js
|
||||
packages/app-mobile/components/ExtendedWebView/index.web.js
|
||||
@ -634,18 +644,6 @@ packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/startAutosaveLoop.
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/types.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/watchEditorForTemplateChanges.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/promptRestoreAutosave.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleSpaceButton.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useActionButtons.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useHeaderButtons.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useInlineFormattingButtons.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useListButtons.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/usePluginButtons.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.test.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.js
|
||||
packages/app-mobile/components/NoteEditor/SearchPanel.js
|
||||
@ -653,7 +651,6 @@ packages/app-mobile/components/NoteEditor/commandDeclarations.js
|
||||
packages/app-mobile/components/NoteEditor/hooks/useCodeMirrorPlugins.js
|
||||
packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.test.js
|
||||
packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.js
|
||||
packages/app-mobile/components/NoteEditor/hooks/useKeyboardVisible.js
|
||||
packages/app-mobile/components/NoteEditor/types.js
|
||||
packages/app-mobile/components/NoteItem.js
|
||||
packages/app-mobile/components/NoteList.js
|
||||
@ -670,6 +667,7 @@ packages/app-mobile/components/SelectDateTimeDialog.js
|
||||
packages/app-mobile/components/SideMenu.js
|
||||
packages/app-mobile/components/SideMenuContentNote.js
|
||||
packages/app-mobile/components/TextInput.js
|
||||
packages/app-mobile/components/ToggleSpaceButton.js
|
||||
packages/app-mobile/components/accessibility/AccessibleModalMenu.js
|
||||
packages/app-mobile/components/accessibility/AccessibleView.js
|
||||
packages/app-mobile/components/app-nav.js
|
||||
@ -753,8 +751,13 @@ packages/app-mobile/components/screens/ConfigScreen/plugins/utils/useUpdateState
|
||||
packages/app-mobile/components/screens/ConfigScreen/types.js
|
||||
packages/app-mobile/components/screens/JoplinCloudLoginScreen.js
|
||||
packages/app-mobile/components/screens/LogScreen.js
|
||||
packages/app-mobile/components/screens/Note.test.js
|
||||
packages/app-mobile/components/screens/Note.js
|
||||
packages/app-mobile/components/screens/Note/Note.test.js
|
||||
packages/app-mobile/components/screens/Note/Note.js
|
||||
packages/app-mobile/components/screens/Note/commands/attachFile.js
|
||||
packages/app-mobile/components/screens/Note/commands/hideKeyboard.js
|
||||
packages/app-mobile/components/screens/Note/commands/index.js
|
||||
packages/app-mobile/components/screens/Note/commands/setTags.js
|
||||
packages/app-mobile/components/screens/Note/types.js
|
||||
packages/app-mobile/components/screens/NoteTagsDialog.js
|
||||
packages/app-mobile/components/screens/Notes.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
|
||||
@ -778,6 +781,7 @@ packages/app-mobile/services/AlarmServiceDriver.android.js
|
||||
packages/app-mobile/services/AlarmServiceDriver.ios.js
|
||||
packages/app-mobile/services/AlarmServiceDriver.web.js
|
||||
packages/app-mobile/services/BackButtonService.js
|
||||
packages/app-mobile/services/commands/stateToWhenClauseContext.js
|
||||
packages/app-mobile/services/e2ee/RSA.react-native.js
|
||||
packages/app-mobile/services/e2ee/crypto.js
|
||||
packages/app-mobile/services/plugins/PlatformImplementation.js
|
||||
@ -819,6 +823,7 @@ packages/app-mobile/utils/fs-driver/testUtil/createFilesFromPathRecord.js
|
||||
packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js
|
||||
packages/app-mobile/utils/getPackageInfo.js
|
||||
packages/app-mobile/utils/getVersionInfoText.js
|
||||
packages/app-mobile/utils/hooks/useKeyboardVisible.js
|
||||
packages/app-mobile/utils/hooks/useOnLongPressProps.js
|
||||
packages/app-mobile/utils/hooks/useReduceMotionEnabled.js
|
||||
packages/app-mobile/utils/image/fileToImage.web.js
|
||||
@ -843,6 +848,7 @@ packages/app-mobile/utils/shim-init-react/shimInitShared.js
|
||||
packages/app-mobile/utils/testing/createMockReduxStore.js
|
||||
packages/app-mobile/utils/testing/getWebViewDomById.js
|
||||
packages/app-mobile/utils/testing/getWebViewWindowById.js
|
||||
packages/app-mobile/utils/testing/setupGlobalStore.js
|
||||
packages/app-mobile/utils/types.js
|
||||
packages/app-mobile/web/serviceWorker.js
|
||||
packages/default-plugins/build.js
|
||||
@ -915,6 +921,7 @@ packages/editor/CodeMirror/utils/formatting/toggleSelectedLinesStartWith.js
|
||||
packages/editor/CodeMirror/utils/formatting/types.js
|
||||
packages/editor/CodeMirror/utils/getSearchState.js
|
||||
packages/editor/CodeMirror/utils/growSelectionToNode.js
|
||||
packages/editor/CodeMirror/utils/handleLinkEditRequests.js
|
||||
packages/editor/CodeMirror/utils/handlePasteEvent.js
|
||||
packages/editor/CodeMirror/utils/isCursorAtBeginning.js
|
||||
packages/editor/CodeMirror/utils/isInSyntaxNode.js
|
||||
@ -1195,6 +1202,8 @@ packages/lib/services/interop/InteropService_Importer_Raw.js
|
||||
packages/lib/services/interop/Module.test.js
|
||||
packages/lib/services/interop/Module.js
|
||||
packages/lib/services/interop/types.js
|
||||
packages/lib/services/interop/utils.test.js
|
||||
packages/lib/services/interop/utils.js
|
||||
packages/lib/services/joplinCloudUtils.js
|
||||
packages/lib/services/joplinServer/personalizedUserContentBaseUrl.js
|
||||
packages/lib/services/keychain/KeychainService.test.js
|
||||
|
39
.gitignore
vendored
39
.gitignore
vendored
@ -572,6 +572,16 @@ packages/app-mobile/components/DialogManager/types.js
|
||||
packages/app-mobile/components/DismissibleDialog.js
|
||||
packages/app-mobile/components/Dropdown.test.js
|
||||
packages/app-mobile/components/Dropdown.js
|
||||
packages/app-mobile/components/EditorToolbar/EditorToolbar.test.js
|
||||
packages/app-mobile/components/EditorToolbar/EditorToolbar.js
|
||||
packages/app-mobile/components/EditorToolbar/ToolbarButton.js
|
||||
packages/app-mobile/components/EditorToolbar/ToolbarEditorDialog.js
|
||||
packages/app-mobile/components/EditorToolbar/testing/mockCommandRuntimes.js
|
||||
packages/app-mobile/components/EditorToolbar/types.js
|
||||
packages/app-mobile/components/EditorToolbar/utils/allToolbarCommandNamesFromState.js
|
||||
packages/app-mobile/components/EditorToolbar/utils/isSelected.js
|
||||
packages/app-mobile/components/EditorToolbar/utils/selectedCommandNamesFromState.js
|
||||
packages/app-mobile/components/EditorToolbar/utils/toolbarButtonsFromState.js
|
||||
packages/app-mobile/components/ExtendedWebView/index.jest.js
|
||||
packages/app-mobile/components/ExtendedWebView/index.js
|
||||
packages/app-mobile/components/ExtendedWebView/index.web.js
|
||||
@ -610,18 +620,6 @@ packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/startAutosaveLoop.
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/types.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/watchEditorForTemplateChanges.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/promptRestoreAutosave.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleSpaceButton.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useActionButtons.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useHeaderButtons.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useInlineFormattingButtons.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useListButtons.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/usePluginButtons.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.test.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.js
|
||||
packages/app-mobile/components/NoteEditor/SearchPanel.js
|
||||
@ -629,7 +627,6 @@ packages/app-mobile/components/NoteEditor/commandDeclarations.js
|
||||
packages/app-mobile/components/NoteEditor/hooks/useCodeMirrorPlugins.js
|
||||
packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.test.js
|
||||
packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.js
|
||||
packages/app-mobile/components/NoteEditor/hooks/useKeyboardVisible.js
|
||||
packages/app-mobile/components/NoteEditor/types.js
|
||||
packages/app-mobile/components/NoteItem.js
|
||||
packages/app-mobile/components/NoteList.js
|
||||
@ -646,6 +643,7 @@ packages/app-mobile/components/SelectDateTimeDialog.js
|
||||
packages/app-mobile/components/SideMenu.js
|
||||
packages/app-mobile/components/SideMenuContentNote.js
|
||||
packages/app-mobile/components/TextInput.js
|
||||
packages/app-mobile/components/ToggleSpaceButton.js
|
||||
packages/app-mobile/components/accessibility/AccessibleModalMenu.js
|
||||
packages/app-mobile/components/accessibility/AccessibleView.js
|
||||
packages/app-mobile/components/app-nav.js
|
||||
@ -729,8 +727,13 @@ packages/app-mobile/components/screens/ConfigScreen/plugins/utils/useUpdateState
|
||||
packages/app-mobile/components/screens/ConfigScreen/types.js
|
||||
packages/app-mobile/components/screens/JoplinCloudLoginScreen.js
|
||||
packages/app-mobile/components/screens/LogScreen.js
|
||||
packages/app-mobile/components/screens/Note.test.js
|
||||
packages/app-mobile/components/screens/Note.js
|
||||
packages/app-mobile/components/screens/Note/Note.test.js
|
||||
packages/app-mobile/components/screens/Note/Note.js
|
||||
packages/app-mobile/components/screens/Note/commands/attachFile.js
|
||||
packages/app-mobile/components/screens/Note/commands/hideKeyboard.js
|
||||
packages/app-mobile/components/screens/Note/commands/index.js
|
||||
packages/app-mobile/components/screens/Note/commands/setTags.js
|
||||
packages/app-mobile/components/screens/Note/types.js
|
||||
packages/app-mobile/components/screens/NoteTagsDialog.js
|
||||
packages/app-mobile/components/screens/Notes.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
|
||||
@ -754,6 +757,7 @@ packages/app-mobile/services/AlarmServiceDriver.android.js
|
||||
packages/app-mobile/services/AlarmServiceDriver.ios.js
|
||||
packages/app-mobile/services/AlarmServiceDriver.web.js
|
||||
packages/app-mobile/services/BackButtonService.js
|
||||
packages/app-mobile/services/commands/stateToWhenClauseContext.js
|
||||
packages/app-mobile/services/e2ee/RSA.react-native.js
|
||||
packages/app-mobile/services/e2ee/crypto.js
|
||||
packages/app-mobile/services/plugins/PlatformImplementation.js
|
||||
@ -795,6 +799,7 @@ packages/app-mobile/utils/fs-driver/testUtil/createFilesFromPathRecord.js
|
||||
packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js
|
||||
packages/app-mobile/utils/getPackageInfo.js
|
||||
packages/app-mobile/utils/getVersionInfoText.js
|
||||
packages/app-mobile/utils/hooks/useKeyboardVisible.js
|
||||
packages/app-mobile/utils/hooks/useOnLongPressProps.js
|
||||
packages/app-mobile/utils/hooks/useReduceMotionEnabled.js
|
||||
packages/app-mobile/utils/image/fileToImage.web.js
|
||||
@ -819,6 +824,7 @@ packages/app-mobile/utils/shim-init-react/shimInitShared.js
|
||||
packages/app-mobile/utils/testing/createMockReduxStore.js
|
||||
packages/app-mobile/utils/testing/getWebViewDomById.js
|
||||
packages/app-mobile/utils/testing/getWebViewWindowById.js
|
||||
packages/app-mobile/utils/testing/setupGlobalStore.js
|
||||
packages/app-mobile/utils/types.js
|
||||
packages/app-mobile/web/serviceWorker.js
|
||||
packages/default-plugins/build.js
|
||||
@ -891,6 +897,7 @@ packages/editor/CodeMirror/utils/formatting/toggleSelectedLinesStartWith.js
|
||||
packages/editor/CodeMirror/utils/formatting/types.js
|
||||
packages/editor/CodeMirror/utils/getSearchState.js
|
||||
packages/editor/CodeMirror/utils/growSelectionToNode.js
|
||||
packages/editor/CodeMirror/utils/handleLinkEditRequests.js
|
||||
packages/editor/CodeMirror/utils/handlePasteEvent.js
|
||||
packages/editor/CodeMirror/utils/isCursorAtBeginning.js
|
||||
packages/editor/CodeMirror/utils/isInSyntaxNode.js
|
||||
@ -1171,6 +1178,8 @@ packages/lib/services/interop/InteropService_Importer_Raw.js
|
||||
packages/lib/services/interop/Module.test.js
|
||||
packages/lib/services/interop/Module.js
|
||||
packages/lib/services/interop/types.js
|
||||
packages/lib/services/interop/utils.test.js
|
||||
packages/lib/services/interop/utils.js
|
||||
packages/lib/services/joplinCloudUtils.js
|
||||
packages/lib/services/joplinServer/personalizedUserContentBaseUrl.js
|
||||
packages/lib/services/keychain/KeychainService.test.js
|
||||
|
@ -4,14 +4,14 @@ import ToolbarBase from '../../../ToolbarBase';
|
||||
import { utils as pluginUtils } from '@joplin/lib/services/plugins/reducer';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from '../../../../app.reducer';
|
||||
import ToolbarButtonUtils, { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||
import ToolbarButtonUtils, { ToolbarItem } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||
import stateToWhenClauseContext from '../../../../services/commands/stateToWhenClauseContext';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
const { buildStyle } = require('@joplin/lib/theme');
|
||||
|
||||
interface ToolbarProps {
|
||||
themeId: number;
|
||||
toolbarButtonInfos: ToolbarButtonInfo[];
|
||||
toolbarButtonInfos: ToolbarItem[];
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,7 @@ import attachedResources from '@joplin/lib/utils/attachedResources';
|
||||
import useScroll from './utils/useScroll';
|
||||
import styles_ from './styles';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||
import { ToolbarItem } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||
import ToggleEditorsButton, { Value as ToggleEditorsButtonValue } from '../../../ToggleEditorsButton/ToggleEditorsButton';
|
||||
import ToolbarButton from '../../../../gui/ToolbarButton/ToolbarButton';
|
||||
import usePluginServiceRegistration from '../../utils/usePluginServiceRegistration';
|
||||
@ -1383,7 +1383,9 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
function renderExtraToolbarButton(key: string, info: ToolbarButtonInfo) {
|
||||
function renderExtraToolbarButton(key: string, info: ToolbarItem) {
|
||||
if (info.type === 'separator') return null;
|
||||
|
||||
return <ToolbarButton
|
||||
key={key}
|
||||
themeId={props.themeId}
|
||||
@ -1412,7 +1414,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
for (const info of props.noteToolbarButtonInfos) {
|
||||
if (leftButtonCommandNames.includes(info.name)) continue;
|
||||
|
||||
if (info.name === 'toggleEditors') {
|
||||
if (info.type === 'button' && info.name === 'toggleEditors') {
|
||||
buttons.push(<ToggleEditorsButton
|
||||
key={info.name}
|
||||
value={ToggleEditorsButtonValue.RichText}
|
||||
|
@ -20,7 +20,7 @@ import ToolbarButton from '../ToolbarButton/ToolbarButton';
|
||||
import Button, { ButtonLevel } from '../Button/Button';
|
||||
import eventManager, { EventName } from '@joplin/lib/eventManager';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import ToolbarButtonUtils from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||
import ToolbarButtonUtils, { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||
import { _, _n } from '@joplin/lib/locale';
|
||||
import TagList from '../TagList';
|
||||
import NoteTitleBar from './NoteTitle/NoteTitleBar';
|
||||
@ -742,7 +742,7 @@ const mapStateToProps = (state: AppState, ownProps: ConnectProps) => {
|
||||
], whenClauseContext),
|
||||
setTagsToolbarButtonInfo: toolbarButtonUtils.commandsToToolbarButtons([
|
||||
'setTags',
|
||||
], whenClauseContext)[0],
|
||||
], whenClauseContext)[0] as ToolbarButtonInfo,
|
||||
contentMaxWidth: state.settings['style.editor.contentMaxWidth'],
|
||||
isSafeMode: state.settings.isSafeMode,
|
||||
useCustomPdfViewer: false,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import AsyncActionQueue from '@joplin/lib/AsyncActionQueue';
|
||||
import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||
import { ToolbarButtonInfo, ToolbarItem } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||
import { PluginHtmlContents, PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import { MarkupLanguage } from '@joplin/renderer';
|
||||
import { RenderResult, RenderResultPluginAsset } from '@joplin/renderer/types';
|
||||
@ -48,7 +48,7 @@ export interface NoteEditorProps {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
highlightedWords: any[];
|
||||
plugins: PluginStates;
|
||||
toolbarButtonInfos: ToolbarButtonInfo[];
|
||||
toolbarButtonInfos: ToolbarItem[];
|
||||
setTagsToolbarButtonInfo: ToolbarButtonInfo;
|
||||
contentMaxWidth: number;
|
||||
isSafeMode: boolean;
|
||||
@ -136,7 +136,7 @@ export interface NoteBodyEditorProps {
|
||||
locale: string;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
onDrop: DropHandler;
|
||||
noteToolbarButtonInfos: ToolbarButtonInfo[];
|
||||
noteToolbarButtonInfos: ToolbarItem[];
|
||||
plugins: PluginStates;
|
||||
fontSize: number;
|
||||
contentMaxWidth: number;
|
||||
|
@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
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 ToolbarButtonUtils, { ToolbarItem } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||
import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseContext';
|
||||
import { connect } from 'react-redux';
|
||||
import { buildStyle } from '@joplin/lib/theme';
|
||||
@ -14,7 +14,7 @@ interface NoteToolbarProps {
|
||||
themeId: number;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
style: any;
|
||||
toolbarButtonInfos: ToolbarButtonInfo[];
|
||||
toolbarButtonInfos: ToolbarItem[];
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
|
@ -2,29 +2,25 @@ import * as React from 'react';
|
||||
import ToolbarButton from './ToolbarButton/ToolbarButton';
|
||||
import ToggleEditorsButton, { Value } from './ToggleEditorsButton/ToggleEditorsButton';
|
||||
import ToolbarSpace from './ToolbarSpace';
|
||||
import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||
import { ToolbarItem } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||
import { AppState } from '../app.reducer';
|
||||
import { connect } from 'react-redux';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
|
||||
interface ToolbarItemInfo extends ToolbarButtonInfo {
|
||||
type?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
style: React.CSSProperties;
|
||||
items: ToolbarItemInfo[];
|
||||
items: ToolbarItem[];
|
||||
disabled: boolean;
|
||||
'aria-label': string;
|
||||
}
|
||||
|
||||
const getItemType = (item: ToolbarItemInfo) => {
|
||||
const getItemType = (item: ToolbarItem) => {
|
||||
return item.type ?? 'button';
|
||||
};
|
||||
|
||||
const isFocusable = (item: ToolbarItemInfo) => {
|
||||
const isFocusable = (item: ToolbarItem) => {
|
||||
if (!item.enabled) {
|
||||
return false;
|
||||
}
|
||||
@ -32,11 +28,11 @@ const isFocusable = (item: ToolbarItemInfo) => {
|
||||
return getItemType(item) === 'button';
|
||||
};
|
||||
|
||||
const useCategorizedItems = (items: ToolbarItemInfo[]) => {
|
||||
const useCategorizedItems = (items: ToolbarItem[]) => {
|
||||
return useMemo(() => {
|
||||
const itemsLeft: ToolbarItemInfo[] = [];
|
||||
const itemsCenter: ToolbarItemInfo[] = [];
|
||||
const itemsRight: ToolbarItemInfo[] = [];
|
||||
const itemsLeft: ToolbarItem[] = [];
|
||||
const itemsCenter: ToolbarItem[] = [];
|
||||
const itemsRight: ToolbarItem[] = [];
|
||||
|
||||
if (items) {
|
||||
for (const item of items) {
|
||||
@ -63,7 +59,7 @@ const useCategorizedItems = (items: ToolbarItemInfo[]) => {
|
||||
|
||||
const useKeyboardHandler = (
|
||||
setSelectedIndex: React.Dispatch<React.SetStateAction<number>>,
|
||||
focusableItems: ToolbarItemInfo[],
|
||||
focusableItems: ToolbarItem[],
|
||||
) => {
|
||||
const onKeyDown: React.KeyboardEventHandler<HTMLElement> = useCallback(event => {
|
||||
let direction = 0;
|
||||
@ -110,11 +106,10 @@ const ToolbarBaseComponent: React.FC<Props> = props => {
|
||||
const containerHasFocus = !!containerRef.current?.contains(doc?.activeElement);
|
||||
|
||||
let keyCounter = 0;
|
||||
const renderItem = (o: ToolbarItemInfo, indexInFocusable: number) => {
|
||||
const renderItem = (o: ToolbarItem, indexInFocusable: number) => {
|
||||
let key = o.iconName ? o.iconName : '';
|
||||
key += o.title ? o.title : '';
|
||||
key += o.name ? o.name : '';
|
||||
const itemType = !('type' in o) ? 'button' : o.type;
|
||||
|
||||
if (!key) key = `${o.type}_${keyCounter++}`;
|
||||
|
||||
@ -132,7 +127,7 @@ const ToolbarBaseComponent: React.FC<Props> = props => {
|
||||
}
|
||||
};
|
||||
|
||||
if (o.name === 'toggleEditors') {
|
||||
if (o.type === 'button' && o.name === 'toggleEditors') {
|
||||
return <ToggleEditorsButton
|
||||
key={o.name}
|
||||
buttonRef={setButtonRefCallback}
|
||||
@ -141,7 +136,7 @@ const ToolbarBaseComponent: React.FC<Props> = props => {
|
||||
toolbarButtonInfo={o}
|
||||
tabIndex={tabIndex}
|
||||
/>;
|
||||
} else if (itemType === 'button') {
|
||||
} else if (o.type === 'button') {
|
||||
return (
|
||||
<ToolbarButton
|
||||
tabIndex={tabIndex}
|
||||
@ -149,7 +144,7 @@ const ToolbarBaseComponent: React.FC<Props> = props => {
|
||||
{...buttonProps}
|
||||
/>
|
||||
);
|
||||
} else if (itemType === 'separator') {
|
||||
} else if (o.type === 'separator') {
|
||||
return <ToolbarSpace {...buttonProps} />;
|
||||
}
|
||||
|
||||
@ -157,7 +152,7 @@ const ToolbarBaseComponent: React.FC<Props> = props => {
|
||||
};
|
||||
|
||||
let focusableIndex = 0;
|
||||
const renderList = (items: ToolbarItemInfo[]) => {
|
||||
const renderList = (items: ToolbarItem[]) => {
|
||||
const result: React.ReactNode[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "3.2.3",
|
||||
"version": "3.2.4",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"private": true,
|
||||
|
@ -79,8 +79,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097758
|
||||
versionName "3.2.2"
|
||||
versionCode 2097759
|
||||
versionName "3.2.3"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { StyleSheet, View, ViewStyle, useWindowDimensions } from 'react-native';
|
||||
import { IconButton, Surface } from 'react-native-paper';
|
||||
import { IconButton, Surface, Text } from 'react-native-paper';
|
||||
import { themeStyle } from './global-style';
|
||||
import Modal from './Modal';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
@ -19,6 +19,7 @@ interface Props {
|
||||
onDismiss: ()=> void;
|
||||
containerStyle?: ViewStyle;
|
||||
children: React.ReactNode;
|
||||
heading?: string;
|
||||
|
||||
size: DialogSize;
|
||||
}
|
||||
@ -35,7 +36,11 @@ const useStyles = (themeId: number, containerStyle: ViewStyle, size: DialogSize)
|
||||
return StyleSheet.create({
|
||||
closeButtonContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
justifyContent: 'space-between',
|
||||
alignContent: 'center',
|
||||
},
|
||||
heading: {
|
||||
alignSelf: 'center',
|
||||
},
|
||||
dialogContainer: {
|
||||
maxHeight,
|
||||
@ -66,8 +71,12 @@ const useStyles = (themeId: number, containerStyle: ViewStyle, size: DialogSize)
|
||||
const DismissibleDialog: React.FC<Props> = props => {
|
||||
const styles = useStyles(props.themeId, props.containerStyle, props.size);
|
||||
|
||||
const closeButton = (
|
||||
const heading = props.heading ? (
|
||||
<Text variant='headlineSmall' role='heading' style={styles.heading}>{props.heading}</Text>
|
||||
) : null;
|
||||
const closeButtonRow = (
|
||||
<View style={styles.closeButtonContainer}>
|
||||
{heading ?? <View/>}
|
||||
<IconButton
|
||||
icon='close'
|
||||
accessibilityLabel={_('Close')}
|
||||
@ -87,7 +96,7 @@ const DismissibleDialog: React.FC<Props> = props => {
|
||||
transparent={true}
|
||||
>
|
||||
<Surface style={styles.dialogSurface} elevation={1}>
|
||||
{closeButton}
|
||||
{closeButtonRow}
|
||||
{props.children}
|
||||
</Surface>
|
||||
</Modal>
|
||||
|
@ -0,0 +1,139 @@
|
||||
import * as React from 'react';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react-native';
|
||||
import '@testing-library/jest-native/extend-expect';
|
||||
|
||||
import { Store } from 'redux';
|
||||
import { AppState } from '../../utils/types';
|
||||
import TestProviderStack from '../testing/TestProviderStack';
|
||||
import EditorToolbar from './EditorToolbar';
|
||||
import { setupDatabase, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
import createMockReduxStore from '../../utils/testing/createMockReduxStore';
|
||||
import setupGlobalStore from '../../utils/testing/setupGlobalStore';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { RegisteredRuntime } from '@joplin/lib/services/CommandService';
|
||||
import mockCommandRuntimes from './testing/mockCommandRuntimes';
|
||||
|
||||
let store: Store<AppState>;
|
||||
|
||||
interface WrapperProps { }
|
||||
|
||||
const WrappedToolbar: React.FC<WrapperProps> = _props => {
|
||||
return <TestProviderStack store={store}>
|
||||
<EditorToolbar editorState={null} />
|
||||
</TestProviderStack>;
|
||||
};
|
||||
|
||||
const queryToolbarButton = (label: string) => {
|
||||
return screen.queryByRole('button', { name: label });
|
||||
};
|
||||
|
||||
const openSettings = async () => {
|
||||
const settingButton = screen.getByRole('button', { name: 'Settings' });
|
||||
fireEvent.press(settingButton);
|
||||
|
||||
// Settings should be open:
|
||||
const settingsHeader = await screen.findByRole('heading', { name: 'Manage toolbar options' });
|
||||
expect(settingsHeader).toBeVisible();
|
||||
};
|
||||
|
||||
interface ToggleSettingItemProps {
|
||||
name: string;
|
||||
expectedInitialState: boolean;
|
||||
}
|
||||
const toggleSettingsItem = async (props: ToggleSettingItemProps) => {
|
||||
const initialChecked = props.expectedInitialState;
|
||||
const finalChecked = !props.expectedInitialState;
|
||||
|
||||
const itemCheckbox = await screen.findByRole('checkbox', { name: props.name });
|
||||
expect(itemCheckbox).toBeVisible();
|
||||
expect(itemCheckbox).toHaveAccessibilityState({ checked: initialChecked });
|
||||
fireEvent.press(itemCheckbox);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(itemCheckbox).toHaveAccessibilityState({ checked: finalChecked });
|
||||
});
|
||||
};
|
||||
|
||||
let mockCommands: RegisteredRuntime|null = null;
|
||||
|
||||
describe('EditorToolbar', () => {
|
||||
beforeEach(async () => {
|
||||
await setupDatabase(0);
|
||||
await switchClient(0);
|
||||
|
||||
store = createMockReduxStore();
|
||||
setupGlobalStore(store);
|
||||
mockCommands = mockCommandRuntimes(store);
|
||||
|
||||
// Start with the default set of buttons
|
||||
Setting.setValue('editor.toolbarButtons', []);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockCommands?.deregister();
|
||||
mockCommands = null;
|
||||
});
|
||||
|
||||
it('unchecking items in settings should remove them from the toolbar', async () => {
|
||||
const toolbar = render(<WrappedToolbar/>);
|
||||
|
||||
// The bold button should be visible by default (if this changes, switch this
|
||||
// test to a button that is present by default).
|
||||
const boldLabel = 'Bold';
|
||||
const boldButton = queryToolbarButton(boldLabel);
|
||||
expect(boldButton).toBeVisible();
|
||||
|
||||
await openSettings();
|
||||
await toggleSettingsItem({ name: boldLabel, expectedInitialState: true });
|
||||
|
||||
// Bold button should be removed from the toolbar
|
||||
await waitFor(() => {
|
||||
expect(queryToolbarButton(boldLabel)).toBe(null);
|
||||
});
|
||||
|
||||
toolbar.unmount();
|
||||
});
|
||||
|
||||
it('checking items in settings should add them to the toolbar', async () => {
|
||||
// Start with a mostly-empty toolbar for testing
|
||||
Setting.setValue('editor.toolbarButtons', ['textBold', 'textItalic']);
|
||||
|
||||
const toolbar = render(<WrappedToolbar/>);
|
||||
|
||||
// Initially, the button shouldn't be present in the toolbar.
|
||||
const commandLabel = 'Code';
|
||||
expect(queryToolbarButton(commandLabel)).toBeNull();
|
||||
|
||||
await openSettings();
|
||||
await toggleSettingsItem({ name: commandLabel, expectedInitialState: false });
|
||||
|
||||
// The button should now be added to the toolbar
|
||||
await waitFor(() => {
|
||||
expect(queryToolbarButton(commandLabel)).toBeVisible();
|
||||
});
|
||||
|
||||
toolbar.unmount();
|
||||
});
|
||||
|
||||
it('should only include the math toolbar button if math is enabled in global settings', async () => {
|
||||
Setting.setValue('editor.toolbarButtons', ['editor.textMath']);
|
||||
Setting.setValue('markdown.plugin.katex', true);
|
||||
|
||||
const toolbar = render(<WrappedToolbar/>);
|
||||
|
||||
// Should initially show in the toolbar
|
||||
expect(queryToolbarButton('Math')).toBeVisible();
|
||||
|
||||
// After disabled: Should not show in the toolbar
|
||||
await waitFor(() => {
|
||||
Setting.setValue('markdown.plugin.katex', false);
|
||||
expect(queryToolbarButton('Math')).toBeNull();
|
||||
});
|
||||
|
||||
// Should not show in settings
|
||||
await openSettings();
|
||||
expect(screen.queryByRole('checkbox', { name: 'Math' })).toBeNull();
|
||||
|
||||
toolbar.unmount();
|
||||
});
|
||||
});
|
117
packages/app-mobile/components/EditorToolbar/EditorToolbar.tsx
Normal file
117
packages/app-mobile/components/EditorToolbar/EditorToolbar.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
import * as React from 'react';
|
||||
import { AppState } from '../../utils/types';
|
||||
import { connect } from 'react-redux';
|
||||
import { ScrollView, StyleSheet, View } from 'react-native';
|
||||
import { ToolbarButtonInfo, ToolbarItem } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||
import toolbarButtonsFromState from './utils/toolbarButtonsFromState';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { themeStyle } from '../global-style';
|
||||
import ToggleSpaceButton from '../ToggleSpaceButton';
|
||||
import ToolbarEditorDialog from './ToolbarEditorDialog';
|
||||
import { EditorState } from './types';
|
||||
import ToolbarButton from './ToolbarButton';
|
||||
import isSelected from './utils/isSelected';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
toolbarButtonInfos: ToolbarItem[];
|
||||
editorState: EditorState;
|
||||
}
|
||||
|
||||
const useStyles = (themeId: number) => {
|
||||
return useMemo(() => {
|
||||
const theme = themeStyle(themeId);
|
||||
return StyleSheet.create({
|
||||
content: {
|
||||
flexGrow: 0,
|
||||
backgroundColor: theme.backgroundColor3,
|
||||
},
|
||||
contentContainer: {
|
||||
flexGrow: 1,
|
||||
paddingVertical: 0,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
spacer: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
});
|
||||
}, [themeId]);
|
||||
};
|
||||
|
||||
type SetSettingsVisible = React.Dispatch<React.SetStateAction<boolean>>;
|
||||
const useSettingButtonInfo = (setSettingsVisible: SetSettingsVisible) => {
|
||||
return useMemo((): ToolbarButtonInfo => ({
|
||||
type: 'button',
|
||||
name: 'showToolbarSettings',
|
||||
tooltip: _('Settings'),
|
||||
iconName: 'material cogs',
|
||||
enabled: true,
|
||||
onClick: () => setSettingsVisible(true),
|
||||
title: '',
|
||||
}), [setSettingsVisible]);
|
||||
};
|
||||
|
||||
const EditorToolbar: React.FC<Props> = props => {
|
||||
const styles = useStyles(props.themeId);
|
||||
|
||||
const buttonInfos: ToolbarButtonInfo[] = [];
|
||||
|
||||
for (const info of props.toolbarButtonInfos) {
|
||||
if (info.type !== 'separator') {
|
||||
buttonInfos.push(info);
|
||||
}
|
||||
}
|
||||
|
||||
const renderButton = (info: ToolbarButtonInfo) => {
|
||||
return <ToolbarButton
|
||||
key={`command-${info.name}`}
|
||||
buttonInfo={info}
|
||||
themeId={props.themeId}
|
||||
selected={isSelected(info.name, props.editorState)}
|
||||
/>;
|
||||
};
|
||||
|
||||
const [settingsVisible, setSettingsVisible] = useState(false);
|
||||
const scrollViewRef = useRef<ScrollView|null>(null);
|
||||
const onDismissSettingsDialog = useCallback(() => {
|
||||
setSettingsVisible(false);
|
||||
|
||||
// On Android, if the ScrollView isn't manually scrolled to the end,
|
||||
// all items can be invisible in some cases. This causes issues with
|
||||
// TalkBack on Android.
|
||||
// In particular, if 1) the toolbar initially has many items on a device
|
||||
// with a small screen, and 2) the user removes most items, then most/all
|
||||
// items are scrolled offscreen. Calling .scrollToEnd corrects this:
|
||||
scrollViewRef.current?.scrollToEnd();
|
||||
}, []);
|
||||
|
||||
const settingsButtonInfo = useSettingButtonInfo(setSettingsVisible);
|
||||
const settingsButton = <ToolbarButton
|
||||
buttonInfo={settingsButtonInfo}
|
||||
themeId={props.themeId}
|
||||
/>;
|
||||
|
||||
return <>
|
||||
<ToggleSpaceButton themeId={props.themeId}>
|
||||
<ScrollView
|
||||
ref={scrollViewRef}
|
||||
horizontal={true}
|
||||
style={styles.content}
|
||||
contentContainerStyle={styles.contentContainer}
|
||||
>
|
||||
{buttonInfos.map(renderButton)}
|
||||
<View style={styles.spacer}/>
|
||||
{settingsButton}
|
||||
</ScrollView>
|
||||
</ToggleSpaceButton>
|
||||
<ToolbarEditorDialog visible={settingsVisible} onDismiss={onDismissSettingsDialog} />
|
||||
</>;
|
||||
};
|
||||
|
||||
export default connect((state: AppState) => {
|
||||
return {
|
||||
themeId: state.settings.theme,
|
||||
toolbarButtonInfos: toolbarButtonsFromState(state),
|
||||
};
|
||||
})(EditorToolbar);
|
@ -0,0 +1,58 @@
|
||||
import * as React from 'react';
|
||||
import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||
import IconButton from '../IconButton';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { StyleSheet, useWindowDimensions } from 'react-native';
|
||||
import { themeStyle } from '../global-style';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
buttonInfo: ToolbarButtonInfo;
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
const useStyles = (themeId: number, selected: boolean, enabled: boolean) => {
|
||||
const { fontScale } = useWindowDimensions();
|
||||
|
||||
return useMemo(() => {
|
||||
const theme = themeStyle(themeId);
|
||||
return StyleSheet.create({
|
||||
icon: {
|
||||
color: theme.color,
|
||||
fontSize: 22 * fontScale,
|
||||
},
|
||||
button: {
|
||||
// Scaling the button width/height by the device font scale causes the button to scale
|
||||
// with the user's device font size.
|
||||
width: 48 * fontScale,
|
||||
height: 48 * fontScale,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: selected ? theme.backgroundColorHover3 : theme.backgroundColor3,
|
||||
opacity: enabled ? 1 : theme.disabledOpacity,
|
||||
},
|
||||
});
|
||||
}, [themeId, selected, enabled, fontScale]);
|
||||
};
|
||||
|
||||
const ToolbarButton: React.FC<Props> = memo(({ themeId, buttonInfo, selected }) => {
|
||||
const styles = useStyles(themeId, selected, buttonInfo.enabled);
|
||||
const isToggleButton = selected !== undefined;
|
||||
|
||||
return <IconButton
|
||||
iconName={buttonInfo.iconName}
|
||||
description={buttonInfo.title || buttonInfo.tooltip}
|
||||
onPress={buttonInfo.onClick}
|
||||
disabled={!buttonInfo.enabled}
|
||||
iconStyle={styles.icon}
|
||||
containerStyle={styles.button}
|
||||
accessibilityState={{ selected }}
|
||||
accessibilityRole={isToggleButton ? 'togglebutton' : 'button'}
|
||||
role={'button'}
|
||||
aria-pressed={selected}
|
||||
preventKeyboardDismiss={true}
|
||||
themeId={themeId}
|
||||
/>;
|
||||
});
|
||||
|
||||
export default ToolbarButton;
|
@ -0,0 +1,191 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import createRootStyle from '../../utils/createRootStyle';
|
||||
import { View, StyleSheet, ScrollView } from 'react-native';
|
||||
import { Divider, Text, TouchableRipple } from 'react-native-paper';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { themeStyle } from '../global-style';
|
||||
import { connect } from 'react-redux';
|
||||
import ToolbarButtonUtils, { ToolbarButtonInfo, ToolbarItem } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||
import Icon from '../Icon';
|
||||
import { AppState } from '../../utils/types';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import allToolbarCommandNamesFromState from './utils/allToolbarCommandNamesFromState';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import DismissibleDialog, { DialogSize } from '../DismissibleDialog';
|
||||
import selectedCommandNamesFromState from './utils/selectedCommandNamesFromState';
|
||||
import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseContext';
|
||||
import { DeleteButton } from '../buttons';
|
||||
import shim from '@joplin/lib/shim';
|
||||
|
||||
const toolbarButtonUtils = new ToolbarButtonUtils(CommandService.instance());
|
||||
|
||||
interface EditorDialogProps {
|
||||
themeId: number;
|
||||
defaultToolbarButtonInfos: ToolbarItem[];
|
||||
selectedCommandNames: string[];
|
||||
allCommandNames: string[];
|
||||
hasCustomizedLayout: boolean;
|
||||
|
||||
visible: boolean;
|
||||
onDismiss: ()=> void;
|
||||
}
|
||||
|
||||
const useStyle = (themeId: number) => {
|
||||
return useMemo(() => {
|
||||
const theme = themeStyle(themeId);
|
||||
|
||||
return StyleSheet.create({
|
||||
...createRootStyle(themeId),
|
||||
icon: {
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSizeLarge,
|
||||
},
|
||||
labelText: {
|
||||
fontSize: theme.fontSize,
|
||||
},
|
||||
listContainer: {
|
||||
marginTop: theme.marginTop,
|
||||
flex: 1,
|
||||
},
|
||||
resetButton: {
|
||||
marginTop: theme.marginTop,
|
||||
},
|
||||
listItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
gap: theme.margin,
|
||||
padding: 4,
|
||||
paddingTop: theme.itemMarginTop,
|
||||
paddingBottom: theme.itemMarginBottom,
|
||||
},
|
||||
});
|
||||
}, [themeId]);
|
||||
};
|
||||
type Styles = ReturnType<typeof useStyle>;
|
||||
|
||||
const setCommandIncluded = (
|
||||
commandName: string,
|
||||
lastSelectedCommands: string[],
|
||||
allCommandNames: string[],
|
||||
include: boolean,
|
||||
) => {
|
||||
let newSelectedCommands;
|
||||
if (include) {
|
||||
newSelectedCommands = [];
|
||||
for (const name of allCommandNames) {
|
||||
const isDivider = name === '-';
|
||||
if (isDivider || name === commandName || lastSelectedCommands.includes(name)) {
|
||||
newSelectedCommands.push(name);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newSelectedCommands = lastSelectedCommands.filter(name => name !== commandName);
|
||||
}
|
||||
Setting.setValue('editor.toolbarButtons', newSelectedCommands);
|
||||
};
|
||||
|
||||
interface ItemToggleProps {
|
||||
item: ToolbarButtonInfo;
|
||||
selectedCommandNames: string[];
|
||||
allCommandNames: string[];
|
||||
styles: Styles;
|
||||
}
|
||||
const ToolbarItemToggle: React.FC<ItemToggleProps> = ({
|
||||
item, selectedCommandNames, styles, allCommandNames,
|
||||
}) => {
|
||||
const title = item.title || item.tooltip;
|
||||
const checked = selectedCommandNames.includes(item.name);
|
||||
|
||||
const onToggle = useCallback(() => {
|
||||
setCommandIncluded(item.name, selectedCommandNames, allCommandNames, !checked);
|
||||
}, [item, selectedCommandNames, allCommandNames, checked]);
|
||||
|
||||
return (
|
||||
<TouchableRipple
|
||||
accessibilityRole='checkbox'
|
||||
accessibilityState={{ checked }}
|
||||
aria-checked={checked}
|
||||
onPress={onToggle}
|
||||
>
|
||||
<View style={styles.listItem}>
|
||||
<Icon name={checked ? 'ionicon checkbox-outline' : 'ionicon square-outline'} style={styles.icon} accessibilityLabel={null}/>
|
||||
<Icon name={item.iconName} style={styles.icon} accessibilityLabel={null}/>
|
||||
<Text style={styles.labelText}>
|
||||
{title}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
);
|
||||
};
|
||||
|
||||
const ToolbarEditorScreen: React.FC<EditorDialogProps> = props => {
|
||||
const styles = useStyle(props.themeId);
|
||||
|
||||
const renderItem = (item: ToolbarItem, index: number) => {
|
||||
if (item.type === 'separator') {
|
||||
return <Divider key={`separator-${index}`} />;
|
||||
}
|
||||
|
||||
return <ToolbarItemToggle
|
||||
key={`command-${item.name}`}
|
||||
item={item}
|
||||
styles={styles}
|
||||
allCommandNames={props.allCommandNames}
|
||||
selectedCommandNames={props.selectedCommandNames}
|
||||
/>;
|
||||
};
|
||||
|
||||
const onRestoreDefaultLayout = useCallback(async () => {
|
||||
// Dismiss before showing the confirm dialog to prevent modal conflicts.
|
||||
// On some platforms (web and possibly iOS) showing multiple modals
|
||||
// at the same time can cause issues.
|
||||
props.onDismiss();
|
||||
|
||||
const message = _('Are you sure that you want to restore the default toolbar layout?\nThis cannot be undone.');
|
||||
if (await shim.showConfirmationDialog(message)) {
|
||||
Setting.setValue('editor.toolbarButtons', []);
|
||||
}
|
||||
}, [props.onDismiss]);
|
||||
|
||||
const restoreButton = <DeleteButton
|
||||
style={styles.resetButton}
|
||||
onPress={onRestoreDefaultLayout}
|
||||
>
|
||||
{_('Restore defaults')}
|
||||
</DeleteButton>;
|
||||
|
||||
return (
|
||||
<DismissibleDialog
|
||||
size={DialogSize.Small}
|
||||
themeId={props.themeId}
|
||||
visible={props.visible}
|
||||
onDismiss={props.onDismiss}
|
||||
heading={_('Manage toolbar options')}
|
||||
>
|
||||
<View>
|
||||
<Text variant='bodyMedium'>{_('Check elements to display in the toolbar')}</Text>
|
||||
</View>
|
||||
<ScrollView style={styles.listContainer}>
|
||||
{props.defaultToolbarButtonInfos.map((item, index) => renderItem(item, index))}
|
||||
{props.hasCustomizedLayout ? restoreButton : null}
|
||||
</ScrollView>
|
||||
</DismissibleDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect((state: AppState) => {
|
||||
const whenClauseContext = stateToWhenClauseContext(state);
|
||||
|
||||
const allCommandNames = allToolbarCommandNamesFromState(state);
|
||||
const selectedCommandNames = selectedCommandNamesFromState(state);
|
||||
|
||||
return {
|
||||
themeId: state.settings.theme,
|
||||
selectedCommandNames,
|
||||
allCommandNames,
|
||||
hasCustomizedLayout: state.settings['editor.toolbarButtons'].length > 0,
|
||||
defaultToolbarButtonInfos: toolbarButtonUtils.commandsToToolbarButtons(allCommandNames, whenClauseContext),
|
||||
};
|
||||
})(ToolbarEditorScreen);
|
@ -0,0 +1,28 @@
|
||||
import { Store } from 'redux';
|
||||
import { AppState } from '../../../utils/types';
|
||||
import CommandService, { CommandRuntime } from '@joplin/lib/services/CommandService';
|
||||
import allToolbarCommandNamesFromState from '../utils/allToolbarCommandNamesFromState';
|
||||
|
||||
// The toolbar expects all toolbar command runtimes to be registered before it can be
|
||||
// rendered:
|
||||
const mockCommandRuntimes = (store: Store<AppState>) => {
|
||||
const makeMockRuntime = (commandName: string) => ({
|
||||
declaration: { name: commandName },
|
||||
runtime: (_props: null): CommandRuntime => ({
|
||||
execute: jest.fn(),
|
||||
}),
|
||||
});
|
||||
|
||||
const isSeparator = (commandName: string) => commandName === '-';
|
||||
|
||||
const mockRuntimes = allToolbarCommandNamesFromState(
|
||||
store.getState(),
|
||||
).filter(
|
||||
name => !isSeparator(name),
|
||||
).map(makeMockRuntime);
|
||||
return CommandService.instance().componentRegisterCommands(
|
||||
null, mockRuntimes,
|
||||
);
|
||||
};
|
||||
|
||||
export default mockCommandRuntimes;
|
6
packages/app-mobile/components/EditorToolbar/types.ts
Normal file
6
packages/app-mobile/components/EditorToolbar/types.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import SelectionFormatting from '@joplin/editor/SelectionFormatting';
|
||||
|
||||
export interface EditorState {
|
||||
selectionState: SelectionFormatting;
|
||||
searchVisible: boolean;
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
import { AppState } from '../../../utils/types';
|
||||
import { utils as pluginUtils } from '@joplin/lib/services/plugins/reducer';
|
||||
import { EditorCommandType } from '@joplin/editor/types';
|
||||
|
||||
const builtInCommandNames = [
|
||||
'attachFile',
|
||||
'-',
|
||||
'editor.textHeading1',
|
||||
'editor.textHeading2',
|
||||
'editor.textHeading3',
|
||||
'editor.textHeading4',
|
||||
'editor.textHeading5',
|
||||
EditorCommandType.ToggleBolded,
|
||||
EditorCommandType.ToggleItalicized,
|
||||
'-',
|
||||
EditorCommandType.ToggleCode,
|
||||
`editor.${EditorCommandType.ToggleMath}`,
|
||||
'-',
|
||||
EditorCommandType.ToggleNumberedList,
|
||||
EditorCommandType.ToggleBulletedList,
|
||||
EditorCommandType.ToggleCheckList,
|
||||
'-',
|
||||
EditorCommandType.IndentLess,
|
||||
EditorCommandType.IndentMore,
|
||||
'-',
|
||||
EditorCommandType.EditLink,
|
||||
'setTags',
|
||||
EditorCommandType.ToggleSearch,
|
||||
'hideKeyboard',
|
||||
];
|
||||
|
||||
|
||||
const allToolbarCommandNamesFromState = (state: AppState) => {
|
||||
const pluginCommandNames = pluginUtils.commandNamesFromViews(state.pluginService.plugins, 'editorToolbar');
|
||||
|
||||
let allCommandNames = builtInCommandNames;
|
||||
if (pluginCommandNames.length > 0) {
|
||||
allCommandNames = allCommandNames.concat(['-'], pluginCommandNames);
|
||||
}
|
||||
|
||||
// If the user disables math markup, the "toggle math" button won't be useful.
|
||||
// Disabling the math markup button maintains compatibility with the previous
|
||||
// toolbar.
|
||||
const mathEnabled = state.settings['markdown.plugin.katex'];
|
||||
if (!mathEnabled) {
|
||||
allCommandNames = allCommandNames.filter(
|
||||
name => name !== `editor.${EditorCommandType.ToggleMath}`,
|
||||
);
|
||||
}
|
||||
|
||||
return allCommandNames;
|
||||
};
|
||||
|
||||
export default allToolbarCommandNamesFromState;
|
@ -0,0 +1,40 @@
|
||||
import SelectionFormatting from '@joplin/editor/SelectionFormatting';
|
||||
import { EditorCommandType } from '@joplin/editor/types';
|
||||
import { EditorState } from '../types';
|
||||
|
||||
type StateSelector = (selectionState: SelectionFormatting, searchVisible: boolean)=> boolean;
|
||||
|
||||
const commandNameToSelectionState: Record<string, StateSelector> = {
|
||||
[EditorCommandType.ToggleBolded]: state => state.bolded,
|
||||
[EditorCommandType.ToggleItalicized]: state => state.italicized,
|
||||
[EditorCommandType.ToggleCode]: state => state.inCode,
|
||||
[EditorCommandType.ToggleMath]: state => state.inMath,
|
||||
[EditorCommandType.ToggleHeading1]: state => state.headerLevel === 1,
|
||||
[EditorCommandType.ToggleHeading2]: state => state.headerLevel === 2,
|
||||
[EditorCommandType.ToggleHeading3]: state => state.headerLevel === 3,
|
||||
[EditorCommandType.ToggleHeading4]: state => state.headerLevel === 4,
|
||||
[EditorCommandType.ToggleHeading5]: state => state.headerLevel === 5,
|
||||
|
||||
[EditorCommandType.ToggleBulletedList]: state => state.inUnorderedList,
|
||||
[EditorCommandType.ToggleNumberedList]: state => state.inOrderedList,
|
||||
[EditorCommandType.ToggleCheckList]: state => state.inChecklist,
|
||||
[EditorCommandType.EditLink]: state => state.inLink,
|
||||
[EditorCommandType.ToggleSearch]: (_selectionState, searchVisible) => searchVisible,
|
||||
};
|
||||
|
||||
// Returns undefined if not a toggle button
|
||||
const isSelected = (commandName: string, editorState: EditorState) => {
|
||||
// Newer editor commands are registered with the "editor." prefix. Remove this
|
||||
// prefix to simplify looking up the selection state:
|
||||
commandName = commandName.replace(/^editor\./, '');
|
||||
|
||||
if (commandName in commandNameToSelectionState) {
|
||||
if (!editorState) return false;
|
||||
return commandNameToSelectionState[commandName as EditorCommandType](
|
||||
editorState.selectionState, editorState.searchVisible,
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export default isSelected;
|
@ -0,0 +1,30 @@
|
||||
import { AppState } from '../../../utils/types';
|
||||
import allToolbarCommandNamesFromState from './allToolbarCommandNamesFromState';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
const omitFromDefault: string[] = [
|
||||
'editor.textHeading1',
|
||||
'editor.textHeading3',
|
||||
'editor.textHeading4',
|
||||
'editor.textHeading5',
|
||||
];
|
||||
|
||||
// The "hide keyboard" button is only needed on iOS, so only show it there by default.
|
||||
// (There's no default "dismiss" button on iPhone software keyboards).
|
||||
if (Platform.OS !== 'ios') {
|
||||
omitFromDefault.push('hideKeyboard');
|
||||
}
|
||||
|
||||
const selectedCommandNamesFromState = (state: AppState) => {
|
||||
const allCommandNames = allToolbarCommandNamesFromState(state);
|
||||
const defaultCommandNames = allCommandNames.filter(commandName => {
|
||||
return !omitFromDefault.includes(commandName);
|
||||
});
|
||||
|
||||
const commandNameSetting = state.settings['editor.toolbarButtons'] ?? [];
|
||||
const selectedCommands = commandNameSetting.length > 0 ? commandNameSetting : defaultCommandNames;
|
||||
|
||||
return selectedCommands.filter(command => allCommandNames.includes(command));
|
||||
};
|
||||
|
||||
export default selectedCommandNamesFromState;
|
@ -0,0 +1,16 @@
|
||||
import { AppState } from '../../../utils/types';
|
||||
import ToolbarButtonUtils from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import selectedCommandNamesFromState from './selectedCommandNamesFromState';
|
||||
import stateToWhenClauseContext from '../../../services/commands/stateToWhenClauseContext';
|
||||
|
||||
const toolbarButtonUtils = new ToolbarButtonUtils(CommandService.instance());
|
||||
|
||||
const toolbarButtonsFromState = (state: AppState) => {
|
||||
const whenClauseContext = stateToWhenClauseContext(state);
|
||||
|
||||
const commandNames = selectedCommandNamesFromState(state);
|
||||
return toolbarButtonUtils.commandsToToolbarButtons(commandNames, whenClauseContext);
|
||||
};
|
||||
|
||||
export default toolbarButtonsFromState;
|
@ -6,7 +6,7 @@ import * as React from 'react';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import { Theme } from '@joplin/lib/themes/type';
|
||||
import { useState, useMemo, useCallback, useRef } from 'react';
|
||||
import { Text, Pressable, ViewStyle, StyleSheet, LayoutChangeEvent, LayoutRectangle, Animated, AccessibilityState, AccessibilityRole, TextStyle, GestureResponderEvent, Platform } from 'react-native';
|
||||
import { Text, Pressable, ViewStyle, StyleSheet, LayoutChangeEvent, LayoutRectangle, Animated, AccessibilityState, AccessibilityRole, TextStyle, GestureResponderEvent, Platform, Role } from 'react-native';
|
||||
import { Menu, MenuOptions, MenuTrigger, renderers } from 'react-native-popup-menu';
|
||||
import Icon from './Icon';
|
||||
import AccessibleView from './accessibility/AccessibleView';
|
||||
@ -36,6 +36,8 @@ interface ButtonProps {
|
||||
// Role of the button. Defaults to 'button'.
|
||||
accessibilityRole?: AccessibilityRole;
|
||||
accessibilityState?: AccessibilityState;
|
||||
'aria-pressed'?: boolean;
|
||||
role?: Role;
|
||||
|
||||
disabled?: boolean;
|
||||
}
|
||||
@ -102,7 +104,9 @@ const IconButton = (props: ButtonProps) => {
|
||||
accessibilityLabel={props.description}
|
||||
accessibilityHint={props.accessibilityHint}
|
||||
accessibilityRole={props.accessibilityRole ?? 'button'}
|
||||
role={props.role}
|
||||
accessibilityState={props.accessibilityState}
|
||||
aria-pressed={props['aria-pressed']}
|
||||
>
|
||||
<Animated.View style={{
|
||||
opacity: fadeAnim,
|
||||
|
@ -37,10 +37,6 @@ class ModalDialog extends React.Component<Props, State> {
|
||||
this.styles_ = {};
|
||||
|
||||
const styles: Record<string, ViewStyle|TextStyle> = {
|
||||
modalWrapper: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
modalContentWrapper: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
@ -76,7 +72,6 @@ class ModalDialog extends React.Component<Props, State> {
|
||||
const buttonBarEnabled = this.props.buttonBarEnabled !== false;
|
||||
|
||||
return (
|
||||
<View style={this.styles().modalWrapper}>
|
||||
<Modal transparent={true} visible={true} onRequestClose={() => {}} containerStyle={this.styles().modalContentWrapper}>
|
||||
<Text style={this.styles().title}>{this.props.title}</Text>
|
||||
<View style={this.styles().modalContentWrapper2}>{ContentComponent}</View>
|
||||
@ -89,7 +84,6 @@ class ModalDialog extends React.Component<Props, State> {
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -84,6 +84,7 @@ const EditLinkDialog = (props: LinkDialogProps) => {
|
||||
const onSubmit = useCallback(() => {
|
||||
props.editorControl.updateLink(linkLabel, linkURL);
|
||||
props.editorControl.hideLinkDialog();
|
||||
focus('EditLinkDialog::onSubmit', props.editorControl);
|
||||
}, [props.editorControl, linkLabel, linkURL]);
|
||||
|
||||
// See https://www.hingehealth.com/engineering-blog/accessible-react-native-textinput/
|
||||
|
@ -1,140 +0,0 @@
|
||||
// A toolbar for the markdown editor.
|
||||
|
||||
import * as React from 'react';
|
||||
import { Platform, StyleSheet } from 'react-native';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { MarkdownToolbarProps, StyleSheetData } from './types';
|
||||
import Toolbar from './Toolbar';
|
||||
import { buttonSize } from './ToolbarButton';
|
||||
import { Theme } from '@joplin/lib/themes/type';
|
||||
import ToggleSpaceButton from './ToggleSpaceButton';
|
||||
import useHeaderButtons from './buttons/useHeaderButtons';
|
||||
import useInlineFormattingButtons from './buttons/useInlineFormattingButtons';
|
||||
import useActionButtons from './buttons/useActionButtons';
|
||||
import useListButtons from './buttons/useListButtons';
|
||||
import useKeyboardVisible from '../hooks/useKeyboardVisible';
|
||||
import usePluginButtons from './buttons/usePluginButtons';
|
||||
|
||||
|
||||
const MarkdownToolbar: React.FC<MarkdownToolbarProps> = (props: MarkdownToolbarProps) => {
|
||||
const themeData = props.editorSettings.themeData;
|
||||
const styles = useStyles(props.style, themeData);
|
||||
|
||||
const { keyboardVisible, hasSoftwareKeyboard } = useKeyboardVisible();
|
||||
const buttonProps = {
|
||||
...props,
|
||||
iconStyle: styles.text,
|
||||
keyboardVisible,
|
||||
hasSoftwareKeyboard,
|
||||
};
|
||||
const headerButtons = useHeaderButtons(buttonProps);
|
||||
const inlineFormattingBtns = useInlineFormattingButtons(buttonProps);
|
||||
const actionButtons = useActionButtons(buttonProps);
|
||||
const listButtons = useListButtons(buttonProps);
|
||||
const pluginButtons = usePluginButtons(buttonProps);
|
||||
|
||||
const styleData: StyleSheetData = useMemo(() => ({
|
||||
styles: styles,
|
||||
themeId: props.editorSettings.themeId,
|
||||
}), [styles, props.editorSettings.themeId]);
|
||||
|
||||
const toolbarButtons = useMemo(() => {
|
||||
const buttons = [
|
||||
{
|
||||
title: _('Formatting'),
|
||||
items: inlineFormattingBtns,
|
||||
},
|
||||
{
|
||||
title: _('Headers'),
|
||||
items: headerButtons,
|
||||
},
|
||||
{
|
||||
title: _('Lists'),
|
||||
items: listButtons,
|
||||
},
|
||||
{
|
||||
title: _('Actions'),
|
||||
items: actionButtons,
|
||||
},
|
||||
];
|
||||
|
||||
if (pluginButtons.length > 0) {
|
||||
buttons.push({
|
||||
title: _('Plugins'),
|
||||
items: pluginButtons,
|
||||
});
|
||||
}
|
||||
|
||||
return buttons;
|
||||
}, [headerButtons, inlineFormattingBtns, listButtons, actionButtons, pluginButtons]);
|
||||
|
||||
return (
|
||||
<ToggleSpaceButton
|
||||
spaceApplicable={ Platform.OS === 'ios' && keyboardVisible }
|
||||
themeId={props.editorSettings.themeId}
|
||||
style={styles.container}
|
||||
>
|
||||
<Toolbar
|
||||
styleSheet={styleData}
|
||||
buttons={toolbarButtons}
|
||||
/>
|
||||
</ToggleSpaceButton>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const useStyles = (styleProps: any, theme: Theme) => {
|
||||
return useMemo(() => {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
...styleProps,
|
||||
},
|
||||
button: {
|
||||
width: buttonSize,
|
||||
height: buttonSize,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: theme.backgroundColor,
|
||||
},
|
||||
buttonDisabled: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
buttonDisabledContent: {
|
||||
},
|
||||
buttonActive: {
|
||||
backgroundColor: theme.backgroundColor3,
|
||||
color: theme.color3,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.color3,
|
||||
borderRadius: 6,
|
||||
},
|
||||
buttonActiveContent: {
|
||||
color: theme.color3,
|
||||
},
|
||||
text: {
|
||||
fontSize: 22,
|
||||
color: theme.color,
|
||||
},
|
||||
toolbarRow: {
|
||||
flex: 0,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'baseline',
|
||||
justifyContent: 'center',
|
||||
|
||||
// Add a small amount of additional padding for button borders
|
||||
height: buttonSize + 6,
|
||||
},
|
||||
toolbarContainer: {
|
||||
flexShrink: 1,
|
||||
},
|
||||
toolbarContent: {
|
||||
flexGrow: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
}, [styleProps, theme]);
|
||||
};
|
||||
|
||||
export default MarkdownToolbar;
|
@ -1,31 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import ToolbarButton from './ToolbarButton';
|
||||
import { ButtonSpec, StyleSheetData } from './types';
|
||||
|
||||
type OnToggleOverflowCallback = ()=> void;
|
||||
interface ToggleOverflowButtonProps {
|
||||
overflowVisible: boolean;
|
||||
onToggleOverflowVisible: OnToggleOverflowCallback;
|
||||
styleSheet: StyleSheetData;
|
||||
}
|
||||
|
||||
// Button that shows/hides the overflow menu.
|
||||
const ToggleOverflowButton: React.FC<ToggleOverflowButtonProps> = (props: ToggleOverflowButtonProps) => {
|
||||
const spec: ButtonSpec = {
|
||||
icon: 'material dots-horizontal',
|
||||
description:
|
||||
props.overflowVisible ? _('Hide more actions') : _('Show more actions'),
|
||||
active: props.overflowVisible,
|
||||
onPress: props.onToggleOverflowVisible,
|
||||
};
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
styleSheet={props.styleSheet}
|
||||
spec={spec}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default ToggleOverflowButton;
|
@ -1,124 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { ReactElement, useCallback, useMemo, useState } from 'react';
|
||||
import { LayoutChangeEvent, ScrollView, View, ViewStyle } from 'react-native';
|
||||
import ToggleOverflowButton from './ToggleOverflowButton';
|
||||
import ToolbarButton, { buttonSize } from './ToolbarButton';
|
||||
import ToolbarOverflowRows from './ToolbarOverflowRows';
|
||||
import { ButtonGroup, ButtonSpec, StyleSheetData } from './types';
|
||||
|
||||
interface ToolbarProps {
|
||||
buttons: ButtonGroup[];
|
||||
styleSheet: StyleSheetData;
|
||||
style?: ViewStyle;
|
||||
}
|
||||
|
||||
// Displays a list of buttons with an overflow menu.
|
||||
const Toolbar: React.FC<ToolbarProps> = (props: ToolbarProps) => {
|
||||
const [overflowButtonsVisible, setOverflowPopupVisible] = useState(false);
|
||||
const [maxButtonsEachSide, setMaxButtonsEachSide] = useState(0);
|
||||
|
||||
const allButtonSpecs = useMemo(() => {
|
||||
const buttons = props.buttons.reduce((accumulator: ButtonSpec[], current: ButtonGroup) => {
|
||||
const newItems: ButtonSpec[] = [];
|
||||
for (const item of current.items) {
|
||||
if (item.visible ?? true) {
|
||||
newItems.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return accumulator.concat(...newItems);
|
||||
}, []);
|
||||
|
||||
// Sort from highest priority to lowest
|
||||
buttons.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
||||
return buttons;
|
||||
}, [props.buttons]);
|
||||
|
||||
const allButtonComponents: ReactElement[] = [];
|
||||
let key = 0;
|
||||
for (const spec of allButtonSpecs) {
|
||||
key++;
|
||||
allButtonComponents.push(
|
||||
<ToolbarButton
|
||||
key={key.toString()}
|
||||
styleSheet={props.styleSheet}
|
||||
spec={spec}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
const onContainerLayout = useCallback((event: LayoutChangeEvent) => {
|
||||
const containerWidth = event.nativeEvent.layout.width;
|
||||
const maxButtonsTotal = Math.floor(containerWidth / buttonSize);
|
||||
setMaxButtonsEachSide(Math.floor(
|
||||
Math.min((maxButtonsTotal - 1) / 2, allButtonSpecs.length / 2),
|
||||
));
|
||||
}, [allButtonSpecs.length]);
|
||||
|
||||
const onToggleOverflowVisible = useCallback(() => {
|
||||
setOverflowPopupVisible(!overflowButtonsVisible);
|
||||
}, [overflowButtonsVisible]);
|
||||
|
||||
const toggleOverflowButton = (
|
||||
<ToggleOverflowButton
|
||||
key={(++key).toString()}
|
||||
styleSheet={props.styleSheet}
|
||||
overflowVisible={overflowButtonsVisible}
|
||||
onToggleOverflowVisible={onToggleOverflowVisible}
|
||||
/>
|
||||
);
|
||||
|
||||
const mainButtons: ReactElement[] = [];
|
||||
if (maxButtonsEachSide >= allButtonComponents.length) {
|
||||
mainButtons.push(...allButtonComponents);
|
||||
} else if (maxButtonsEachSide > 0) {
|
||||
// We want the menu to look something like this:
|
||||
// B I (…) 🔍 ⌨
|
||||
// where (…) shows/hides overflow.
|
||||
// Add from the left and right of [allButtonComponents] to ensure that
|
||||
// the (…) button is in the center:
|
||||
mainButtons.push(...allButtonComponents.slice(0, maxButtonsEachSide));
|
||||
mainButtons.push(toggleOverflowButton);
|
||||
mainButtons.push(...allButtonComponents.slice(-maxButtonsEachSide));
|
||||
} else {
|
||||
mainButtons.push(toggleOverflowButton);
|
||||
}
|
||||
|
||||
const styles = props.styleSheet.styles;
|
||||
const mainButtonRow = (
|
||||
<View style={styles.toolbarRow}>
|
||||
{ mainButtons }
|
||||
</View>
|
||||
);
|
||||
|
||||
const overflow = (
|
||||
<ScrollView>
|
||||
<ToolbarOverflowRows
|
||||
buttonGroups={props.buttons}
|
||||
styleSheet={props.styleSheet}
|
||||
onToggleOverflow={onToggleOverflowVisible}
|
||||
/>
|
||||
</ScrollView>
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
...styles.toolbarContainer,
|
||||
|
||||
// The number of buttons displayed is based on the width of the
|
||||
// container. As such, we can't base the container's width on the
|
||||
// size of its content.
|
||||
width: '100%',
|
||||
|
||||
...props.style,
|
||||
}}
|
||||
onLayout={onContainerLayout}
|
||||
>
|
||||
{ overflowButtonsVisible ? overflow : null }
|
||||
{ !overflowButtonsVisible ? mainButtonRow : null }
|
||||
</View>
|
||||
);
|
||||
};
|
||||
export default Toolbar;
|
@ -1,74 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { TextStyle, StyleSheet } from 'react-native';
|
||||
import { ButtonSpec, StyleSheetData } from './types';
|
||||
import IconButton from '../../IconButton';
|
||||
|
||||
export const buttonSize = 54;
|
||||
|
||||
interface ToolbarButtonProps {
|
||||
styleSheet: StyleSheetData;
|
||||
style?: TextStyle;
|
||||
spec: ButtonSpec;
|
||||
onActionComplete?: ()=> void;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const useStyles = (baseStyleSheet: any, baseButtonStyle: any, buttonSpec: ButtonSpec, visible: boolean, disabled: boolean) => {
|
||||
return useMemo(() => {
|
||||
const activatedStyle = buttonSpec.active ? baseStyleSheet.buttonActive : {};
|
||||
const disabledStyle = disabled ? baseStyleSheet.buttonDisabled : {};
|
||||
|
||||
const activatedTextStyle = buttonSpec.active ? baseStyleSheet.buttonActiveContent : {};
|
||||
const disabledTextStyle = disabled ? baseStyleSheet.buttonDisabledContent : {};
|
||||
|
||||
return StyleSheet.create({
|
||||
iconStyle: {
|
||||
...activatedTextStyle,
|
||||
...disabledTextStyle,
|
||||
...baseStyleSheet.text,
|
||||
},
|
||||
buttonStyle: {
|
||||
...baseStyleSheet.button,
|
||||
...activatedStyle,
|
||||
...disabledStyle,
|
||||
...baseButtonStyle,
|
||||
...(!visible ? { opacity: 0 } : null),
|
||||
},
|
||||
});
|
||||
}, [
|
||||
baseStyleSheet.button, baseStyleSheet.text, baseButtonStyle, baseStyleSheet.buttonActive,
|
||||
baseStyleSheet.buttonDisabled, baseStyleSheet.buttonActiveContent, baseStyleSheet.buttonDisabledContent,
|
||||
buttonSpec.active, visible, disabled,
|
||||
]);
|
||||
};
|
||||
|
||||
const ToolbarButton = ({ styleSheet, spec, onActionComplete, style }: ToolbarButtonProps) => {
|
||||
const visible = spec.visible ?? true;
|
||||
const disabled = (spec.disabled ?? false) && visible;
|
||||
const styles = useStyles(styleSheet.styles, style, spec, visible, disabled);
|
||||
|
||||
const sourceOnPress = spec.onPress;
|
||||
const onPress = useCallback(() => {
|
||||
if (!disabled) {
|
||||
sourceOnPress();
|
||||
onActionComplete?.();
|
||||
}
|
||||
}, [disabled, sourceOnPress, onActionComplete]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
containerStyle={styles.buttonStyle}
|
||||
themeId={styleSheet.themeId}
|
||||
onPress={onPress}
|
||||
description={ spec.description }
|
||||
disabled={ disabled }
|
||||
preventKeyboardDismiss={true}
|
||||
|
||||
iconName={spec.icon}
|
||||
iconStyle={styles.iconStyle}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolbarButton;
|
@ -1,134 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { ReactElement, useCallback, useState } from 'react';
|
||||
import { LayoutChangeEvent, ScrollView, View } from 'react-native';
|
||||
import ToggleOverflowButton from './ToggleOverflowButton';
|
||||
import ToolbarButton, { buttonSize } from './ToolbarButton';
|
||||
import { ButtonGroup, ButtonSpec, StyleSheetData } from './types';
|
||||
|
||||
type OnToggleOverflowCallback = ()=> void;
|
||||
interface OverflowPopupProps {
|
||||
buttonGroups: ButtonGroup[];
|
||||
styleSheet: StyleSheetData;
|
||||
|
||||
// Should be created using useCallback
|
||||
onToggleOverflow: OnToggleOverflowCallback;
|
||||
}
|
||||
|
||||
// Specification for a button that acts as padding.
|
||||
const paddingButtonSpec = { visible: false, icon: '', onPress: ()=>{}, description: '' };
|
||||
|
||||
// Contains buttons that overflow the available space.
|
||||
// Displays all buttons in [props.buttonGroups] if [props.visible].
|
||||
// Otherwise, displays nothing.
|
||||
const ToolbarOverflowRows: React.FC<OverflowPopupProps> = (props: OverflowPopupProps) => {
|
||||
const overflowRows: ReactElement[] = [];
|
||||
|
||||
let key = 0;
|
||||
for (let i = 0; i < props.buttonGroups.length; i++) {
|
||||
key++;
|
||||
const row: ReactElement[] = [];
|
||||
|
||||
const group = props.buttonGroups[i];
|
||||
for (let j = 0; j < group.items.length; j++) {
|
||||
key++;
|
||||
|
||||
const buttonSpec = group.items[j];
|
||||
row.push(
|
||||
<ToolbarButton
|
||||
key={key.toString()}
|
||||
styleSheet={props.styleSheet}
|
||||
spec={buttonSpec}
|
||||
|
||||
// After invoking this button's action, hide the overflow menu
|
||||
onActionComplete={props.onToggleOverflow}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Show the "hide overflow" button if in the center of the last row
|
||||
const isLastRow = i === props.buttonGroups.length - 1;
|
||||
const isCenterOfRow = j + 1 === Math.floor(group.items.length / 2);
|
||||
if (isLastRow && (isCenterOfRow || group.items.length === 1)) {
|
||||
row.push(
|
||||
<ToggleOverflowButton
|
||||
key={(++key).toString()}
|
||||
styleSheet={props.styleSheet}
|
||||
overflowVisible={true}
|
||||
onToggleOverflowVisible={props.onToggleOverflow}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Pad to an odd number of items to ensure that buttons are centered properly
|
||||
if (row.length % 2 === 0) {
|
||||
row.push(
|
||||
<ToolbarButton
|
||||
key={`padding-${i}`}
|
||||
styleSheet={props.styleSheet}
|
||||
spec={paddingButtonSpec}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
overflowRows.push(
|
||||
<View
|
||||
key={key.toString()}
|
||||
>
|
||||
<ScrollView
|
||||
horizontal={true}
|
||||
contentContainerStyle={props.styleSheet.styles.toolbarContent}
|
||||
>
|
||||
{row}
|
||||
</ScrollView>
|
||||
</View>,
|
||||
);
|
||||
}
|
||||
|
||||
const [hasSpaceForCloseBtn, setHasSpaceForCloseBtn] = useState(true);
|
||||
const onContainerLayout = useCallback((event: LayoutChangeEvent) => {
|
||||
if (props.buttonGroups.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add 1 to account for the close button
|
||||
const totalButtonCount = props.buttonGroups[0].items.length + 1;
|
||||
|
||||
const newWidth = event.nativeEvent.layout.width;
|
||||
setHasSpaceForCloseBtn(newWidth > totalButtonCount * buttonSize);
|
||||
}, [setHasSpaceForCloseBtn, props.buttonGroups]);
|
||||
|
||||
const closeButtonSpec: ButtonSpec = {
|
||||
icon: 'text ⨉',
|
||||
description: _('Close'),
|
||||
onPress: props.onToggleOverflow,
|
||||
};
|
||||
const closeButton = (
|
||||
<ToolbarButton
|
||||
styleSheet={props.styleSheet}
|
||||
spec={closeButtonSpec}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
zIndex: 1,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
height: props.buttonGroups.length * buttonSize,
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
}}
|
||||
onLayout={onContainerLayout}
|
||||
>
|
||||
{hasSpaceForCloseBtn ? closeButton : null}
|
||||
{overflowRows}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
export default ToolbarOverflowRows;
|
@ -1,83 +0,0 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { ButtonSpec } from '../types';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { ButtonRowProps } from '../types';
|
||||
import time from '@joplin/lib/time';
|
||||
import { Keyboard, Platform } from 'react-native';
|
||||
|
||||
export interface ActionButtonRowProps extends ButtonRowProps {
|
||||
keyboardVisible: boolean;
|
||||
hasSoftwareKeyboard: boolean;
|
||||
}
|
||||
|
||||
const useActionButtons = (props: ActionButtonRowProps) => {
|
||||
const onDismissKeyboard = useCallback(() => {
|
||||
// Keyboard.dismiss() doesn't dismiss the keyboard if it's editing the WebView.
|
||||
Keyboard.dismiss();
|
||||
|
||||
// As such, dismiss the keyboard by sending a message to the View.
|
||||
props.editorControl.hideKeyboard();
|
||||
}, [props.editorControl]);
|
||||
|
||||
const onSearch = useCallback(() => {
|
||||
if (props.searchState.dialogVisible) {
|
||||
props.editorControl.searchControl.hideSearch();
|
||||
} else {
|
||||
props.editorControl.searchControl.showSearch();
|
||||
}
|
||||
}, [props.editorControl, props.searchState.dialogVisible]);
|
||||
|
||||
const onAttach = useCallback(() => {
|
||||
onDismissKeyboard();
|
||||
props.onAttach();
|
||||
}, [props.onAttach, onDismissKeyboard]);
|
||||
|
||||
return useMemo(() => {
|
||||
const actionButtons: ButtonSpec[] = [];
|
||||
actionButtons.push({
|
||||
icon: 'fa calendar-plus',
|
||||
description: _('Insert time'),
|
||||
onPress: () => {
|
||||
props.editorControl.insertText(time.formatDateToLocal(new Date()));
|
||||
},
|
||||
disabled: props.readOnly,
|
||||
});
|
||||
|
||||
actionButtons.push({
|
||||
icon: 'material attachment',
|
||||
description: _('Attach'),
|
||||
onPress: onAttach,
|
||||
disabled: props.readOnly,
|
||||
});
|
||||
|
||||
actionButtons.push({
|
||||
icon: 'material magnify',
|
||||
description: (
|
||||
props.searchState.dialogVisible ? _('Close') : _('Find and replace')
|
||||
),
|
||||
active: props.searchState.dialogVisible,
|
||||
onPress: onSearch,
|
||||
|
||||
priority: -3,
|
||||
disabled: props.readOnly,
|
||||
});
|
||||
|
||||
actionButtons.push({
|
||||
icon: 'material keyboard-close',
|
||||
description: _('Hide keyboard'),
|
||||
disabled: !props.keyboardVisible,
|
||||
visible: props.hasSoftwareKeyboard && Platform.OS === 'ios',
|
||||
onPress: onDismissKeyboard,
|
||||
|
||||
priority: -3,
|
||||
});
|
||||
|
||||
return actionButtons;
|
||||
}, [
|
||||
props.editorControl, props.keyboardVisible, props.hasSoftwareKeyboard,
|
||||
props.readOnly, props.searchState.dialogVisible,
|
||||
onAttach, onDismissKeyboard, onSearch,
|
||||
]);
|
||||
};
|
||||
|
||||
export default useActionButtons;
|
@ -1,34 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { ButtonSpec } from '../types';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { ButtonRowProps } from '../types';
|
||||
|
||||
const useHeaderButtons = ({ selectionState, editorControl, readOnly }: ButtonRowProps) => {
|
||||
return useMemo(() => {
|
||||
const headerButtons: ButtonSpec[] = [];
|
||||
for (let level = 1; level <= 5; level++) {
|
||||
const active = selectionState.headerLevel === level;
|
||||
|
||||
headerButtons.push({
|
||||
icon: `text H${level}`,
|
||||
description: _('Header %d', level),
|
||||
active,
|
||||
|
||||
// We only call addHeaderButton 5 times and in the same order, so
|
||||
// the linter error is safe to ignore.
|
||||
// eslint-disable-next-line @seiyab/react-hooks/rules-of-hooks
|
||||
onPress: () => {
|
||||
editorControl.toggleHeaderLevel(level);
|
||||
},
|
||||
|
||||
// Make it likely for the first three header buttons to show, less likely for
|
||||
// the others.
|
||||
priority: level < 3 ? 2 : 0,
|
||||
disabled: readOnly,
|
||||
});
|
||||
}
|
||||
return headerButtons;
|
||||
}, [selectionState, editorControl, readOnly]);
|
||||
};
|
||||
|
||||
export default useHeaderButtons;
|
@ -1,67 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { ButtonSpec } from '../types';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { ButtonRowProps } from '../types';
|
||||
|
||||
|
||||
const useInlineFormattingButtons = ({ selectionState, editorControl, readOnly, editorSettings }: ButtonRowProps) => {
|
||||
const { bolded, italicized, inCode, inMath, inLink } = selectionState;
|
||||
|
||||
return useMemo(() => {
|
||||
const inlineFormattingBtns: ButtonSpec[] = [];
|
||||
inlineFormattingBtns.push({
|
||||
icon: 'fa bold',
|
||||
description: _('Bold'),
|
||||
active: bolded,
|
||||
onPress: editorControl.toggleBolded,
|
||||
|
||||
priority: 3,
|
||||
disabled: readOnly,
|
||||
});
|
||||
|
||||
inlineFormattingBtns.push({
|
||||
icon: 'fa italic',
|
||||
description: _('Italic'),
|
||||
active: italicized,
|
||||
onPress: editorControl.toggleItalicized,
|
||||
|
||||
priority: 2,
|
||||
disabled: readOnly,
|
||||
});
|
||||
|
||||
inlineFormattingBtns.push({
|
||||
icon: 'text {;}',
|
||||
description: _('Code'),
|
||||
active: inCode,
|
||||
onPress: editorControl.toggleCode,
|
||||
|
||||
priority: 2,
|
||||
disabled: readOnly,
|
||||
});
|
||||
|
||||
if (editorSettings.katexEnabled) {
|
||||
inlineFormattingBtns.push({
|
||||
icon: 'text ∑',
|
||||
description: _('KaTeX'),
|
||||
active: inMath,
|
||||
onPress: editorControl.toggleMath,
|
||||
|
||||
priority: 1,
|
||||
disabled: readOnly,
|
||||
});
|
||||
}
|
||||
|
||||
inlineFormattingBtns.push({
|
||||
icon: 'fa link',
|
||||
description: _('Link'),
|
||||
active: inLink,
|
||||
onPress: editorControl.showLinkDialog,
|
||||
|
||||
priority: -3,
|
||||
disabled: readOnly,
|
||||
});
|
||||
return inlineFormattingBtns;
|
||||
}, [readOnly, editorControl, editorSettings.katexEnabled, inLink, inMath, inCode, italicized, bolded]);
|
||||
};
|
||||
|
||||
export default useInlineFormattingButtons;
|
@ -1,63 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { ButtonSpec } from '../types';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { ButtonRowProps } from '../types';
|
||||
|
||||
const useListButtons = ({ selectionState, editorControl, readOnly }: ButtonRowProps) => {
|
||||
return useMemo(() => {
|
||||
const listButtons: ButtonSpec[] = [];
|
||||
|
||||
listButtons.push({
|
||||
icon: 'fa list-ul',
|
||||
description: _('Unordered list'),
|
||||
active: selectionState.inUnorderedList,
|
||||
onPress: editorControl.toggleUnorderedList,
|
||||
|
||||
priority: -2,
|
||||
disabled: readOnly,
|
||||
});
|
||||
|
||||
listButtons.push({
|
||||
icon: 'fa list-ol',
|
||||
description: _('Ordered list'),
|
||||
active: selectionState.inOrderedList,
|
||||
onPress: editorControl.toggleOrderedList,
|
||||
|
||||
priority: -2,
|
||||
disabled: readOnly,
|
||||
});
|
||||
|
||||
listButtons.push({
|
||||
icon: 'fa tasks',
|
||||
description: _('Task list'),
|
||||
active: selectionState.inChecklist,
|
||||
onPress: editorControl.toggleTaskList,
|
||||
|
||||
priority: -2,
|
||||
disabled: readOnly,
|
||||
});
|
||||
|
||||
|
||||
listButtons.push({
|
||||
icon: 'ant indent-left',
|
||||
description: _('Decrease indent level'),
|
||||
onPress: editorControl.decreaseIndent,
|
||||
|
||||
priority: -1,
|
||||
disabled: readOnly,
|
||||
});
|
||||
|
||||
listButtons.push({
|
||||
icon: 'ant indent-right',
|
||||
description: _('Increase indent level'),
|
||||
onPress: editorControl.increaseIndent,
|
||||
|
||||
priority: -1,
|
||||
disabled: readOnly,
|
||||
});
|
||||
|
||||
return listButtons;
|
||||
}, [readOnly, editorControl, selectionState]);
|
||||
};
|
||||
|
||||
export default useListButtons;
|
@ -1,38 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { ButtonSpec } from '../types';
|
||||
import { ButtonRowProps } from '../types';
|
||||
import { PluginStates, utils as pluginUtils } from '@joplin/lib/services/plugins/reducer';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
|
||||
interface PluginButtonsRowProps extends ButtonRowProps {
|
||||
pluginStates: PluginStates;
|
||||
}
|
||||
|
||||
const usePluginButtons = (props: PluginButtonsRowProps) => {
|
||||
return useMemo(() => {
|
||||
const pluginButtons: ButtonSpec[] = [];
|
||||
|
||||
const pluginCommands =
|
||||
pluginUtils
|
||||
.commandNamesFromViews(props.pluginStates, 'editorToolbar')
|
||||
// Remove separators
|
||||
.filter(name => name !== '-');
|
||||
|
||||
const commandService = CommandService.instance();
|
||||
for (const commandName of pluginCommands) {
|
||||
const command = commandService.commandByName(commandName, { runtimeMustBeRegistered: true });
|
||||
|
||||
pluginButtons.push({
|
||||
description: commandService.description(commandName),
|
||||
icon: command.declaration.iconName ?? 'fas fa-cog',
|
||||
onPress: async () => {
|
||||
void commandService.execute(commandName);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return pluginButtons;
|
||||
}, [props.pluginStates]);
|
||||
};
|
||||
|
||||
export default usePluginButtons;
|
@ -1,56 +0,0 @@
|
||||
|
||||
import { TextStyle, ViewStyle } from 'react-native';
|
||||
import { EditorControl, EditorSettings } from '../types';
|
||||
import SelectionFormatting from '@joplin/editor/SelectionFormatting';
|
||||
import { SearchState } from '@joplin/editor/types';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
|
||||
|
||||
export type OnPressListener = ()=> void;
|
||||
|
||||
export interface ButtonSpec {
|
||||
// Name of an icon, as accepted by components/Icon.tsx
|
||||
icon: string;
|
||||
|
||||
// Tooltip/accessibility label
|
||||
description: string;
|
||||
onPress: OnPressListener;
|
||||
|
||||
// Priority for showing the button in the main toolbar.
|
||||
// Higher priority => more likely to be shown on the left of the toolbar
|
||||
// Lower (negative) priority => more likely to be shown on the right side of the
|
||||
// toolbar.
|
||||
priority?: number;
|
||||
|
||||
// True if the button is connected to an enabled action.
|
||||
// E.g. the cursor is in a header and the button is a header button.
|
||||
active?: boolean;
|
||||
disabled?: boolean;
|
||||
visible?: boolean;
|
||||
}
|
||||
export interface ButtonGroup {
|
||||
title: string;
|
||||
items: ButtonSpec[];
|
||||
}
|
||||
|
||||
export interface StyleSheetData {
|
||||
themeId: number;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
styles: any;
|
||||
}
|
||||
|
||||
type OnAttachCallback = ()=> void;
|
||||
export interface MarkdownToolbarProps {
|
||||
editorControl: EditorControl;
|
||||
selectionState: SelectionFormatting;
|
||||
searchState: SearchState;
|
||||
editorSettings: EditorSettings;
|
||||
pluginStates: PluginStates;
|
||||
onAttach: OnAttachCallback;
|
||||
style?: ViewStyle;
|
||||
readOnly: boolean;
|
||||
}
|
||||
|
||||
export interface ButtonRowProps extends MarkdownToolbarProps {
|
||||
iconStyle: TextStyle;
|
||||
}
|
@ -7,10 +7,18 @@ import '@testing-library/jest-native';
|
||||
import NoteEditor from './NoteEditor';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { MenuProvider } from 'react-native-popup-menu';
|
||||
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
import commandDeclarations from './commandDeclarations';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import CommandService, { RegisteredRuntime } from '@joplin/lib/services/CommandService';
|
||||
import TestProviderStack from '../testing/TestProviderStack';
|
||||
import createMockReduxStore from '../../utils/testing/createMockReduxStore';
|
||||
import mockCommandRuntimes from '../EditorToolbar/testing/mockCommandRuntimes';
|
||||
import setupGlobalStore from '../../utils/testing/setupGlobalStore';
|
||||
import { Store } from 'redux';
|
||||
import { AppState } from '../../utils/types';
|
||||
|
||||
let store: Store<AppState>;
|
||||
let registeredRuntime: RegisteredRuntime;
|
||||
|
||||
describe('NoteEditor', () => {
|
||||
beforeAll(() => {
|
||||
@ -24,11 +32,19 @@ describe('NoteEditor', () => {
|
||||
// Required to use ExtendedWebView
|
||||
await setupDatabaseAndSynchronizer(0);
|
||||
await switchClient(0);
|
||||
|
||||
store = createMockReduxStore();
|
||||
setupGlobalStore(store);
|
||||
registeredRuntime = mockCommandRuntimes(store);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
registeredRuntime.deregister();
|
||||
});
|
||||
|
||||
it('should hide the markdown toolbar when the window is small', async () => {
|
||||
const wrappedNoteEditor = render(
|
||||
<MenuProvider>
|
||||
<TestProviderStack store={store}>
|
||||
<NoteEditor
|
||||
themeId={Setting.THEME_ARITIM_DARK}
|
||||
initialText='Testing...'
|
||||
@ -41,7 +57,7 @@ describe('NoteEditor', () => {
|
||||
onAttach={async ()=>{}}
|
||||
plugins={{}}
|
||||
/>
|
||||
</MenuProvider>,
|
||||
</TestProviderStack>,
|
||||
);
|
||||
|
||||
// Maps from screen height to whether the markdown toolbar should be visible.
|
||||
@ -70,11 +86,11 @@ describe('NoteEditor', () => {
|
||||
setRootHeight(height);
|
||||
|
||||
await waitFor(async () => {
|
||||
const showMoreButton = await screen.queryByLabelText(_('Show more actions'));
|
||||
const toolbarButton = await screen.queryByLabelText(_('Bold'));
|
||||
if (visible) {
|
||||
expect(showMoreButton).not.toBeNull();
|
||||
expect(toolbarButton).not.toBeNull();
|
||||
} else {
|
||||
expect(showMoreButton).toBeNull();
|
||||
expect(toolbarButton).toBeNull();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -16,7 +16,6 @@ import { editorFont } from '../global-style';
|
||||
import { EditorControl as EditorBodyControl, ContentScriptData } from '@joplin/editor/types';
|
||||
import { EditorControl, EditorSettings, SelectionRange, WebViewToEditorApi } from './types';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import MarkdownToolbar from './MarkdownToolbar/MarkdownToolbar';
|
||||
import { ChangeEvent, EditorEvent, EditorEventType, SelectionRangeChangeEvent, UndoRedoDepthChangeEvent } from '@joplin/editor/events';
|
||||
import { EditorCommandType, EditorKeymap, EditorLanguageType, SearchState } from '@joplin/editor/types';
|
||||
import SelectionFormatting, { defaultSelectionFormatting } from '@joplin/editor/SelectionFormatting';
|
||||
@ -30,6 +29,7 @@ import { OnMessageEvent } from '../ExtendedWebView/types';
|
||||
import { join, dirname } from 'path';
|
||||
import * as mimeUtils from '@joplin/lib/mime-utils';
|
||||
import uuid from '@joplin/lib/uuid';
|
||||
import EditorToolbar from '../EditorToolbar/EditorToolbar';
|
||||
|
||||
type ChangeEventHandler = (event: ChangeEvent)=> void;
|
||||
type UndoRedoDepthChangeHandler = (event: UndoRedoDepthChangeEvent)=> void;
|
||||
@ -184,7 +184,7 @@ const useEditorControl = (
|
||||
setSearchState: OnSearchStateChangeCallback,
|
||||
): EditorControl => {
|
||||
return useMemo(() => {
|
||||
const execCommand = (command: EditorCommandType) => {
|
||||
const execEditorCommand = (command: EditorCommandType) => {
|
||||
void bodyControl.execCommand(command);
|
||||
};
|
||||
|
||||
@ -229,25 +229,25 @@ const useEditorControl = (
|
||||
},
|
||||
|
||||
toggleBolded() {
|
||||
execCommand(EditorCommandType.ToggleBolded);
|
||||
execEditorCommand(EditorCommandType.ToggleBolded);
|
||||
},
|
||||
toggleItalicized() {
|
||||
execCommand(EditorCommandType.ToggleItalicized);
|
||||
execEditorCommand(EditorCommandType.ToggleItalicized);
|
||||
},
|
||||
toggleOrderedList() {
|
||||
execCommand(EditorCommandType.ToggleNumberedList);
|
||||
execEditorCommand(EditorCommandType.ToggleNumberedList);
|
||||
},
|
||||
toggleUnorderedList() {
|
||||
execCommand(EditorCommandType.ToggleBulletedList);
|
||||
execEditorCommand(EditorCommandType.ToggleBulletedList);
|
||||
},
|
||||
toggleTaskList() {
|
||||
execCommand(EditorCommandType.ToggleCheckList);
|
||||
execEditorCommand(EditorCommandType.ToggleCheckList);
|
||||
},
|
||||
toggleCode() {
|
||||
execCommand(EditorCommandType.ToggleCode);
|
||||
execEditorCommand(EditorCommandType.ToggleCode);
|
||||
},
|
||||
toggleMath() {
|
||||
execCommand(EditorCommandType.ToggleMath);
|
||||
execEditorCommand(EditorCommandType.ToggleMath);
|
||||
},
|
||||
toggleHeaderLevel(level: number) {
|
||||
const levelToCommand = [
|
||||
@ -264,19 +264,19 @@ const useEditorControl = (
|
||||
throw new Error(`Unsupported header level ${level}`);
|
||||
}
|
||||
|
||||
execCommand(levelToCommand[index]);
|
||||
execEditorCommand(levelToCommand[index]);
|
||||
},
|
||||
increaseIndent() {
|
||||
execCommand(EditorCommandType.IndentMore);
|
||||
execEditorCommand(EditorCommandType.IndentMore);
|
||||
},
|
||||
decreaseIndent() {
|
||||
execCommand(EditorCommandType.IndentLess);
|
||||
execEditorCommand(EditorCommandType.IndentLess);
|
||||
},
|
||||
updateLink(label: string, url: string) {
|
||||
bodyControl.updateLink(label, url);
|
||||
},
|
||||
scrollSelectionIntoView() {
|
||||
execCommand(EditorCommandType.ScrollSelectionIntoView);
|
||||
execEditorCommand(EditorCommandType.ScrollSelectionIntoView);
|
||||
},
|
||||
showLinkDialog() {
|
||||
setLinkDialogVisible(true);
|
||||
@ -296,23 +296,23 @@ const useEditorControl = (
|
||||
|
||||
searchControl: {
|
||||
findNext() {
|
||||
execCommand(EditorCommandType.FindNext);
|
||||
execEditorCommand(EditorCommandType.FindNext);
|
||||
},
|
||||
findPrevious() {
|
||||
execCommand(EditorCommandType.FindPrevious);
|
||||
execEditorCommand(EditorCommandType.FindPrevious);
|
||||
},
|
||||
replaceNext() {
|
||||
execCommand(EditorCommandType.ReplaceNext);
|
||||
execEditorCommand(EditorCommandType.ReplaceNext);
|
||||
},
|
||||
replaceAll() {
|
||||
execCommand(EditorCommandType.ReplaceAll);
|
||||
execEditorCommand(EditorCommandType.ReplaceAll);
|
||||
},
|
||||
|
||||
showSearch() {
|
||||
execCommand(EditorCommandType.ShowSearch);
|
||||
execEditorCommand(EditorCommandType.ShowSearch);
|
||||
},
|
||||
hideSearch() {
|
||||
execCommand(EditorCommandType.HideSearch);
|
||||
execEditorCommand(EditorCommandType.HideSearch);
|
||||
},
|
||||
|
||||
setSearchState: setSearchStateCallback,
|
||||
@ -535,20 +535,12 @@ function NoteEditor(props: Props, ref: any) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const toolbar = <MarkdownToolbar
|
||||
style={{
|
||||
// Don't show the markdown toolbar if there isn't enough space
|
||||
// for it:
|
||||
flexShrink: 1,
|
||||
}}
|
||||
editorSettings={editorSettings}
|
||||
editorControl={editorControl}
|
||||
selectionState={selectionState}
|
||||
searchState={searchState}
|
||||
pluginStates={props.plugins}
|
||||
onAttach={props.onAttach}
|
||||
readOnly={props.readOnly}
|
||||
/>;
|
||||
const toolbarEditorState = useMemo(() => ({
|
||||
selectionState,
|
||||
searchVisible: searchState.dialogVisible,
|
||||
}), [selectionState, searchState.dialogVisible]);
|
||||
|
||||
const toolbar = <EditorToolbar editorState={toolbarEditorState} />;
|
||||
|
||||
return (
|
||||
<View
|
||||
|
@ -1,14 +1,28 @@
|
||||
import { EditorCommandType } from '@joplin/editor/types';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { CommandDeclaration } from '@joplin/lib/services/CommandService';
|
||||
|
||||
export const enabledCondition = (_commandName: string) => {
|
||||
const output = [
|
||||
'!modalDialogVisible',
|
||||
'!noteIsReadOnly',
|
||||
];
|
||||
|
||||
return output.filter(c => !!c).join(' && ');
|
||||
};
|
||||
|
||||
const headerDeclarations = () => {
|
||||
const result: CommandDeclaration[] = [];
|
||||
for (let level = 1; level <= 5; level++) {
|
||||
result.push({
|
||||
name: `editor.textHeading${level}`,
|
||||
iconName: `material format-header-${level}`,
|
||||
label: () => _('Header %d', level),
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const declarations: CommandDeclaration[] = [
|
||||
{
|
||||
name: 'insertText',
|
||||
@ -34,6 +48,65 @@ const declarations: CommandDeclaration[] = [
|
||||
{
|
||||
name: 'editor.execCommand',
|
||||
},
|
||||
|
||||
{
|
||||
name: EditorCommandType.ToggleBolded,
|
||||
label: () => _('Bold'),
|
||||
iconName: 'material format-bold',
|
||||
},
|
||||
{
|
||||
name: EditorCommandType.ToggleItalicized,
|
||||
label: () => _('Italic'),
|
||||
iconName: 'material format-italic',
|
||||
},
|
||||
...headerDeclarations(),
|
||||
{
|
||||
name: EditorCommandType.ToggleCode,
|
||||
label: () => _('Code'),
|
||||
iconName: 'material code-json',
|
||||
},
|
||||
{
|
||||
// The 'editor.' prefix needs to be included because ToggleMath is not a legacy
|
||||
// editor command. Without this, ToggleMath is not recognised as an editor command.
|
||||
name: `editor.${EditorCommandType.ToggleMath}`,
|
||||
label: () => _('Math'),
|
||||
iconName: 'material sigma',
|
||||
},
|
||||
{
|
||||
name: EditorCommandType.ToggleNumberedList,
|
||||
label: () => _('Ordered list'),
|
||||
iconName: 'material format-list-numbered',
|
||||
},
|
||||
{
|
||||
name: EditorCommandType.ToggleBulletedList,
|
||||
label: () => _('Unordered list'),
|
||||
iconName: 'material format-list-bulleted',
|
||||
},
|
||||
{
|
||||
name: EditorCommandType.ToggleCheckList,
|
||||
label: () => _('Task list'),
|
||||
iconName: 'material format-list-checks',
|
||||
},
|
||||
{
|
||||
name: EditorCommandType.IndentLess,
|
||||
label: () => _('Decrease indent level'),
|
||||
iconName: 'ant indent-left',
|
||||
},
|
||||
{
|
||||
name: EditorCommandType.IndentMore,
|
||||
label: () => _('Increase indent level'),
|
||||
iconName: 'ant indent-right',
|
||||
},
|
||||
{
|
||||
name: EditorCommandType.ToggleSearch,
|
||||
label: () => _('Search'),
|
||||
iconName: 'material magnify',
|
||||
},
|
||||
{
|
||||
name: EditorCommandType.EditLink,
|
||||
label: () => _('Link'),
|
||||
iconName: 'material link',
|
||||
},
|
||||
];
|
||||
|
||||
export default declarations;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import CommandService, { CommandContext, CommandDeclaration } from '@joplin/lib/services/CommandService';
|
||||
import { EditorControl } from '@joplin/editor/types';
|
||||
import { useEffect } from 'react';
|
||||
import useNowEffect from '@joplin/lib/hooks/useNowEffect';
|
||||
import commandDeclarations, { enabledCondition } from '../commandDeclarations';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
|
||||
@ -34,7 +34,9 @@ const commandRuntime = (declaration: CommandDeclaration, editor: EditorControl)
|
||||
};
|
||||
|
||||
const useEditorCommandHandler = (editorControl: EditorControl) => {
|
||||
useEffect(() => {
|
||||
// useNowEffect: The command runtimes need to be registered before child components
|
||||
// can render.
|
||||
useNowEffect(() => {
|
||||
const commandService = CommandService.instance();
|
||||
for (const declaration of commandDeclarations) {
|
||||
commandService.registerRuntime(declaration.name, commandRuntime(declaration, editorControl));
|
||||
@ -45,7 +47,7 @@ const useEditorCommandHandler = (editorControl: EditorControl) => {
|
||||
commandService.unregisterRuntime(declaration.name);
|
||||
}
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
};
|
||||
|
||||
export default useEditorCommandHandler;
|
||||
|
@ -42,12 +42,12 @@ const WebBetaButton: React.FC<Props> = props => {
|
||||
iconStyle={props.iconStyle}
|
||||
/>
|
||||
<DismissibleDialog
|
||||
heading={_('Beta')}
|
||||
size={DialogSize.Small}
|
||||
themeId={props.themeId}
|
||||
visible={dialogVisible}
|
||||
onDismiss={onHideDialog}
|
||||
>
|
||||
<Text variant='headlineMedium'>{_('Beta')}</Text>
|
||||
<Text>{'At present, the web client is in beta. In the future, it may change significantly, or be removed.'}</Text>
|
||||
<View style={feedbackContainerStyles}>
|
||||
<LinkButton onPress={onLeaveFeedback}>{_('Give feedback')}</LinkButton>
|
||||
|
@ -9,16 +9,15 @@
|
||||
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import { Theme } from '@joplin/lib/themes/type';
|
||||
|
||||
import * as React from 'react';
|
||||
import { ReactNode, useCallback, useState, useEffect } from 'react';
|
||||
import { View, ViewStyle } from 'react-native';
|
||||
import IconButton from '../../IconButton';
|
||||
import { Platform, View, ViewStyle } from 'react-native';
|
||||
import IconButton from './IconButton';
|
||||
import useKeyboardVisible from '../utils/hooks/useKeyboardVisible';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
spaceApplicable: boolean;
|
||||
themeId: number;
|
||||
style?: ViewStyle;
|
||||
}
|
||||
@ -44,7 +43,7 @@ const ToggleSpaceButton = (props: Props) => {
|
||||
}
|
||||
}, [onDecreaseSpace]);
|
||||
|
||||
const theme: Theme = themeStyle(props.themeId);
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
const decreaseSpaceButton = (
|
||||
<>
|
||||
@ -77,15 +76,18 @@ const ToggleSpaceButton = (props: Props) => {
|
||||
</>
|
||||
);
|
||||
|
||||
const { keyboardVisible } = useKeyboardVisible();
|
||||
const spaceApplicable = keyboardVisible && Platform.OS === 'ios';
|
||||
|
||||
const style: ViewStyle = {
|
||||
marginBottom: props.spaceApplicable ? additionalSpace : 0,
|
||||
marginBottom: spaceApplicable ? additionalSpace : 0,
|
||||
...props.style,
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={style}>
|
||||
{props.children}
|
||||
{ decreaseSpaceBtnVisible && props.spaceApplicable ? decreaseSpaceButton : null }
|
||||
{ decreaseSpaceBtnVisible && spaceApplicable ? decreaseSpaceButton : null }
|
||||
</View>
|
||||
);
|
||||
};
|
@ -12,3 +12,4 @@ const makeTextButtonComponent = (type: ButtonType) => {
|
||||
export const PrimaryButton = makeTextButtonComponent(ButtonType.Primary);
|
||||
export const SecondaryButton = makeTextButtonComponent(ButtonType.Secondary);
|
||||
export const LinkButton = makeTextButtonComponent(ButtonType.Link);
|
||||
export const DeleteButton = makeTextButtonComponent(ButtonType.Delete);
|
||||
|
@ -8,6 +8,7 @@ const Color = require('color');
|
||||
const baseStyle = {
|
||||
appearance: 'light',
|
||||
fontSize: 16,
|
||||
fontSizeLarge: 20,
|
||||
noteViewerFontSize: 16,
|
||||
margin: 15, // No text and no interactive component should be within this margin
|
||||
itemMarginTop: 10,
|
||||
|
@ -8,11 +8,10 @@ import NoteScreen from './Note';
|
||||
import { setupDatabaseAndSynchronizer, switchClient, simulateReadOnlyShareEnv, supportDir, synchronizerStart, resourceFetcher, runWithFakeTimers } from '@joplin/lib/testing/test-utils';
|
||||
import { waitFor as waitForWithRealTimers } from '@joplin/lib/testing/test-utils';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import { AppState } from '../../utils/types';
|
||||
import { AppState } from '../../../utils/types';
|
||||
import { Store } from 'redux';
|
||||
import createMockReduxStore from '../../utils/testing/createMockReduxStore';
|
||||
import initializeCommandService from '../../utils/initializeCommandService';
|
||||
import getWebViewDomById from '../../utils/testing/getWebViewDomById';
|
||||
import createMockReduxStore from '../../../utils/testing/createMockReduxStore';
|
||||
import getWebViewDomById from '../../../utils/testing/getWebViewDomById';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import BaseItem from '@joplin/lib/models/BaseItem';
|
||||
@ -22,11 +21,12 @@ import { getDisplayParentId } from '@joplin/lib/services/trash';
|
||||
import { itemIsReadOnlySync, ItemSlice } from '@joplin/lib/models/utils/readOnly';
|
||||
import { LayoutChangeEvent } from 'react-native';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import getWebViewWindowById from '../../utils/testing/getWebViewWindowById';
|
||||
import getWebViewWindowById from '../../../utils/testing/getWebViewWindowById';
|
||||
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
import TestProviderStack from '../testing/TestProviderStack';
|
||||
import TestProviderStack from '../../testing/TestProviderStack';
|
||||
import setupGlobalStore from '../../../utils/testing/setupGlobalStore';
|
||||
|
||||
interface WrapperProps {
|
||||
}
|
||||
@ -138,7 +138,7 @@ describe('screens/Note', () => {
|
||||
await switchClient(0);
|
||||
|
||||
store = createMockReduxStore();
|
||||
initializeCommandService(store);
|
||||
setupGlobalStore(store);
|
||||
|
||||
// In order for note changes to be saved, note-screen-shared requires
|
||||
// that at least one folder exist.
|
@ -3,66 +3,65 @@ import uuid from '@joplin/lib/uuid';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import UndoRedoService from '@joplin/lib/services/UndoRedoService';
|
||||
import NoteBodyViewer from '../NoteBodyViewer/NoteBodyViewer';
|
||||
import checkPermissions from '../../utils/checkPermissions';
|
||||
import NoteEditor from '../NoteEditor/NoteEditor';
|
||||
import NoteBodyViewer from '../../NoteBodyViewer/NoteBodyViewer';
|
||||
import checkPermissions from '../../../utils/checkPermissions';
|
||||
import NoteEditor from '../../NoteEditor/NoteEditor';
|
||||
import * as React from 'react';
|
||||
import { Keyboard, View, TextInput, StyleSheet, Linking, Share, NativeSyntheticEvent } from 'react-native';
|
||||
import { Platform, PermissionsAndroid } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
// const { MarkdownEditor } = require('@joplin/lib/../MarkdownEditor/index.js');
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import BaseItem from '@joplin/lib/models/BaseItem';
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
const Clipboard = require('@react-native-clipboard/clipboard').default;
|
||||
const md5 = require('md5');
|
||||
import BackButtonService from '../../services/BackButtonService';
|
||||
import BackButtonService from '../../../services/BackButtonService';
|
||||
import NavService, { OnNavigateCallback as OnNavigateCallback } from '@joplin/lib/services/NavService';
|
||||
import { ModelType } from '@joplin/lib/BaseModel';
|
||||
import FloatingActionButton from '../buttons/FloatingActionButton';
|
||||
import FloatingActionButton from '../../buttons/FloatingActionButton';
|
||||
const { fileExtension, safeFileExtension } = require('@joplin/lib/path-utils');
|
||||
import * as mimeUtils from '@joplin/lib/mime-utils';
|
||||
import ScreenHeader, { MenuOptionType } from '../ScreenHeader';
|
||||
import NoteTagsDialog from './NoteTagsDialog';
|
||||
import ScreenHeader, { MenuOptionType } from '../../ScreenHeader';
|
||||
import NoteTagsDialog from '../NoteTagsDialog';
|
||||
import time from '@joplin/lib/time';
|
||||
import Checkbox from '../Checkbox';
|
||||
import Checkbox from '../../Checkbox';
|
||||
import { _, currentLocale } from '@joplin/lib/locale';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import ResourceFetcher from '@joplin/lib/services/ResourceFetcher';
|
||||
import { BaseScreenComponent } from '../base-screen';
|
||||
import { themeStyle, editorFont } from '../global-style';
|
||||
import { BaseScreenComponent } from '../../base-screen';
|
||||
import { themeStyle, editorFont } from '../../global-style';
|
||||
import shared, { BaseNoteScreenComponent, Props as BaseProps } from '@joplin/lib/components/shared/note-screen-shared';
|
||||
import { Asset, ImagePickerResponse, launchImageLibrary } from 'react-native-image-picker';
|
||||
import SelectDateTimeDialog from '../SelectDateTimeDialog';
|
||||
import ShareExtension from '../../utils/ShareExtension.js';
|
||||
import CameraView from '../CameraView/CameraView';
|
||||
import SelectDateTimeDialog from '../../SelectDateTimeDialog';
|
||||
import ShareExtension from '../../../utils/ShareExtension.js';
|
||||
import CameraView from '../../CameraView/CameraView';
|
||||
import { FolderEntity, NoteEntity, ResourceEntity } from '@joplin/lib/services/database/types';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import ImageEditor from '../NoteEditor/ImageEditor/ImageEditor';
|
||||
import promptRestoreAutosave from '../NoteEditor/ImageEditor/promptRestoreAutosave';
|
||||
import isEditableResource from '../NoteEditor/ImageEditor/isEditableResource';
|
||||
import VoiceTypingDialog from '../voiceTyping/VoiceTypingDialog';
|
||||
import { isSupportedLanguage } from '../../services/voiceTyping/vosk';
|
||||
import ImageEditor from '../../NoteEditor/ImageEditor/ImageEditor';
|
||||
import promptRestoreAutosave from '../../NoteEditor/ImageEditor/promptRestoreAutosave';
|
||||
import isEditableResource from '../../NoteEditor/ImageEditor/isEditableResource';
|
||||
import VoiceTypingDialog from '../../voiceTyping/VoiceTypingDialog';
|
||||
import { isSupportedLanguage } from '../../../services/voiceTyping/vosk';
|
||||
import { ChangeEvent as EditorChangeEvent, SelectionRangeChangeEvent, UndoRedoDepthChangeEvent } from '@joplin/editor/events';
|
||||
import { join } from 'path';
|
||||
import { Dispatch } from 'redux';
|
||||
import { RefObject, useContext, useRef } from 'react';
|
||||
import { SelectionRange } from '../NoteEditor/types';
|
||||
import { SelectionRange } from '../../NoteEditor/types';
|
||||
import { getNoteCallbackUrl } from '@joplin/lib/callbackUrlUtils';
|
||||
import { AppState } from '../../utils/types';
|
||||
import { AppState } from '../../../utils/types';
|
||||
import restoreItems from '@joplin/lib/services/trash/restoreItems';
|
||||
import { getDisplayParentTitle } from '@joplin/lib/services/trash';
|
||||
import { PluginStates, utils as pluginUtils } from '@joplin/lib/services/plugins/reducer';
|
||||
import pickDocument from '../../utils/pickDocument';
|
||||
import debounce from '../../utils/debounce';
|
||||
import debounce from '../../../utils/debounce';
|
||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import { ResourceInfo } from '../NoteBodyViewer/hooks/useRerenderHandler';
|
||||
import getImageDimensions from '../../utils/image/getImageDimensions';
|
||||
import resizeImage from '../../utils/image/resizeImage';
|
||||
import { CameraResult } from '../CameraView/types';
|
||||
import { DialogContext, DialogControl } from '../DialogManager';
|
||||
import CommandService, { RegisteredRuntime } from '@joplin/lib/services/CommandService';
|
||||
import { ResourceInfo } from '../../NoteBodyViewer/hooks/useRerenderHandler';
|
||||
import getImageDimensions from '../../../utils/image/getImageDimensions';
|
||||
import resizeImage from '../../../utils/image/resizeImage';
|
||||
import { CameraResult } from '../../CameraView/types';
|
||||
import { DialogContext, DialogControl } from '../../DialogManager';
|
||||
import { CommandRuntimeProps, PickerResponse } from './types';
|
||||
import commands from './commands';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const emptyArray: any[] = [];
|
||||
@ -150,6 +149,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
private folderPickerOptions_: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public dialogbox: any;
|
||||
private commandRegistration_: RegisteredRuntime|null = null;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public static navigationOptions(): any {
|
||||
@ -292,7 +292,6 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
}
|
||||
};
|
||||
|
||||
this.takePhoto_onPress = this.takePhoto_onPress.bind(this);
|
||||
this.cameraView_onPhoto = this.cameraView_onPhoto.bind(this);
|
||||
this.cameraView_onCancel = this.cameraView_onCancel.bind(this);
|
||||
this.properties_onPress = this.properties_onPress.bind(this);
|
||||
@ -315,6 +314,38 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
this.voiceTypingDialog_onDismiss = this.voiceTypingDialog_onDismiss.bind(this);
|
||||
}
|
||||
|
||||
private registerCommands() {
|
||||
if (this.commandRegistration_) return;
|
||||
|
||||
const dialogs = () => this.props.dialogs;
|
||||
this.commandRegistration_ = CommandService.instance().componentRegisterCommands<CommandRuntimeProps>(
|
||||
{
|
||||
attachFile: this.attachFile.bind(this),
|
||||
hideKeyboard: () => {
|
||||
if (this.useEditorBeta()) {
|
||||
this.editorRef?.current?.hideKeyboard();
|
||||
} else {
|
||||
Keyboard.dismiss();
|
||||
}
|
||||
},
|
||||
insertText: this.insertText.bind(this),
|
||||
get dialogs() {
|
||||
return dialogs();
|
||||
},
|
||||
setCameraVisible: (visible) => {
|
||||
this.setState({ showCamera: visible });
|
||||
},
|
||||
setTagDialogVisible: (visible) => {
|
||||
if (!this.state.note || !this.state.note.id) return;
|
||||
|
||||
this.setState({ noteTagDialogShown: visible });
|
||||
},
|
||||
},
|
||||
commands,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
private useEditorBeta(): boolean {
|
||||
return this.props.useEditorBeta;
|
||||
}
|
||||
@ -574,6 +605,9 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
// It cannot theoretically be undefined, since componentDidMount should always be called before
|
||||
// componentWillUnmount, but with React Native the impossible often becomes possible.
|
||||
if (this.undoRedoService_) this.undoRedoService_.off('stackChange', this.undoRedoService_stackChange);
|
||||
|
||||
this.commandRegistration_?.deregister();
|
||||
this.commandRegistration_ = null;
|
||||
}
|
||||
|
||||
private title_changeText(text: string) {
|
||||
@ -636,11 +670,6 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
await shared.saveOneProperty(this, name, value);
|
||||
}
|
||||
|
||||
private async pickDocuments() {
|
||||
const result = await pickDocument({ multiple: true });
|
||||
return result;
|
||||
}
|
||||
|
||||
public async resizeImage(localFilePath: string, targetPath: string, mimeType: string) {
|
||||
const maxSize = Resource.IMAGE_MAX_DIMENSION;
|
||||
const dimensions = await getImageDimensions(localFilePath);
|
||||
@ -720,7 +749,10 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
return newNote;
|
||||
}
|
||||
|
||||
public async attachFile(pickerResponse: Asset, fileType: string): Promise<ResourceEntity|null> {
|
||||
public async attachFile(
|
||||
pickerResponse: PickerResponse,
|
||||
fileType: string,
|
||||
): Promise<ResourceEntity|null> {
|
||||
if (!pickerResponse) {
|
||||
// User has cancelled
|
||||
return null;
|
||||
@ -802,36 +834,6 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
return resource;
|
||||
}
|
||||
|
||||
private async attachPhoto_onPress() {
|
||||
// the selection Limit should be specified. I think 200 is enough?
|
||||
const response: ImagePickerResponse = await launchImageLibrary({ mediaType: 'photo', includeBase64: false, selectionLimit: 200 });
|
||||
|
||||
if (response.errorCode) {
|
||||
reg.logger().warn('Got error from picker', response.errorCode);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.didCancel) {
|
||||
reg.logger().info('User cancelled picker');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const asset of response.assets) {
|
||||
await this.attachFile(asset, 'image');
|
||||
}
|
||||
}
|
||||
|
||||
private async takePhoto_onPress() {
|
||||
if (Platform.OS === 'web') {
|
||||
const response = await pickDocument({ multiple: true, preferCamera: true });
|
||||
for (const asset of response) {
|
||||
await this.attachFile(asset, 'image');
|
||||
}
|
||||
} else {
|
||||
this.setState({ showCamera: true });
|
||||
}
|
||||
}
|
||||
|
||||
private cameraView_onPhoto(data: CameraResult) {
|
||||
void this.attachFile(
|
||||
data,
|
||||
@ -935,25 +937,12 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
}
|
||||
};
|
||||
|
||||
private async attachFile_onPress() {
|
||||
const response = await this.pickDocuments();
|
||||
for (const asset of response) {
|
||||
await this.attachFile(asset, 'all');
|
||||
}
|
||||
}
|
||||
|
||||
private toggleIsTodo_onPress() {
|
||||
shared.toggleIsTodo_onPress(this);
|
||||
|
||||
this.scheduleSave();
|
||||
}
|
||||
|
||||
private tags_onPress() {
|
||||
if (!this.state.note || !this.state.note.id) return;
|
||||
|
||||
this.setState({ noteTagDialogShown: true });
|
||||
}
|
||||
|
||||
private async share_onPress() {
|
||||
await Share.share({
|
||||
message: `${this.state.note.title}\n\n${this.state.note.body}`,
|
||||
@ -1059,37 +1048,8 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
return output;
|
||||
}
|
||||
|
||||
public async showAttachMenu() {
|
||||
// If the keyboard is editing a WebView, the standard Keyboard.dismiss()
|
||||
// may not work. As such, we also need to call hideKeyboard on the editorRef
|
||||
this.editorRef.current?.hideKeyboard();
|
||||
|
||||
const buttons = [];
|
||||
|
||||
// On iOS, it will show "local files", which means certain files saved from the browser
|
||||
// and the iCloud files, but it doesn't include photos and images from the CameraRoll
|
||||
//
|
||||
// On Android, it will depend on the phone, but usually it will allow browsing all files and photos.
|
||||
buttons.push({ text: _('Attach file'), id: 'attachFile' });
|
||||
|
||||
// Disabled on Android because it doesn't work due to permission issues, but enabled on iOS
|
||||
// because that's only way to browse photos from the camera roll.
|
||||
if (Platform.OS === 'ios') buttons.push({ text: _('Attach photo'), id: 'attachPhoto' });
|
||||
buttons.push({ text: _('Take photo'), id: 'takePhoto' });
|
||||
|
||||
const buttonId = await this.props.dialogs.showMenu(_('Choose an option'), buttons);
|
||||
|
||||
if (buttonId === 'takePhoto') await this.takePhoto_onPress();
|
||||
if (buttonId === 'attachFile') await this.attachFile_onPress();
|
||||
if (buttonId === 'attachPhoto') await this.attachPhoto_onPress();
|
||||
}
|
||||
|
||||
public onAttach = async (filePath?: string) => {
|
||||
if (filePath) {
|
||||
await this.attachFile({ uri: filePath }, 'all');
|
||||
} else {
|
||||
await this.showAttachMenu();
|
||||
}
|
||||
await CommandService.instance().execute('attachFile', filePath);
|
||||
};
|
||||
|
||||
// private vosk_:Vosk;
|
||||
@ -1183,7 +1143,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
if (canAttachPicture) {
|
||||
output.push({
|
||||
title: _('Attach...'),
|
||||
onPress: () => this.showAttachMenu(),
|
||||
onPress: () => this.onAttach(),
|
||||
disabled: readOnly,
|
||||
});
|
||||
}
|
||||
@ -1227,14 +1187,25 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
});
|
||||
}
|
||||
|
||||
if (isSaved && !isDeleted) {
|
||||
const commandService = CommandService.instance();
|
||||
const whenContext = commandService.currentWhenClauseContext();
|
||||
const addButtonFromCommand = (commandName: string, title?: string) => {
|
||||
if (commandName === '-') {
|
||||
output.push({ isDivider: true });
|
||||
} else {
|
||||
output.push({
|
||||
title: _('Tags'),
|
||||
onPress: () => {
|
||||
this.tags_onPress();
|
||||
title: title ?? commandService.description(commandName),
|
||||
onPress: async () => {
|
||||
void commandService.execute(commandName);
|
||||
},
|
||||
disabled: !commandService.isEnabled(commandName, whenContext),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (isSaved && !isDeleted) {
|
||||
addButtonFromCommand('setTags');
|
||||
}
|
||||
|
||||
output.push({
|
||||
title: isTodo ? _('Convert to note') : _('Convert to todo'),
|
||||
@ -1283,22 +1254,6 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
});
|
||||
}
|
||||
|
||||
const commandService = CommandService.instance();
|
||||
const whenContext = commandService.currentWhenClauseContext();
|
||||
const addButtonFromCommand = (commandName: string, title?: string) => {
|
||||
if (commandName === '-') {
|
||||
output.push({ isDivider: true });
|
||||
} else {
|
||||
output.push({
|
||||
title: title ?? commandService.description(commandName),
|
||||
onPress: async () => {
|
||||
void commandService.execute(commandName);
|
||||
},
|
||||
disabled: !commandService.isEnabled(commandName, whenContext),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (whenContext.inTrash) {
|
||||
addButtonFromCommand('permanentlyDeleteNote');
|
||||
} else {
|
||||
@ -1440,6 +1395,12 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
}
|
||||
|
||||
public render() {
|
||||
// Commands must be registered before child components can render.
|
||||
// Calling this in the constructor won't work in strict mode, where
|
||||
// componentWillUnmount (which removes the commands) can be called
|
||||
// multiple times.
|
||||
this.registerCommands();
|
||||
|
||||
if (this.state.isLoading) {
|
||||
return (
|
||||
<View style={this.styles().screen}>
|
@ -0,0 +1,87 @@
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { CommandRuntimeProps } from '../types';
|
||||
import { Platform } from 'react-native';
|
||||
import pickDocument from '../../../../utils/pickDocument';
|
||||
import { ImagePickerResponse, launchImageLibrary } from 'react-native-image-picker';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
|
||||
const logger = Logger.create('attachFile');
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'attachFile',
|
||||
label: () => _('Attach file'),
|
||||
iconName: 'material attachment',
|
||||
};
|
||||
|
||||
export const runtime = (props: CommandRuntimeProps): CommandRuntime => {
|
||||
const takePhoto = async () => {
|
||||
if (Platform.OS === 'web') {
|
||||
const response = await pickDocument({ multiple: true, preferCamera: true });
|
||||
for (const asset of response) {
|
||||
await props.attachFile(asset, 'image');
|
||||
}
|
||||
} else {
|
||||
props.setCameraVisible(true);
|
||||
}
|
||||
};
|
||||
const attachFile = async () => {
|
||||
const response = await pickDocument({ multiple: true });
|
||||
for (const asset of response) {
|
||||
await props.attachFile(asset, 'all');
|
||||
}
|
||||
};
|
||||
const attachPhoto = async () => {
|
||||
// the selection Limit should be specified. I think 200 is enough?
|
||||
const response: ImagePickerResponse = await launchImageLibrary({ mediaType: 'photo', includeBase64: false, selectionLimit: 200 });
|
||||
|
||||
if (response.errorCode) {
|
||||
logger.warn('Got error from picker', response.errorCode);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.didCancel) {
|
||||
logger.info('User cancelled picker');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const asset of response.assets) {
|
||||
await props.attachFile(asset, 'image');
|
||||
}
|
||||
};
|
||||
|
||||
const showAttachMenu = async () => {
|
||||
props.hideKeyboard();
|
||||
|
||||
const buttons = [];
|
||||
|
||||
// On iOS, it will show "local files", which means certain files saved from the browser
|
||||
// and the iCloud files, but it doesn't include photos and images from the CameraRoll
|
||||
//
|
||||
// On Android, it will depend on the phone, but usually it will allow browsing all files and photos.
|
||||
buttons.push({ text: _('Attach file'), id: 'attachFile' });
|
||||
|
||||
// Disabled on Android because it doesn't work due to permission issues, but enabled on iOS
|
||||
// because that's only way to browse photos from the camera roll.
|
||||
if (Platform.OS === 'ios') buttons.push({ text: _('Attach photo'), id: 'attachPhoto' });
|
||||
buttons.push({ text: _('Take photo'), id: 'takePhoto' });
|
||||
|
||||
const buttonId = await props.dialogs.showMenu(_('Choose an option'), buttons);
|
||||
|
||||
if (buttonId === 'takePhoto') await takePhoto();
|
||||
if (buttonId === 'attachFile') await attachFile();
|
||||
if (buttonId === 'attachPhoto') await attachPhoto();
|
||||
};
|
||||
|
||||
return {
|
||||
execute: async (_context: CommandContext, filePath?: string) => {
|
||||
if (filePath) {
|
||||
await props.attachFile({ uri: filePath }, 'all');
|
||||
} else {
|
||||
await showAttachMenu();
|
||||
}
|
||||
},
|
||||
|
||||
enabledCondition: '!noteIsReadOnly',
|
||||
};
|
||||
};
|
@ -0,0 +1,18 @@
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { CommandRuntimeProps } from '../types';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'hideKeyboard',
|
||||
label: () => _('Hide keyboard'),
|
||||
iconName: 'material keyboard-close',
|
||||
};
|
||||
|
||||
export const runtime = (props: CommandRuntimeProps): CommandRuntime => {
|
||||
return {
|
||||
execute: async (_context: CommandContext) => {
|
||||
props.hideKeyboard();
|
||||
},
|
||||
enabledCondition: 'keyboardVisible',
|
||||
};
|
||||
};
|
@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED using `gulp buildScriptIndexes`
|
||||
import * as attachFile from './attachFile';
|
||||
import * as hideKeyboard from './hideKeyboard';
|
||||
import * as setTags from './setTags';
|
||||
|
||||
const index: any[] = [
|
||||
attachFile,
|
||||
hideKeyboard,
|
||||
setTags,
|
||||
];
|
||||
|
||||
export default index;
|
||||
// AUTO-GENERATED using `gulp buildScriptIndexes`
|
@ -0,0 +1,19 @@
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { CommandRuntimeProps } from '../types';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'setTags',
|
||||
label: () => _('Tags'),
|
||||
iconName: 'material tag-multiple',
|
||||
};
|
||||
|
||||
export const runtime = (props: CommandRuntimeProps): CommandRuntime => {
|
||||
return {
|
||||
execute: async (_context: CommandContext) => {
|
||||
props.setTagDialogVisible(true);
|
||||
},
|
||||
|
||||
enabledCondition: '!noteIsReadOnly',
|
||||
};
|
||||
};
|
17
packages/app-mobile/components/screens/Note/types.ts
Normal file
17
packages/app-mobile/components/screens/Note/types.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { ResourceEntity } from '@joplin/lib/services/database/types';
|
||||
import { DialogControl } from '../../DialogManager';
|
||||
|
||||
export interface PickerResponse {
|
||||
uri?: string;
|
||||
type?: string;
|
||||
fileName?: string;
|
||||
}
|
||||
|
||||
export interface CommandRuntimeProps {
|
||||
attachFile(pickerResponse: PickerResponse, fileType: string): Promise<ResourceEntity|null>;
|
||||
hideKeyboard(): void;
|
||||
insertText(text: string): void;
|
||||
setCameraVisible(visible: boolean): void;
|
||||
setTagDialogVisible(visible: boolean): void;
|
||||
dialogs: DialogControl;
|
||||
}
|
@ -32,7 +32,7 @@
|
||||
"@react-native-community/geolocation": "3.3.0",
|
||||
"@react-native-community/netinfo": "11.3.2",
|
||||
"@react-native-community/push-notification-ios": "1.11.0",
|
||||
"@react-native-community/slider": "4.5.2",
|
||||
"@react-native-community/slider": "4.5.3",
|
||||
"assert-browserify": "2.0.0",
|
||||
"buffer": "6.0.3",
|
||||
"color": "3.2.1",
|
||||
|
@ -12,7 +12,7 @@ import BaseModel from '@joplin/lib/BaseModel';
|
||||
import BaseService from '@joplin/lib/services/BaseService';
|
||||
import ResourceService from '@joplin/lib/services/ResourceService';
|
||||
import KvStore from '@joplin/lib/services/KvStore';
|
||||
import NoteScreen from './components/screens/Note';
|
||||
import NoteScreen from './components/screens/Note/Note';
|
||||
import UpgradeSyncTargetScreen from './components/screens/UpgradeSyncTargetScreen';
|
||||
import Setting, { AppType, Env } from '@joplin/lib/models/Setting';
|
||||
import PoorManIntervals from '@joplin/lib/PoorManIntervals';
|
||||
@ -27,7 +27,7 @@ import SyncTargetJoplinCloud from '@joplin/lib/SyncTargetJoplinCloud';
|
||||
import SyncTargetOneDrive from '@joplin/lib/SyncTargetOneDrive';
|
||||
import initProfile from '@joplin/lib/services/profileConfig/initProfile';
|
||||
const VersionInfo = require('react-native-version-info').default;
|
||||
const { Keyboard, BackHandler, Animated, StatusBar, Platform, Dimensions } = require('react-native');
|
||||
import { Keyboard, BackHandler, Animated, StatusBar, Platform, Dimensions } from 'react-native';
|
||||
import { AppState as RNAppState, EmitterSubscription, View, Text, Linking, NativeEventSubscription, Appearance, ActivityIndicator } from 'react-native';
|
||||
import getResponsiveValue from './components/getResponsiveValue';
|
||||
import NetInfo from '@react-native-community/netinfo';
|
||||
@ -443,6 +443,9 @@ const appReducer = (state = appDefaultState, action: any) => {
|
||||
newState.isOnMobileData = action.isOnMobileData;
|
||||
break;
|
||||
|
||||
case 'KEYBOARD_VISIBLE_CHANGE':
|
||||
newState = { ...state, keyboardVisible: action.visible };
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
error.message = `In reducer: ${error.message} Action: ${JSON.stringify(action)}`;
|
||||
@ -853,6 +856,8 @@ class AppComponent extends React.Component {
|
||||
private urlOpenListener_: EmitterSubscription|null = null;
|
||||
private appStateChangeListener_: NativeEventSubscription|null = null;
|
||||
private themeChangeListener_: NativeEventSubscription|null = null;
|
||||
private keyboardShowListener_: EmitterSubscription|null = null;
|
||||
private keyboardHideListener_: EmitterSubscription|null = null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private dropdownAlert_ = (_data: any) => new Promise<any>(res => res);
|
||||
private callbackUrl: string|null = null;
|
||||
@ -1038,6 +1043,19 @@ class AppComponent extends React.Component {
|
||||
|
||||
await setupNotifications(this.props.dispatch);
|
||||
|
||||
this.keyboardShowListener_ = Keyboard.addListener('keyboardDidShow', () => {
|
||||
this.props.dispatch({
|
||||
type: 'KEYBOARD_VISIBLE_CHANGE',
|
||||
visible: true,
|
||||
});
|
||||
});
|
||||
this.keyboardHideListener_ = Keyboard.addListener('keyboardDidHide', () => {
|
||||
this.props.dispatch({
|
||||
type: 'KEYBOARD_VISIBLE_CHANGE',
|
||||
visible: false,
|
||||
});
|
||||
});
|
||||
|
||||
// Setting.setValue('encryption.masterPassword', 'WRONG');
|
||||
// setTimeout(() => NavService.go('EncryptionConfig'), 2000);
|
||||
}
|
||||
@ -1074,6 +1092,15 @@ class AppComponent extends React.Component {
|
||||
this.quickActionShortcutListener_.remove();
|
||||
this.quickActionShortcutListener_ = undefined;
|
||||
}
|
||||
|
||||
if (this.keyboardShowListener_) {
|
||||
this.keyboardShowListener_.remove();
|
||||
this.keyboardShowListener_ = undefined;
|
||||
}
|
||||
if (this.keyboardHideListener_) {
|
||||
this.keyboardHideListener_.remove();
|
||||
this.keyboardHideListener_ = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
|
@ -0,0 +1,16 @@
|
||||
// This extends the generic stateToWhenClauseContext (potentially shared by
|
||||
// all apps) with additional properties specific to the desktop app. So in
|
||||
// general, any desktop component should import this file, and not the lib
|
||||
// one.
|
||||
|
||||
import libStateToWhenClauseContext, { WhenClauseContextOptions } from '@joplin/lib/services/commands/stateToWhenClauseContext';
|
||||
import { AppState } from '../../utils/types';
|
||||
|
||||
const stateToWhenClauseContext = (state: AppState, options: WhenClauseContextOptions = null) => {
|
||||
return {
|
||||
...libStateToWhenClauseContext(state, options),
|
||||
keyboardVisible: state.keyboardVisible,
|
||||
};
|
||||
};
|
||||
|
||||
export default stateToWhenClauseContext;
|
@ -11,6 +11,7 @@ export const DEFAULT_ROUTE = {
|
||||
const appDefaultState: AppState = {
|
||||
smartFilterId: undefined,
|
||||
...defaultState,
|
||||
keyboardVisible: false,
|
||||
route: DEFAULT_ROUTE,
|
||||
noteSelectionEnabled: false,
|
||||
noteSideMenuOptions: null,
|
||||
|
@ -3,9 +3,6 @@ import { themeStyle } from '../components/global-style';
|
||||
export default (themeId: number) => {
|
||||
const theme = themeStyle(themeId);
|
||||
return {
|
||||
root: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
},
|
||||
root: theme.rootStyle,
|
||||
};
|
||||
};
|
||||
|
@ -1,11 +1,12 @@
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import CommandService, { CommandDeclaration, CommandRuntime } from '@joplin/lib/services/CommandService';
|
||||
import stateToWhenClauseContext from '@joplin/lib/services/commands/stateToWhenClauseContext';
|
||||
import { AppState } from './types';
|
||||
import { Store } from 'redux';
|
||||
import editorCommandDeclarations from '../components/NoteEditor/commandDeclarations';
|
||||
import noteCommands from '../components/screens/Note/commands';
|
||||
import globalCommands from '../commands';
|
||||
import libCommands from '@joplin/lib/commands';
|
||||
import stateToWhenClauseContext from '../services/commands/stateToWhenClauseContext';
|
||||
|
||||
interface CommandSpecification {
|
||||
declaration: CommandDeclaration;
|
||||
@ -25,6 +26,9 @@ const initializeCommandService = (store: Store<AppState, any>) => {
|
||||
for (const declaration of editorCommandDeclarations) {
|
||||
CommandService.instance().registerDeclaration(declaration);
|
||||
}
|
||||
for (const command of noteCommands) {
|
||||
CommandService.instance().registerDeclaration(command.declaration);
|
||||
}
|
||||
registerCommands(globalCommands);
|
||||
registerCommands(libCommands);
|
||||
};
|
||||
|
@ -2,13 +2,10 @@ import { createReduxStore } from '@joplin/lib/testing/test-utils';
|
||||
import appDefaultState from '../appDefaultState';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
const defaultState = {
|
||||
...appDefaultState,
|
||||
// Mocking theme in the default state is necessary to prevent "Theme not set!" warnings.
|
||||
settings: { theme: Setting.THEME_LIGHT },
|
||||
};
|
||||
|
||||
const createMockReduxStore = () => {
|
||||
return createReduxStore(defaultState);
|
||||
return createReduxStore({
|
||||
...appDefaultState,
|
||||
settings: Setting.toPlainObject(),
|
||||
});
|
||||
};
|
||||
export default createMockReduxStore;
|
||||
|
16
packages/app-mobile/utils/testing/setupGlobalStore.ts
Normal file
16
packages/app-mobile/utils/testing/setupGlobalStore.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { Store } from 'redux';
|
||||
import { AppState } from '../types';
|
||||
import initializeCommandService from '../initializeCommandService';
|
||||
import BaseSyncTarget from '@joplin/lib/BaseSyncTarget';
|
||||
import NavService from '@joplin/lib/services/NavService';
|
||||
import BaseModel from '@joplin/lib/BaseModel';
|
||||
|
||||
// Sets a given Redux store as global
|
||||
const setupGlobalStore = (store: Store<AppState>) => {
|
||||
BaseModel.dispatch = store.dispatch;
|
||||
BaseSyncTarget.dispatch = store.dispatch;
|
||||
NavService.dispatch = store.dispatch;
|
||||
initializeCommandService(store);
|
||||
};
|
||||
|
||||
export default setupGlobalStore;
|
@ -3,6 +3,7 @@ import { State } from '@joplin/lib/reducer';
|
||||
export interface AppState extends State {
|
||||
showPanelsDialog: boolean;
|
||||
isOnMobileData: boolean;
|
||||
keyboardVisible: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
route: any;
|
||||
smartFilterId: string;
|
||||
|
@ -34,6 +34,7 @@ import biDirectionalTextExtension from './utils/biDirectionalTextExtension';
|
||||
import searchExtension from './utils/searchExtension';
|
||||
import isCursorAtBeginning from './utils/isCursorAtBeginning';
|
||||
import overwriteModeExtension from './utils/overwriteModeExtension';
|
||||
import handleLinkEditRequests, { showLinkEditor } from './utils/handleLinkEditRequests';
|
||||
|
||||
// Newer versions of CodeMirror by default use Chrome's EditContext API.
|
||||
// While this might be stable enough for desktop use, it causes significant
|
||||
@ -97,12 +98,6 @@ const createEditor = (
|
||||
}
|
||||
};
|
||||
|
||||
const notifyLinkEditRequest = () => {
|
||||
props.onEvent({
|
||||
kind: EditorEventType.EditLink,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const globalSpellcheckEnabled = () => {
|
||||
return editor.contentDOM.spellcheck;
|
||||
@ -184,10 +179,7 @@ const createEditor = (
|
||||
keyCommand('Mod-`', toggleCode),
|
||||
keyCommand('Mod-[', decreaseIndent),
|
||||
keyCommand('Mod-]', increaseIndent),
|
||||
keyCommand('Mod-k', (_: EditorView) => {
|
||||
notifyLinkEditRequest();
|
||||
return true;
|
||||
}),
|
||||
keyCommand('Mod-k', showLinkEditor),
|
||||
keyCommand('Tab', (view: EditorView) => {
|
||||
if (settings.autocompleteMarkup) {
|
||||
return insertOrIncreaseIndent(view);
|
||||
@ -289,6 +281,11 @@ const createEditor = (
|
||||
notifySelectionFormattingChange(viewUpdate);
|
||||
}),
|
||||
|
||||
handleLinkEditRequests(() => {
|
||||
props.onEvent({
|
||||
kind: EditorEventType.EditLink,
|
||||
});
|
||||
}),
|
||||
],
|
||||
doc: initialText,
|
||||
}),
|
||||
|
@ -10,8 +10,9 @@ import {
|
||||
} from '../markdown/markdownCommands';
|
||||
import duplicateLine from './duplicateLine';
|
||||
import sortSelectedLines from './sortSelectedLines';
|
||||
import { closeSearchPanel, findNext, findPrevious, openSearchPanel, replaceAll, replaceNext } from '@codemirror/search';
|
||||
import { closeSearchPanel, findNext, findPrevious, openSearchPanel, replaceAll, replaceNext, searchPanelOpen } from '@codemirror/search';
|
||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
import { showLinkEditor } from '../utils/handleLinkEditRequests';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
export type EditorCommandFunction = (editor: EditorView, ...args: any[])=> void|any;
|
||||
@ -71,6 +72,15 @@ const editorCommands: Record<EditorCommandType, EditorCommandFunction> = {
|
||||
[EditorCommandType.UndoSelection]: undoSelection,
|
||||
[EditorCommandType.RedoSelection]: redoSelection,
|
||||
|
||||
[EditorCommandType.EditLink]: showLinkEditor,
|
||||
|
||||
[EditorCommandType.ToggleSearch]: (view) => {
|
||||
if (searchPanelOpen(view.state)) {
|
||||
return closeSearchPanel(view);
|
||||
} else {
|
||||
return openSearchPanel(view);
|
||||
}
|
||||
},
|
||||
[EditorCommandType.ShowSearch]: openSearchPanel,
|
||||
[EditorCommandType.HideSearch]: closeSearchPanel,
|
||||
[EditorCommandType.FindNext]: findNext,
|
||||
|
25
packages/editor/CodeMirror/utils/handleLinkEditRequests.ts
Normal file
25
packages/editor/CodeMirror/utils/handleLinkEditRequests.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { EditorState, StateEffect } from '@codemirror/state';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
|
||||
export const showLinkEditorEffect = StateEffect.define<void>();
|
||||
|
||||
export const showLinkEditor = (view: EditorView) => {
|
||||
view.dispatch({
|
||||
effects: [
|
||||
showLinkEditorEffect.of(),
|
||||
],
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleLinkEditRequests = (onShowEditor: ()=> void) => [
|
||||
EditorState.transactionExtender.of(tr => {
|
||||
if (tr.effects.some(e => e.is(showLinkEditorEffect))) {
|
||||
onShowEditor();
|
||||
}
|
||||
|
||||
return null;
|
||||
}),
|
||||
];
|
||||
|
||||
export default handleLinkEditRequests;
|
@ -33,6 +33,7 @@ export enum EditorCommandType {
|
||||
InsertHorizontalRule = 'textHorizontalRule',
|
||||
|
||||
// Find commands
|
||||
ToggleSearch = 'textSearch',
|
||||
ShowSearch = 'find',
|
||||
HideSearch = 'hideSearchDialog',
|
||||
FindNext = 'findNext',
|
||||
@ -40,6 +41,8 @@ export enum EditorCommandType {
|
||||
ReplaceNext = 'replace',
|
||||
ReplaceAll = 'replaceAll',
|
||||
|
||||
EditLink = 'textLink',
|
||||
|
||||
// Editing and navigation commands
|
||||
ScrollSelectionIntoView = 'scrollSelectionIntoView',
|
||||
DeleteLine = 'deleteLine',
|
||||
|
@ -1,4 +1,5 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import { pathExistsSync } from 'fs-extra';
|
||||
const Entities = require('html-entities').AllHtmlEntities;
|
||||
const htmlparser2 = require('@joplin/fork-htmlparser2');
|
||||
const Datauri = require('datauri/sync');
|
||||
@ -102,6 +103,7 @@ const processLinkTag = (baseDir: string, _name: string, attrs: any): string => {
|
||||
|
||||
const filePath = `${baseDir}/${href}`;
|
||||
|
||||
if (!pathExistsSync(filePath)) return null;
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
return `<style>${processCssContent(dirname(filePath), content)}</style>`;
|
||||
};
|
||||
|
@ -668,6 +668,15 @@ const builtInMetadata = (Setting: typeof SettingType) => {
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
},
|
||||
'editor.toolbarButtons': {
|
||||
value: [] as string[],
|
||||
public: false,
|
||||
type: SettingItemType.Array,
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
appTypes: [AppType.Mobile],
|
||||
label: () => 'buttons included in the editor toolbar',
|
||||
},
|
||||
'notes.columns': {
|
||||
value: defaultListColumns(),
|
||||
public: false,
|
||||
|
@ -257,7 +257,7 @@ export default class CommandService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
public componentRegisterCommands<ComponentType>(component: ComponentType, commands: ComponentCommandSpec<ComponentType>[], allowMultiple?: boolean) {
|
||||
public componentRegisterCommands<ComponentType>(component: ComponentType, commands: ComponentCommandSpec<ComponentType>[], allowMultiple?: boolean): RegisteredRuntime {
|
||||
const runtimeHandles: RegisteredRuntime[] = [];
|
||||
for (const command of commands) {
|
||||
runtimeHandles.push(
|
||||
|
@ -1,10 +1,11 @@
|
||||
import CommandService from '../CommandService';
|
||||
import { stateUtils } from '../../reducer';
|
||||
import focusEditorIfEditorCommand from './focusEditorIfEditorCommand';
|
||||
import { WhenClauseContext } from './stateToWhenClauseContext';
|
||||
|
||||
const separatorItem = { type: 'separator' };
|
||||
|
||||
export interface ToolbarButtonInfo {
|
||||
type: 'button';
|
||||
name: string;
|
||||
tooltip: string;
|
||||
iconName: string;
|
||||
@ -13,6 +14,16 @@ export interface ToolbarButtonInfo {
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface SeparatorItem extends Omit<Partial<ToolbarButtonInfo>, 'type'> {
|
||||
type: 'separator';
|
||||
}
|
||||
|
||||
export const separatorItem: SeparatorItem = {
|
||||
type: 'separator',
|
||||
};
|
||||
|
||||
export type ToolbarItem = ToolbarButtonInfo|SeparatorItem;
|
||||
|
||||
interface ToolbarButtonCacheItem {
|
||||
info: ToolbarButtonInfo;
|
||||
}
|
||||
@ -34,8 +45,7 @@ export default class ToolbarButtonUtils {
|
||||
return this.service_;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private commandToToolbarButton(commandName: string, whenClauseContext: any): ToolbarButtonInfo {
|
||||
private commandToToolbarButton(commandName: string, whenClauseContext: WhenClauseContext): ToolbarButtonInfo {
|
||||
const newEnabled = this.service.isEnabled(commandName, whenClauseContext);
|
||||
const newTitle = this.service.title(commandName);
|
||||
|
||||
@ -49,13 +59,14 @@ export default class ToolbarButtonUtils {
|
||||
|
||||
const command = this.service.commandByName(commandName, { runtimeMustBeRegistered: true });
|
||||
|
||||
const output = {
|
||||
const output: ToolbarButtonInfo = {
|
||||
type: 'button',
|
||||
name: commandName,
|
||||
tooltip: this.service.label(commandName),
|
||||
iconName: command.declaration.iconName,
|
||||
enabled: newEnabled,
|
||||
onClick: async () => {
|
||||
void this.service.execute(commandName);
|
||||
await this.service.execute(commandName);
|
||||
void focusEditorIfEditorCommand(commandName, this.service);
|
||||
},
|
||||
title: newTitle,
|
||||
@ -72,13 +83,12 @@ export default class ToolbarButtonUtils {
|
||||
// the output also won't change. Invididual toolbarButtonInfo also won't changed
|
||||
// if the state they use hasn't changed. This is to avoid useless renders of the toolbars.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public commandsToToolbarButtons(commandNames: string[], whenClauseContext: any): ToolbarButtonInfo[] {
|
||||
const output: ToolbarButtonInfo[] = [];
|
||||
public commandsToToolbarButtons(commandNames: string[], whenClauseContext: any): ToolbarItem[] {
|
||||
const output: ToolbarItem[] = [];
|
||||
|
||||
for (const commandName of commandNames) {
|
||||
if (commandName === '-') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
output.push(separatorItem as any);
|
||||
output.push(separatorItem);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,10 @@ const { escapeHtml } = require('../../string-utils.js');
|
||||
import { assetsToHeaders } from '@joplin/renderer';
|
||||
import getPluginSettingValue from '../plugins/utils/getPluginSettingValue';
|
||||
import { LinkRenderingType } from '@joplin/renderer/MdToHtml';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { parseRenderedNoteMetadata } from './utils';
|
||||
|
||||
const logger = Logger.create('InteropService_Exporter_Html');
|
||||
|
||||
export default class InteropService_Exporter_Html extends InteropService_Exporter_Base {
|
||||
|
||||
@ -125,8 +129,11 @@ export default class InteropService_Exporter_Html extends InteropService_Exporte
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const noteContent = [];
|
||||
if (item.title) noteContent.push(`<div class="exported-note-title">${escapeHtml(item.title)}</div>`);
|
||||
const metadata = parseRenderedNoteMetadata(result.html ? result.html : '');
|
||||
if (!metadata.printTitle) logger.info('Not printing title because joplin-metadata-print-title tag is set to false');
|
||||
if (metadata.printTitle && item.title) noteContent.push(`<div class="exported-note-title">${escapeHtml(item.title)}</div>`);
|
||||
if (result.html) noteContent.push(result.html);
|
||||
|
||||
const libRootPath = dirname(dirname(__dirname));
|
||||
@ -136,12 +143,16 @@ export default class InteropService_Exporter_Html extends InteropService_Exporte
|
||||
for (let i = 0; i < result.pluginAssets.length; i++) {
|
||||
const asset = result.pluginAssets[i];
|
||||
const filePath = asset.pathIsAbsolute ? asset.path : `${libRootPath}/node_modules/@joplin/renderer/assets/${asset.name}`;
|
||||
if (!(await shim.fsDriver().exists(filePath))) {
|
||||
logger.warn(`File does not exist and cannot be exported: ${filePath}`);
|
||||
} else {
|
||||
const destPath = `${dirname(noteFilePath)}/pluginAssets/${asset.name}`;
|
||||
const dir = dirname(destPath);
|
||||
await shim.fsDriver().mkdir(dir);
|
||||
this.createdDirs_.push(dir);
|
||||
await shim.fsDriver().copy(filePath, destPath);
|
||||
}
|
||||
}
|
||||
|
||||
const fullHtml = `
|
||||
<!DOCTYPE html>
|
||||
|
35
packages/lib/services/interop/utils.test.ts
Normal file
35
packages/lib/services/interop/utils.test.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { RenderedNoteMetadata, parseRenderedNoteMetadata } from './utils';
|
||||
|
||||
describe('interop/utils', () => {
|
||||
|
||||
test.each<[string, RenderedNoteMetadata]>([
|
||||
[
|
||||
'',
|
||||
{ printTitle: true },
|
||||
],
|
||||
[
|
||||
'<!-- joplin-metadata-print-title = false -->',
|
||||
{ printTitle: false },
|
||||
],
|
||||
[
|
||||
'<!-- joplin-metadata-print-title = true -->',
|
||||
{ printTitle: true },
|
||||
],
|
||||
[
|
||||
'<!-- joplin-metadata-print-title = 0 -->',
|
||||
{ printTitle: false },
|
||||
],
|
||||
[
|
||||
'<!-- joplin-metadata-print-title = 1 -->',
|
||||
{ printTitle: true },
|
||||
],
|
||||
[
|
||||
'<!-- joplin-metadata-print-title = 0 -->',
|
||||
{ printTitle: false },
|
||||
],
|
||||
])('should parse metadata from the note HTML body', async (bodyHtml, expected) => {
|
||||
const actual = parseRenderedNoteMetadata(bodyHtml);
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
});
|
25
packages/lib/services/interop/utils.ts
Normal file
25
packages/lib/services/interop/utils.ts
Normal file
@ -0,0 +1,25 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
|
||||
export interface RenderedNoteMetadata {
|
||||
printTitle: boolean;
|
||||
}
|
||||
|
||||
export const parseRenderedNoteMetadata = (noteHtml: string) => {
|
||||
const output: RenderedNoteMetadata = {
|
||||
printTitle: true,
|
||||
};
|
||||
|
||||
// <!-- joplin-metadata-print-title = false -->
|
||||
const match = noteHtml.match(/<!--[\s]+joplin-metadata-(.*?)[\s]+=[\s]+(.*?)[\s]+-->/);
|
||||
if (match) {
|
||||
const [, propName, propValue] = match;
|
||||
|
||||
if (propName === 'print-title') {
|
||||
output.printTitle = propValue.toLowerCase() === 'true' || propValue === '1';
|
||||
} else {
|
||||
throw new Error(`Unknown view metadata: ${propName}`);
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
@ -68,6 +68,7 @@ module.exports = {
|
||||
await processDirectory(`${rootDir}/packages/app-desktop/gui/NoteListControls/commands`);
|
||||
await processDirectory(`${rootDir}/packages/app-desktop/gui/Sidebar/commands`);
|
||||
await processDirectory(`${rootDir}/packages/app-mobile/commands`);
|
||||
await processDirectory(`${rootDir}/packages/app-mobile/components/screens/Note/commands`);
|
||||
await processDirectory(`${rootDir}/packages/lib/commands`);
|
||||
|
||||
await processDirectory(
|
||||
|
@ -1,5 +1,27 @@
|
||||
# Joplin Android Changelog
|
||||
|
||||
## [android-v3.2.3](https://github.com/laurent22/joplin/releases/tag/android-v3.2.3) (Pre-release) - 2024-12-11T13:58:14Z
|
||||
|
||||
- New: Accessibility: Add checked/unchecked accessibility information to the "sort notes by" dialog (#11411 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- New: Translation: Add sk_SK.po (Slovak) (#11433 by [@dodog](https://github.com/dodog))
|
||||
- Improved: Accessibility: Describe the notebook dropdown for accessibility tools (#11430 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Improved: Accessibility: Improve note list accessibility (#11419 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Improved: Accessibility: Improve note selection screen reader accessibility (#11424 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Improved: Accessibility: Improve screen reader accessibility of the tag list (#11420 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Improved: Accessibility: Improve side menu and heading screen reader accessibility (#11427 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Improved: Accessibility: Improve tag dialog screen reader accessibility (#11421 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Improved: Accessibility: Improve voice typing dialog screen reader accessibility (#11428 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Improved: Accessibility: Mark note properties buttons as buttons (#11432 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Improved: Accessibility: Search screen: Hide the progress bar from accessibility tools when invisible (#11431 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Improved: Close voice typing session when closing the editor (#11466 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Improved: Editor: Switch to a scrolling toolbar, allow adding/removing toolbar items (#11472 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Improved: Reactivate pCloud synchronisation (23032b9)
|
||||
- Improved: Removed deprecation notice on OneDrive sync method (ceea0bc)
|
||||
- Fixed: Accessibility: Fix screen reader is unable to scroll settings tab list (#11429 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Fixed: Fix switching notes then unloading app causes blank screen (#11396) (#11384 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Fixed: Fix the error caused by undefined isCodeBlock_ (turndown-plugin-gfm) (#11471 by Manabu Nakazawa)
|
||||
- Fixed: Upgrade CodeMirror packages (#11440) (#11318 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.2.2](https://github.com/laurent22/joplin/releases/tag/android-v3.2.2) (Pre-release) - 2024-11-19T01:12:43Z
|
||||
|
||||
- Improved: Accessibility: Improve dialog accessibility (#11395 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
11
readme/api/references/plugin_rendered_note_metadata.md
Normal file
11
readme/api/references/plugin_rendered_note_metadata.md
Normal file
@ -0,0 +1,11 @@
|
||||
# Rendered note metadata
|
||||
|
||||
Joplin allows the use of certain metadata tags within the rendered (HTML) version of a note.
|
||||
|
||||
At present, the "print-title" metadata tag is the only one supported. By default, the title of a note is displayed at the top when the note is printed or exported. However, this behavior may not always be desired, such as when a plugin needs to produce a document with a specific custom header. In such cases, you can disable the default behavior by including this tag and setting its value to `false`.
|
||||
|
||||
You can add this tag either directly in the Markdown document or through a Markdown-it content script in the rendered note.
|
||||
|
||||
To prevent the note title from being printed, include the following line in the document:
|
||||
|
||||
`<!-- joplin-metadata-print-title = false -->`
|
@ -8369,7 +8369,7 @@ __metadata:
|
||||
"@react-native-community/geolocation": 3.3.0
|
||||
"@react-native-community/netinfo": 11.3.2
|
||||
"@react-native-community/push-notification-ios": 1.11.0
|
||||
"@react-native-community/slider": 4.5.2
|
||||
"@react-native-community/slider": 4.5.3
|
||||
"@react-native/babel-preset": 0.74.86
|
||||
"@react-native/metro-config": 0.74.87
|
||||
"@sqlite.org/sqlite-wasm": 3.46.0-build2
|
||||
|
Loading…
Reference in New Issue
Block a user