1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Merge branch 'dev' into seamless-updates

This commit is contained in:
Henry Heino 2024-11-05 13:56:34 -08:00 committed by GitHub
commit 3f09c91a63
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
272 changed files with 11673 additions and 4769 deletions

View File

@ -155,6 +155,8 @@ packages/app-desktop/commands/exportNotes.js
packages/app-desktop/commands/focusElement.js
packages/app-desktop/commands/index.js
packages/app-desktop/commands/openProfileDirectory.js
packages/app-desktop/commands/renderMarkup.test.js
packages/app-desktop/commands/renderMarkup.js
packages/app-desktop/commands/replaceMisspelling.js
packages/app-desktop/commands/restoreNoteRevision.js
packages/app-desktop/commands/startExternalEditing.js
@ -284,6 +286,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useScrollUtils.
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/CodeMirror.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/localisation.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useKeymap.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useRefocusOnVisiblePaneChange.js
packages/app-desktop/gui/NoteEditor/NoteBody/PlainEditor/PlainEditor.js
@ -374,6 +377,7 @@ packages/app-desktop/gui/NoteListItem/utils/useItemElement.js
packages/app-desktop/gui/NoteListItem/utils/useItemEventHandlers.js
packages/app-desktop/gui/NoteListItem/utils/useOnContextMenu.js
packages/app-desktop/gui/NoteListItem/utils/useRenderedNote.js
packages/app-desktop/gui/NoteListItem/utils/useRootElement.test.js
packages/app-desktop/gui/NoteListItem/utils/useRootElement.js
packages/app-desktop/gui/NoteListWrapper/NoteListWrapper.js
packages/app-desktop/gui/NotePropertiesDialog.js
@ -384,7 +388,9 @@ packages/app-desktop/gui/NoteTextViewer.js
packages/app-desktop/gui/NoteToolbar/NoteToolbar.js
packages/app-desktop/gui/NotyfContext.js
packages/app-desktop/gui/OneDriveLoginScreen.js
packages/app-desktop/gui/PasswordInput/LabelledPasswordInput.js
packages/app-desktop/gui/PasswordInput/PasswordInput.js
packages/app-desktop/gui/PasswordInput/types.js
packages/app-desktop/gui/PdfViewer.js
packages/app-desktop/gui/PromptDialog.js
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
@ -418,16 +424,19 @@ packages/app-desktop/gui/Sidebar/commands/focusElementSideBar.js
packages/app-desktop/gui/Sidebar/commands/index.js
packages/app-desktop/gui/Sidebar/hooks/useFocusHandler.js
packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.js
packages/app-desktop/gui/Sidebar/hooks/useOnRenderListWrapper.js
packages/app-desktop/gui/Sidebar/hooks/useOnSidebarKeyDownHandler.js
packages/app-desktop/gui/Sidebar/hooks/useSelectedSidebarIndex.js
packages/app-desktop/gui/Sidebar/hooks/useSidebarCommandHandler.js
packages/app-desktop/gui/Sidebar/hooks/useSidebarListData.js
packages/app-desktop/gui/Sidebar/hooks/utils/toggleHeader.js
packages/app-desktop/gui/Sidebar/listItemComponents/AllNotesItem.js
packages/app-desktop/gui/Sidebar/listItemComponents/EmptyExpandLink.js
packages/app-desktop/gui/Sidebar/listItemComponents/ExpandIcon.js
packages/app-desktop/gui/Sidebar/listItemComponents/ExpandLink.js
packages/app-desktop/gui/Sidebar/listItemComponents/FolderItem.js
packages/app-desktop/gui/Sidebar/listItemComponents/HeaderItem.js
packages/app-desktop/gui/Sidebar/listItemComponents/ListItemWrapper.js
packages/app-desktop/gui/Sidebar/listItemComponents/NoteCount.js
packages/app-desktop/gui/Sidebar/listItemComponents/TagItem.js
packages/app-desktop/gui/Sidebar/styles/index.js
@ -441,7 +450,6 @@ packages/app-desktop/gui/ToggleEditorsButton/ToggleEditorsButton.js
packages/app-desktop/gui/ToggleEditorsButton/styles/index.js
packages/app-desktop/gui/ToolbarBase.js
packages/app-desktop/gui/ToolbarButton/ToolbarButton.js
packages/app-desktop/gui/ToolbarButton/styles/index.js
packages/app-desktop/gui/ToolbarSpace.js
packages/app-desktop/gui/TrashNotification/TrashNotification.js
packages/app-desktop/gui/UpdateNotification/UpdateNotification.js
@ -475,6 +483,7 @@ packages/app-desktop/integration-tests/models/NoteList.js
packages/app-desktop/integration-tests/models/SettingsScreen.js
packages/app-desktop/integration-tests/models/Sidebar.js
packages/app-desktop/integration-tests/noteList.spec.js
packages/app-desktop/integration-tests/pluginApi.spec.js
packages/app-desktop/integration-tests/richTextEditor.spec.js
packages/app-desktop/integration-tests/settings.spec.js
packages/app-desktop/integration-tests/sidebar.spec.js
@ -531,7 +540,6 @@ packages/app-desktop/utils/customProtocols/handleCustomProtocols.js
packages/app-desktop/utils/customProtocols/registerCustomProtocols.js
packages/app-desktop/utils/isSafeToOpen.test.js
packages/app-desktop/utils/isSafeToOpen.js
packages/app-desktop/utils/markupLanguageUtils.js
packages/app-desktop/utils/restartInSafeModeFromMain.test.js
packages/app-desktop/utils/restartInSafeModeFromMain.js
packages/app-mobile/PluginAssetsLoader.js
@ -545,7 +553,16 @@ packages/app-mobile/commands/util/goToNote.js
packages/app-mobile/commands/util/showResource.js
packages/app-mobile/components/BackButtonDialogBox.js
packages/app-mobile/components/BetaChip.js
packages/app-mobile/components/CameraView.js
packages/app-mobile/components/CameraView/ActionButtons.js
packages/app-mobile/components/CameraView/Camera/index.jest.js
packages/app-mobile/components/CameraView/Camera/index.js
packages/app-mobile/components/CameraView/Camera/types.js
packages/app-mobile/components/CameraView/CameraView.test.js
packages/app-mobile/components/CameraView/CameraView.js
packages/app-mobile/components/CameraView/ScannedBarcodes.js
packages/app-mobile/components/CameraView/types.js
packages/app-mobile/components/CameraView/utils/fitRectIntoBounds.js
packages/app-mobile/components/CameraView/utils/useBarcodeScanner.js
packages/app-mobile/components/Checkbox.js
packages/app-mobile/components/DialogManager.js
packages/app-mobile/components/DismissibleDialog.js
@ -722,6 +739,7 @@ packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
packages/app-mobile/components/screens/encryption-config.js
packages/app-mobile/components/screens/status.js
packages/app-mobile/components/side-menu-content.js
packages/app-mobile/components/testing/TestProviderStack.js
packages/app-mobile/components/voiceTyping/VoiceTypingDialog.js
packages/app-mobile/gulpfile.js
packages/app-mobile/index.web.js
@ -731,10 +749,15 @@ packages/app-mobile/services/AlarmServiceDriver.ios.js
packages/app-mobile/services/AlarmServiceDriver.web.js
packages/app-mobile/services/BackButtonService.js
packages/app-mobile/services/e2ee/RSA.react-native.js
packages/app-mobile/services/e2ee/crypto.js
packages/app-mobile/services/plugins/PlatformImplementation.js
packages/app-mobile/services/profiles/index.js
packages/app-mobile/services/voiceTyping/VoiceTyping.js
packages/app-mobile/services/voiceTyping/utils/splitWhisperText.test.js
packages/app-mobile/services/voiceTyping/utils/splitWhisperText.js
packages/app-mobile/services/voiceTyping/vosk.android.js
packages/app-mobile/services/voiceTyping/vosk.js
packages/app-mobile/services/voiceTyping/whisper.js
packages/app-mobile/setupQuickActions.js
packages/app-mobile/tools/buildInjectedJs/BundledFile.js
packages/app-mobile/tools/buildInjectedJs/constants.js
@ -786,6 +809,7 @@ packages/app-mobile/utils/shim-init-react/injectedJs.js
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/types.js
packages/app-mobile/web/serviceWorker.js
packages/default-plugins/build.js
@ -861,6 +885,8 @@ packages/editor/CodeMirror/utils/growSelectionToNode.js
packages/editor/CodeMirror/utils/handlePasteEvent.js
packages/editor/CodeMirror/utils/isCursorAtBeginning.js
packages/editor/CodeMirror/utils/isInSyntaxNode.js
packages/editor/CodeMirror/utils/overwriteModeExtension.test.js
packages/editor/CodeMirror/utils/overwriteModeExtension.js
packages/editor/CodeMirror/utils/searchExtension.js
packages/editor/CodeMirror/utils/setupVim.js
packages/editor/SelectionFormatting.js
@ -1095,6 +1121,10 @@ packages/lib/services/debug/populateDatabase.js
packages/lib/services/e2ee/EncryptionService.test.js
packages/lib/services/e2ee/EncryptionService.js
packages/lib/services/e2ee/RSA.node.js
packages/lib/services/e2ee/crypto.test.js
packages/lib/services/e2ee/crypto.js
packages/lib/services/e2ee/cryptoShared.js
packages/lib/services/e2ee/cryptoTestUtils.js
packages/lib/services/e2ee/ppk.test.js
packages/lib/services/e2ee/ppk.js
packages/lib/services/e2ee/ppkTestUtils.js
@ -1133,7 +1163,6 @@ packages/lib/services/keychain/KeychainService.test.js
packages/lib/services/keychain/KeychainService.js
packages/lib/services/keychain/KeychainServiceDriver.dummy.js
packages/lib/services/keychain/KeychainServiceDriver.electron.js
packages/lib/services/keychain/KeychainServiceDriver.mobile.js
packages/lib/services/keychain/KeychainServiceDriver.node.js
packages/lib/services/keychain/KeychainServiceDriverBase.js
packages/lib/services/noteList/defaultLeftToRightListRenderer.js
@ -1331,6 +1360,7 @@ packages/lib/types.js
packages/lib/urlUtils.js
packages/lib/utils/ActionLogger.test.js
packages/lib/utils/ActionLogger.js
packages/lib/utils/attachedResources.js
packages/lib/utils/credentialFiles.js
packages/lib/utils/dom/makeSandboxedIframe.js
packages/lib/utils/focusHandler.js
@ -1350,6 +1380,7 @@ packages/lib/utils/ipc/utils/separateCallbacksFromSerializable.js
packages/lib/utils/ipc/utils/separateCallbacksFromSerializableArray.js
packages/lib/utils/joplinCloud/index.js
packages/lib/utils/joplinCloud/types.js
packages/lib/utils/markupLanguageUtils.js
packages/lib/utils/processStartFlags.js
packages/lib/utils/replaceUnsupportedCharacters.test.js
packages/lib/utils/replaceUnsupportedCharacters.js

View File

@ -287,6 +287,14 @@ module.exports = {
'match': true,
},
},
{
selector: 'enumMember',
format: null,
'filter': {
'regex': '^(sha1|sha256|sha384|sha512|AES_128_GCM|AES_192_GCM|AES_256_GCM)$',
'match': true,
},
},
// -----------------------------------
// INTERFACE

View File

@ -3,6 +3,14 @@ on:
schedule:
- cron: '*/10 * * * *'
jobs:
# This job will make the action fail if any of the checks hasn't passed
# https://github.com/marketplace/actions/allcheckspassed
# allchecks:
# runs-on: ubuntu-latest
# steps:
# - uses: wechuli/allcheckspassed@v1
automerge:
runs-on: ubuntu-latest
permissions:

View File

@ -5,20 +5,8 @@ name: react-native-android-build-apk
on: [push, pull_request]
jobs:
pre_job:
if: github.repository == 'laurent22/joplin'
runs-on: ubuntu-latest
outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip }}
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
concurrent_skipping: 'same_content_newer'
AssembleRelease:
needs: pre_job
if: github.repository == 'laurent22/joplin' && needs.pre_job.outputs.should_skip != 'true'
if: github.repository == 'laurent22/joplin'
runs-on: ubuntu-latest
steps:
- name: Install Linux dependencies

View File

@ -1,21 +1,9 @@
name: Build macOS M1
on: [push, pull_request]
jobs:
pre_job:
if: github.repository == 'laurent22/joplin'
runs-on: ubuntu-latest
outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip }}
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
concurrent_skipping: 'same_content_newer'
Main:
needs: pre_job
# We always process desktop release tags, because they also publish the release
if: github.repository == 'laurent22/joplin' && (needs.pre_job.outputs.should_skip != 'true' || startsWith(github.ref, 'refs/tags/v'))
if: github.repository == 'laurent22/joplin' && (startsWith(github.ref, 'refs/tags/v'))
runs-on: macos-latest
steps:

View File

@ -1,29 +1,15 @@
name: Joplin Continuous Integration
on: [push, pull_request]
jobs:
pre_job:
if: github.repository == 'laurent22/joplin'
# Do not use unbuntu-latest because it causes `The operation was canceled` failures:
# https://github.com/actions/runner-images/issues/6709
runs-on: ubuntu-20.04
outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip }}
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
concurrent_skipping: 'same_content_newer'
Main:
needs: pre_job
# We always process server or desktop release tags, because they also publish the release
if: github.repository == 'laurent22/joplin' && (needs.pre_job.outputs.should_skip != 'true' || startsWith(github.ref, 'refs/tags/server-v') || startsWith(github.ref, 'refs/tags/v'))
if: github.repository == 'laurent22/joplin' && (startsWith(github.ref, 'refs/tags/server-v') || startsWith(github.ref, 'refs/tags/v'))
runs-on: ${{ matrix.os }}
strategy:
matrix:
# Do not use unbuntu-latest because it causes `The operation was canceled` failures:
# https://github.com/actions/runner-images/issues/6709
os: [macos-12, ubuntu-20.04, windows-2019]
os: [macos-13, ubuntu-20.04, windows-2019]
steps:
# Trying to fix random networking issues on Windows
@ -157,8 +143,7 @@ jobs:
yarn install && cd packages/app-desktop && yarn dist --publish=never
ServerDockerImage:
needs: pre_job
if: github.repository == 'laurent22/joplin' && needs.pre_job.outputs.should_skip != 'true'
if: github.repository == 'laurent22/joplin'
runs-on: ${{ matrix.os }}
strategy:
matrix:

39
.gitignore vendored
View File

