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:
commit
3f09c91a63
@ -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
|
||||
|
@ -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
|
||||
|
8
.github/workflows/automerge.yml
vendored
8
.github/workflows/automerge.yml
vendored
@ -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:
|
||||
|
14
.github/workflows/build-android.yml
vendored
14
.github/workflows/build-android.yml
vendored
@ -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
|
||||
|
14
.github/workflows/build-macos-m1.yml
vendored
14
.github/workflows/build-macos-m1.yml
vendored
@ -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:
|
||||
|
||||
|
21
.github/workflows/github-actions-main.yml
vendored
21
.github/workflows/github-actions-main.yml
vendored
@ -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
39
.gitignore
vendored
@ -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
|
||||
|
874
.yarn/releases/yarn-3.6.4.cjs
vendored
874
.yarn/releases/yarn-3.6.4.cjs
vendored
File diff suppressed because one or more lines are too long
875
.yarn/releases/yarn-3.8.3.cjs
vendored
Executable file
875
.yarn/releases/yarn-3.8.3.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
@ -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:
|
||||
|
||||
|
BIN
Assets/WebsiteAssets/images/note_list/LeftToRight_Thumbnails.png
Normal file
BIN
Assets/WebsiteAssets/images/note_list/LeftToRight_Thumbnails.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 35 KiB |
BIN
Assets/WebsiteAssets/images/note_list/TopToBottom.png
Normal file
BIN
Assets/WebsiteAssets/images/note_list/TopToBottom.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 75 KiB |
BIN
Assets/WebsiteAssets/images/note_list/TopToBottom_Editable.png
Normal file
BIN
Assets/WebsiteAssets/images/note_list/TopToBottom_Editable.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
BIN
Assets/WebsiteAssets/images/sponsors/CasinoReviews.png
Normal file
BIN
Assets/WebsiteAssets/images/sponsors/CasinoReviews.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 63 KiB |
@ -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
|
||||
|
||||
|
@ -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&mtm_kwd=joplinapp&mtm_source=joplinapp-webseite&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&mtm_kwd=joplinapp&mtm_source=joplinapp-webseite&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 -->
|
||||
|
||||
* * *
|
||||
|
13
fastlane/metadata/android/hu/full_description.txt
Normal file
13
fastlane/metadata/android/hu/full_description.txt
Normal 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>.
|
1
fastlane/metadata/android/hu/short_description.txt
Normal file
1
fastlane/metadata/android/hu/short_description.txt
Normal file
@ -0,0 +1 @@
|
||||
Jegyzetkészítő és teendők alkalmazás Linux, macOS, Windows és mobileszközök közötti összehangolással
|
26
package.json
26
package.json
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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>');
|
||||
});
|
||||
});
|
||||
|
@ -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": {
|
||||
|
@ -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) => {
|
||||
|
@ -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') {
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
47
packages/app-desktop/commands/renderMarkup.test.ts
Normal file
47
packages/app-desktop/commands/renderMarkup.test.ts
Normal 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>');
|
||||
}
|
||||
});
|
||||
|
||||
});
|
32
packages/app-desktop/commands/renderMarkup.ts
Normal file
32
packages/app-desktop/commands/renderMarkup.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
};
|
@ -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']}
|
||||
|
@ -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);
|
||||
}}
|
||||
|
@ -234,7 +234,7 @@ export default function(props: Props) {
|
||||
return (
|
||||
<CellFooter>
|
||||
<NeedUpgradeMessage>
|
||||
{PluginService.instance().describeIncompatibility(props.manifest)}
|
||||
{PluginService.instance().describeIncompatibility(item.manifest)}
|
||||
</NeedUpgradeMessage>
|
||||
</CellFooter>
|
||||
);
|
||||
|
@ -3,3 +3,4 @@
|
||||
@use "./setting-label.scss";
|
||||
@use "./setting-header.scss";
|
||||
@use "./setting-tab-panel.scss";
|
||||
@use "./setting-select-control.scss";
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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}`);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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 (
|
||||
|
@ -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
|
||||
/>
|
||||
|
2
packages/app-desktop/gui/KeymapConfig/style.scss
Normal file
2
packages/app-desktop/gui/KeymapConfig/style.scss
Normal file
@ -0,0 +1,2 @@
|
||||
@use "./styles/keymap-shortcut-row-content.scss";
|
||||
@use "./styles/shortcut-recorder.scss";
|
@ -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',
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -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'),
|
||||
|
@ -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),
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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'),
|
||||
});
|
@ -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';
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -38,7 +38,6 @@ const incompatiblePluginIds = [
|
||||
'ylc395.noteLinkSystem',
|
||||
'outline',
|
||||
'joplin.plugin.cmoptions',
|
||||
'plugin.calebjohn.MathMode',
|
||||
'com.ckant.joplin-plugin-better-code-blocks',
|
||||
// cSpell:enable
|
||||
];
|
||||
|
@ -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";
|
||||
|
@ -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%;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 = {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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]);
|
||||
}
|
||||
|
@ -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)}
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
@ -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]);
|
||||
|
@ -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');
|
||||
|
@ -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>
|
||||
|
@ -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;
|
@ -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;
|
||||
|
@ -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";
|
@ -0,0 +1,10 @@
|
||||
.labelled-password-input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> .password {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
6
packages/app-desktop/gui/PasswordInput/types.ts
Normal file
6
packages/app-desktop/gui/PasswordInput/types.ts
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
export interface ChangeEvent {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export type ChangeEventHandler = (event: ChangeEvent)=> void;
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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 };
|
||||
};
|
||||
|
@ -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,
|
||||
]);
|
||||
};
|
||||
|
||||
|
@ -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;
|
@ -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;
|
||||
|
@ -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,
|
||||
};
|
||||
|
10
packages/app-desktop/gui/Sidebar/hooks/utils/toggleHeader.ts
Normal file
10
packages/app-desktop/gui/Sidebar/hooks/utils/toggleHeader.ts
Normal 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;
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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}/>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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;
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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';
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@
|
||||
opacity: 0.8;
|
||||
text-decoration: none;
|
||||
padding-right: 8px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 16px;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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}`;
|
@ -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
Loading…
Reference in New Issue
Block a user