@ -132,6 +132,8 @@ packages/app-desktop/commands/exportNotes.js
packages/app-desktop/commands/focusElement.js
packages/app-desktop/commands/index.js
packages/app-desktop/commands/openProfileDirectory.js
packages/app-desktop/commands/renderMarkup.test.js
packages/app-desktop/commands/renderMarkup.js
packages/app-desktop/commands/replaceMisspelling.js
packages/app-desktop/commands/restoreNoteRevision.js
packages/app-desktop/commands/startExternalEditing.js
@ -261,6 +263,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useScrollUtils.
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/CodeMirror.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/localisation.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useKeymap.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useRefocusOnVisiblePaneChange.js
packages/app-desktop/gui/NoteEditor/NoteBody/PlainEditor/PlainEditor.js
@ -351,6 +354,7 @@ packages/app-desktop/gui/NoteListItem/utils/useItemElement.js
packages/app-desktop/gui/NoteListItem/utils/useItemEventHandlers.js
packages/app-desktop/gui/NoteListItem/utils/useOnContextMenu.js
packages/app-desktop/gui/NoteListItem/utils/useRenderedNote.js
packages/app-desktop/gui/NoteListItem/utils/useRootElement.test.js
packages/app-desktop/gui/NoteListItem/utils/useRootElement.js
packages/app-desktop/gui/NoteListWrapper/NoteListWrapper.js
packages/app-desktop/gui/NotePropertiesDialog.js
@ -361,7 +365,9 @@ packages/app-desktop/gui/NoteTextViewer.js
packages/app-desktop/gui/NoteToolbar/NoteToolbar.js
packages/app-desktop/gui/NotyfContext.js
packages/app-desktop/gui/OneDriveLoginScreen.js
packages/app-desktop/gui/PasswordInput/LabelledPasswordInput.js
packages/app-desktop/gui/PasswordInput/PasswordInput.js
packages/app-desktop/gui/PasswordInput/types.js
packages/app-desktop/gui/PdfViewer.js
packages/app-desktop/gui/PromptDialog.js
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
@ -395,16 +401,19 @@ packages/app-desktop/gui/Sidebar/commands/focusElementSideBar.js
packages/app-desktop/gui/Sidebar/commands/index.js
packages/app-desktop/gui/Sidebar/hooks/useFocusHandler.js
packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.js
packages/app-desktop/gui/Sidebar/hooks/useOnRenderListWrapper.js
packages/app-desktop/gui/Sidebar/hooks/useOnSidebarKeyDownHandler.js
packages/app-desktop/gui/Sidebar/hooks/useSelectedSidebarIndex.js
packages/app-desktop/gui/Sidebar/hooks/useSidebarCommandHandler.js
packages/app-desktop/gui/Sidebar/hooks/useSidebarListData.js
packages/app-desktop/gui/Sidebar/hooks/utils/toggleHeader.js
packages/app-desktop/gui/Sidebar/listItemComponents/AllNotesItem.js
packages/app-desktop/gui/Sidebar/listItemComponents/EmptyExpandLink.js
packages/app-desktop/gui/Sidebar/listItemComponents/ExpandIcon.js
packages/app-desktop/gui/Sidebar/listItemComponents/ExpandLink.js
packages/app-desktop/gui/Sidebar/listItemComponents/FolderItem.js
packages/app-desktop/gui/Sidebar/listItemComponents/HeaderItem.js
packages/app-desktop/gui/Sidebar/listItemComponents/ListItemWrapper.js
packages/app-desktop/gui/Sidebar/listItemComponents/NoteCount.js
packages/app-desktop/gui/Sidebar/listItemComponents/TagItem.js
packages/app-desktop/gui/Sidebar/styles/index.js
@ -418,7 +427,6 @@ packages/app-desktop/gui/ToggleEditorsButton/ToggleEditorsButton.js
packages/app-desktop/gui/ToggleEditorsButton/styles/index.js
packages/app-desktop/gui/ToolbarBase.js
packages/app-desktop/gui/ToolbarButton/ToolbarButton.js
packages/app-desktop/gui/ToolbarButton/styles/index.js
packages/app-desktop/gui/ToolbarSpace.js
packages/app-desktop/gui/TrashNotification/TrashNotification.js
packages/app-desktop/gui/UpdateNotification/UpdateNotification.js
@ -452,6 +460,7 @@ packages/app-desktop/integration-tests/models/NoteList.js
packages/app-desktop/integration-tests/models/SettingsScreen.js
packages/app-desktop/integration-tests/models/Sidebar.js
packages/app-desktop/integration-tests/noteList.spec.js
packages/app-desktop/integration-tests/pluginApi.spec.js
packages/app-desktop/integration-tests/richTextEditor.spec.js
packages/app-desktop/integration-tests/settings.spec.js
packages/app-desktop/integration-tests/sidebar.spec.js
@ -508,7 +517,6 @@ packages/app-desktop/utils/customProtocols/handleCustomProtocols.js
packages/app-desktop/utils/customProtocols/registerCustomProtocols.js
packages/app-desktop/utils/isSafeToOpen.test.js
packages/app-desktop/utils/isSafeToOpen.js
packages/app-desktop/utils/markupLanguageUtils.js
packages/app-desktop/utils/restartInSafeModeFromMain.test.js
packages/app-desktop/utils/restartInSafeModeFromMain.js
packages/app-mobile/PluginAssetsLoader.js
@ -522,7 +530,16 @@ packages/app-mobile/commands/util/goToNote.js
packages/app-mobile/commands/util/showResource.js
packages/app-mobile/components/BackButtonDialogBox.js
packages/app-mobile/components/BetaChip.js
packages/app-mobile/components/CameraView.js
packages/app-mobile/components/CameraView/ActionButtons.js
packages/app-mobile/components/CameraView/Camera/index.jest.js
packages/app-mobile/components/CameraView/Camera/index.js
packages/app-mobile/components/CameraView/Camera/types.js
packages/app-mobile/components/CameraView/CameraView.test.js
packages/app-mobile/components/CameraView/CameraView.js
packages/app-mobile/components/CameraView/ScannedBarcodes.js
packages/app-mobile/components/CameraView/types.js
packages/app-mobile/components/CameraView/utils/fitRectIntoBounds.js
packages/app-mobile/components/CameraView/utils/useBarcodeScanner.js
packages/app-mobile/components/Checkbox.js
packages/app-mobile/components/DialogManager.js
packages/app-mobile/components/DismissibleDialog.js
@ -699,6 +716,7 @@ packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
packages/app-mobile/components/screens/encryption-config.js
packages/app-mobile/components/screens/status.js
packages/app-mobile/components/side-menu-content.js
packages/app-mobile/components/testing/TestProviderStack.js
packages/app-mobile/components/voiceTyping/VoiceTypingDialog.js
packages/app-mobile/gulpfile.js
packages/app-mobile/index.web.js
@ -708,10 +726,15 @@ packages/app-mobile/services/AlarmServiceDriver.ios.js
packages/app-mobile/services/AlarmServiceDriver.web.js
packages/app-mobile/services/BackButtonService.js
packages/app-mobile/services/e2ee/RSA.react-native.js
packages/app-mobile/services/e2ee/crypto.js
packages/app-mobile/services/plugins/PlatformImplementation.js
packages/app-mobile/services/profiles/index.js
packages/app-mobile/services/voiceTyping/VoiceTyping.js
packages/app-mobile/services/voiceTyping/utils/splitWhisperText.test.js
packages/app-mobile/services/voiceTyping/utils/splitWhisperText.js
packages/app-mobile/services/voiceTyping/vosk.android.js
packages/app-mobile/services/voiceTyping/vosk.js
packages/app-mobile/services/voiceTyping/whisper.js
packages/app-mobile/setupQuickActions.js
packages/app-mobile/tools/buildInjectedJs/BundledFile.js
packages/app-mobile/tools/buildInjectedJs/constants.js
@ -763,6 +786,7 @@ packages/app-mobile/utils/shim-init-react/injectedJs.js
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/types.js
packages/app-mobile/web/serviceWorker.js
packages/default-plugins/build.js
@ -838,6 +862,8 @@ packages/editor/CodeMirror/utils/growSelectionToNode.js
packages/editor/CodeMirror/utils/handlePasteEvent.js
packages/editor/CodeMirror/utils/isCursorAtBeginning.js
packages/editor/CodeMirror/utils/isInSyntaxNode.js
packages/editor/CodeMirror/utils/overwriteModeExtension.test.js
packages/editor/CodeMirror/utils/overwriteModeExtension.js
packages/editor/CodeMirror/utils/searchExtension.js
packages/editor/CodeMirror/utils/setupVim.js
packages/editor/SelectionFormatting.js
@ -1072,6 +1098,10 @@ packages/lib/services/debug/populateDatabase.js
packages/lib/services/e2ee/EncryptionService.test.js
packages/lib/services/e2ee/EncryptionService.js
packages/lib/services/e2ee/RSA.node.js
packages/lib/services/e2ee/crypto.test.js
packages/lib/services/e2ee/crypto.js
packages/lib/services/e2ee/cryptoShared.js
packages/lib/services/e2ee/cryptoTestUtils.js
packages/lib/services/e2ee/ppk.test.js
packages/lib/services/e2ee/ppk.js
packages/lib/services/e2ee/ppkTestUtils.js
@ -1110,7 +1140,6 @@ packages/lib/services/keychain/KeychainService.test.js
packages/lib/services/keychain/KeychainService.js
packages/lib/services/keychain/KeychainServiceDriver.dummy.js
packages/lib/services/keychain/KeychainServiceDriver.electron.js
packages/lib/services/keychain/KeychainServiceDriver.mobile.js
packages/lib/services/keychain/KeychainServiceDriver.node.js
packages/lib/services/keychain/KeychainServiceDriverBase.js
packages/lib/services/noteList/defaultLeftToRightListRenderer.js
@ -1308,6 +1337,7 @@ packages/lib/types.js
packages/lib/urlUtils.js
packages/lib/utils/ActionLogger.test.js
packages/lib/utils/ActionLogger.js
packages/lib/utils/attachedResources.js
packages/lib/utils/credentialFiles.js
packages/lib/utils/dom/makeSandboxedIframe.js
packages/lib/utils/focusHandler.js
@ -1327,6 +1357,7 @@ packages/lib/utils/ipc/utils/separateCallbacksFromSerializable.js
packages/lib/utils/ipc/utils/separateCallbacksFromSerializableArray.js
packages/lib/utils/joplinCloud/index.js
packages/lib/utils/joplinCloud/types.js
packages/lib/utils/markupLanguageUtils.js
packages/lib/utils/processStartFlags.js
packages/lib/utils/replaceUnsupportedCharacters.test.js
packages/lib/utils/replaceUnsupportedCharacters.js

File diff suppressed because one or more lines are too long

875
.yarn/releases/yarn-3.8.3.cjs vendored Executable file

File diff suppressed because one or more lines are too long

View File

@ -6,7 +6,7 @@ plugins:
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
spec: "@yarnpkg/plugin-workspace-tools"
yarnPath: .yarn/releases/yarn-3.6.4.cjs
yarnPath: .yarn/releases/yarn-3.8.3.cjs
logFilters:

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@ -263,7 +263,8 @@ StartupWMClass=Joplin
Type=Application
Categories=Office;
MimeType=x-scheme-handler/joplin;
X-GNOME-SingleWindow=true // should be removed eventually as it was upstream to be an XDG specification
# should be removed eventually as it was upstream to be an XDG specification
X-GNOME-SingleWindow=true
SingleMainWindow=true
EOF

View File

@ -31,7 +31,7 @@ Please see the [donation page](https://github.com/laurent22/joplin/blob/dev/read
# Sponsors
<!-- SPONSORS-ORG -->
<a href="https://seirei.ne.jp"><img title="Serei Network" width="256" src="https://joplinapp.org/images/sponsors/SeireiNetwork.png"/></a> <a href="https://www.hosting.de/nextcloud/?mtm_campaign=managed-nextcloud&amp;mtm_kwd=joplinapp&amp;mtm_source=joplinapp-webseite&amp;mtm_medium=banner"><img title="Hosting.de" width="256" src="https://joplinapp.org/images/sponsors/HostingDe.png"/></a> <a href="https://citricsheep.com"><img title="Citric Sheep" width="256" src="https://joplinapp.org/images/sponsors/CitricSheep.png"/></a> <a href="https://sorted.travel/?utm_source=joplinapp"><img title="Sorted Travel" width="256" src="https://joplinapp.org/images/sponsors/SortedTravel.png"/></a> <a href="https://celebian.com"><img title="Celebian" width="256" src="https://joplinapp.org/images/sponsors/Celebian.png"/></a> <a href="https://bestkru.com"><img title="BestKru" width="256" src="https://joplinapp.org/images/sponsors/BestKru.png"/></a> <a href="https://www.socialfollowers.uk/buy-tiktok-followers/"><img title="Social Followers" width="256" src="https://joplinapp.org/images/sponsors/SocialFollowers.png"/></a> <a href="https://stormlikes.com/"><img title="Stormlikes" width="256" src="https://joplinapp.org/images/sponsors/Stormlikes.png"/></a> <a href="https://route4me.com"><img title="Route4Me" width="256" src="https://joplinapp.org/images/sponsors/Route4Me.png"/></a> <a href="https://buyyoutubviews.com"><img title="BYTV" width="256" src="https://joplinapp.org/images/sponsors/BYTV.png"/></a> <a href="https://www.famegear.com"><img title="Famegear" width="256" src="https://joplinapp.org/images/sponsors/Famegear.png"/></a>
<a href="https://seirei.ne.jp"><img title="Serei Network" width="256" src="https://joplinapp.org/images/sponsors/SeireiNetwork.png"/></a> <a href="https://www.hosting.de/nextcloud/?mtm_campaign=managed-nextcloud&amp;mtm_kwd=joplinapp&amp;mtm_source=joplinapp-webseite&amp;mtm_medium=banner"><img title="Hosting.de" width="256" src="https://joplinapp.org/images/sponsors/HostingDe.png"/></a> <a href="https://citricsheep.com"><img title="Citric Sheep" width="256" src="https://joplinapp.org/images/sponsors/CitricSheep.png"/></a> <a href="https://sorted.travel/?utm_source=joplinapp"><img title="Sorted Travel" width="256" src="https://joplinapp.org/images/sponsors/SortedTravel.png"/></a> <a href="https://celebian.com"><img title="Celebian" width="256" src="https://joplinapp.org/images/sponsors/Celebian.png"/></a> <a href="https://bestkru.com"><img title="BestKru" width="256" src="https://joplinapp.org/images/sponsors/BestKru.png"/></a> <a href="https://www.socialfollowers.uk/buy-tiktok-followers/"><img title="Social Followers" width="256" src="https://joplinapp.org/images/sponsors/SocialFollowers.png"/></a> <a href="https://stormlikes.com/"><img title="Stormlikes" width="256" src="https://joplinapp.org/images/sponsors/Stormlikes.png"/></a> <a href="https://route4me.com"><img title="Route4Me" width="256" src="https://joplinapp.org/images/sponsors/Route4Me.png"/></a> <a href="https://buyyoutubviews.com"><img title="BYTV" width="256" src="https://joplinapp.org/images/sponsors/BYTV.png"/></a> <a href="https://www.famegear.com"><img title="Famegear" width="256" src="https://joplinapp.org/images/sponsors/Famegear.png"/></a> <a href="https://casinoreviews.net"><img title="Casino Reviews" width="256" src="https://joplinapp.org/images/sponsors/CasinoReviews.png"/></a>
<!-- SPONSORS-ORG -->
* * *

View File

@ -0,0 +1,13 @@
A <strong>Joplin</strong> egy ingyenes, nyílt forráskódú jegyzetkészítő és teendők készítésére szolgáló alkalmazás, amely számos jegyzetfüzetbe rendezett jegyzet kezelésére képes. A jegyzetek kereshetők, másolhatók, címkézhetők és módosíthatók akár közvetlenül az alkalmazásokból, akár saját szövegszerkesztőből.
A jegyzetek <a href="https://joplinapp.org/help/apps/markdown">Markdown formátumban</a> vannak.
Az Evernote-ból exportált jegyzetek <a href="https://joplinapp.org/help/apps/import_export">importálhatók</a> a Joplinba, beleértve a formázott tartalmat (amelyet Markdown-ba konvertálunk), az erőforrásokat (képeket, mellékletek stb.) és teljes metaadatok (földrajzi hely, frissített idő, létrehozási idő stb.). Sima Markdown fájlok is importálhatók.
A Joplin „offline elszobb”, ami azt jelenti, hogy mindig minden adata a telefonján vagy a számítógépén van. Ez biztosítja, hogy jegyzetei mindig elérhetőek legyenek, akár van internetkapcsolata, akár nem.</p>
A jegyzetek biztonságosan <a href="https://joplinapp.org/help/apps/sync">összehangolhatók</a> a <a href="https://joplinapp.org/help/apps/sync/e2ee">végpontok közötti titkosítás</a> segítségével különféle felhőszolgáltatásokkal, köztük a Nextcloud, a Dropbox, a OneDrive és a <a href="https://joplinapp.org/plans/">Joplin Cloud</a> segítségével.
A teljes szöveges keresés minden platformon elérhető, hogy gyorsan megtalálja a szükséges információkat. Az alkalmazás beépülő modulok és témák segítségével testreszabható, és könnyedén létrehozhatja sajátját is.
Az alkalmazás elérhető Android, iOS, Linux, macOS és Windows rendszeren. Egy <a href="https://joplinapp.org/help/apps/clipper">Web Clipper</a>, amely weboldalakat és képernyőképeket menthet a böngészőből, szintén elérhető a <a href="https://addons.mozilla.org/firefox/addon/joplin-web-clipper/">Firefox</a> és <a href="https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek">Chrome</a>.

View File

@ -0,0 +1 @@
Jegyzetkészítő és teendők alkalmazás Linux, macOS, Windows és mobileszközök közötti összehangolással

View File

@ -72,38 +72,38 @@
"@crowdin/cli": "3",
"@joplin/utils": "~2.12",
"@seiyab/eslint-plugin-react-hooks": "4.5.1-beta.0",
"@typescript-eslint/eslint-plugin": "6.8.0",
"@typescript-eslint/parser": "6.8.0",
"@typescript-eslint/eslint-plugin": "6.21.0",
"@typescript-eslint/parser": "6.21.0",
"cspell": "5.21.2",
"eslint": "8.52.0",
"eslint": "8.57.0",
"eslint-interactive": "10.8.0",
"eslint-plugin-import": "2.28.1",
"eslint-plugin-jest": "27.4.3",
"eslint-plugin-promise": "6.1.1",
"eslint-plugin-react": "7.33.2",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-jest": "27.9.0",
"eslint-plugin-promise": "6.2.0",
"eslint-plugin-react": "7.34.3",
"execa": "5.1.1",
"fs-extra": "11.2.0",
"glob": "10.4.2",
"glob": "10.4.5",
"gulp": "4.0.2",
"husky": "3.1.0",
"lerna": "3.22.1",
"lint-staged": "15.2.7",
"madge": "6.1.0",
"npm-package-json-lint": "7.1.0",
"typescript": "5.2.2"
"typescript": "5.4.5"
},
"dependencies": {
"@types/fs-extra": "11.0.4",
"eslint-plugin-github": "4.10.1",
"eslint-plugin-github": "4.10.2",
"http-server": "14.1.1",
"node-gyp": "9.4.1",
"nodemon": "3.0.3"
"nodemon": "3.1.7"
},
"packageManager": "yarn@3.6.4",
"packageManager": "yarn@3.8.3",
"resolutions": {
"react-native-camera@4.2.1": "patch:react-native-camera@npm%3A4.2.1#./.yarn/patches/react-native-camera-npm-4.2.1-24b2600a7e.patch",
"react-native-vosk@0.1.12": "patch:react-native-vosk@npm%3A0.1.12#./.yarn/patches/react-native-vosk-npm-0.1.12-76b1caaae8.patch",
"eslint": "patch:eslint@8.52.0#./.yarn/patches/eslint-npm-8.39.0-d92bace04d.patch",
"eslint": "patch:eslint@8.57.0#./.yarn/patches/eslint-npm-8.39.0-d92bace04d.patch",
"app-builder-lib@24.4.0": "patch:app-builder-lib@npm%3A24.4.0#./.yarn/patches/app-builder-lib-npm-24.4.0-05322ff057.patch",
"nanoid": "patch:nanoid@npm%3A3.3.7#./.yarn/patches/nanoid-npm-3.3.7-98824ba130.patch",
"pdfjs-dist": "patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch",

View File

@ -35,15 +35,15 @@
],
"owner": "Laurent Cozic"
},
"version": "3.1.0",
"version": "3.2.0",
"bin": "./main.js",
"engines": {
"node": ">=10.0.0"
},
"dependencies": {
"@joplin/lib": "~3.1",
"@joplin/renderer": "~3.1",
"@joplin/utils": "~3.1",
"@joplin/lib": "~3.2",
"@joplin/renderer": "~3.2",
"@joplin/utils": "~3.2",
"aws-sdk": "2.1340.0",
"chalk": "4.1.2",
"compare-version": "0.1.2",
@ -70,14 +70,14 @@
"yargs-parser": "21.1.1"
},
"devDependencies": {
"@joplin/tools": "~3.1",
"@joplin/tools": "~3.2",
"@types/fs-extra": "11.0.4",
"@types/jest": "29.5.8",
"@types/node": "18.19.39",
"@types/jest": "29.5.12",
"@types/node": "18.19.42",
"@types/proper-lockfile": "^4.1.2",
"gulp": "4.0.2",
"jest": "29.7.0",
"temp": "0.9.4",
"typescript": "5.2.2"
"typescript": "5.4.5"
}
}

View File

@ -352,4 +352,12 @@ describe('MdToHtml', () => {
expect(html).toContain('Inline</span>');
expect(html).toContain('Block</span>');
});
it('should sanitize KaTeX errors', async () => {
const markdown = '$\\a<svg>$';
const renderResult = await newTestMdToHtml().render(markdown, null, { bodyOnly: true });
// Should not contain the HTML in unsanitized form
expect(renderResult.html).not.toContain('<svg>');
});
});

View File

@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Joplin Web Clipper [DEV]",
"version": "3.1.1",
"version": "3.2.0",
"description": "Capture and save web pages and screenshots from your browser to Joplin.",
"homepage_url": "https://joplinapp.org",
"content_security_policy": {

View File

@ -5,7 +5,7 @@ import type ShimType from '@joplin/lib/shim';
const shim: typeof ShimType = require('@joplin/lib/shim').default;
import { isCallbackUrl } from '@joplin/lib/callbackUrlUtils';
import { BrowserWindow, IpcMainEvent, Tray, screen } from 'electron';
import { BrowserWindow, IpcMainEvent, Tray, WebContents, screen } from 'electron';
import bridge from './bridge';
const url = require('url');
const path = require('path');
@ -232,14 +232,35 @@ export default class ElectronAppWrapper {
}, 3000);
}
// will-frame-navigate is fired by clicking on a link within the BrowserWindow.
this.win_.webContents.on('will-frame-navigate', event => {
// If the link changes the URL of the browser window,
if (event.isMainFrame) {
event.preventDefault();
void bridge().openExternal(event.url);
}
});
const addWindowEventHandlers = (webContents: WebContents) => {
// will-frame-navigate is fired by clicking on a link within the BrowserWindow.
webContents.on('will-frame-navigate', event => {
// If the link changes the URL of the browser window,
if (event.isMainFrame) {
event.preventDefault();
void bridge().openExternal(event.url);
}
});
// Override calls to window.open and links with target="_blank": Open most in a browser instead
// of Electron:
webContents.setWindowOpenHandler((event) => {
if (event.url === 'about:blank') {
// Script-controlled pages: Used for opening notes in new windows
return {
action: 'allow',
};
} else if (event.url.match(/^https?:\/\//)) {
void bridge().openExternal(event.url);
}
return { action: 'deny' };
});
webContents.on('did-create-window', (event) => {
addWindowEventHandlers(event.webContents);
});
};
addWindowEventHandlers(this.win_.webContents);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
this.win_.on('close', (event: any) => {

View File

@ -118,13 +118,29 @@ class Application extends BaseApplication {
}
}
private updateLanguage() {
setLocale(Setting.value('locale'));
// The bridge runs within the main process, with its own instance of locale.js
// so it needs to be set too here.
bridge().setLocale(Setting.value('locale'));
const htmlContainer = document.querySelector('html');
// HTML expects the lang attribute to be in BCP47 format, with a dash rather than
// an underscore:
const htmlLang = Setting.value('locale').replace(/_/g, '-');
htmlContainer.setAttribute('lang', htmlLang);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
protected async generalMiddleware(store: any, next: any, action: any) {
if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'locale' || action.type === 'SETTING_UPDATE_ALL') {
setLocale(Setting.value('locale'));
// The bridge runs within the main process, with its own instance of locale.js
// so it needs to be set too here.
bridge().setLocale(Setting.value('locale'));
this.updateLanguage();
}
if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'renderer.fileUrls' || action.type === 'SETTING_UPDATE_ALL') {
bridge().electronApp().getCustomProtocolHandler().setMediaAccessEnabled(
Setting.value('renderer.fileUrls'),
);
}
if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'showTrayIcon' || action.type === 'SETTING_UPDATE_ALL') {

View File

@ -452,8 +452,7 @@ export class Bridge {
return nativeTheme.shouldUseDarkColors;
}
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
public addEventListener(name: string, fn: Function) {
public addEventListener(name: string, fn: ()=> void) {
if (name === 'nativeThemeUpdated') {
nativeTheme.on('updated', fn);
} else {

View File

@ -6,6 +6,7 @@ import * as exportFolders from './exportFolders';
import * as exportNotes from './exportNotes';
import * as focusElement from './focusElement';
import * as openProfileDirectory from './openProfileDirectory';
import * as renderMarkup from './renderMarkup';
import * as replaceMisspelling from './replaceMisspelling';
import * as restoreNoteRevision from './restoreNoteRevision';
import * as startExternalEditing from './startExternalEditing';
@ -25,6 +26,7 @@ const index: any[] = [
exportNotes,
focusElement,
openProfileDirectory,
renderMarkup,
replaceMisspelling,
restoreNoteRevision,
startExternalEditing,

View File

@ -0,0 +1,47 @@
import shim from '@joplin/lib/shim';
import Resource from '@joplin/lib/models/Resource';
import Note from '@joplin/lib/models/Note';
import { setupDatabaseAndSynchronizer, supportDir, switchClient } from '@joplin/lib/testing/test-utils';
import { runtime } from './renderMarkup';
import { MarkupLanguage } from '@joplin/renderer';
const testImagePath = `${supportDir}/photo.jpg`;
const command = runtime();
describe('renderMarkup', () => {
beforeEach(async () => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
});
test('should return the rendered note as HTML', async () => {
{
const renderedNote = await command.execute(null, MarkupLanguage.Markdown, 'hello **strong**');
expect(renderedNote.html).toBe('<div id="rendered-md"><p>hello <strong>strong</strong></p>\n</div>');
expect(!!renderedNote.pluginAssets).toBe(true);
expect(!!renderedNote.cssStrings).toBe(true);
}
{
const renderedNote = await await command.execute(null, MarkupLanguage.Markdown, '- [ ] Beer\n- [x] Milk\n- [ ] Eggs');
expect(renderedNote.html).toContain('checkbox-label-unchecked">Beer');
expect(renderedNote.html).toContain('checkbox-label-checked">Milk');
expect(renderedNote.html).toContain('checkbox-label-unchecked">Eggs');
expect(!!renderedNote.pluginAssets).toBe(true);
expect(!!renderedNote.cssStrings).toBe(true);
}
{
const note = await Note.save({ });
await shim.attachFileToNote(note, testImagePath, { resizeLargeImages: 'never' });
const resource = (await Resource.all())[0];
const noteBody = (await Note.load(note.id)).body;
const renderedNote = await await command.execute(null, MarkupLanguage.Markdown, noteBody);
expect(renderedNote.html).toContain(`<div id="rendered-md"><p><img data-from-md data-resource-id="${resource.id}" src="joplin-content://note-viewer/`);
expect(renderedNote.html).toContain(`/resources-1/${resource.id}.jpg?t=`);
expect(renderedNote.html).toContain('" title alt="photo.jpg" /></p>');
}
});
});

View File

@ -0,0 +1,32 @@
import markupLanguageUtils from '@joplin/lib/markupLanguageUtils';
import Setting from '@joplin/lib/models/Setting';
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import { themeStyle } from '@joplin/lib/theme';
import attachedResources from '@joplin/lib/utils/attachedResources';
import { MarkupLanguage } from '@joplin/renderer';
export const declaration: CommandDeclaration = {
name: 'renderMarkup',
};
const getMarkupToHtml = () => {
const resourceBaseUrl = `joplin-content://note-viewer/${Setting.value('resourceDir')}/`;
return markupLanguageUtils.newMarkupToHtml({}, {
resourceBaseUrl,
customCss: '',
});
};
export const runtime = (): CommandRuntime => {
return {
execute: async (_context: CommandContext, markupLanguage: MarkupLanguage, markup: string) => {
const markupToHtml = getMarkupToHtml();
const html = await markupToHtml.render(markupLanguage, markup, themeStyle(Setting.value('theme')), {
resources: await attachedResources(markup),
splitted: true,
});
return html;
},
};
};

View File

@ -256,6 +256,9 @@ const Button = React.forwardRef((props: Props, ref: any) => {
iconOnly={iconOnly}
onClick={onClick}
// When there's no title, the button needs a label. In this case, fall back
// to the tooltip.
aria-label={props.title ? undefined : props.tooltip}
aria-disabled={props.disabled}
aria-expanded={props['aria-expanded']}
aria-controls={props['aria-controls']}

View File

@ -107,19 +107,12 @@ const SettingComponent: React.FC<Props> = props => {
);
}
const selectStyle = { ...controlStyle, paddingLeft: 6,
paddingRight: 6,
paddingTop: 4,
paddingBottom: 4,
borderColor: theme.borderColor4,
borderRadius: 3 };
return (
<div style={rowStyle}>
<SettingLabel htmlFor={inputId} text={md.label()}/>
<select
value={value}
style={selectStyle}
className='setting-select-control'
onChange={(event) => {
updateSettingValue(key, event.target.value);
}}

View File

@ -234,7 +234,7 @@ export default function(props: Props) {
return (
<CellFooter>
<NeedUpgradeMessage>
{PluginService.instance().describeIncompatibility(props.manifest)}
{PluginService.instance().describeIncompatibility(item.manifest)}
</NeedUpgradeMessage>
</CellFooter>
);

View File

@ -3,3 +3,4 @@
@use "./setting-label.scss";
@use "./setting-header.scss";
@use "./setting-tab-panel.scss";
@use "./setting-select-control.scss";

View File

@ -0,0 +1,20 @@
.setting-select-control {
display: inline-block;
color: var(--joplin-color);
font-family: var(--joplin-font-family);
background-color: var(--joplin-background-color);
padding-left: 6px;
padding-right: 6px;
padding-top: 4px;
padding-bottom: 4px;
border-radius: 3px;
border-color: var(--joplin-border-color4);
&:focus-visible {
border-color: var(--joplin-focus-outline-color);
outline: none;
}
}

View File

@ -206,7 +206,7 @@ const EncryptionConfigScreen = (props: Props) => {
if (hasMasterPassword && newEnabled) {
if (!(await masterPasswordIsValid(newPassword))) {
alert('Invalid password. Please try again. If you have forgotten your password you will need to reset it.');
await dialogs.alert('Invalid password. Please try again. If you have forgotten your password you will need to reset it.');
return;
}
}

View File

@ -17,7 +17,7 @@ export default function(props: Props) {
} else if (folderIcon.type === FolderIconType.DataUrl) {
return <img style={{ width, height, opacity }} src={folderIcon.dataUrl} />;
} else if (folderIcon.type === FolderIconType.FontAwesome) {
return <i style={{ fontSize: 18, width, opacity }} className={folderIcon.name}></i>;
return <i style={{ fontSize: 18, width, opacity }} className={folderIcon.name} role='img'></i>;
} else {
throw new Error(`Unsupported folder icon type: ${folderIcon.type}`);
}

View File

@ -5,12 +5,18 @@ interface Props<ItemType> {
style: React.CSSProperties & { height: number };
itemHeight: number;
items: ItemType[];
disabled?: boolean;
onKeyDown?: KeyboardEventHandler<HTMLElement>;
itemRenderer: (item: ItemType, index: number)=> React.JSX.Element;
className?: string;
itemRenderer: (item: ItemType, index: number)=> React.JSX.Element;
renderContentWrapper?: (listItems: React.ReactNode[])=> React.ReactNode;
onKeyDown?: KeyboardEventHandler<HTMLElement>;
onItemDrop?: DragEventHandler<HTMLElement>;
selectedIndex?: number;
alwaysRenderSelection?: boolean;
id?: string;
role?: string;
'aria-label'?: string;
@ -23,13 +29,13 @@ interface State {
class ItemList<ItemType> extends React.Component<Props<ItemType>, State> {
private scrollTop_: number;
private lastScrollTop_: number;
private listRef: React.MutableRefObject<HTMLDivElement>;
public constructor(props: Props<ItemType>) {
super(props);
this.scrollTop_ = 0;
this.lastScrollTop_ = 0;
this.listRef = React.createRef();
@ -46,10 +52,10 @@ class ItemList<ItemType> extends React.Component<Props<ItemType>, State> {
public updateStateItemIndexes(props: Props<ItemType> = undefined) {
if (typeof props === 'undefined') props = this.props;
const topItemIndex = Math.floor(this.scrollTop_ / props.itemHeight);
const topItemIndex = Math.floor(this.offsetScroll() / props.itemHeight);
const visibleItemCount = this.visibleItemCount(props);
let bottomItemIndex = topItemIndex + (visibleItemCount - 1);
let bottomItemIndex = topItemIndex + visibleItemCount;
if (bottomItemIndex >= props.items.length) bottomItemIndex = props.items.length - 1;
this.setState({
@ -63,7 +69,7 @@ class ItemList<ItemType> extends React.Component<Props<ItemType>, State> {
}
public offsetScroll() {
return this.scrollTop_;
return this.container?.scrollTop ?? this.lastScrollTop_;
}
public get container() {
@ -79,7 +85,7 @@ class ItemList<ItemType> extends React.Component<Props<ItemType>, State> {
}
public onScroll: UIEventHandler<HTMLDivElement> = event => {
this.scrollTop_ = (event.target as HTMLElement).scrollTop;
this.lastScrollTop_ = (event.target as HTMLElement).scrollTop;
this.updateStateItemIndexes();
};
@ -104,23 +110,28 @@ class ItemList<ItemType> extends React.Component<Props<ItemType>, State> {
}
public makeItemIndexVisible(itemIndex: number) {
if (this.isIndexVisible(itemIndex)) return;
// The first and last visible indices are often partially out of view and can thus be made more visible
if (this.isIndexVisible(itemIndex) && itemIndex !== this.lastVisibleIndex && itemIndex !== this.firstVisibleIndex) {
return;
}
const top = this.firstVisibleIndex;
let scrollTop = 0;
if (itemIndex < top) {
const currentScroll = this.offsetScroll();
let scrollTop = currentScroll;
if (itemIndex <= this.firstVisibleIndex) {
scrollTop = this.props.itemHeight * itemIndex;
} else {
scrollTop = this.props.itemHeight * itemIndex - (this.visibleItemCount() - 1) * this.props.itemHeight;
} else if (itemIndex >= this.lastVisibleIndex - 1) {
const scrollBottom = this.props.itemHeight * (itemIndex + 1);
scrollTop = scrollBottom - this.props.style.height;
}
if (scrollTop < 0) scrollTop = 0;
this.scrollTop_ = scrollTop;
this.listRef.current.scrollTop = scrollTop;
if (currentScroll !== scrollTop) {
this.lastScrollTop_ = scrollTop;
this.listRef.current.scrollTop = scrollTop;
this.updateStateItemIndexes();
this.updateStateItemIndexes();
}
}
// shouldComponentUpdate(nextProps, nextState) {
@ -155,18 +166,42 @@ class ItemList<ItemType> extends React.Component<Props<ItemType>, State> {
return <div key={key} style={{ height: height }}></div>;
};
const itemComps = [blankItem('top', this.state.topItemIndex * this.props.itemHeight)];
type RenderRange = { from: number; to: number };
const renderableBlocks: RenderRange[] = [];
for (let i = this.state.topItemIndex; i <= this.state.bottomItemIndex; i++) {
const itemComp = this.props.itemRenderer(items[i], i);
itemComps.push(itemComp);
if (this.props.alwaysRenderSelection && isFinite(this.props.selectedIndex)) {
const selectionVisible = this.props.selectedIndex >= this.state.topItemIndex && this.props.selectedIndex <= this.state.bottomItemIndex;
const isValidSelection = this.props.selectedIndex >= 0 && this.props.selectedIndex < items.length;
if (!selectionVisible && isValidSelection) {
renderableBlocks.push({ from: this.props.selectedIndex, to: this.props.selectedIndex });
}
}
itemComps.push(blankItem('bottom', (items.length - this.state.bottomItemIndex - 1) * this.props.itemHeight));
renderableBlocks.push({ from: this.state.topItemIndex, to: this.state.bottomItemIndex });
// Ascending order
renderableBlocks.sort(({ from: fromA }, { from: fromB }) => fromA - fromB);
const itemComps: React.ReactNode[] = [];
for (let i = 0; i < renderableBlocks.length; i++) {
const currentBlock = renderableBlocks[i];
if (i === 0) {
itemComps.push(blankItem('top', currentBlock.from * this.props.itemHeight));
}
for (let j = currentBlock.from; j <= currentBlock.to; j++) {
const itemComp = this.props.itemRenderer(items[j], j);
itemComps.push(itemComp);
}
const nextBlockFrom = i + 1 < renderableBlocks.length ? renderableBlocks[i + 1].from : items.length;
itemComps.push(blankItem(`after-${i}`, (nextBlockFrom - currentBlock.to - 1) * this.props.itemHeight));
}
const classes = ['item-list'];
if (this.props.className) classes.push(this.props.className);
const wrapContent = this.props.renderContentWrapper ?? ((children) => <>{children}</>);
return (
<div
ref={this.listRef}
@ -182,7 +217,7 @@ class ItemList<ItemType> extends React.Component<Props<ItemType>, State> {
onKeyDown={this.onKeyDown}
onDrop={this.onDrop}
>
{itemComps}
{wrapContent(itemComps)}
</div>
);
}

View File

@ -9,8 +9,8 @@ import useCommandStatus from './utils/useCommandStatus';
import styles_ from './styles';
import { _ } from '@joplin/lib/locale';
const bridge = require('@electron/remote').require('./bridge').default;
import shim from '@joplin/lib/shim';
import bridge from '../../services/bridge';
const keymapService = KeymapService.instance();
@ -25,7 +25,6 @@ export const KeymapConfigScreen = ({ themeId }: KeymapConfigScreenProps) => {
const [keymapItems, keymapError, overrideKeymapItems, setAccelerator, resetAccelerator] = useKeymap();
const [recorderError, setRecorderError] = useState<Error>(null);
const [editing, enableEditing, disableEditing] = useCommandStatus();
const [hovering, enableHovering, disableHovering] = useCommandStatus();
const handleSave = (event: { commandName: string; accelerator: string }) => {
const { commandName, accelerator } = event;
@ -95,13 +94,14 @@ export const KeymapConfigScreen = ({ themeId }: KeymapConfigScreenProps) => {
};
const renderStatus = (commandName: string) => {
if (editing[commandName]) {
return (recorderError && <i className="fa fa-exclamation-triangle" title={recorderError.message} />);
} else if (hovering[commandName]) {
return (<i className="fa fa-pen" />);
} else {
return null;
if (!editing[commandName]) {
const editLabel = _('Change shortcut for "%s"', getLabel(commandName));
return <i className="fa fa-pen" role='img' aria-label={editLabel} title={editLabel}/>;
} else if (recorderError) {
return <i className="fa fa-exclamation-triangle" role='img' aria-label={recorderError.message} title={recorderError.message} />;
}
return null;
};
const renderError = (error: Error) => {
@ -117,11 +117,16 @@ export const KeymapConfigScreen = ({ themeId }: KeymapConfigScreenProps) => {
};
const renderKeymapRow = ({ command, accelerator }: KeymapItem) => {
const handleClick = () => enableEditing(command);
const handleMouseEnter = () => enableHovering(command);
const handleMouseLeave = () => disableHovering(command);
const handleClick = () => {
if (!editing[command]) {
enableEditing(command);
} else if (recorderError) {
void bridge().showErrorMessageBox(recorderError.message);
}
};
const statusContent = renderStatus(command);
const cellContent =
<div style={styles.tableCell} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
<div className='keymap-shortcut-row-content'>
{editing[command] ?
<ShortcutRecorder
onSave={handleSave}
@ -139,9 +144,15 @@ export const KeymapConfigScreen = ({ themeId }: KeymapConfigScreenProps) => {
}
</div>
}
<div style={styles.tableCellStatus} onClick={handleClick}>
{renderStatus(command)}
</div>
<button
className={`flat-button edit ${editing[command] ? '-editing' : ''}`}
style={styles.tableCellStatus}
aria-live={recorderError ? 'polite' : null}
tabIndex={statusContent ? 0 : -1}
onClick={handleClick}
>
{statusContent}
</button>
</div>;
return (

View File

@ -43,6 +43,12 @@ export const ShortcutRecorder = ({ onSave, onReset, onCancel, onError, initialAc
}, [accelerator]);
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
// Shift-tab and tab are needed for navigating the shortcuts screen with the keyboard. Do not
// .preventDefault.
if (event.code === 'Tab' && !event.metaKey && !event.altKey && !event.ctrlKey) {
return;
}
event.preventDefault();
const newAccelerator = keymapService.domToElectronAccelerator(event);
@ -60,14 +66,25 @@ export const ShortcutRecorder = ({ onSave, onReset, onCancel, onError, initialAc
}
};
const hintText = _('Press the shortcut and then press ENTER. Or, press BACKSPACE to clear the shortcut.');
const placeholderText = _('Press the shortcut');
return (
<div style={styles.recorderContainer}>
<div className='shortcut-recorder' style={styles.recorderContainer}>
<input
className='shortcut text-input'
value={accelerator}
placeholder={_('Press the shortcut')}
aria-label={accelerator ? accelerator : placeholderText}
placeholder={placeholderText}
title={hintText}
aria-description={hintText}
aria-invalid={accelerator && !saveAllowed}
// With readOnly, aria-live polite seems necessary for screen readers to read
// the shortcut as it updates.
aria-live='polite'
onKeyDown={handleKeyDown}
style={styles.recorderInput}
title={_('Press the shortcut and then press ENTER. Or, press BACKSPACE to clear the shortcut.')}
readOnly
autoFocus
/>

View File

@ -0,0 +1,2 @@
@use "./styles/keymap-shortcut-row-content.scss";
@use "./styles/shortcut-recorder.scss";

View File

@ -14,21 +14,12 @@ export default function styles(themeId: number) {
display: 'flex',
flexDirection: 'row',
},
recorderContainer: {
padding: 2,
flexGrow: 1,
},
filterInput: {
...theme.inputStyle,
flexGrow: 1,
minHeight: 29,
alignSelf: 'center',
},
recorderInput: {
...theme.inputStyle,
minHeight: 29,
width: '200px',
},
label: {
...theme.textStyle,
alignSelf: 'center',
@ -48,10 +39,6 @@ export default function styles(themeId: number) {
...theme.textStyle,
width: 'auto',
},
tableCell: {
display: 'flex',
flexDirection: 'row',
},
tableCellContent: {
flexGrow: 1,
alignSelf: 'center',
@ -59,6 +46,8 @@ export default function styles(themeId: number) {
tableCellStatus: {
height: '100%',
alignSelf: 'center',
border: 'none',
background: 'transparent',
},
kbd: {
fontFamily: 'sans-serif',

View File

@ -0,0 +1,17 @@
.keymap-shortcut-row-content {
display: flex;
flex-direction: row;
> .edit {
opacity: 0;
&:focus-visible, &.-editing {
opacity: 1;
}
}
&:hover > .edit {
opacity: 1;
}
}

View File

@ -0,0 +1,13 @@
.shortcut-recorder {
padding: 2px;
flex-grow: 1;
> .shortcut {
min-height: 29px;
width: 200px;
}
> .shortcut:focus-visible {
border-color: var(--joplin-focus-outline-color);
}
}

View File

@ -10,7 +10,7 @@ import { reg } from '@joplin/lib/registry';
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import KvStore from '@joplin/lib/services/KvStore';
import ShareService from '@joplin/lib/services/share/ShareService';
import { PasswordInput } from '../PasswordInput/PasswordInput';
import LabelledPasswordInput from '../PasswordInput/LabelledPasswordInput';
interface Props {
themeId: number;
@ -136,11 +136,6 @@ export default function(props: Props) {
setCurrentPasswordIsValid(isValid);
}, [currentPassword]);
function renderCurrentPasswordIcon() {
if (!currentPassword || status === MasterPasswordStatus.NotSet) return null;
return currentPasswordIsValid ? <i className="fas fa-check password-valid-icon"></i> : <i className="fas fa-times"></i>;
}
function renderPasswordForm() {
const renderCurrentPassword = () => {
if (!showCurrentPassword) return null;
@ -151,14 +146,14 @@ export default function(props: Props) {
// having to reset the password (and lose access to any data that's
// been encrypted with it).
const showValidIcon = currentPassword && status !== MasterPasswordStatus.NotSet;
return (
<div className="form-input-group">
<label>{'Current password'}</label>
<div className="current-password-wrapper">
<PasswordInput value={currentPassword} onChange={onCurrentPasswordChange}/>
{renderCurrentPasswordIcon()}
</div>
</div>
<LabelledPasswordInput
labelText={_('Current password')}
value={currentPassword}
onChange={onCurrentPasswordChange}
valid={showValidIcon ? currentPasswordIsValid : undefined}
/>
);
};
@ -175,15 +170,17 @@ export default function(props: Props) {
<div>
<div className="form">
{renderCurrentPassword()}
<div className="form-input-group">
<label>{enterPasswordLabel}</label>
<PasswordInput value={password1} onChange={onPasswordChange1}/>
</div>
<LabelledPasswordInput
labelText={enterPasswordLabel}
value={password1}
onChange={onPasswordChange1}
/>
{needToRepeatPassword && (
<div className="form-input-group">
<label>{'Re-enter password'}</label>
<PasswordInput value={password2} onChange={onPasswordChange2}/>
</div>
<LabelledPasswordInput
labelText={_('Re-enter password')}
value={password2}
onChange={onPasswordChange2}
/>
)}
</div>
<p className="bold">Please make sure you remember your password. For security reasons, it is not possible to recover it if it is lost.</p>

View File

@ -4,7 +4,7 @@ import { _ } from '@joplin/lib/locale';
import DialogButtonRow from './DialogButtonRow';
const { themeStyle } = require('@joplin/lib/theme');
const Countable = require('@joplin/lib/countable/Countable');
import markupLanguageUtils from '../utils/markupLanguageUtils';
import markupLanguageUtils from '@joplin/lib/utils/markupLanguageUtils';
import Dialog from './Dialog';
interface NoteContentPropertiesDialogProps {

View File

@ -378,9 +378,12 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
katexEnabled: Setting.value('markdown.plugin.katex'),
themeData: {
...styles.globalTheme,
marginLeft: 0,
marginRight: 0,
monospaceFont: Setting.value('style.editor.monospaceFontFamily'),
},
automatchBraces: Setting.value('editor.autoMatchingBraces'),
autocompleteMarkup: Setting.value('editor.autocompleteMarkup'),
useExternalSearch: false,
ignoreModifiers: true,
spellcheckEnabled: Setting.value('editor.spellcheckBeta'),

View File

@ -14,6 +14,7 @@ import useKeymap from './utils/useKeymap';
import useEditorSearch from '../utils/useEditorSearchExtension';
import CommandService from '@joplin/lib/services/CommandService';
import { SearchMarkers } from '../../../utils/useSearchMarkers';
import localisation from './utils/localisation';
interface Props extends EditorProps {
style: React.CSSProperties;
@ -98,6 +99,7 @@ const Editor = (props: Props, ref: ForwardedRef<CodeMirrorControl>) => {
const editorProps: EditorProps = {
...props,
localisations: localisation(),
onEvent: event => onEventRef.current(event),
onLogMessage: message => onLogMessageRef.current(message),
};

View File

@ -1,10 +1,10 @@
import { RefObject, useMemo } from 'react';
import { CommandValue } from '../../../utils/types';
import { CommandValue, DropCommandValue } from '../../../utils/types';
import { commandAttachFileToBody } from '../../../utils/resourceHandling';
import { _ } from '@joplin/lib/locale';
import dialogs from '../../../../dialogs';
import { EditorCommandType } from '@joplin/editor/types';
import { EditorCommandType, UserEventSource } from '@joplin/editor/types';
import Logger from '@joplin/utils/Logger';
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
import { MarkupLanguage } from '@joplin/renderer';
@ -38,13 +38,22 @@ const useEditorCommands = (props: Props) => {
};
return {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
dropItems: async (cmd: any) => {
dropItems: async (cmd: DropCommandValue) => {
let pos = cmd.pos && editorRef.current.editor.posAtCoords({ x: cmd.pos.clientX, y: cmd.pos.clientY });
if (cmd.type === 'notes') {
editorRef.current.insertText(cmd.markdownTags.join('\n'));
const text = cmd.markdownTags.join('\n');
if ((pos ?? null) !== null) {
editorRef.current.select(pos, pos);
}
editorRef.current.insertText(text, UserEventSource.Drop);
} else if (cmd.type === 'files') {
const pos = props.selectionRange.from;
const newBody = await commandAttachFileToBody(props.editorContent, cmd.paths, { createFileURL: !!cmd.createFileURL, position: pos, markupLanguage: props.contentMarkupLanguage });
pos ??= props.selectionRange.from;
const newBody = await commandAttachFileToBody(props.editorContent, cmd.paths, {
createFileURL: !!cmd.createFileURL,
position: pos,
markupLanguage: props.contentMarkupLanguage,
});
editorRef.current.updateBody(newBody);
} else {
logger.warn('CodeMirror: unsupported drop item: ', cmd);

View File

@ -0,0 +1,28 @@
import { _ } from '@joplin/lib/locale';
// See https://codemirror.net/examples/translate/
export default () => ({
// @codemirror/view
'Control character': _('Control character'),
// @codemirror/commands
'Selection deleted': _('Selection deleted'),
// @codemirror/search
'Go to line': _('Go to line'),
'go': _('go'),
'Find': _('Find'),
'Replace': _('Replace'),
'next': _('next'),
'previous': _('previous'),
'all': _('all'),
'match case': _('match case'),
'by word': _('by word'),
'replace': _('replace'),
'replace all': _('replace all'),
'close': _('close'),
'current match': _('current match'),
'replaced $ matches': _('replaced $ matches'),
'replaced match on line $': _('replaced match on line $'),
'on line': _('on line'),
});

View File

@ -1,7 +1,8 @@
import * as React from 'react';
import { useState, useEffect, useCallback, useRef, forwardRef, useImperativeHandle } from 'react';
import { ScrollOptions, ScrollOptionTypes, EditorCommand, NoteBodyEditorProps, ResourceInfos, HtmlToMarkdownHandler } from '../../utils/types';
import { resourcesStatus, commandAttachFileToBody, getResourcesFromPasteEvent, processPastedHtml, attachedResources } from '../../utils/resourceHandling';
import { resourcesStatus, commandAttachFileToBody, getResourcesFromPasteEvent, processPastedHtml } from '../../utils/resourceHandling';
import attachedResources from '@joplin/lib/utils/attachedResources';
import useScroll from './utils/useScroll';
import styles_ from './styles';
import CommandService from '@joplin/lib/services/CommandService';

View File

@ -24,7 +24,7 @@ import ToolbarButtonUtils from '@joplin/lib/services/commands/ToolbarButtonUtils
import { _, _n } from '@joplin/lib/locale';
import TagList from '../TagList';
import NoteTitleBar from './NoteTitle/NoteTitleBar';
import markupLanguageUtils from '../../utils/markupLanguageUtils';
import markupLanguageUtils from '@joplin/lib/utils/markupLanguageUtils';
import Setting from '@joplin/lib/models/Setting';
import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseContext';
import ExternalEditWatcher from '@joplin/lib/services/ExternalEditWatcher';
@ -218,15 +218,6 @@ function NoteEditor(props: NoteEditorProps) {
}
}, [handleProvisionalFlag, formNote, setFormNote, isNewNote, titleHasBeenManuallyChanged, scheduleNoteListResort, scheduleSaveNote]);
useWindowCommandHandler({
dispatch: props.dispatch,
setShowLocalSearch,
noteSearchBarRef,
editorRef,
titleInputRef,
setFormNote,
});
const onDrop = useDropHandler({ editorRef });
const onBodyChange = useCallback((event: OnChangeEvent) => onFieldChange('body', event.content, event.changeId), [onFieldChange]);
@ -234,6 +225,15 @@ function NoteEditor(props: NoteEditorProps) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const onTitleChange = useCallback((event: any) => onFieldChange('title', event.target.value), [onFieldChange]);
useWindowCommandHandler({
dispatch: props.dispatch,
setShowLocalSearch,
noteSearchBarRef,
editorRef,
titleInputRef,
onBodyChange,
});
// const onTitleKeydown = useCallback((event:any) => {
// const keyCode = event.keyCode;

View File

@ -5,30 +5,6 @@ import { ChangeEvent, useCallback, useRef } from 'react';
import NoteToolbar from '../../NoteToolbar/NoteToolbar';
import { buildStyle } from '@joplin/lib/theme';
import time from '@joplin/lib/time';
import styled from 'styled-components';
const StyledRoot = styled.div`
display: flex;
flex-direction: row;
align-items: center;
padding-left: ${props => props.theme.editorPaddingLeft}px;
@media (max-width: 800px) {
flex-direction: column;
align-items: flex-start;
}
`;
const InfoGroup = styled.div`
display: flex;
flex-direction: row;
align-items: center;
@media (max-width: 800px) {
border-top: 1px solid ${props => props.theme.dividerColor};
width: 100%;
}
`;
interface Props {
themeId: number;
@ -130,7 +106,7 @@ export default function NoteTitleBar(props: Props) {
}
return (
<StyledRoot>
<div className='note-title-wrapper'>
<input
className="title-input"
type="text"
@ -144,10 +120,10 @@ export default function NoteTitleBar(props: Props) {
onBlur={onTitleBlur}
value={props.noteTitle}
/>
<InfoGroup>
<div className='note-title-info-group'>
{renderTitleBarDate()}
{renderNoteToolbar()}
</InfoGroup>
</StyledRoot>
</div>
</div>
);
}

View File

@ -38,7 +38,6 @@ const incompatiblePluginIds = [
'ylc395.noteLinkSystem',
'outline',
'joplin.plugin.cmoptions',
'plugin.calebjohn.MathMode',
'com.ckant.joplin-plugin-better-code-blocks',
// cSpell:enable
];

View File

@ -1,3 +1,5 @@
@use "./styles/warning-banner.scss";
@use "./styles/warning-banner-link.scss";
@use "./styles/note-title-info-group.scss";
@use "./styles/note-title-wrapper.scss";

View File

@ -0,0 +1,11 @@
.note-title-info-group {
display: flex;
flex-direction: row;
align-items: center;
@media (max-width: 800px) {
border-top: 1px solid var(--joplin-divider-color);
width: 100%;
}
}

View File

@ -0,0 +1,12 @@
.note-title-wrapper {
display: flex;
flex-direction: row;
align-items: center;
padding-left: var(--joplin-editor-padding-left);
@media (max-width: 800px) {
flex-direction: column;
align-items: flex-start;
}
}

View File

@ -1,7 +1,6 @@
import shim from '@joplin/lib/shim';
import Setting from '@joplin/lib/models/Setting';
import Note from '@joplin/lib/models/Note';
import BaseModel from '@joplin/lib/BaseModel';
import Resource from '@joplin/lib/models/Resource';
const bridge = require('@electron/remote').require('./bridge').default;
import ResourceFetcher from '@joplin/lib/services/ResourceFetcher';
@ -28,43 +27,6 @@ export async function handleResourceDownloadMode(noteBody: string) {
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
let resourceCache_: any = {};
export function clearResourceCache() {
resourceCache_ = {};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
export async function attachedResources(noteBody: string): Promise<any> {
if (!noteBody) return {};
const resourceIds = await Note.linkedItemIdsByType(BaseModel.TYPE_RESOURCE, noteBody);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const output: any = {};
for (let i = 0; i < resourceIds.length; i++) {
const id = resourceIds[i];
if (resourceCache_[id]) {
output[id] = resourceCache_[id];
} else {
const resource = await Resource.load(id);
const localState = await Resource.localState(resource);
const o = {
item: resource,
localState: localState,
};
// eslint-disable-next-line require-atomic-updates
resourceCache_[id] = o;
output[id] = o;
}
}
return output;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
export async function commandAttachFileToBody(body: string, filePaths: string[] = null, options: any = null) {
options = {

View File

@ -252,3 +252,19 @@ export interface CommandValue {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
value?: any; // For TinyMCE only
}
type DropCommandBase = {
pos: {
clientX: number;
clientY: number;
}|undefined;
};
export type DropCommandValue = ({
type: 'notes';
markdownTags: string[];
}|{
type: 'files';
paths: string[];
createFileURL: boolean;
}) & DropCommandBase;

View File

@ -1,6 +1,7 @@
import { useCallback } from 'react';
import Note from '@joplin/lib/models/Note';
import { DragEvent as ReactDragEvent } from 'react';
import { DropCommandValue } from './types';
interface HookDependencies {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
@ -19,6 +20,11 @@ export default function useDropHandler(dependencies: HookDependencies): DropHand
const dt = event.dataTransfer;
const createFileURL = event.altKey;
const eventPosition = {
clientX: event.clientX,
clientY: event.clientY,
};
if (dt.types.indexOf('text/x-jop-note-ids') >= 0) {
const noteIds = JSON.parse(dt.getData('text/x-jop-note-ids'));
@ -29,12 +35,15 @@ export default function useDropHandler(dependencies: HookDependencies): DropHand
noteMarkdownTags.push(Note.markdownTag(note));
}
const props: DropCommandValue = {
type: 'notes',
pos: eventPosition,
markdownTags: noteMarkdownTags,
};
editorRef.current.execCommand({
name: 'dropItems',
value: {
type: 'notes',
markdownTags: noteMarkdownTags,
},
value: props,
});
};
void dropNotes();
@ -51,13 +60,16 @@ export default function useDropHandler(dependencies: HookDependencies): DropHand
paths.push(file.path);
}
const props: DropCommandValue = {
type: 'files',
pos: eventPosition,
paths: paths,
createFileURL: createFileURL,
};
editorRef.current.execCommand({
name: 'dropItems',
value: {
type: 'files',
paths: paths,
createFileURL: createFileURL,
},
value: props,
});
return true;
}

View File

@ -1,12 +1,11 @@
import { useState, useEffect, useCallback, RefObject, useRef } from 'react';
import { FormNote, defaultFormNote, ResourceInfos } from './types';
import { clearResourceCache, attachedResources } from './resourceHandling';
import AsyncActionQueue from '@joplin/lib/AsyncActionQueue';
import { handleResourceDownloadMode } from './resourceHandling';
import { splitHtml } from '@joplin/renderer/HtmlToHtml';
import Setting from '@joplin/lib/models/Setting';
import usePrevious from '../../hooks/usePrevious';
import attachedResources, { clearResourceCache } from '@joplin/lib/utils/attachedResources';
import { MarkupToHtml } from '@joplin/renderer';
import Note from '@joplin/lib/models/Note';
import ResourceFetcher from '@joplin/lib/services/ResourceFetcher';

View File

@ -1,6 +1,6 @@
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
import { useCallback, useMemo } from 'react';
import markupLanguageUtils from '../../../utils/markupLanguageUtils';
import markupLanguageUtils from '@joplin/lib/utils/markupLanguageUtils';
import Setting from '@joplin/lib/models/Setting';
import shim from '@joplin/lib/shim';

View File

@ -1,5 +1,5 @@
import { RefObject, useEffect } from 'react';
import { FormNote, NoteBodyEditorRef, ScrollOptionTypes } from './types';
import { NoteBodyEditorRef, OnChangeEvent, ScrollOptionTypes } from './types';
import editorCommandDeclarations, { enabledCondition } from '../editorCommandDeclarations';
import CommandService, { CommandDeclaration, CommandRuntime, CommandContext } from '@joplin/lib/services/CommandService';
import time from '@joplin/lib/time';
@ -12,7 +12,7 @@ const commandsWithDependencies = [
require('../commands/pasteAsText'),
];
type SetFormNoteCallback = (callback: (prev: FormNote)=> FormNote)=> void;
type OnBodyChange = (event: OnChangeEvent)=> void;
interface HookDependencies {
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
@ -23,13 +23,13 @@ interface HookDependencies {
noteSearchBarRef: any;
editorRef: RefObject<NoteBodyEditorRef>;
titleInputRef: RefObject<HTMLInputElement>;
setFormNote: SetFormNoteCallback;
onBodyChange: OnBodyChange;
}
function editorCommandRuntime(
declaration: CommandDeclaration,
editorRef: RefObject<NoteBodyEditorRef>,
setFormNote: SetFormNoteCallback,
onBodyChange: OnBodyChange,
): CommandRuntime {
return {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
@ -55,9 +55,7 @@ function editorCommandRuntime(
value: args[0],
});
} else if (declaration.name === 'editor.setText') {
setFormNote((prev: FormNote) => {
return { ...prev, body: args[0] };
});
onBodyChange({ content: args[0], changeId: 0 });
} else {
return editorRef.current.execCommand({
name: declaration.name,
@ -78,11 +76,11 @@ function editorCommandRuntime(
}
export default function useWindowCommandHandler(dependencies: HookDependencies) {
const { setShowLocalSearch, noteSearchBarRef, editorRef, titleInputRef, setFormNote } = dependencies;
const { setShowLocalSearch, noteSearchBarRef, editorRef, titleInputRef, onBodyChange } = dependencies;
useEffect(() => {
for (const declaration of editorCommandDeclarations) {
CommandService.instance().registerRuntime(declaration.name, editorCommandRuntime(declaration, editorRef, setFormNote));
CommandService.instance().registerRuntime(declaration.name, editorCommandRuntime(declaration, editorRef, onBodyChange));
}
const dependencies = {
@ -105,5 +103,5 @@ export default function useWindowCommandHandler(dependencies: HookDependencies)
CommandService.instance().unregisterRuntime(command.declaration.name);
}
};
}, [editorRef, setShowLocalSearch, noteSearchBarRef, titleInputRef, setFormNote]);
}, [editorRef, setShowLocalSearch, noteSearchBarRef, titleInputRef, onBodyChange]);
}

View File

@ -1,5 +1,5 @@
import * as React from 'react';
import { useMemo, useRef, useEffect } from 'react';
import { useMemo, useRef, useEffect, useCallback } from 'react';
import { AppState } from '../../app.reducer';
import BaseModel, { ModelType } from '@joplin/lib/BaseModel';
import { Props } from './utils/types';
@ -275,6 +275,12 @@ const NoteList = (props: Props) => {
return output;
}, [listRenderer.flow]);
const onContainerContextMenu = useCallback((event: React.MouseEvent) => {
const isFromKeyboard = event.button === -1;
if (event.isDefaultPrevented() || !isFromKeyboard) return;
onItemContextMenu({ itemId: activeNoteId });
}, [onItemContextMenu, activeNoteId]);
return (
<div
role='listbox'
@ -293,6 +299,7 @@ const NoteList = (props: Props) => {
onKeyDown={onKeyDown}
onKeyUp={onKeyUp}
onDrop={onDrop}
onContextMenu={onContainerContextMenu}
>
{renderEmptyList()}
{renderFiller('top', topFillerStyle)}

View File

@ -1,3 +1,4 @@
import * as React from 'react';
import Folder from '@joplin/lib/models/Folder';
import { NoteEntity } from '@joplin/lib/services/database/types';
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
@ -6,6 +7,13 @@ import { Dispatch } from 'redux';
import bridge from '../../../services/bridge';
import NoteListUtils from '../../utils/NoteListUtils';
interface CustomContextMenuEvent {
itemId: string;
currentTarget?: undefined;
preventDefault?: undefined;
}
type ContextMenuEvent = React.MouseEvent|CustomContextMenuEvent;
const useOnContextMenu = (
selectedNoteIds: string[],
selectedFolderId: string,
@ -15,10 +23,14 @@ const useOnContextMenu = (
plugins: PluginStates,
customCss: string,
) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
return useCallback((event: any) => {
const currentNoteId = event.currentTarget.getAttribute('data-id');
return useCallback((event: ContextMenuEvent) => {
let currentNoteId = event.currentTarget?.getAttribute('data-id');
if ('itemId' in event) {
currentNoteId = event.itemId;
}
if (!currentNoteId) return;
event.preventDefault?.();
let noteIds = [];
if (selectedNoteIds.indexOf(currentNoteId) < 0) {

View File

@ -0,0 +1,55 @@
import { act, renderHook } from '@testing-library/react-hooks';
import useRootElement from './useRootElement';
describe('useRootElement', () => {
beforeEach(() => {
jest.useFakeTimers({ advanceTimers: true });
});
test('should find an element with a matching ID', async () => {
const testElement = document.createElement('div');
testElement.id = 'test-element-id';
document.body.appendChild(testElement);
const { result } = renderHook(useRootElement, {
initialProps: testElement.id,
});
await act(async () => {
await jest.advanceTimersByTimeAsync(100);
});
expect(result.current).toBe(testElement);
testElement.remove();
});
test('should redo the element search when the elementId prop changes', async () => {
const testElement = document.createElement('div');
document.body.appendChild(testElement);
const { rerender, result } = renderHook(useRootElement, {
initialProps: 'some-id-here',
});
await jest.advanceTimersByTimeAsync(100);
expect(result.current).toBe(null);
// Searching for another non-existent ID: Should not match
rerender('updated-id');
await jest.advanceTimersByTimeAsync(100);
expect(result.current).toBe(null);
// Should not match the first element if its ID is set to the original (search
// should be cancelled).
testElement.id = 'some-id-here';
await jest.advanceTimersByTimeAsync(100);
expect(result.current).toBe(null);
// Should match if the element ID changes to the updated ID.
await act(async () => {
testElement.id = 'updated-id';
await jest.advanceTimersByTimeAsync(100);
});
expect(result.current).toBe(testElement);
testElement.remove();
});
});

View File

@ -6,7 +6,7 @@ const useRootElement = (elementId: string) => {
const [rootElement, setRootElement] = useState<HTMLDivElement>(null);
useAsyncEffect(async (event) => {
const element = await waitForElement(document, elementId);
const element = await waitForElement(document, elementId, event);
if (event.cancelled) return;
setRootElement(element);
}, [document, elementId]);

View File

@ -10,7 +10,7 @@ import RevisionService from '@joplin/lib/services/RevisionService';
import { MarkupToHtml } from '@joplin/renderer';
import time from '@joplin/lib/time';
import bridge from '../services/bridge';
import markupLanguageUtils from '../utils/markupLanguageUtils';
import markupLanguageUtils from '@joplin/lib/utils/markupLanguageUtils';
import { NoteEntity, RevisionEntity } from '@joplin/lib/services/database/types';
import { AppState } from '../app.reducer';
const urlUtils = require('@joplin/lib/urlUtils');

View File

@ -29,6 +29,7 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
private webviewRef_: React.RefObject<HTMLIFrameElement>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
private webviewListeners_: any = null;
private removePluginAssetsCallback_: RemovePluginAssetsCallback|null = null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
@ -110,7 +111,7 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
window.addEventListener('message', this.webview_message);
}
public destroyWebview() {
private destroyWebview() {
const wv = this.webviewRef_.current;
if (!wv || !this.initialized_) return;
@ -194,14 +195,13 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
public setHtml(html: string, options: SetHtmlOptions) {
const protocolHandler = bridge().electronApp().getCustomProtocolHandler();
// Grant & remove asset access.
if (options.pluginAssets) {
this.removePluginAssetsCallback_?.();
const protocolHandler = bridge().electronApp().getCustomProtocolHandler();
const pluginAssetPaths: string[] = options.pluginAssets.map((asset) => asset.path);
const assetAccesses = pluginAssetPaths.map(
path => protocolHandler.allowReadAccessToFile(path),
@ -216,7 +216,10 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
};
}
this.send('setHtml', html, options);
this.send('setHtml', html, {
...options,
mediaAccessKey: protocolHandler.getMediaAccessKey(),
});
}
// ----------------------------------------------------------------
@ -232,7 +235,7 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
className="noteTextViewer"
ref={this.webviewRef_}
style={viewerStyle}
allow='fullscreen=* autoplay=* local-fonts=* encrypted-media=*'
allow='clipboard-write=(self) fullscreen=(self) autoplay=(self) local-fonts=(self) encrypted-media=(self)'
allowFullScreen={true}
src={`joplin-content://note-viewer/${__dirname}/note-viewer/index.html`}
></iframe>

View File

@ -0,0 +1,56 @@
import * as React from 'react';
import PasswordInput from './PasswordInput';
import { useId } from 'react';
import { ChangeEventHandler } from './types';
import { _ } from '@joplin/lib/locale';
interface Props {
labelText: string;
value: string;
onChange: ChangeEventHandler;
valid?: boolean;
}
const LabelledPasswordInput: React.FC<Props> = props => {
const inputId = useId();
const statusIconId = useId();
const canRenderStatusIcon = (props.valid ?? null) !== null && props.value;
const renderStatusIcon = () => {
if (!canRenderStatusIcon) return null;
let title, classNames;
if (props.valid) {
title = _('Valid');
classNames = 'fas fa-check -valid';
} else {
title = _('Invalid');
classNames = 'fas fa-times -invalid';
}
return <i
className={`password-status-icon ${classNames}`}
id={statusIconId}
role='img'
aria-label={title}
title={title}
aria-live='polite'
></i>;
};
return <div className='labelled-password-input form-input-group'>
<label htmlFor={inputId}>{props.labelText}</label>
<div className='password'>
<PasswordInput
inputId={inputId}
aria-invalid={canRenderStatusIcon ? !props.valid : undefined}
aria-errormessage={canRenderStatusIcon ? statusIconId : undefined}
value={props.value}
onChange={props.onChange}
/>
{renderStatusIcon()}
</div>
</div>;
};
export default LabelledPasswordInput;

View File

@ -1,22 +1,24 @@
import * as React from 'react';
import { useState, useCallback } from 'react';
import StyledInput from '../style/StyledInput';
export interface ChangeEvent {
value: string;
}
type ChangeEventHandler = (event: ChangeEvent)=> void;
import { _ } from '@joplin/lib/locale';
import { ChangeEventHandler } from './types';
interface Props {
value: string;
inputId: string;
onChange: ChangeEventHandler;
'aria-invalid'?: boolean;
'aria-errormessage'?: string;
}
export const PasswordInput = (props: Props) => {
const PasswordInput = (props: Props) => {
const [showPassword, setShowPassword] = useState(false);
const inputType = showPassword ? 'text' : 'password';
const icon = showPassword ? 'far fa-eye-slash' : 'far fa-eye';
const title = showPassword ? _('Hide password') : _('Show password');
const onShowPassword = useCallback(() => {
setShowPassword(current => !current);
@ -24,8 +26,20 @@ export const PasswordInput = (props: Props) => {
return (
<div className="password-input">
<StyledInput className="field" type={inputType} value={props.value} onChange={props.onChange}/>
<button onClick={onShowPassword} className="showpasswordbutton"><i className={icon}></i></button>
<StyledInput
id={props.inputId}
aria-errormessage={props['aria-errormessage']}
aria-invalid={props['aria-invalid']}
className="field"
type={inputType}
value={props.value}
onChange={props.onChange}
/>
<button onClick={onShowPassword} className="showpasswordbutton">
<i className={icon} role='img' aria-label={title} title={title}></i>
</button>
</div>
);
};
export default PasswordInput;

View File

@ -1,19 +1,3 @@
.password-input {
display: flex;
position: relative;
flex: 1;
> .field {
display: flex;
flex: 1;
width: 100%;
}
> .showpasswordbutton {
position: absolute;
right: 5px;
top: 4px;
border: none;
background: none;
}
}
@use "styles/password-input.scss";
@use "styles/labelled-password-input.scss";
@use "styles/password-status-icon.scss";

View File

@ -0,0 +1,10 @@
.labelled-password-input {
display: flex;
flex-direction: column;
> .password {
display: flex;
flex-direction: row;
align-items: center;
}
}

View File

@ -0,0 +1,19 @@
.password-input {
display: flex;
position: relative;
flex: 1;
> .field {
display: flex;
flex: 1;
width: 100%;
}
> .showpasswordbutton {
position: absolute;
right: 5px;
top: 4px;
border: none;
background: none;
}
}

View File

@ -0,0 +1,13 @@
.password-status-icon {
margin-left: 10px;
&.-valid {
color: var(--joplin-color-correct);
}
&.-invalid {
color: var(--joplin-color-error);
margin-left: 5px;
}
}

View File

@ -0,0 +1,6 @@
export interface ChangeEvent {
value: string;
}
export type ChangeEventHandler = (event: ChangeEvent)=> void;

View File

@ -60,15 +60,6 @@ interface ActiveSorting {
const ResourceTableComp = (props: ResourceTable) => {
const theme = themeStyle(props.themeId);
const sortOrderEngagedMarker = (s: SortingOrder) => {
return (
<a href="#"
style={{ color: theme.urlColor }}
onClick={() => props.onToggleSorting(s)}>{
(props.sorting.order === s && props.sorting.type === 'desc') ? '▾' : '▴'}</a>
);
};
const titleCellStyle = {
...theme.textStyle,
textOverflow: 'ellipsis',
@ -96,12 +87,28 @@ const ResourceTableComp = (props: ResourceTable) => {
(resource: InnerResource) => !props.filter || resource.title?.includes(props.filter) || resource.id.includes(props.filter),
);
const renderSortableHeader = (title: string, order: SortingOrder) => {
const sortedDescending = props.sorting.order === order && props.sorting.type === 'desc';
const sortButtonLabel = sortedDescending ? _('Sort "%s" in ascending order', title) : _('Sort "%s" in descending order', title);
const reverseSortButton = (
<a
href="#"
style={{ color: theme.urlColor }}
onClick={() => props.onToggleSorting(order)}
aria-label={sortButtonLabel}
title={sortButtonLabel}
role='button'
>{sortedDescending ? '▾' : '▴'}</a>
);
return <th key={`header-${title}`} style={headerStyle}>{title} {reverseSortButton}</th>;
};
return (
<table style={{ width: '100%' }}>
<thead>
<tr>
<th style={headerStyle}>{_('Title')} {sortOrderEngagedMarker('name')}</th>
<th style={headerStyle}>{_('Size')} {sortOrderEngagedMarker('size')}</th>
{renderSortableHeader(_('Title'), 'name')}
{renderSortableHeader(_('Size'), 'size')}
<th style={headerStyle}>{_('ID')}</th>
<th style={headerStyle}>{_('Action')}</th>
</tr>

View File

@ -321,7 +321,13 @@ function ShareFolderDialog(props: Props) {
<StyledRecipientName>{shareUser.user.email}</StyledRecipientName>
{dropdown}
<StyledRecipientStatusIcon title={statusToMessage[shareUser.status]} className={statusToIcon[shareUser.status]}></StyledRecipientStatusIcon>
<Button disabled={!enabled} size={ButtonSize.Small} iconName="far fa-times-circle" onClick={() => recipient_delete({ shareUserId: shareUser.id })}/>
<Button
disabled={!enabled}
size={ButtonSize.Small}
iconName="far fa-times-circle"
onClick={() => recipient_delete({ shareUserId: shareUser.id })}
tooltip={_('Remove %s from share', shareUser.user.email)}
/>
</StyledRecipient>
);
}

View File

@ -14,6 +14,7 @@ import useFocusHandler from './hooks/useFocusHandler';
import useOnRenderItem from './hooks/useOnRenderItem';
import { ListItem } from './types';
import useSidebarCommandHandler from './hooks/useSidebarCommandHandler';
import useOnRenderListWrapper from './hooks/useOnRenderListWrapper';
interface Props {
dispatch: Dispatch;
@ -39,11 +40,12 @@ const FolderAndTagList: React.FC<Props> = props => {
listItems: listItems,
});
const [selectedListElement, setSelectedListElement] = useState<HTMLElement|null>(null);
const listContainerRef = useRef<HTMLDivElement|null>(null);
const onRenderItem = useOnRenderItem({
...props,
selectedIndex,
onSelectedElementShown: setSelectedListElement,
listItems,
containerRef: listContainerRef,
});
const onKeyEventHandler = useOnSidebarKeyDownHandler({
@ -55,14 +57,17 @@ const FolderAndTagList: React.FC<Props> = props => {
});
const itemListRef = useRef<ItemList<ListItem>>();
const { focusSidebar } = useFocusHandler({ itemListRef, selectedListElement, selectedIndex, listItems });
const { focusSidebar } = useFocusHandler({ itemListRef, selectedIndex, listItems });
useSidebarCommandHandler({ focusSidebar });
const [itemListContainer, setItemListContainer] = useState<HTMLDivElement|null>(null);
listContainerRef.current = itemListContainer;
const listHeight = useElementHeight(itemListContainer);
const listStyle = useMemo(() => ({ height: listHeight }), [listHeight]);
const onRenderContentWrapper = useOnRenderListWrapper({ selectedIndex, onKeyDown: onKeyEventHandler });
return (
<div
className='folder-and-tag-list'
@ -72,9 +77,15 @@ const FolderAndTagList: React.FC<Props> = props => {
className='items'
ref={itemListRef}
style={listStyle}
items={listItems}
itemRenderer={onRenderItem}
onKeyDown={onKeyEventHandler}
renderContentWrapper={onRenderContentWrapper}
// The selected item is the only item with tabindex=0. Always render it
// to allow the item list to be focused.
alwaysRenderSelection={true}
selectedIndex={selectedIndex}
itemHeight={30}
/>

View File

@ -1,29 +1,16 @@
import { MutableRefObject, RefObject, useCallback, useEffect, useMemo, useRef } from 'react';
import { RefObject, useCallback, useEffect, useMemo, useRef } from 'react';
import { ListItem } from '../types';
import ItemList from '../../ItemList';
import { focus } from '@joplin/lib/utils/focusHandler';
interface Props {
itemListRef: RefObject<ItemList<ListItem>>;
selectedListElement: HTMLElement|null;
selectedIndex: number;
listItems: ListItem[];
}
const useFocusAfterNextRenderHandler = (
shouldFocusAfterNextRender: MutableRefObject<boolean>,
selectedListElement: HTMLElement|null,
) => {
useEffect(() => {
if (!shouldFocusAfterNextRender.current || !selectedListElement) return;
focus('FolderAndTagList/useFocusHandler/afterRender', selectedListElement);
shouldFocusAfterNextRender.current = false;
}, [selectedListElement, shouldFocusAfterNextRender]);
};
const useRefocusOnSelectionChangeHandler = (
const useScrollToSelectionHandler = (
itemListRef: RefObject<ItemList<ListItem>>,
shouldFocusAfterNextRender: MutableRefObject<boolean>,
listItems: ListItem[],
selectedIndex: number,
) => {
@ -49,32 +36,33 @@ const useRefocusOnSelectionChangeHandler = (
useEffect(() => {
if (!itemListRef.current || !selectedItemKey) return;
const hasFocus = !!itemListRef.current.container.querySelector(':scope :focus');
shouldFocusAfterNextRender.current = hasFocus;
const hasFocus = !!itemListRef.current.container.contains(document.activeElement);
if (hasFocus) {
itemListRef.current.makeItemIndexVisible(selectedIndexRef.current);
}
}, [selectedItemKey, itemListRef, shouldFocusAfterNextRender]);
}, [selectedItemKey, itemListRef]);
};
const useFocusHandler = (props: Props) => {
const { itemListRef, selectedListElement, selectedIndex, listItems } = props;
const { itemListRef, selectedIndex, listItems } = props;
// When set to true, when selectedListElement next changes, select it.
const shouldFocusAfterNextRender = useRef(false);
useRefocusOnSelectionChangeHandler(itemListRef, shouldFocusAfterNextRender, listItems, selectedIndex);
useFocusAfterNextRenderHandler(shouldFocusAfterNextRender, selectedListElement);
useScrollToSelectionHandler(itemListRef, listItems, selectedIndex);
const focusSidebar = useCallback(() => {
if (!selectedListElement || !itemListRef.current.isIndexVisible(selectedIndex)) {
if (!itemListRef.current.isIndexVisible(selectedIndex)) {
itemListRef.current.makeItemIndexVisible(selectedIndex);
shouldFocusAfterNextRender.current = true;
} else {
focus('FolderAndTagList/useFocusHandler/focusSidebar', selectedListElement);
}
}, [selectedListElement, selectedIndex, itemListRef]);
const focusableItem = itemListRef.current.container.querySelector('[role="treeitem"][tabindex="0"]');
const focusableContainer = itemListRef.current.container.querySelector('[role="tree"][tabindex="0"]');
if (focusableItem) {
focus('FolderAndTagList/focusSidebarItem', focusableItem);
} else if (focusableContainer) {
// Handles the case where no items in the tree can be focused.
focus('FolderAndTagList/focusSidebarTree', focusableContainer);
}
}, [selectedIndex, itemListRef]);
return { focusSidebar };
};

View File

@ -29,6 +29,8 @@ import Logger from '@joplin/utils/Logger';
import onFolderDrop from '@joplin/lib/models/utils/onFolderDrop';
import HeaderItem from '../listItemComponents/HeaderItem';
import AllNotesItem from '../listItemComponents/AllNotesItem';
import ListItemWrapper from '../listItemComponents/ListItemWrapper';
import { focus } from '@joplin/lib/utils/focusHandler';
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
@ -41,15 +43,27 @@ interface Props {
plugins: PluginStates;
folders: FolderEntity[];
collapsedFolderIds: string[];
containerRef: React.RefObject<HTMLDivElement>;
selectedIndex: number;
onSelectedElementShown: (element: HTMLElement)=> void;
listItems: ListItem[];
}
type ItemContextMenuListener = MouseEventHandler<HTMLElement>;
const menuUtils = new MenuUtils(CommandService.instance());
const focusListItem = (item: HTMLElement|null) => {
if (item) {
// Avoid scrolling to the selected item when refocusing the note list. Such a refocus
// can happen if the note list rerenders and the selection is scrolled out of view and
// can cause scroll to change unexpectedly.
focus('useOnRenderItem', item, { preventScroll: true });
}
};
const noFocusListItem = () => {};
const useOnRenderItem = (props: Props) => {
const pluginsRef = useRef<PluginStates>(null);
@ -326,26 +340,24 @@ const useOnRenderItem = (props: Props) => {
const selectedIndexRef = useRef(props.selectedIndex);
selectedIndexRef.current = props.selectedIndex;
const itemCount = props.listItems.length;
return useCallback((item: ListItem, index: number) => {
const selected = props.selectedIndex === index;
const anchorRefCallback = selected ? (
(element: HTMLElement) => {
if (selectedIndexRef.current === index) {
props.onSelectedElementShown(element);
}
}
) : null;
const focusInList = document.hasFocus() && props.containerRef.current?.contains(document.activeElement);
const anchorRef = (focusInList && selected) ? focusListItem : noFocusListItem;
if (item.kind === ListItemType.Tag) {
const tag = item.tag;
return <TagItem
key={item.key}
anchorRef={anchorRefCallback}
anchorRef={anchorRef}
selected={selected}
onClick={tagItem_click}
onTagDrop={onTagDrop_}
onContextMenu={onItemContextMenu}
tag={tag}
itemCount={itemCount}
index={index}
/>;
} else if (item.kind === ListItemType.Folder) {
const folder = item.folder;
@ -368,7 +380,7 @@ const useOnRenderItem = (props: Props) => {
}
return <FolderItem
key={item.key}
anchorRef={anchorRefCallback}
anchorRef={anchorRef}
selected={selected}
folderId={folder.id}
folderTitle={Folder.displayTitle(folder)}
@ -386,23 +398,41 @@ const useOnRenderItem = (props: Props) => {
shareId={folder.share_id}
parentId={folder.parent_id}
showFolderIcon={showFolderIcons}
index={index}
itemCount={itemCount}
/>;
} else if (item.kind === ListItemType.Header) {
return <HeaderItem
key={item.id}
anchorRef={anchorRef}
item={item}
anchorRef={anchorRefCallback}
isSelected={selected}
onDrop={item.supportsFolderDrop ? onFolderDrop_ : null}
index={index}
itemCount={itemCount}
/>;
} else if (item.kind === ListItemType.AllNotes) {
return <AllNotesItem
key={item.key}
anchorRef={anchorRef}
selected={selected}
anchorRef={anchorRefCallback}
index={index}
itemCount={itemCount}
/>;
} else if (item.kind === ListItemType.Spacer) {
return (
<a key={item.key} className='sidebar-spacer-item' ref={anchorRefCallback} aria-label={_('Spacer')}></a>
<ListItemWrapper
key={item.key}
containerRef={anchorRef}
depth={0}
selected={selected}
itemIndex={index}
itemCount={itemCount}
highlightOnHover={false}
className='sidebar-spacer-item'
>
<div aria-label={_('Spacer')}></div>
</ListItemWrapper>
);
} else {
const exhaustivenessCheck: never = item;
@ -421,7 +451,8 @@ const useOnRenderItem = (props: Props) => {
showFolderIcons,
tagItem_click,
props.selectedIndex,
props.onSelectedElementShown,
props.containerRef,
itemCount,
]);
};

View File

@ -0,0 +1,46 @@
import * as React from 'react';
import { useCallback } from 'react';
import { _ } from '@joplin/lib/locale';
import CommandService from '@joplin/lib/services/CommandService';
interface Props {
selectedIndex: number;
onKeyDown: React.KeyboardEventHandler;
}
const onAddFolderButtonClick = () => {
void CommandService.instance().execute('newFolder');
};
const NewFolderButton = () => {
// To allow it to be accessed by accessibility tools, the new folder button
// is not included in the portion of the list with role='tree'.
return <button onClick={onAddFolderButtonClick} className='new-folder-button'>
<i
aria-label={_('New notebook')}
role='img'
className='fas fa-plus'
/>
</button>;
};
const useOnRenderListWrapper = ({ selectedIndex, onKeyDown }: Props) => {
return useCallback((listItems: React.ReactNode[]) => {
const listHasValidSelection = selectedIndex >= 0;
const allowContainerFocus = !listHasValidSelection;
return <>
<NewFolderButton/>
<div
role='tree'
className='sidebar-list-items-wrapper'
aria-setsize={listItems.length}
tabIndex={allowContainerFocus ? 0 : undefined}
onKeyDown={onKeyDown}
>
{...listItems}
</div>
</>;
}, [selectedIndex, onKeyDown]);
};
export default useOnRenderListWrapper;

View File

@ -1,7 +1,8 @@
import { Dispatch } from 'redux';
import { FolderListItem, ListItem, ListItemType, SetSelectedIndexCallback } from '../types';
import { ListItem, ListItemType, SetSelectedIndexCallback } from '../types';
import { KeyboardEventHandler, useCallback } from 'react';
import CommandService from '@joplin/lib/services/CommandService';
import toggleHeader from './utils/toggleHeader';
interface Props {
dispatch: Dispatch;
@ -12,15 +13,20 @@ interface Props {
}
const isToggleShortcut = (keyCode: string, selectedItem: FolderListItem, collapsedFolderIds: string[]) => {
const isToggleShortcut = (keyCode: string, selectedItem: ListItem, collapsedFolderIds: string[]) => {
if (selectedItem.kind !== ListItemType.Header && selectedItem.kind !== ListItemType.Folder) {
return false;
}
if (!['Space', 'ArrowLeft', 'ArrowRight'].includes(keyCode)) {
return false;
}
if (keyCode === 'Space') {
return true;
}
const isCollapsed = collapsedFolderIds.includes(selectedItem.folder.id);
const isCollapsed = 'expanded' in selectedItem ? !selectedItem.expanded : collapsedFolderIds.includes(selectedItem.folder.id);
return (keyCode === 'ArrowRight') === isCollapsed;
};
@ -29,21 +35,22 @@ const useOnSidebarKeyDownHandler = (props: Props) => {
return useCallback<KeyboardEventHandler<HTMLElement>>((event) => {
const selectedItem = listItems[selectedIndex];
if (selectedItem?.kind === ListItemType.Folder && isToggleShortcut(event.code, selectedItem, collapsedFolderIds)) {
event.preventDefault();
dispatch({
type: 'FOLDER_TOGGLE',
id: selectedItem.folder.id,
});
}
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyA') { // ctrl+a or cmd+a
event.preventDefault();
}
let indexChange = 0;
if (event.code === 'ArrowUp') {
if (selectedItem && isToggleShortcut(event.code, selectedItem, collapsedFolderIds)) {
event.preventDefault();
if (selectedItem.kind === ListItemType.Folder) {
dispatch({
type: 'FOLDER_TOGGLE',
id: selectedItem.folder.id,
});
} else if (selectedItem.kind === ListItemType.Header) {
toggleHeader(selectedItem.id);
}
} else if ((event.ctrlKey || event.metaKey) && event.code === 'KeyA') { // ctrl+a or cmd+a
event.preventDefault();
} else if (event.code === 'ArrowUp') {
indexChange = -1;
} else if (event.code === 'ArrowDown') {
indexChange = 1;

View File

@ -3,8 +3,7 @@ import { FolderListItem, HeaderId, HeaderListItem, ListItem, ListItemType, TagLi
import { FolderEntity, TagsWithNoteCountEntity } from '@joplin/lib/services/database/types';
import { buildFolderTree, renderFolders, renderTags } from '@joplin/lib/components/shared/side-menu-shared';
import { _ } from '@joplin/lib/locale';
import CommandService from '@joplin/lib/services/CommandService';
import Setting from '@joplin/lib/models/Setting';
import toggleHeader from './utils/toggleHeader';
interface Props {
tags: TagsWithNoteCountEntity[];
@ -14,16 +13,6 @@ interface Props {
tagHeaderIsExpanded: boolean;
}
const onAddFolderButtonClick = () => {
void CommandService.instance().execute('newFolder');
};
const onHeaderClick = (headerId: HeaderId) => {
const settingKey = headerId === HeaderId.TagHeader ? 'tagHeaderIsExpanded' : 'folderHeaderIsExpanded';
const current = Setting.value(settingKey);
Setting.setValue(settingKey, !current);
};
const useSidebarListData = (props: Props): ListItem[] => {
const tagItems = useMemo(() => {
return renderTags<ListItem>(props.tags, (tag): TagListItem => {
@ -60,10 +49,10 @@ const useSidebarListData = (props: Props): ListItem[] => {
kind: ListItemType.Header,
label: _('Notebooks'),
iconName: 'icon-notebooks',
expanded: props.folderHeaderIsExpanded,
id: HeaderId.FolderHeader,
key: HeaderId.FolderHeader,
onClick: onHeaderClick,
onPlusButtonClick: onAddFolderButtonClick,
onClick: toggleHeader,
extraProps: {
['data-folder-id']: '',
},
@ -79,10 +68,10 @@ const useSidebarListData = (props: Props): ListItem[] => {
kind: ListItemType.Header,
label: _('Tags'),
iconName: 'icon-tags',
expanded: props.tagHeaderIsExpanded,
id: HeaderId.TagHeader,
key: HeaderId.TagHeader,
onClick: onHeaderClick,
onPlusButtonClick: null,
onClick: toggleHeader,
extraProps: { },
supportsFolderDrop: false,
};

View File

@ -0,0 +1,10 @@
import Setting from '@joplin/lib/models/Setting';
import { HeaderId } from '../../types';
const toggleHeader = (headerId: HeaderId) => {
const settingKey = headerId === HeaderId.TagHeader ? 'tagHeaderIsExpanded' : 'folderHeaderIsExpanded';
const current = Setting.value(settingKey);
Setting.setValue(settingKey, !current);
};
export default toggleHeader;

View File

@ -1,5 +1,5 @@
import * as React from 'react';
import { StyledAllNotesIcon, StyledListItem, StyledListItemAnchor } from '../styles';
import { StyledAllNotesIcon, StyledListItemAnchor } from '../styles';
import { useCallback } from 'react';
import { Dispatch } from 'redux';
import bridge from '../../../services/bridge';
@ -10,6 +10,7 @@ import PerFolderSortOrderService from '../../../services/sortOrder/PerFolderSort
import { _ } from '@joplin/lib/locale';
import { connect } from 'react-redux';
import EmptyExpandLink from './EmptyExpandLink';
import ListItemWrapper, { ListItemRef } from './ListItemWrapper';
const { ALL_NOTES_FILTER_ID } = require('@joplin/lib/reserved-ids');
const Menu = bridge().Menu;
@ -17,8 +18,10 @@ const MenuItem = bridge().MenuItem;
interface Props {
dispatch: Dispatch;
anchorRef: ListItemRef;
selected: boolean;
anchorRef: React.Ref<HTMLAnchorElement>;
index: number;
itemCount: number;
}
const menuUtils = new MenuUtils(CommandService.instance());
@ -46,21 +49,28 @@ const AllNotesItem: React.FC<Props> = props => {
}, []);
return (
<StyledListItem key="allNotesHeader" selected={props.selected} className={'list-item-container list-item-depth-0 all-notes'} isSpecialItem={true}>
<ListItemWrapper
containerRef={props.anchorRef}
key="allNotesHeader"
selected={props.selected}
depth={1}
className={'list-item-container list-item-depth-0 all-notes'}
highlightOnHover={true}
itemIndex={props.index}
itemCount={props.itemCount}
>
<EmptyExpandLink/>
<StyledAllNotesIcon className="icon-notes"/>
<StyledAllNotesIcon aria-label='' role='img' className='icon-notes'/>
<StyledListItemAnchor
ref={props.anchorRef}
className="list-item"
isSpecialItem={true}
href="#"
selected={props.selected}
onClick={onAllNotesClick_}
onContextMenu={toggleAllNotesContextMenu}
>
{_('All notes')}
</StyledListItemAnchor>
</StyledListItem>
</ListItemWrapper>
);
};

View File

@ -2,10 +2,11 @@ import * as React from 'react';
import ExpandIcon from './ExpandIcon';
interface Props {
className?: string;
}
const EmptyExpandLink: React.FC<Props> = _props => {
return <a className='sidebar-expand-link'><ExpandIcon isVisible={false} isExpanded={false}/></a>;
const EmptyExpandLink: React.FC<Props> = props => {
return <a className={`sidebar-expand-link ${props.className ?? ''}`}><ExpandIcon isVisible={false} isExpanded={false}/></a>;
};
export default EmptyExpandLink;

View File

@ -23,11 +23,12 @@ const ExpandIcon: React.FC<ExpandIconProps> = props => {
return undefined;
}
if (props.isExpanded) {
return _('Collapse %s', props.targetTitle);
return _('Expanded, press space to collapse.');
}
return _('Expand %s', props.targetTitle);
return _('Collapsed, press space to expand.');
};
return <i className={classNames.join(' ')} aria-label={getLabel()}></i>;
const label = getLabel();
return <i className={classNames.join(' ')} aria-label={label} role='img'></i>;
};
export default ExpandIcon;

View File

@ -8,16 +8,17 @@ interface ExpandLinkProps {
folderTitle: string;
hasChildren: boolean;
isExpanded: boolean;
className: string;
onClick: MouseEventHandler<HTMLElement>;
}
const ExpandLink: React.FC<ExpandLinkProps> = props => {
return props.hasChildren ? (
<a className='sidebar-expand-link' href="#" data-folder-id={props.folderId} onClick={props.onClick}>
<a className={`sidebar-expand-link ${props.className}`} data-folder-id={props.folderId} onClick={props.onClick} role='button'>
<ExpandIcon isVisible={true} isExpanded={props.isExpanded} targetTitle={props.folderTitle}/>
</a>
) : (
<EmptyExpandLink/>
<EmptyExpandLink className={props.className}/>
);
};

View File

@ -2,7 +2,7 @@ import * as React from 'react';
import { FolderIcon, FolderIconType } from '@joplin/lib/services/database/types';
import ExpandLink from './ExpandLink';
import { StyledListItem, StyledListItemAnchor, StyledShareIcon, StyledSpanFix } from '../styles';
import { StyledListItemAnchor, StyledShareIcon, StyledSpanFix } from '../styles';
import { ItemClickListener, ItemContextMenuListener, ItemDragListener } from '../types';
import FolderIconBox from '../../FolderIconBox';
import { getTrashFolderIcon, getTrashFolderId } from '@joplin/lib/services/trash';
@ -10,6 +10,7 @@ import Folder from '@joplin/lib/models/Folder';
import { ModelType } from '@joplin/lib/BaseModel';
import { _ } from '@joplin/lib/locale';
import NoteCount from './NoteCount';
import ListItemWrapper, { ListItemRef } from './ListItemWrapper';
const renderFolderIcon = (folderIcon: FolderIcon) => {
if (!folderIcon) {
@ -26,6 +27,7 @@ const renderFolderIcon = (folderIcon: FolderIcon) => {
};
interface FolderItemProps {
anchorRef: ListItemRef;
hasChildren: boolean;
showFolderIcon: boolean;
isExpanded: boolean;
@ -43,7 +45,9 @@ interface FolderItemProps {
onFolderToggleClick_: ItemClickListener;
shareId: string;
selected: boolean;
anchorRef: React.Ref<HTMLElement>;
index: number;
itemCount: number;
}
function FolderItem(props: FolderItemProps) {
@ -63,29 +67,50 @@ function FolderItem(props: FolderItemProps) {
};
return (
<StyledListItem depth={depth} selected={selected} className={`list-item-container list-item-depth-${depth} ${selected ? 'selected' : ''}`} onDragStart={onFolderDragStart_} onDragOver={onFolderDragOver_} onDrop={onFolderDrop_} draggable={draggable} data-folder-id={folderId}>
<ExpandLink hasChildren={hasChildren} folderTitle={folderTitle} folderId={folderId} onClick={onFolderToggleClick_} isExpanded={isExpanded}/>
<ListItemWrapper
containerRef={props.anchorRef}
// Folders are contained within the "Notebooks" section (which has depth 0):
depth={depth + 1}
selected={selected}
itemIndex={props.index}
itemCount={props.itemCount}
expanded={hasChildren ? props.isExpanded : undefined}
className={`list-item-container list-item-depth-${depth} ${selected ? 'selected' : ''}`}
highlightOnHover={true}
onDragStart={onFolderDragStart_}
onDragOver={onFolderDragOver_}
onDrop={onFolderDrop_}
onContextMenu={itemContextMenu}
draggable={draggable}
data-folder-id={folderId}
data-id={folderId}
data-type={ModelType.Folder}
>
<StyledListItemAnchor
ref={props.anchorRef}
className="list-item"
isConflictFolder={folderId === Folder.conflictFolderId()}
href="#"
selected={selected}
aria-selected={selected}
shareId={shareId}
data-id={folderId}
data-type={ModelType.Folder}
onContextMenu={itemContextMenu}
data-folder-id={folderId}
onDoubleClick={onFolderToggleClick_}
onClick={() => {
folderItem_click(folderId);
}}
onDoubleClick={onFolderToggleClick_}
>
{doRenderFolderIcon()}<StyledSpanFix className="title">{folderTitle}</StyledSpanFix>
{shareIcon} <NoteCount count={noteCount}/>
</StyledListItemAnchor>
</StyledListItem>
<ExpandLink
// The ExpandLink is included after the title so that the screen reader reads the
// title first.
className='toggle'
hasChildren={hasChildren}
folderTitle={folderTitle}
folderId={folderId}
onClick={onFolderToggleClick_}
isExpanded={isExpanded}
/>
</ListItemWrapper>
);
}

View File

@ -1,12 +1,11 @@
import * as React from 'react';
import { useCallback } from 'react';
import { ButtonLevel } from '../../Button/Button';
import { StyledAddButton, StyledHeader, StyledHeaderIcon, StyledHeaderLabel } from '../styles';
import { StyledHeader, StyledHeaderIcon, StyledHeaderLabel } from '../styles';
import { HeaderId, HeaderListItem } from '../types';
import { _ } from '@joplin/lib/locale';
import bridge from '../../../services/bridge';
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
import CommandService from '@joplin/lib/services/CommandService';
import ListItemWrapper, { ListItemRef } from './ListItemWrapper';
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
@ -14,9 +13,12 @@ const menuUtils = new MenuUtils(CommandService.instance());
interface Props {
anchorRef: ListItemRef;
item: HeaderListItem;
isSelected: boolean;
onDrop: React.DragEventHandler|null;
anchorRef: React.Ref<HTMLElement>;
index: number;
itemCount: number;
}
const HeaderItem: React.FC<Props> = props => {
@ -42,30 +44,25 @@ const HeaderItem: React.FC<Props> = props => {
}
}, [itemId]);
const addButton = <StyledAddButton
iconLabel={_('New')}
onClick={item.onPlusButtonClick}
iconName='fas fa-plus'
level={ButtonLevel.SidebarSecondary}
/>;
return (
<div
<ListItemWrapper
containerRef={props.anchorRef}
selected={props.isSelected}
itemIndex={props.index}
itemCount={props.itemCount}
expanded={props.item.expanded}
onContextMenu={onContextMenu}
depth={0}
highlightOnHover={false}
className='sidebar-header-container'
{...item.extraProps}
onDrop={props.onDrop}
>
<StyledHeader
onContextMenu={onContextMenu}
onClick={onClick}
tabIndex={0}
ref={props.anchorRef}
>
<StyledHeaderIcon aria-label='' className={item.iconName}/>
<StyledHeader onClick={onClick}>
<StyledHeaderIcon aria-label='' role='img' className={item.iconName}/>
<StyledHeaderLabel>{item.label}</StyledHeaderLabel>
</StyledHeader>
{ item.onPlusButtonClick && addButton }
</div>
</ListItemWrapper>
);
};

View File

@ -0,0 +1,66 @@
import { ModelType } from '@joplin/lib/BaseModel';
import * as React from 'react';
import { useMemo } from 'react';
export type ListItemRef = React.Ref<HTMLDivElement>;
interface Props {
containerRef: ListItemRef;
selected: boolean;
itemIndex: number;
itemCount: number;
expanded?: boolean|undefined;
depth: number;
className?: string;
highlightOnHover: boolean;
children: (React.ReactNode[])|React.ReactNode;
onContextMenu?: React.MouseEventHandler;
onDrag?: React.DragEventHandler;
onDragStart?: React.DragEventHandler;
onDragOver?: React.DragEventHandler;
onDrop?: React.DragEventHandler;
draggable?: boolean;
'data-folder-id'?: string;
'data-id'?: string;
'data-type'?: ModelType;
}
const ListItemWrapper: React.FC<Props> = props => {
const style = useMemo(() => {
return {
'--depth': props.depth,
} as React.CSSProperties;
}, [props.depth]);
return (
<div
ref={props.containerRef}
aria-posinset={props.itemIndex + 1}
aria-setsize={props.itemCount}
aria-selected={props.selected}
aria-expanded={props.expanded}
// aria-level is 1-based, where depth is zero-based
aria-level={props.depth + 1}
tabIndex={props.selected ? 0 : -1}
onContextMenu={props.onContextMenu}
onDrag={props.onDrag}
onDragStart={props.onDragStart}
onDragOver={props.onDragOver}
onDrop={props.onDrop}
draggable={props.draggable}
role='treeitem'
className={`list-item-wrapper ${props.highlightOnHover ? '-highlight-on-hover' : ''} ${props.selected ? '-selected' : ''} ${props.className ?? ''}`}
style={style}
data-folder-id={props['data-folder-id']}
data-id={props['data-id']}
data-type={props['data-type']}
>
{props.children}
</div>
);
};
export default ListItemWrapper;

View File

@ -1,22 +1,26 @@
import Setting from '@joplin/lib/models/Setting';
import * as React from 'react';
import { useCallback } from 'react';
import { StyledListItem, StyledListItemAnchor, StyledSpanFix } from '../styles';
import { StyledListItemAnchor, StyledSpanFix } from '../styles';
import { TagsWithNoteCountEntity } from '@joplin/lib/services/database/types';
import BaseModel from '@joplin/lib/BaseModel';
import NoteCount from './NoteCount';
import Tag from '@joplin/lib/models/Tag';
import EmptyExpandLink from './EmptyExpandLink';
import ListItemWrapper, { ListItemRef } from './ListItemWrapper';
export type TagLinkClickEvent = { tag: TagsWithNoteCountEntity|undefined };
interface Props {
anchorRef: ListItemRef;
selected: boolean;
anchorRef: React.Ref<HTMLElement>;
tag: TagsWithNoteCountEntity;
onTagDrop: React.DragEventHandler<HTMLElement>;
onContextMenu: React.MouseEventHandler<HTMLElement>;
onClick: (event: TagLinkClickEvent)=> void;
itemCount: number;
index: number;
}
const TagItem = (props: Props) => {
@ -33,18 +37,21 @@ const TagItem = (props: Props) => {
}, [props.onClick, tag]);
return (
<StyledListItem
<ListItemWrapper
containerRef={props.anchorRef}
selected={selected}
depth={1}
className={`list-item-container ${selected ? 'selected' : ''}`}
highlightOnHover={true}
onDrop={props.onTagDrop}
data-tag-id={tag.id}
aria-selected={selected}
itemIndex={props.index}
itemCount={props.itemCount}
>
<EmptyExpandLink/>
<StyledListItemAnchor
ref={props.anchorRef}
className="list-item"
href="#"
selected={selected}
data-id={tag.id}
data-type={BaseModel.TYPE_TAG}
@ -54,7 +61,7 @@ const TagItem = (props: Props) => {
<StyledSpanFix className="tag-label">{Tag.displayTitle(tag)}</StyledSpanFix>
{noteCount}
</StyledListItemAnchor>
</StyledListItem>
</ListItemWrapper>
);
};

View File

@ -1,6 +1,8 @@
@use 'styles/folder-and-tag-list.scss';
@use 'styles/list-item-wrapper.scss';
@use 'styles/note-count-label.scss';
@use 'styles/sidebar-expand-icon.scss';
@use 'styles/sidebar-expand-link.scss';
@use 'styles/sidebar-header-container.scss';
@use 'styles/sidebar-spacer-item.scss';
@use 'styles/sidebar-spacer-item.scss';
@use 'styles/new-folder-button.scss';

View File

@ -49,22 +49,6 @@ export const StyledHeaderLabel = styled.span`
font-weight: bold;
`;
export const StyledListItem = styled.div`
box-sizing: border-box;
height: 30px;
display: flex;
flex-direction: row;
align-items: center;
padding-left: ${(props: StyleProps) => props.theme.mainPadding + ('depth' in props ? props.depth : 0) * 16}px;
background: ${(props: StyleProps) => props.selected ? props.theme.selectedColor2 : 'none'};
/*text-transform: ${(props: StyleProps) => props.isSpecialItem ? 'uppercase' : 'none'};*/
transition: 0.1s;
&:hover {
background-color: ${(props: StyleProps) => props.theme.backgroundColorHover2};
}
`;
function listItemTextColor(props: StyleProps) {
if (props.isConflictFolder) return props.theme.colorError2;
if (props.isSpecialItem) return props.theme.colorFaded2;

View File

@ -0,0 +1,25 @@
.list-item-wrapper {
box-sizing: border-box;
height: 30px;
display: flex;
flex-direction: row;
align-items: center;
padding-left: calc(var(--joplin-main-padding) + (var(--depth) * 16px) - 16px);
background: none;
transition: 0.1s;
// Show the toggle button first, even if it's markup is included later for a better screen reader
// experience.
> .toggle {
order: -1;
}
&.-selected {
background: var(--joplin-selected-color2);
}
&.-highlight-on-hover:hover {
background-color: var(--joplin-background-color-hover2);
}
}

View File

@ -0,0 +1,25 @@
.new-folder-button {
position: absolute;
top: 0;
inset-inline-end: 0;
padding-inline-end: 15px;
padding-top: 4px;
height: 30px;
border: none;
background-color: transparent;
font-size: var(--joplin-toolbar-icon-size);
color: var(--joplin-color2);
&:hover {
color: var(--joplin-color-hover2);
background: none;
}
&:active {
color: var(--joplin-color-active2);
background: none;
}
}

View File

@ -5,6 +5,7 @@
opacity: 0.8;
text-decoration: none;
padding-right: 8px;
text-align: center;
display: flex;
align-items: center;
width: 16px;

View File

@ -21,10 +21,10 @@ interface BaseListItem {
export interface HeaderListItem extends BaseListItem {
kind: ListItemType.Header;
label: string;
expanded: boolean;
iconName: string;
id: HeaderId;
onClick: ((headerId: HeaderId, event: ReactMouseEvent<HTMLElement>)=> void)|null;
onPlusButtonClick: MouseEventHandler<HTMLElement>|null;
extraProps: Record<string, string>;
supportsFolderDrop: boolean;
}

View File

@ -1,6 +1,5 @@
import * as React from 'react';
import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
import { StyledIconSpan, StyledIconI } from './styles';
interface Props {
readonly themeId: number;
@ -36,8 +35,12 @@ export default function ToolbarButton(props: Props) {
let icon = null;
const iconName = getProp(props, 'iconName');
if (iconName) {
const IconClass = isFontAwesomeIcon(iconName) ? StyledIconI : StyledIconSpan;
icon = <IconClass className={iconName} aria-label='' hasTitle={!!title} role='img'/>;
const iconProps: React.HTMLProps<HTMLDivElement> = {
'aria-label': '',
role: 'img',
className: `toolbar-icon ${title ? '-has-title' : ''} ${iconName}`,
};
icon = isFontAwesomeIcon(iconName) ? <i {...iconProps} /> : <span {...iconProps} />;
}
// Keep this for legacy compatibility but for consistency we should use "disabled" prop

View File

@ -1,19 +0,0 @@
import { ThemeStyle } from '@joplin/lib/theme';
const styled = require('styled-components').default;
const { css } = require('styled-components');
interface IconProps {
readonly theme: ThemeStyle;
readonly hasTitle: boolean;
}
const iconStyle = css<IconProps>`
font-size: ${(props: IconProps) => props.theme.toolbarIconSize}px;
color: ${(props: IconProps) => props.theme.color3};
margin-right: ${(props: IconProps) => props.hasTitle ? 5 : 0}px;
pointer-events: none; /* Need this to get button tooltip to work */
`;
export const StyledIconI = styled.i`${iconStyle}`;
export const StyledIconSpan = styled.span`${iconStyle}`;

View File

@ -377,6 +377,20 @@
contentElement.scrollTop = scrollTop;
}
const rewriteFileUrls = (accessKey) => {
if (!accessKey) return;
// To allow accessing local files from the viewer's non-file URL, file:// URLs are re-written
// to joplin-content:// URLs:
const mediaElements = document.querySelectorAll('video[src], audio[src], source[src], img[src]');
for (const element of mediaElements) {
if (element.src?.startsWith('file:')) {
const newUrl = element.src.replace(/^file:\/\//, 'joplin-content://file-media/');
element.src = `${newUrl}?access-key=${accessKey}`;
}
}
};
ipc.setHtml = (event) => {
const html = event.html;
@ -388,6 +402,10 @@
contentElement.innerHTML = html;
if (html.includes('file://')) {
rewriteFileUrls(event.options.mediaAccessKey);
}
scrollmap.create(event.options.markupLineCount);
if (typeof event.options.percent !== 'number') {
restorePercentScroll(); // First, a quick treatment is applied.
@ -733,6 +751,13 @@
}));
document.addEventListener('click', webviewLib.logEnabledEventHandler(e => {
// Links should all have custom click handlers. Allowing Electron to load custom links
// can cause security issues, particularly if these links have the same domain as the
// top-level page.
if (e.target.hasAttribute('href')) {
e.preventDefault();
}
document.querySelectorAll('.media-pdf').forEach(element => {
if(!!element.contentWindow){
element.contentWindow.postMessage({

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