You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-12-29 23:48:19 +02:00
Compare commits
202 Commits
v3.5.2
...
join_serve
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c44aad544e | ||
|
|
996a0894ae | ||
|
|
66fa3fc808 | ||
|
|
dab55daf95 | ||
|
|
7f1c31e03f | ||
|
|
0a8255f091 | ||
|
|
9f3e6650a9 | ||
|
|
4a17da3df5 | ||
|
|
2c4f0d4d8c | ||
|
|
9c1c2fb0d4 | ||
|
|
2332e4bf62 | ||
|
|
a488ac1b27 | ||
|
|
6daa41ca66 | ||
|
|
cc9517f1a2 | ||
|
|
200a471e55 | ||
|
|
c21d37bd91 | ||
|
|
e36cd0e60b | ||
|
|
871f55bf11 | ||
|
|
22c9fed663 | ||
|
|
ea362d7a82 | ||
|
|
9ae9347f89 | ||
|
|
ae8bb902f9 | ||
|
|
90eeec23de | ||
|
|
474fd094c4 | ||
|
|
937d8fa4f7 | ||
|
|
45c9844616 | ||
|
|
12b8ef5a54 | ||
|
|
18f72c224e | ||
|
|
7ca3aaa83f | ||
|
|
04b1443e5a | ||
|
|
c461741778 | ||
|
|
2865b0a803 | ||
|
|
21e49be22f | ||
|
|
fef761cbab | ||
|
|
c15a353dc2 | ||
|
|
ffb32766c1 | ||
|
|
038908550e | ||
|
|
42f59134ae | ||
|
|
fc0014c0b5 | ||
|
|
42d8df3036 | ||
|
|
1fad9ca1cc | ||
|
|
ae289be77a | ||
|
|
7f6bfe9c6e | ||
|
|
ead4001b7a | ||
|
|
7b95ef72a0 | ||
|
|
a4556bf598 | ||
|
|
8d6268dc92 | ||
|
|
7ffcbdf60a | ||
|
|
76989ddc45 | ||
|
|
1db1254617 | ||
|
|
9810bffddc | ||
|
|
b25e18107b | ||
|
|
edc5fe5d1b | ||
|
|
7ffb44b3a4 | ||
|
|
32f4c33140 | ||
|
|
1a7b09c91c | ||
|
|
e5bf8e0e58 | ||
|
|
94725c533c | ||
|
|
359c92b64f | ||
|
|
8f8b8ad943 | ||
|
|
dd2f329fd5 | ||
|
|
813f594cb4 | ||
|
|
0e0ce49867 | ||
|
|
e485d318b7 | ||
|
|
4e82d81df1 | ||
|
|
d5dbda201b | ||
|
|
831258506b | ||
|
|
67f3329ecb | ||
|
|
ed7e6751f0 | ||
|
|
35e69486d3 | ||
|
|
918c8830e0 | ||
|
|
c3b4a4b955 | ||
|
|
44a14fabbd | ||
|
|
49399cd1fa | ||
|
|
fc4cd2e942 | ||
|
|
cd6e457dc5 | ||
|
|
2e9bf3a4e5 | ||
|
|
547ceea4b0 | ||
|
|
776ff5e7ea | ||
|
|
2b3bac0d43 | ||
|
|
e48efe2e8d | ||
|
|
5f6382fbc0 | ||
|
|
3d5d82081a | ||
|
|
cff96b1306 | ||
|
|
98c5a9c096 | ||
|
|
e92430b3ed | ||
|
|
848d1bfe64 | ||
|
|
a386283530 | ||
|
|
6101031269 | ||
|
|
2fc3431f46 | ||
|
|
361fa2c768 | ||
|
|
f4a0a2466b | ||
|
|
dbf225d6ad | ||
|
|
4773a3831c | ||
|
|
6a19690581 | ||
|
|
b7a771d58d | ||
|
|
e3daefb81a | ||
|
|
b4253dace8 | ||
|
|
fcf3be1be1 | ||
|
|
99aebbad81 | ||
|
|
81b695a2a9 | ||
|
|
2dbba27357 | ||
|
|
8713cd2fd8 | ||
|
|
d0fc4ea21b | ||
|
|
8bd62800ef | ||
|
|
00f9e932e6 | ||
|
|
b8b55e4a55 | ||
|
|
ef5be2ded3 | ||
|
|
00702dde00 | ||
|
|
2a6af9bed9 | ||
|
|
c26fe0960b | ||
|
|
ab9d36fc08 | ||
|
|
28eb53bd9f | ||
|
|
3097c3e589 | ||
|
|
08371ef718 | ||
|
|
561716efea | ||
|
|
0d457d1bde | ||
|
|
8c11f17c93 | ||
|
|
f7a90ee1d2 | ||
|
|
8822409f7c | ||
|
|
cd3e7f485a | ||
|
|
8d42b01d4f | ||
|
|
2c37197641 | ||
|
|
c2c37b3741 | ||
|
|
3e770300dc | ||
|
|
683291d5df | ||
|
|
d239035417 | ||
|
|
5ef37d9de0 | ||
|
|
1111bde017 | ||
|
|
468cf00d77 | ||
|
|
3c5b41b992 | ||
|
|
5f66c51dba | ||
|
|
bfeaa67ec4 | ||
|
|
348fd0333f | ||
|
|
51c4d6d6ef | ||
|
|
09d77a65e8 | ||
|
|
d1aec4a9f7 | ||
|
|
cab1525589 | ||
|
|
a52f3fea9e | ||
|
|
dfbd5eb8ed | ||
|
|
3131f36033 | ||
|
|
dc5b2cfa21 | ||
|
|
cad0f35fcc | ||
|
|
38ea92ff57 | ||
|
|
830deada22 | ||
|
|
38cd4033ea | ||
|
|
02900752d9 | ||
|
|
091e9813b5 | ||
|
|
e61e5ac32a | ||
|
|
414970c9a1 | ||
|
|
d4ed49ff23 | ||
|
|
8751d5d152 | ||
|
|
2e846fe15d | ||
|
|
e54b7696d9 | ||
|
|
553c61d628 | ||
|
|
6a15db3a36 | ||
|
|
6f1d0a4b90 | ||
|
|
33b995672c | ||
|
|
8ee46bb4e7 | ||
|
|
b35d9a64cf | ||
|
|
64ef74dd01 | ||
|
|
53035839a5 | ||
|
|
af5287de99 | ||
|
|
45a7554774 | ||
|
|
b06ffe3d25 | ||
|
|
53ea51b758 | ||
|
|
820acdc1f0 | ||
|
|
ef0a79666e | ||
|
|
d096a90c0e | ||
|
|
191775310e | ||
|
|
4fc351b861 | ||
|
|
396decd26c | ||
|
|
01f8fa7bef | ||
|
|
c40856ac7e | ||
|
|
d869cce413 | ||
|
|
a83e8311d8 | ||
|
|
aa884fcb39 | ||
|
|
be2a4c3e24 | ||
|
|
520eec555b | ||
|
|
1281fdb9d2 | ||
|
|
6029353fd1 | ||
|
|
8d1d1be79e | ||
|
|
fd180ae0b4 | ||
|
|
6fdfd6eae6 | ||
|
|
cd5bb575c8 | ||
|
|
2df56530ae | ||
|
|
7987137470 | ||
|
|
a1dcd2fd8f | ||
|
|
7826dc064a | ||
|
|
eedf083bfd | ||
|
|
d4aa1f8f8d | ||
|
|
738e749d51 | ||
|
|
8fe818c0b0 | ||
|
|
e603452fad | ||
|
|
3827637b54 | ||
|
|
1da7c54e5f | ||
|
|
e24ebffba6 | ||
|
|
97fa85a3f7 | ||
|
|
defe36bba1 | ||
|
|
711d214741 | ||
|
|
0795c67354 | ||
|
|
e9a9f68568 |
@@ -90,7 +90,7 @@ plugin_types/
|
||||
readme/
|
||||
packages/react-native-vosk/lib/
|
||||
packages/lib/countable/Countable.js
|
||||
packages/onenote-converter/pkg/onenote_converter.js
|
||||
packages/onenote-converter/renderer/pkg/*
|
||||
|
||||
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
|
||||
packages/app-cli/app/LinkSelector.js
|
||||
@@ -270,6 +270,7 @@ 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/CodeMirror/v6/utils/useSyncEditorValue.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/PlainEditor/PlainEditor.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js
|
||||
@@ -280,6 +281,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/types.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useCursorPositioning.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useEditDialog.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useEditDialogEventListeners.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useKeyboardRefocusHandler.js
|
||||
@@ -321,6 +323,7 @@ packages/app-desktop/gui/NoteEditor/utils/useEffectiveNoteId.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useFolder.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useFormNote.test.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useFormNote.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useInitialCursorLocation.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useMessageHandler.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useNoteSearchBar.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/usePluginEditorView.test.js
|
||||
@@ -604,6 +607,7 @@ packages/app-desktop/tools/generateLatestArm64Yml.js
|
||||
packages/app-desktop/tools/githubReleasesUtils.js
|
||||
packages/app-desktop/tools/modifyReleaseAssets.js
|
||||
packages/app-desktop/tools/notarizeMacApp.js
|
||||
packages/app-desktop/tools/resolveSourceMap.js
|
||||
packages/app-desktop/utils/7zip/getPathToExecutable7Zip.js
|
||||
packages/app-desktop/utils/7zip/pathToBundled7Zip.js
|
||||
packages/app-desktop/utils/checkForUpdatesUtils.test.js
|
||||
@@ -697,6 +701,7 @@ packages/app-mobile/components/NoteEditor/ImageEditor/ImageEditor.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/autosave.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/isEditableResource.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/promptRestoreAutosave.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownEditor.test.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownEditor.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.test.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.js
|
||||
@@ -844,6 +849,7 @@ packages/app-mobile/components/screens/NoteTagsDialog.js
|
||||
packages/app-mobile/components/screens/Notes/NewNoteButton.test.js
|
||||
packages/app-mobile/components/screens/Notes/NewNoteButton.js
|
||||
packages/app-mobile/components/screens/Notes/Notes.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.test.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
|
||||
packages/app-mobile/components/screens/SearchScreen/index.js
|
||||
packages/app-mobile/components/screens/ShareManager/AcceptedShareItem.js
|
||||
@@ -1049,6 +1055,7 @@ packages/editor/CodeMirror/extensions/rendering/types.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/makeBlockReplaceExtension.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/makeInlineReplaceExtension.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/nodeIntersectsSelection.js
|
||||
packages/editor/CodeMirror/extensions/searchExtension.test.js
|
||||
packages/editor/CodeMirror/extensions/searchExtension.js
|
||||
packages/editor/CodeMirror/extensions/selectedNoteIdExtension.js
|
||||
packages/editor/CodeMirror/getScrollFraction.js
|
||||
@@ -1095,8 +1102,10 @@ packages/editor/CodeMirror/utils/markdown/renumberSelectedLists.js
|
||||
packages/editor/CodeMirror/utils/markdown/stripBlockquote.js
|
||||
packages/editor/CodeMirror/utils/markdown/toggleCheckboxAt.js
|
||||
packages/editor/CodeMirror/utils/setupVim.js
|
||||
packages/editor/ProseMirror/commands.test.js
|
||||
packages/editor/ProseMirror/commands.js
|
||||
packages/editor/CodeMirror/vendor/announceSearchMatch.js
|
||||
packages/editor/ProseMirror/commands/commands.test.js
|
||||
packages/editor/ProseMirror/commands/commands.js
|
||||
packages/editor/ProseMirror/commands/focusEditor.js
|
||||
packages/editor/ProseMirror/createEditor.js
|
||||
packages/editor/ProseMirror/index.js
|
||||
packages/editor/ProseMirror/plugins/detailsPlugin.test.js
|
||||
@@ -1115,6 +1124,7 @@ packages/editor/ProseMirror/plugins/linkTooltipPlugin.js
|
||||
packages/editor/ProseMirror/plugins/listPlugin.js
|
||||
packages/editor/ProseMirror/plugins/originalMarkupPlugin.js
|
||||
packages/editor/ProseMirror/plugins/searchPlugin.js
|
||||
packages/editor/ProseMirror/plugins/tablePlugin.js
|
||||
packages/editor/ProseMirror/plugins/utils/createExternalEditorPlugin.js
|
||||
packages/editor/ProseMirror/plugins/utils/createFloatingButtonPlugin.js
|
||||
packages/editor/ProseMirror/schema.js
|
||||
@@ -1144,6 +1154,12 @@ packages/editor/ProseMirror/utils/sanitizeHtml.js
|
||||
packages/editor/ProseMirror/utils/selectFirstInstanceOfNode.js
|
||||
packages/editor/ProseMirror/utils/trimEmptyParagraphs.js
|
||||
packages/editor/ProseMirror/vendor/changedDescendants.js
|
||||
packages/editor/ProseMirror/vendor/icons/addColumnRight.js
|
||||
packages/editor/ProseMirror/vendor/icons/addRowBelow.js
|
||||
packages/editor/ProseMirror/vendor/icons/icon.js
|
||||
packages/editor/ProseMirror/vendor/icons/removeColumn.js
|
||||
packages/editor/ProseMirror/vendor/icons/removeRow.js
|
||||
packages/editor/ProseMirror/vendor/icons/types.js
|
||||
packages/editor/ProseMirror/vendor/splitBlockAs.js
|
||||
packages/editor/SelectionFormatting.js
|
||||
packages/editor/events.js
|
||||
@@ -1412,6 +1428,7 @@ packages/lib/services/database/migrations/45.js
|
||||
packages/lib/services/database/migrations/46.js
|
||||
packages/lib/services/database/migrations/47.js
|
||||
packages/lib/services/database/migrations/48.js
|
||||
packages/lib/services/database/migrations/49.js
|
||||
packages/lib/services/database/migrations/index.js
|
||||
packages/lib/services/database/sqlStringToLines.js
|
||||
packages/lib/services/database/types.js
|
||||
@@ -1535,6 +1552,7 @@ packages/lib/services/plugins/utils/createViewHandle.js
|
||||
packages/lib/services/plugins/utils/executeSandboxCall.js
|
||||
packages/lib/services/plugins/utils/getActivePluginEditorView.js
|
||||
packages/lib/services/plugins/utils/getActivePluginEditorViews.js
|
||||
packages/lib/services/plugins/utils/getPluginHelpUrl.js
|
||||
packages/lib/services/plugins/utils/getPluginIssueReportUrl.test.js
|
||||
packages/lib/services/plugins/utils/getPluginIssueReportUrl.js
|
||||
packages/lib/services/plugins/utils/getPluginNamespacedSettingKey.js
|
||||
@@ -1626,6 +1644,7 @@ packages/lib/services/synchronizer/Synchronizer.sharing.test.js
|
||||
packages/lib/services/synchronizer/Synchronizer.tags.test.js
|
||||
packages/lib/services/synchronizer/Synchronizer.tools.test.js
|
||||
packages/lib/services/synchronizer/gui/useSyncTargetUpgrade.js
|
||||
packages/lib/services/synchronizer/handleConflictAction.test.js
|
||||
packages/lib/services/synchronizer/migrations/1.js
|
||||
packages/lib/services/synchronizer/migrations/2.js
|
||||
packages/lib/services/synchronizer/migrations/3.js
|
||||
@@ -1739,6 +1758,7 @@ packages/plugin-repo-cli/lib/gitCompareUrl.test.js
|
||||
packages/plugin-repo-cli/lib/gitCompareUrl.js
|
||||
packages/plugin-repo-cli/lib/overrideUtils.test.js
|
||||
packages/plugin-repo-cli/lib/overrideUtils.js
|
||||
packages/plugin-repo-cli/lib/searchPlugins.js
|
||||
packages/plugin-repo-cli/lib/types.js
|
||||
packages/plugin-repo-cli/lib/updateReadme.test.js
|
||||
packages/plugin-repo-cli/lib/updateReadme.js
|
||||
@@ -1854,6 +1874,8 @@ packages/tools/updateMarkdownDoc.js
|
||||
packages/tools/utils/discourse.test.js
|
||||
packages/tools/utils/discourse.js
|
||||
packages/tools/utils/loadSponsors.js
|
||||
packages/tools/utils/parsePluralLocalizationForm.js
|
||||
packages/tools/utils/parsePlurallLocalizationForm.test.js
|
||||
packages/tools/utils/translation.js
|
||||
packages/tools/validateFilenames.js
|
||||
packages/tools/website/build.js
|
||||
|
||||
30
.github/workflows/github-actions-main.yml
vendored
30
.github/workflows/github-actions-main.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
matrix:
|
||||
# Do not use unbuntu-latest because it causes `The operation was canceled` failures:
|
||||
# https://github.com/actions/runner-images/issues/6709
|
||||
os: [macos-13, ubuntu-22.04, windows-2025, ubuntu-22.04-arm]
|
||||
os: [macos-15-intel, ubuntu-22.04, windows-2025, ubuntu-22.04-arm]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -50,6 +50,22 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
# - name: Test Windows app signing
|
||||
# if: runner.os == 'Windows'
|
||||
# env:
|
||||
# GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
# IS_CONTINUOUS_INTEGRATION: 1
|
||||
# BUILD_SEQUENCIAL: 1
|
||||
# SSL_ESIGNER_USER_NAME: ${{ secrets.SSL_ESIGNER_USER_NAME }}
|
||||
# SSL_ESIGNER_USER_PASSWORD: ${{ secrets.SSL_ESIGNER_USER_PASSWORD }}
|
||||
# SSL_ESIGNER_CREDENTIAL_ID: ${{ secrets.SSL_ESIGNER_CREDENTIAL_ID }}
|
||||
# SSL_ESIGNER_USER_TOTP: ${{ secrets.SSL_ESIGNER_USER_TOTP }}
|
||||
# SIGN_APPLICATION: 1
|
||||
# # To ensure that the operations stop on failure, all commands
|
||||
# # should be on one line with "&&" in between.
|
||||
# run: |
|
||||
# yarn install && cd packages/app-desktop && yarn dist
|
||||
|
||||
- name: Run tests, build and publish Linux and macOS apps
|
||||
if: runner.os == 'Linux' || runner.os == 'macOs'
|
||||
env:
|
||||
@@ -71,11 +87,14 @@ jobs:
|
||||
- name: Build and publish Windows app
|
||||
if: runner.os == 'Windows' && startsWith(github.ref, 'refs/tags/v')
|
||||
env:
|
||||
CSC_KEY_PASSWORD: ${{ secrets.WINDOWS_CSC_KEY_PASSWORD }}
|
||||
CSC_LINK: ${{ secrets.WINDOWS_CSC_LINK }}
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
IS_CONTINUOUS_INTEGRATION: 1
|
||||
BUILD_SEQUENCIAL: 1
|
||||
SSL_ESIGNER_USER_NAME: ${{ secrets.SSL_ESIGNER_USER_NAME }}
|
||||
SSL_ESIGNER_USER_PASSWORD: ${{ secrets.SSL_ESIGNER_USER_PASSWORD }}
|
||||
SSL_ESIGNER_CREDENTIAL_ID: ${{ secrets.SSL_ESIGNER_CREDENTIAL_ID }}
|
||||
SSL_ESIGNER_USER_TOTP: ${{ secrets.SSL_ESIGNER_USER_TOTP }}
|
||||
SIGN_APPLICATION: 1
|
||||
# To ensure that the operations stop on failure, all commands
|
||||
# should be on one line with "&&" in between.
|
||||
run: |
|
||||
@@ -169,7 +188,7 @@ jobs:
|
||||
- name: Check HTTP request
|
||||
run: |
|
||||
# Need to pass environment variables:
|
||||
docker run -p 22300:22300 joplin/server:$(dpkg --print-architecture)-0.0.0 node dist/app.js --env dev &
|
||||
docker run --env MAX_TIME_DRIFT=0 --publish 22300:22300 joplin/server:$(dpkg --print-architecture)-0.0.0 node dist/app.js --env dev &
|
||||
|
||||
# Wait for server to start
|
||||
sleep 120
|
||||
@@ -195,5 +214,4 @@ jobs:
|
||||
if [[ "$actual_body" != "$expected_body" ]]; then
|
||||
echo 'Failed while checking the body response after request to /api/ping'
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
fi
|
||||
26
.gitignore
vendored
26
.gitignore
vendored
@@ -243,6 +243,7 @@ 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/CodeMirror/v6/utils/useSyncEditorValue.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/PlainEditor/PlainEditor.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js
|
||||
@@ -253,6 +254,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/types.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useCursorPositioning.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useEditDialog.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useEditDialogEventListeners.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useKeyboardRefocusHandler.js
|
||||
@@ -294,6 +296,7 @@ packages/app-desktop/gui/NoteEditor/utils/useEffectiveNoteId.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useFolder.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useFormNote.test.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useFormNote.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useInitialCursorLocation.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useMessageHandler.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useNoteSearchBar.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/usePluginEditorView.test.js
|
||||
@@ -577,6 +580,7 @@ packages/app-desktop/tools/generateLatestArm64Yml.js
|
||||
packages/app-desktop/tools/githubReleasesUtils.js
|
||||
packages/app-desktop/tools/modifyReleaseAssets.js
|
||||
packages/app-desktop/tools/notarizeMacApp.js
|
||||
packages/app-desktop/tools/resolveSourceMap.js
|
||||
packages/app-desktop/utils/7zip/getPathToExecutable7Zip.js
|
||||
packages/app-desktop/utils/7zip/pathToBundled7Zip.js
|
||||
packages/app-desktop/utils/checkForUpdatesUtils.test.js
|
||||
@@ -670,6 +674,7 @@ packages/app-mobile/components/NoteEditor/ImageEditor/ImageEditor.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/autosave.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/isEditableResource.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/promptRestoreAutosave.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownEditor.test.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownEditor.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.test.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.js
|
||||
@@ -817,6 +822,7 @@ packages/app-mobile/components/screens/NoteTagsDialog.js
|
||||
packages/app-mobile/components/screens/Notes/NewNoteButton.test.js
|
||||
packages/app-mobile/components/screens/Notes/NewNoteButton.js
|
||||
packages/app-mobile/components/screens/Notes/Notes.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.test.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
|
||||
packages/app-mobile/components/screens/SearchScreen/index.js
|
||||
packages/app-mobile/components/screens/ShareManager/AcceptedShareItem.js
|
||||
@@ -1022,6 +1028,7 @@ packages/editor/CodeMirror/extensions/rendering/types.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/makeBlockReplaceExtension.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/makeInlineReplaceExtension.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/nodeIntersectsSelection.js
|
||||
packages/editor/CodeMirror/extensions/searchExtension.test.js
|
||||
packages/editor/CodeMirror/extensions/searchExtension.js
|
||||
packages/editor/CodeMirror/extensions/selectedNoteIdExtension.js
|
||||
packages/editor/CodeMirror/getScrollFraction.js
|
||||
@@ -1068,8 +1075,10 @@ packages/editor/CodeMirror/utils/markdown/renumberSelectedLists.js
|
||||
packages/editor/CodeMirror/utils/markdown/stripBlockquote.js
|
||||
packages/editor/CodeMirror/utils/markdown/toggleCheckboxAt.js
|
||||
packages/editor/CodeMirror/utils/setupVim.js
|
||||
packages/editor/ProseMirror/commands.test.js
|
||||
packages/editor/ProseMirror/commands.js
|
||||
packages/editor/CodeMirror/vendor/announceSearchMatch.js
|
||||
packages/editor/ProseMirror/commands/commands.test.js
|
||||
packages/editor/ProseMirror/commands/commands.js
|
||||
packages/editor/ProseMirror/commands/focusEditor.js
|
||||
packages/editor/ProseMirror/createEditor.js
|
||||
packages/editor/ProseMirror/index.js
|
||||
packages/editor/ProseMirror/plugins/detailsPlugin.test.js
|
||||
@@ -1088,6 +1097,7 @@ packages/editor/ProseMirror/plugins/linkTooltipPlugin.js
|
||||
packages/editor/ProseMirror/plugins/listPlugin.js
|
||||
packages/editor/ProseMirror/plugins/originalMarkupPlugin.js
|
||||
packages/editor/ProseMirror/plugins/searchPlugin.js
|
||||
packages/editor/ProseMirror/plugins/tablePlugin.js
|
||||
packages/editor/ProseMirror/plugins/utils/createExternalEditorPlugin.js
|
||||
packages/editor/ProseMirror/plugins/utils/createFloatingButtonPlugin.js
|
||||
packages/editor/ProseMirror/schema.js
|
||||
@@ -1117,6 +1127,12 @@ packages/editor/ProseMirror/utils/sanitizeHtml.js
|
||||
packages/editor/ProseMirror/utils/selectFirstInstanceOfNode.js
|
||||
packages/editor/ProseMirror/utils/trimEmptyParagraphs.js
|
||||
packages/editor/ProseMirror/vendor/changedDescendants.js
|
||||
packages/editor/ProseMirror/vendor/icons/addColumnRight.js
|
||||
packages/editor/ProseMirror/vendor/icons/addRowBelow.js
|
||||
packages/editor/ProseMirror/vendor/icons/icon.js
|
||||
packages/editor/ProseMirror/vendor/icons/removeColumn.js
|
||||
packages/editor/ProseMirror/vendor/icons/removeRow.js
|
||||
packages/editor/ProseMirror/vendor/icons/types.js
|
||||
packages/editor/ProseMirror/vendor/splitBlockAs.js
|
||||
packages/editor/SelectionFormatting.js
|
||||
packages/editor/events.js
|
||||
@@ -1385,6 +1401,7 @@ packages/lib/services/database/migrations/45.js
|
||||
packages/lib/services/database/migrations/46.js
|
||||
packages/lib/services/database/migrations/47.js
|
||||
packages/lib/services/database/migrations/48.js
|
||||
packages/lib/services/database/migrations/49.js
|
||||
packages/lib/services/database/migrations/index.js
|
||||
packages/lib/services/database/sqlStringToLines.js
|
||||
packages/lib/services/database/types.js
|
||||
@@ -1508,6 +1525,7 @@ packages/lib/services/plugins/utils/createViewHandle.js
|
||||
packages/lib/services/plugins/utils/executeSandboxCall.js
|
||||
packages/lib/services/plugins/utils/getActivePluginEditorView.js
|
||||
packages/lib/services/plugins/utils/getActivePluginEditorViews.js
|
||||
packages/lib/services/plugins/utils/getPluginHelpUrl.js
|
||||
packages/lib/services/plugins/utils/getPluginIssueReportUrl.test.js
|
||||
packages/lib/services/plugins/utils/getPluginIssueReportUrl.js
|
||||
packages/lib/services/plugins/utils/getPluginNamespacedSettingKey.js
|
||||
@@ -1599,6 +1617,7 @@ packages/lib/services/synchronizer/Synchronizer.sharing.test.js
|
||||
packages/lib/services/synchronizer/Synchronizer.tags.test.js
|
||||
packages/lib/services/synchronizer/Synchronizer.tools.test.js
|
||||
packages/lib/services/synchronizer/gui/useSyncTargetUpgrade.js
|
||||
packages/lib/services/synchronizer/handleConflictAction.test.js
|
||||
packages/lib/services/synchronizer/migrations/1.js
|
||||
packages/lib/services/synchronizer/migrations/2.js
|
||||
packages/lib/services/synchronizer/migrations/3.js
|
||||
@@ -1712,6 +1731,7 @@ packages/plugin-repo-cli/lib/gitCompareUrl.test.js
|
||||
packages/plugin-repo-cli/lib/gitCompareUrl.js
|
||||
packages/plugin-repo-cli/lib/overrideUtils.test.js
|
||||
packages/plugin-repo-cli/lib/overrideUtils.js
|
||||
packages/plugin-repo-cli/lib/searchPlugins.js
|
||||
packages/plugin-repo-cli/lib/types.js
|
||||
packages/plugin-repo-cli/lib/updateReadme.test.js
|
||||
packages/plugin-repo-cli/lib/updateReadme.js
|
||||
@@ -1827,6 +1847,8 @@ packages/tools/updateMarkdownDoc.js
|
||||
packages/tools/utils/discourse.test.js
|
||||
packages/tools/utils/discourse.js
|
||||
packages/tools/utils/loadSponsors.js
|
||||
packages/tools/utils/parsePluralLocalizationForm.js
|
||||
packages/tools/utils/parsePlurallLocalizationForm.test.js
|
||||
packages/tools/utils/translation.js
|
||||
packages/tools/validateFilenames.js
|
||||
packages/tools/website/build.js
|
||||
|
||||
@@ -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://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://topagency.webflow.io"><img title="WebDesignAgency" width="256" src="https://joplinapp.org/images/sponsors/WebDesignAgency.png" alt="topagency"/></a> <a href="https://www.slotozilla.com/nz/no-deposit-bonus"><img title="casino without making any upfront cost" width="256" src="https://joplinapp.org/images/sponsors/Slotozilla.png" alt="casino without making any upfront cost"/></a> <a href="https://writepaper.com/"><img title="best service to write my paper for me" width="256" src="https://joplinapp.org/images/sponsors/WritePaper.png" alt="best service to write my paper for me"/></a> <a href="https://paperwriter.com/"><img title="high-quality paper writing service PaperWriter" width="256" src="https://joplinapp.org/images/sponsors/PaperWriter.png" alt="high-quality paper writing service PaperWriter"/></a> <a href="https://www.bestetf.net/"><img title="BestETF" width="256" src="https://joplinapp.org/images/sponsors/BestEtf.png" alt="BestETF"/></a> <a href="https://freespinny.io/free-spins-no-deposit/"><img title="Freespinny.io Free Spins Bonus site" width="256" src="https://joplinapp.org/images/sponsors/Freespinny.png" alt="Freespinny.io Free Spins Bonus site"/></a> <a href="https://essayshark.com"><img title="EssayShark - essay writers for hire" width="256" src="https://joplinapp.org/images/sponsors/EssayShark.png" alt="EssayShark - essay writers for hire"/></a> <a href="https://pokieslab1.com/real-money-pokies/"><img title="Australian Real Money Pokies" width="256" src="https://joplinapp.org/images/sponsors/PokiesLab.png" alt="Australian Real Money Pokies"/></a> <a href="https://pokiesman1.net/real-money-pokies/"><img title="Australian Real Money Pokies" width="256" src="https://joplinapp.org/images/sponsors/Pokiesman.png" alt="Australian Real Money Pokies"/></a> <a href="https://domyessay.com"><img title="Essay writers DoMyEssay are dedicated to providing top-notch, custom-written papers that meet your academic requirements" width="256" src="https://joplinapp.org/images/sponsors/DoMyEssay.png" alt="DoMyEssay"/></a> <a href="https://essaypro.com/"><img title="best essay writing service" width="256" src="https://joplinapp.org/images/sponsors/EssayPro.png" alt="best essay writing service"/></a> <a href="https://socialkings.online"><img title="Boost your reach and buy real followers" width="256" src="https://joplinapp.org/images/sponsors/SocialKings.png" alt="Boost your reach and buy real followers"/></a> <a href="https://uk.notgamstop.com/bonuses/free-spins-no-deposit-no-gamstop/"><img title="free spins no deposit at NotGamstop" width="256" src="https://joplinapp.org/images/sponsors/NotGamStop.jpg" alt="free spins no deposit at NotGamstop"/></a> <a href="https://www.writemyessay.com/"><img title="writing service for students WriteMyEssay" width="256" src="https://joplinapp.org/images/sponsors/WriteMyEssay.png" alt="writing service for students WriteMyEssay"/></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://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://topagency.webflow.io"><img title="WebDesignAgency" width="256" src="https://joplinapp.org/images/sponsors/WebDesignAgency.png" alt="topagency"/></a> <a href="https://www.slotozilla.com/nz/no-deposit-bonus"><img title="casino without making any upfront cost" width="256" src="https://joplinapp.org/images/sponsors/Slotozilla.png" alt="casino without making any upfront cost"/></a> <a href="https://writepaper.com/"><img title="best service to write my paper for me" width="256" src="https://joplinapp.org/images/sponsors/WritePaper.png" alt="best service to write my paper for me"/></a> <a href="https://paperwriter.com/"><img title="high-quality paper writing service PaperWriter" width="256" src="https://joplinapp.org/images/sponsors/PaperWriter.png" alt="high-quality paper writing service PaperWriter"/></a> <a href="https://www.bestetf.net/"><img title="BestETF" width="256" src="https://joplinapp.org/images/sponsors/BestEtf.png" alt="BestETF"/></a> <a href="https://freespinny.io/free-spins-no-deposit/"><img title="Freespinny.io Free Spins Bonus site" width="256" src="https://joplinapp.org/images/sponsors/Freespinny.png" alt="Freespinny.io Free Spins Bonus site"/></a> <a href="https://essayshark.com"><img title="EssayShark - essay writers for hire" width="256" src="https://joplinapp.org/images/sponsors/EssayShark.png" alt="EssayShark - essay writers for hire"/></a> <a href="https://pokieslab1.com/real-money-pokies/"><img title="Australian Real Money Pokies" width="256" src="https://joplinapp.org/images/sponsors/PokiesLab.png" alt="Australian Real Money Pokies"/></a> <a href="https://pokiesman1.net/real-money-pokies/"><img title="Australian Real Money Pokies" width="256" src="https://joplinapp.org/images/sponsors/Pokiesman.png" alt="Australian Real Money Pokies"/></a> <a href="https://domyessay.com"><img title="Essay writers DoMyEssay are dedicated to providing top-notch, custom-written papers that meet your academic requirements" width="256" src="https://joplinapp.org/images/sponsors/DoMyEssay.png" alt="DoMyEssay"/></a> <a href="https://essaypro.com/"><img title="best essay writing service" width="256" src="https://joplinapp.org/images/sponsors/EssayPro.png" alt="best essay writing service"/></a> <a href="https://socialkings.online"><img title="Boost your reach and buy real followers" width="256" src="https://joplinapp.org/images/sponsors/SocialKings.png" alt="Boost your reach and buy real followers"/></a> <a href="https://uk.notgamstop.com/bonuses/free-spins-no-deposit-no-gamstop/"><img title="free spins no deposit at NotGamstop" width="256" src="https://joplinapp.org/images/sponsors/NotGamStop.jpg" alt="free spins no deposit at NotGamstop"/></a> <a href="https://www.writemyessay.com/"><img title="writing service for students WriteMyEssay" width="256" src="https://joplinapp.org/images/sponsors/WriteMyEssay.png" alt="writing service for students WriteMyEssay"/></a> <a href="https://essayservice.com/"><img title="For those in need of immediate academic assistance, EssayService offers a fast and reliable service to write my essay for me now, ensuring high-quality results within tight deadlines" width="256" src="https://joplinapp.org/images/sponsors/EssayService.png" alt="For those in need of immediate academic assistance, EssayService offers a fast and reliable service to write my essay for me now, ensuring high-quality results within tight deadlines"/></a>
|
||||
<!-- SPONSORS-ORG -->
|
||||
|
||||
* * *
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
"cspell": "5.21.2",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-interactive": "10.8.0",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-jest": "27.9.0",
|
||||
"eslint-plugin-promise": "6.6.0",
|
||||
"eslint-plugin-react": "7.37.5",
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "3.5.0",
|
||||
"version": "3.5.1",
|
||||
"bin": "./main.js",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
@@ -57,7 +57,7 @@
|
||||
"proper-lockfile": "4.1.2",
|
||||
"redux": "4.2.1",
|
||||
"server-destroy": "1.0.1",
|
||||
"sharp": "0.34.2",
|
||||
"sharp": "0.34.3",
|
||||
"sprintf-js": "1.1.3",
|
||||
"sqlite3": "5.1.6",
|
||||
"string-padding": "1.0.2",
|
||||
@@ -73,7 +73,7 @@
|
||||
"@joplin/tools": "~3.5",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/node": "18.19.115",
|
||||
"@types/node": "18.19.130",
|
||||
"@types/proper-lockfile": "^4.1.2",
|
||||
"gulp": "4.0.2",
|
||||
"jest": "29.7.0",
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<en-note>
|
||||
<div>
|
||||
<en-media style="--en-viewerProps:{};" type="image/jpeg" hash="e2d4887c5a32ab1686276c7c5ae733ef" width="1.125in" />
|
||||
</div>
|
||||
<div>
|
||||
<br />
|
||||
</div>
|
||||
</en-note>
|
||||
@@ -0,0 +1,8 @@
|
||||
<en-note>
|
||||
<div>
|
||||
<img src=":/e2d4887c5a32ab1686276c7c5ae733ef" style="--en-viewerProps:{};" type="image/jpeg" hash="e2d4887c5a32ab1686276c7c5ae733ef" width="108" alt="attachment-image" />
|
||||
</div>
|
||||
<div>
|
||||
<br/>
|
||||
</div>
|
||||
</en-note>
|
||||
@@ -1,6 +1,8 @@
|
||||
<en-note>
|
||||
<div><a href=":/21ca2b948f222a38802940ec7e2e5de3" hash="21ca2b948f222a38802940ec7e2e5de3" type="application/pdf" style="cursor:pointer;" alt="attachment-1">attachment-1</a></div>
|
||||
<div>
|
||||
<br>
|
||||
<a href=':/21ca2b948f222a38802940ec7e2e5de3' hash="21ca2b948f222a38802940ec7e2e5de3" type="application/pdf" style="cursor:pointer;" alt="attachment-1"> attachment-1</a>
|
||||
</div>
|
||||
<div>
|
||||
<br/>
|
||||
</div>
|
||||
</en-note>
|
||||
@@ -1,16 +1,11 @@
|
||||
<en-note>
|
||||
<div>
|
||||
<p>For example, consider an exported Evernote list with todo checkboxes like this:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<div><input checked="checked" type="checkbox" onclick="return false;">Foo</div>
|
||||
</li>
|
||||
<li>
|
||||
<div><input type="checkbox" onclick="return false;"><b>Bar</b></div>
|
||||
</li>
|
||||
<li>
|
||||
<div><input type="checkbox" onclick="return false;"><i>Baz</i></div>
|
||||
</li>
|
||||
<li><div><input checked="checked" type="checkbox" onclick="return false;" />Foo</div></li>
|
||||
<li><div><input type="checkbox" onclick="return false;" /><b>Bar</b></div></li>
|
||||
<li><div><input type="checkbox" onclick="return false;" /><i>Baz</i></div></li>
|
||||
</ul>
|
||||
</div>
|
||||
</en-note>
|
||||
@@ -1,19 +1,11 @@
|
||||
<en-note>
|
||||
<div>
|
||||
<p>In Evernote a checklist is not the same as a list with checkboxes.</p>
|
||||
<ul style="--en-todo:true;">
|
||||
<li style="--en-checked:false;">
|
||||
<input type="checkbox" onclick="return false;">
|
||||
<div>One</div>
|
||||
</li>
|
||||
<li style="--en-checked:true;">
|
||||
<input checked="checked" type="checkbox" onclick="return false;">
|
||||
<div>Two</div>
|
||||
</li>
|
||||
<li style="--en-checked:false;">
|
||||
<input type="checkbox" onclick="return false;">
|
||||
<div>Three</div>
|
||||
</li>
|
||||
|
||||
<ul STYLE="--en-todo:true;">
|
||||
<li STYLE="--en-checked:false;"> <input type="checkbox" onclick="return false;" /><div>One</div></li>
|
||||
<li STYLE="--en-checked:true;"> <input checked="checked" type="checkbox" onclick="return false;" /><div>Two</div>
|
||||
</li><li STYLE="--en-checked:false;"> <input type="checkbox" onclick="return false;" /><div>Three</div></li>
|
||||
</ul>
|
||||
</div>
|
||||
</en-note>
|
||||
@@ -1,12 +1 @@
|
||||
<en-note>
|
||||
<div>
|
||||
<audio controls="" preload="none" style="width:480px;">
|
||||
<source src=":/9168ee833d03c5ea7c730ac6673978c1" type="audio/mp4">
|
||||
<p>Your browser does not support HTML5 audio.</p>
|
||||
</audio>
|
||||
<p><a href=":/9168ee833d03c5ea7c730ac6673978c1">audio test</a></p>
|
||||
</div>
|
||||
<div>
|
||||
<br>
|
||||
</div>
|
||||
</en-note>
|
||||
<en-note><div><audio controls preload="none" style="width:480px;"><source src=":/9168ee833d03c5ea7c730ac6673978c1" type="audio/mp4" /><p>Your browser does not support HTML5 audio.</p></audio><p><a href=":/9168ee833d03c5ea7c730ac6673978c1">audio test</a></p></div><div><br/></div></en-note>
|
||||
@@ -1,12 +1 @@
|
||||
<en-note>
|
||||
<div><input type="checkbox" onclick="return false;">This is a test</div>
|
||||
<div><input type="checkbox" onclick="return false;">A test for <span style="font-weight: bold;">bold</span></div>
|
||||
<div>
|
||||
<input type="checkbox" onclick="return false;">A test for <i>italic</i>
|
||||
<br>
|
||||
</div>
|
||||
<div>
|
||||
<br>
|
||||
</div>
|
||||
<div><i><img src=":/89ce7da62c6b2832929a6964237e98e9" hash="89ce7da62c6b2832929a6964237e98e9" type="image/jpeg" alt=""></i></div>
|
||||
</en-note>
|
||||
<en-note><div><input type="checkbox" onclick="return false;" />This is a test</div><div><input type="checkbox" onclick="return false;" />A test for <span STYLE="font-weight: bold;">bold</span></div><div><input type="checkbox" onclick="return false;" />A test for <i>italic</i><br/></div><div><br/></div><div><i><img src=":/89ce7da62c6b2832929a6964237e98e9" hash="89ce7da62c6b2832929a6964237e98e9" type="image/jpeg" alt="" /></i></div></en-note>
|
||||
@@ -1,3 +1,3 @@
|
||||
<en-note>
|
||||
<h1 style="box-sizing:inherit;font-family:"Guardian TextSans Web", "Helvetica Neue", Helvetica, Arial, sans-serif;margin-top:0.2em;margin-bottom:0.35em;font-size:2.125em;font-weight:600;line-height:1.3;">Association Between mRNA Vaccination and COVID-19 Hospitalization and Disease Severity</h1>
|
||||
<h1 STYLE="box-sizing:inherit;font-family:"Guardian TextSans Web", "Helvetica Neue", Helvetica, Arial, sans-serif;margin-top:0.2em;margin-bottom:0.35em;font-size:2.125em;font-weight:600;line-height:1.3;">Association Between mRNA Vaccination and COVID-19 Hospitalization and Disease Severity</h1>
|
||||
</en-note>
|
||||
@@ -1,3 +1,5 @@
|
||||
<en-note>
|
||||
<div><img style="margin:0px;padding:0px;outline:0px;width:74px;height:36px;position:absolute;bottom:-5px;left:0px;transform:translate(0px, 100%);stroke-dasharray:90;transition:stroke-dashoffset 0.5s cubic-bezier(0.97, 0.16, 0.62, 0.76) 0s;stroke-dashoffset:0;" src="data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' data-evernote-id='97' class='js-evernote-checked'%3e%3cuse xlink:href='https://wordminds.com/wp-content/themes/wordminds/assets/img/hint_left.svg%23hint_left' data-evernote-id='98' class='js-evernote-checked'%3e%3c/use%3e%3c/svg%3e"></div>
|
||||
<div>
|
||||
<img STYLE="margin:0px;padding:0px;outline:0px;width:74px;height:36px;position:absolute;bottom:-5px;left:0px;transform:translate(0px, 100%);stroke-dasharray:90;transition:stroke-dashoffset 0.5s cubic-bezier(0.97, 0.16, 0.62, 0.76) 0s;stroke-dashoffset:0;" SRC="data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' data-evernote-id='97' class='js-evernote-checked'%3e%3cuse xlink:href='https://wordminds.com/wp-content/themes/wordminds/assets/img/hint_left.svg%23hint_left' data-evernote-id='98' class='js-evernote-checked'%3e%3c/use%3e%3c/svg%3e"/>
|
||||
</div>
|
||||
</en-note>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE en-export SYSTEM "http://xml.evernote.com/pub/evernote-export4.dtd">
|
||||
<en-export export-date="20230724T173816Z" application="Evernote" version="10.58.8">
|
||||
<note>
|
||||
<title>test.json</title>
|
||||
<content><![CDATA[<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd">
|
||||
<en-note><en-media hash="ac91cc691d21261b222681dd38c1e4ad" type="application/json"/></en-note>
|
||||
]]>
|
||||
</content>
|
||||
<created>20191002T075850Z</created>
|
||||
<updated>20191002T075850Z</updated>
|
||||
<note-attributes><latitude>48.79547119140625</latitude><longitude>9.809423921920198</longitude><altitude>398.0</altitude><author>Laurent</author><source>desktop.mac</source></note-attributes>
|
||||
<resource><data>eyAidGVzdCI6IDEyMyB9</data><mime>application/json</mime><width>0</width><height>0</height><resource-attributes><file-name>test.json</file-name><attachment>false</attachment></resource-attributes></resource></note></en-export>
|
||||
@@ -2,7 +2,7 @@ import PluginRunner from '../../../app/services/plugins/PluginRunner';
|
||||
import PluginService, { PluginSettings, defaultPluginSetting } from '@joplin/lib/services/plugins/PluginService';
|
||||
import { ContentScriptType } from '@joplin/lib/services/plugins/api/types';
|
||||
import MdToHtml from '@joplin/renderer/MdToHtml';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import shim, { MobilePlatform } from '@joplin/lib/shim';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import * as fs from 'fs-extra';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
@@ -310,7 +310,7 @@ describe('services_PluginService', () => {
|
||||
|
||||
let resetPlatformMock = () => {};
|
||||
if (!isDesktop) {
|
||||
resetPlatformMock = mockMobilePlatform('android').reset;
|
||||
resetPlatformMock = mockMobilePlatform(MobilePlatform.Android).reset;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
Binary file not shown.
BIN
packages/app-cli/tests/support/onenote/onenote_desktop.one
Normal file
BIN
packages/app-cli/tests/support/onenote/onenote_desktop.one
Normal file
Binary file not shown.
@@ -52,7 +52,7 @@ describe('app.reducer', () => {
|
||||
...createAppDefaultState({}),
|
||||
backgroundWindows: {
|
||||
testWindow: {
|
||||
...createAppDefaultWindowState(),
|
||||
...createAppDefaultWindowState(null),
|
||||
windowId: 'testWindow',
|
||||
|
||||
visibleDialogs: {
|
||||
|
||||
@@ -26,10 +26,21 @@ export interface AppStateDialog {
|
||||
props: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface EditorScrollPercents {
|
||||
export interface NoteIdToScrollPercent {
|
||||
[noteId: string]: number;
|
||||
}
|
||||
|
||||
type RichTextEditorSelectionBookmark = unknown;
|
||||
|
||||
export interface EditorCursorLocations {
|
||||
readonly richText?: RichTextEditorSelectionBookmark;
|
||||
readonly markdown?: number;
|
||||
}
|
||||
|
||||
export interface NoteIdToEditorCursorLocations {
|
||||
[noteId: string]: EditorCursorLocations;
|
||||
}
|
||||
|
||||
export interface VisibleDialogs {
|
||||
[dialogKey: string]: boolean;
|
||||
}
|
||||
@@ -42,6 +53,9 @@ export interface AppWindowState extends WindowState {
|
||||
devToolsVisible: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
watchedResources: any;
|
||||
|
||||
lastEditorScrollPercents: NoteIdToScrollPercent;
|
||||
lastEditorCursorLocations: NoteIdToEditorCursorLocations;
|
||||
}
|
||||
|
||||
interface BackgroundWindowStates {
|
||||
@@ -55,7 +69,6 @@ export interface AppState extends State, AppWindowState {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
navHistory: any[];
|
||||
watchedNoteFiles: string[];
|
||||
lastEditorScrollPercents: EditorScrollPercents;
|
||||
focusedField: string;
|
||||
layoutMoveMode: boolean;
|
||||
startupPluginsLoaded: boolean;
|
||||
@@ -66,7 +79,7 @@ export interface AppState extends State, AppWindowState {
|
||||
isResettingLayout: boolean;
|
||||
}
|
||||
|
||||
export const createAppDefaultWindowState = (): AppWindowState => {
|
||||
export const createAppDefaultWindowState = (globalState: AppState|null): AppWindowState => {
|
||||
return {
|
||||
...defaultWindowState,
|
||||
visibleDialogs: {},
|
||||
@@ -75,6 +88,12 @@ export const createAppDefaultWindowState = (): AppWindowState => {
|
||||
editorCodeView: true,
|
||||
devToolsVisible: false,
|
||||
watchedResources: {},
|
||||
|
||||
// Maintain the scroll and cursor location for secondary windows separate from the
|
||||
// main window. This prevents scrolling in a secondary window from changing/resetting
|
||||
// the default scroll position in the main window:
|
||||
lastEditorCursorLocations: globalState?.lastEditorCursorLocations ?? {},
|
||||
lastEditorScrollPercents: globalState?.lastEditorScrollPercents ?? {},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -82,7 +101,7 @@ export const createAppDefaultWindowState = (): AppWindowState => {
|
||||
export function createAppDefaultState(resourceEditWatcherDefaultState: any): AppState {
|
||||
return {
|
||||
...defaultState,
|
||||
...createAppDefaultWindowState(),
|
||||
...createAppDefaultWindowState(null),
|
||||
route: {
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Main',
|
||||
@@ -90,7 +109,6 @@ export function createAppDefaultState(resourceEditWatcherDefaultState: any): App
|
||||
},
|
||||
navHistory: [],
|
||||
watchedNoteFiles: [],
|
||||
lastEditorScrollPercents: {},
|
||||
visibleDialogs: {}, // empty object if no dialog is visible. Otherwise contains the list of visible dialogs.
|
||||
focusedField: null,
|
||||
layoutMoveMode: false,
|
||||
@@ -299,6 +317,18 @@ export default function(state: AppState, action: any) {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'EDITOR_CURSOR_POSITION_SET':
|
||||
{
|
||||
newState = { ...state };
|
||||
const newCursorLocations = { ...newState.lastEditorCursorLocations };
|
||||
newCursorLocations[action.noteId] = {
|
||||
...(newCursorLocations[action.noteId] ?? {}),
|
||||
...action.location,
|
||||
};
|
||||
newState.lastEditorCursorLocations = newCursorLocations;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'NOTE_DEVTOOLS_TOGGLE':
|
||||
newState = { ...state };
|
||||
newState.devToolsVisible = !newState.devToolsVisible;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { stateUtils } from '@joplin/lib/reducer';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import { createAppDefaultWindowState } from '../app.reducer';
|
||||
import { AppState, createAppDefaultWindowState } from '../app.reducer';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
@@ -25,7 +25,7 @@ export const runtime = (): CommandRuntime => {
|
||||
folderId: note.parent_id,
|
||||
windowId: `window-${noteId}-${idCounter++}`,
|
||||
defaultAppWindowState: {
|
||||
...createAppDefaultWindowState(),
|
||||
...createAppDefaultWindowState(context.state as AppState),
|
||||
noteVisiblePanes: Setting.value('noteVisiblePanes'),
|
||||
editorCodeView: Setting.value('editor.codeView'),
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ import { PluginManifest } from '@joplin/lib/services/plugins/utils/types';
|
||||
import bridge from '../../../../services/bridge';
|
||||
import { ItemEvent, PluginItem } from '@joplin/lib/components/shared/config/plugins/types';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import getPluginHelpUrl from '@joplin/lib/services/plugins/utils/getPluginHelpUrl';
|
||||
|
||||
export enum InstallState {
|
||||
NotInstalled = 1,
|
||||
@@ -150,9 +151,7 @@ export default function(props: Props) {
|
||||
|
||||
const onNameClick = useCallback(() => {
|
||||
const manifest = item.manifest;
|
||||
const url = manifest.homepage_url ? manifest.homepage_url : manifest.repository_url;
|
||||
if (!url) return;
|
||||
void bridge().openExternal(url);
|
||||
void bridge().openExternal(getPluginHelpUrl(manifest.id));
|
||||
}, [item]);
|
||||
|
||||
const onRecommendedClick = useCallback(() => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import useKeyboardHandler from './DialogButtonRow/useKeyboardHandler';
|
||||
export interface ButtonSpec {
|
||||
name: string;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface ClickEvent {
|
||||
@@ -51,21 +52,29 @@ export default function DialogButtonRow(props: Props) {
|
||||
if (props.onClick) props.onClick(event);
|
||||
}, [props.onClick]);
|
||||
|
||||
const onKeyDown = useKeyboardHandler({ onOkButtonClick, onCancelButtonClick });
|
||||
const okButtonShow = props.okButtonShow ?? true;
|
||||
const cancelButtonShow = props.cancelButtonShow ?? true;
|
||||
const canClickOk = okButtonShow && !props.okButtonDisabled;
|
||||
const canClickCancel = cancelButtonShow && !props.cancelButtonDisabled;
|
||||
|
||||
const onKeyDown = useKeyboardHandler({
|
||||
onOkButtonClick: canClickOk ? onOkButtonClick : null,
|
||||
onCancelButtonClick: canClickCancel ? onCancelButtonClick : null,
|
||||
});
|
||||
|
||||
const buttonComps = [];
|
||||
|
||||
if (props.customButtons) {
|
||||
for (const b of props.customButtons) {
|
||||
buttonComps.push(
|
||||
<button key={b.name} style={buttonStyle} onClick={() => onCustomButtonClick({ buttonName: b.name })} onKeyDown={onKeyDown}>
|
||||
<button key={b.name} style={buttonStyle} onClick={() => onCustomButtonClick({ buttonName: b.name })} disabled={b.disabled} onKeyDown={onKeyDown}>
|
||||
{b.label}
|
||||
</button>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (props.okButtonShow !== false) {
|
||||
if (okButtonShow) {
|
||||
buttonComps.push(
|
||||
<button disabled={props.okButtonDisabled} key="ok" style={buttonStyle} onClick={onOkButtonClick} ref={props.okButtonRef} onKeyDown={onKeyDown}>
|
||||
{props.okButtonLabel ? props.okButtonLabel : _('OK')}
|
||||
@@ -73,7 +82,7 @@ export default function DialogButtonRow(props: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
if (props.cancelButtonShow !== false) {
|
||||
if (cancelButtonShow) {
|
||||
buttonComps.push(
|
||||
<button disabled={props.cancelButtonDisabled} key="cancel" style={{ ...buttonStyle }} onClick={onCancelButtonClick}>
|
||||
{props.cancelButtonLabel ? props.cancelButtonLabel : _('Cancel')}
|
||||
|
||||
@@ -2,11 +2,10 @@ import * as React from 'react';
|
||||
import { useEffect, useState, useRef, useCallback } from 'react';
|
||||
import { isInsideContainer } from '@joplin/lib/dom';
|
||||
|
||||
type OnButtonClick = ()=> void;
|
||||
interface Props {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
onOkButtonClick: Function;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
onCancelButtonClick: Function;
|
||||
onOkButtonClick: null|OnButtonClick;
|
||||
onCancelButtonClick: null|OnButtonClick;
|
||||
}
|
||||
|
||||
const globalKeydownHandlers: string[] = [];
|
||||
@@ -48,15 +47,17 @@ export default (props: Props) => {
|
||||
|
||||
if (!isTopDialog() || isInSubModal(event.target)) return;
|
||||
|
||||
if (event.keyCode === 13) {
|
||||
if (event.keyCode === 13 && props.onOkButtonClick) {
|
||||
if ('nodeName' in event.target && event.target.nodeName === 'INPUT') {
|
||||
const target = event.target as HTMLInputElement;
|
||||
|
||||
if (target.type !== 'button' && target.type !== 'checkbox') {
|
||||
event.preventDefault();
|
||||
props.onOkButtonClick();
|
||||
}
|
||||
}
|
||||
} else if (event.keyCode === 27) {
|
||||
} else if (event.keyCode === 27 && props.onCancelButtonClick) {
|
||||
event.preventDefault();
|
||||
props.onCancelButtonClick();
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
|
||||
@@ -172,7 +172,12 @@ export default function NoteContentPropertiesDialog(props: NoteContentProperties
|
||||
<div style={{ ...labelCompStyle, marginTop: 10 }}>
|
||||
{readTimeLabel}
|
||||
</div>
|
||||
<DialogButtonRow themeId={props.themeId} onClick={buttonRow_click} okButtonShow={false} cancelButtonLabel={_('Close')}/>
|
||||
<DialogButtonRow
|
||||
themeId={props.themeId}
|
||||
onClick={buttonRow_click}
|
||||
okButtonShow={false}
|
||||
cancelButtonLabel={_('Close')}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,36 @@ const logger = Logger.create('useEditorSearch');
|
||||
// Registers a helper CodeMirror extension to be used with
|
||||
// useEditorSearchHandler.
|
||||
|
||||
export default function useEditorSearchExtension(CodeMirror: CodeMirror5Emulation) {
|
||||
interface SetMarkersOptions {
|
||||
selectedIndex: number;
|
||||
searchTimestamp: number;
|
||||
showEditorMarkers?: boolean;
|
||||
withSelection?: boolean;
|
||||
}
|
||||
type Keyword = { value: string };
|
||||
|
||||
export type OnSetMarkers = (cm: CodeMirror5Emulation, keywords: Keyword[], options: SetMarkersOptions)=> number;
|
||||
|
||||
|
||||
// Modified from codemirror/addons/search/search.js
|
||||
const searchOverlay = (query: RegExp) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
return { token: function(stream: any) {
|
||||
query.lastIndex = stream.pos;
|
||||
const match = query.exec(stream.string);
|
||||
if (match && match.index === stream.pos) {
|
||||
stream.pos += match[0].length || 1;
|
||||
return 'search-marker';
|
||||
} else if (match) {
|
||||
stream.pos = match.index;
|
||||
} else {
|
||||
stream.skipToEnd();
|
||||
}
|
||||
return null;
|
||||
} };
|
||||
};
|
||||
|
||||
export default function useEditorSearchExtension() {
|
||||
|
||||
const [markers, setMarkers] = useState([]);
|
||||
const [overlay, setOverlay] = useState(null);
|
||||
@@ -48,23 +77,6 @@ export default function useEditorSearchExtension(CodeMirror: CodeMirror5Emulatio
|
||||
setOverlayTimeout(null);
|
||||
}, [scrollbarMarks, overlay, overlayTimeout]);
|
||||
|
||||
// Modified from codemirror/addons/search/search.js
|
||||
const searchOverlay = useCallback((query: RegExp) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
return { token: function(stream: any) {
|
||||
query.lastIndex = stream.pos;
|
||||
const match = query.exec(stream.string);
|
||||
if (match && match.index === stream.pos) {
|
||||
stream.pos += match[0].length || 1;
|
||||
return 'search-marker';
|
||||
} else if (match) {
|
||||
stream.pos = match.index;
|
||||
} else {
|
||||
stream.skipToEnd();
|
||||
}
|
||||
return null;
|
||||
} };
|
||||
}, []);
|
||||
|
||||
// Highlights the currently active found work
|
||||
// It's possible to get tricky with this functions and just use findNext/findPrev
|
||||
@@ -115,16 +127,17 @@ export default function useEditorSearchExtension(CodeMirror: CodeMirror5Emulatio
|
||||
};
|
||||
}, []);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
CodeMirror?.defineExtension('setMarkers', function(keywords: any, options: any) {
|
||||
const onSetMarkers: OnSetMarkers = (cm, keywords, options) => {
|
||||
// Pass arguments in via options to allow the extension to work if multiple editors are open simultaneously
|
||||
// See https://github.com/laurent22/joplin/issues/13399.
|
||||
if (!options) {
|
||||
options = { selectedIndex: 0, searchTimestamp: 0 };
|
||||
}
|
||||
|
||||
if (options.showEditorMarkers === false) {
|
||||
clearMarkers();
|
||||
clearOverlay(this);
|
||||
return;
|
||||
clearOverlay(cm);
|
||||
return 0;
|
||||
}
|
||||
|
||||
clearMarkers();
|
||||
@@ -145,7 +158,7 @@ export default function useEditorSearchExtension(CodeMirror: CodeMirror5Emulatio
|
||||
const scrollTo = i === 0 && (previousKeywordValue !== keyword.value || previousIndex !== options.selectedIndex || options.searchTimestamp !== previousSearchTimestamp);
|
||||
|
||||
try {
|
||||
const match = highlightSearch(this, searchTerm, options.selectedIndex, scrollTo, !!options.withSelection);
|
||||
const match = highlightSearch(cm, searchTerm, options.selectedIndex, scrollTo, !!options.withSelection);
|
||||
if (match) marks.push(match);
|
||||
} catch (error) {
|
||||
if (error.name !== 'SyntaxError') {
|
||||
@@ -165,7 +178,7 @@ export default function useEditorSearchExtension(CodeMirror: CodeMirror5Emulatio
|
||||
// SEARCHOVERLAY
|
||||
// We only want to highlight all matches when there is only 1 search term
|
||||
if (keywords.length !== 1 || keywords[0].value === '') {
|
||||
clearOverlay(this);
|
||||
clearOverlay(cm);
|
||||
const prev = keywords.length > 1 ? keywords[0].value : '';
|
||||
setPreviousKeywordValue(prev);
|
||||
return 0;
|
||||
@@ -175,22 +188,22 @@ export default function useEditorSearchExtension(CodeMirror: CodeMirror5Emulatio
|
||||
|
||||
// Determine the number of matches in the source, this is passed on
|
||||
// to the NoteEditor component
|
||||
const regexMatches = this.getValue().match(searchTerm);
|
||||
const regexMatches = cm.getValue().match(searchTerm);
|
||||
const nMatches = regexMatches ? regexMatches.length : 0;
|
||||
|
||||
// Don't bother clearing and re-calculating the overlay if the search term
|
||||
// hasn't changed
|
||||
if (keywords[0].value === previousKeywordValue) return nMatches;
|
||||
|
||||
clearOverlay(this);
|
||||
clearOverlay(cm);
|
||||
setPreviousKeywordValue(keywords[0].value);
|
||||
|
||||
// These operations are pretty slow, so we won't add use them until the user
|
||||
// has finished typing, 500ms is probably enough time
|
||||
const timeout = shim.setTimeout(() => {
|
||||
const scrollMarks = this.showMatchesOnScrollbar?.(searchTerm, true, 'cm-search-marker-scrollbar');
|
||||
const scrollMarks = cm.showMatchesOnScrollbar?.(searchTerm, true, 'cm-search-marker-scrollbar');
|
||||
const overlay = searchOverlay(searchTerm);
|
||||
this.addOverlay(overlay);
|
||||
cm.addOverlay(overlay);
|
||||
setOverlay(overlay);
|
||||
setScrollbarMarks(scrollMarks);
|
||||
}, 500);
|
||||
@@ -199,5 +212,9 @@ export default function useEditorSearchExtension(CodeMirror: CodeMirror5Emulatio
|
||||
overlayTimeoutRef.current = timeout;
|
||||
|
||||
return nMatches;
|
||||
});
|
||||
};
|
||||
const onSetMarkersRef = useRef(onSetMarkers);
|
||||
onSetMarkersRef.current = onSetMarkers;
|
||||
|
||||
return { onSetMarkersRef };
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { RefObject, useEffect, useMemo, useRef } from 'react';
|
||||
import usePrevious from '../../../../hooks/usePrevious';
|
||||
import { RenderedBody } from './types';
|
||||
import { SearchMarkers } from '../../../utils/useSearchMarkers';
|
||||
import CodeMirror5Emulation from '@joplin/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation';
|
||||
import useEditorSearchExtension from './useEditorSearchExtension';
|
||||
const debounce = require('debounce');
|
||||
|
||||
interface Props {
|
||||
@@ -10,8 +12,7 @@ interface Props {
|
||||
searchMarkers: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
webviewRef: RefObject<any>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
editorRef: RefObject<any>;
|
||||
editorRef: RefObject<CodeMirror5Emulation>;
|
||||
|
||||
noteContent: string;
|
||||
renderedBody: RenderedBody;
|
||||
@@ -23,6 +24,8 @@ const useEditorSearchHandler = (props: Props) => {
|
||||
webviewRef, editorRef, renderedBody, noteContent, searchMarkers, showEditorMarkers,
|
||||
} = props;
|
||||
|
||||
const { onSetMarkersRef } = useEditorSearchExtension();
|
||||
|
||||
const previousContent = usePrevious(noteContent);
|
||||
const previousRenderedBody = usePrevious(renderedBody);
|
||||
const previousSearchMarkers = usePrevious(searchMarkers);
|
||||
@@ -31,15 +34,15 @@ const useEditorSearchHandler = (props: Props) => {
|
||||
|
||||
// Fixes https://github.com/laurent22/joplin/issues/7565
|
||||
const debouncedMarkers = useMemo(() => debounce((searchMarkers: SearchMarkers) => {
|
||||
if (!editorRef.current) return;
|
||||
if (!onSetMarkersRef.current) return;
|
||||
|
||||
if (showEditorMarkersRef.current) {
|
||||
const matches = editorRef.current.setMarkers(searchMarkers.keywords, searchMarkers.options);
|
||||
const matches = onSetMarkersRef.current(editorRef.current, searchMarkers.keywords, searchMarkers.options);
|
||||
props.setLocalSearchResultCount(matches);
|
||||
} else {
|
||||
editorRef.current.setMarkers(searchMarkers.keywords, { ...searchMarkers.options, showEditorMarkers: false });
|
||||
onSetMarkersRef.current(editorRef.current, searchMarkers.keywords, { ...searchMarkers.options, showEditorMarkers: false });
|
||||
}
|
||||
}, 50), [editorRef, props.setLocalSearchResultCount]);
|
||||
}, 50), [editorRef, onSetMarkersRef, props.setLocalSearchResultCount]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!searchMarkers) return () => {};
|
||||
@@ -59,7 +62,7 @@ const useEditorSearchHandler = (props: Props) => {
|
||||
}
|
||||
return () => {};
|
||||
}, [
|
||||
editorRef,
|
||||
onSetMarkersRef,
|
||||
webviewRef,
|
||||
searchMarkers,
|
||||
previousSearchMarkers,
|
||||
@@ -71,6 +74,10 @@ const useEditorSearchHandler = (props: Props) => {
|
||||
debouncedMarkers,
|
||||
]);
|
||||
|
||||
return {
|
||||
// Returned to allow quickly setting the initial search markers just after the editor loads.
|
||||
onSetInitialMarkersRef: onSetMarkersRef,
|
||||
};
|
||||
};
|
||||
|
||||
export default useEditorSearchHandler;
|
||||
|
||||
@@ -695,7 +695,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [renderedBody, webviewReady]);
|
||||
|
||||
useEditorSearchHandler({
|
||||
const { onSetInitialMarkersRef } = useEditorSearchHandler({
|
||||
setLocalSearchResultCount: props.setLocalSearchResultCount,
|
||||
searchMarkers: props.searchMarkers,
|
||||
webviewRef,
|
||||
@@ -737,6 +737,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
<Editor
|
||||
value={props.content}
|
||||
searchMarkers={props.searchMarkers}
|
||||
onSetMarkersRef={onSetInitialMarkersRef}
|
||||
ref={editorRef}
|
||||
mode={props.contentMarkupLanguage === MarkupToHtml.MARKUP_LANGUAGE_HTML ? 'xml' : 'joplin-markdown'}
|
||||
codeMirrorTheme={styles.editor.codeMirrorTheme}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useEffect, useImperativeHandle, useState, useRef, useCallback, forwardRef } from 'react';
|
||||
import { useEffect, useImperativeHandle, useState, useRef, useCallback, forwardRef, RefObject } from 'react';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
|
||||
import CodeMirror from 'codemirror';
|
||||
@@ -16,7 +16,7 @@ import useListIdent from './utils/useListIdent';
|
||||
import useScrollUtils from './utils/useScrollUtils';
|
||||
import useCursorUtils from './utils/useCursorUtils';
|
||||
import useLineSorting from './utils/useLineSorting';
|
||||
import useEditorSearch from '../utils/useEditorSearchExtension';
|
||||
import { OnSetMarkers } from '../utils/useEditorSearchExtension';
|
||||
import useJoplinMode from './utils/useJoplinMode';
|
||||
import useKeymap from './utils/useKeymap';
|
||||
import useExternalPlugins from './utils/useExternalPlugins';
|
||||
@@ -77,6 +77,7 @@ export interface EditorProps {
|
||||
value: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
searchMarkers: any;
|
||||
onSetMarkersRef: RefObject<OnSetMarkers>;
|
||||
mode: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
style: any;
|
||||
@@ -119,7 +120,6 @@ function Editor(props: EditorProps, ref: any) {
|
||||
useScrollUtils(CodeMirror);
|
||||
useCursorUtils(CodeMirror);
|
||||
useLineSorting(CodeMirror);
|
||||
useEditorSearch(CodeMirror);
|
||||
useJoplinMode(CodeMirror);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const pluginOptions: any = useExternalPlugins(CodeMirror, props.plugins);
|
||||
@@ -228,7 +228,7 @@ function Editor(props: EditorProps, ref: any) {
|
||||
// It's possible for searchMarkers to be available before the editor
|
||||
// In these cases we set the markers asap so the user can see them as
|
||||
// soon as the editor is ready
|
||||
if (props.searchMarkers) { cm.setMarkers(props.searchMarkers.keywords, props.searchMarkers.options); }
|
||||
if (props.searchMarkers) { props.onSetMarkersRef.current(cm, props.searchMarkers.keywords, props.searchMarkers.options); }
|
||||
|
||||
return () => {
|
||||
// Clean up codemirror
|
||||
|
||||
@@ -31,6 +31,7 @@ import CommandService from '@joplin/lib/services/CommandService';
|
||||
import useRefocusOnVisiblePaneChange from './utils/useRefocusOnVisiblePaneChange';
|
||||
import { WindowIdContext } from '../../../../NewWindowOrIFrame';
|
||||
import eventManager, { EventName, ResourceChangeEvent } from '@joplin/lib/eventManager';
|
||||
import useSyncEditorValue from './utils/useSyncEditorValue';
|
||||
|
||||
const logger = Logger.create('CodeMirror6');
|
||||
const logDebug = (message: string) => logger.debug(message);
|
||||
@@ -167,9 +168,8 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
},
|
||||
scrollTo: (options: ScrollOptions) => {
|
||||
if (options.type === ScrollOptionTypes.Hash) {
|
||||
if (!webviewRef.current) return;
|
||||
const hash: string = options.value;
|
||||
webviewRef.current.send('scrollToHash', hash);
|
||||
webviewRef.current?.send('scrollToHash', hash);
|
||||
editorRef.current.jumpToHash(hash);
|
||||
} else if (options.type === ScrollOptionTypes.Percent) {
|
||||
const percent = options.value as number;
|
||||
@@ -342,6 +342,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
} else if (event.kind === EditorEventType.Change) {
|
||||
codeMirror_change(event.value);
|
||||
} else if (event.kind === EditorEventType.SelectionRangeChange) {
|
||||
props.onCursorMotion({ markdown: event.from });
|
||||
setSelectionRange({ from: event.from, to: event.to });
|
||||
} else if (event.kind === EditorEventType.UpdateSearchDialog) {
|
||||
if (lastSearchState.current?.searchText !== event.searchState.searchText) {
|
||||
@@ -355,7 +356,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
} else if (event.kind === EditorEventType.FollowLink) {
|
||||
void CommandService.instance().execute('openItem', event.link);
|
||||
}
|
||||
}, [editor_scroll, codeMirror_change, props.setLocalSearch, props.setShowLocalSearch]);
|
||||
}, [editor_scroll, codeMirror_change, props.setLocalSearch, props.setShowLocalSearch, props.onCursorMotion]);
|
||||
|
||||
const onSelectPastBeginning = useCallback(() => {
|
||||
void CommandService.instance().execute('focusElement', 'noteTitle');
|
||||
@@ -400,15 +401,17 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
props.tabMovesFocus,
|
||||
]);
|
||||
|
||||
// Update the editor's value
|
||||
useEffect(() => {
|
||||
// Include the noteId in the update props to give plugins access
|
||||
// to the current note ID.
|
||||
const updateProps = { noteId: props.noteId };
|
||||
if (editorRef.current?.updateBody(props.content, updateProps)) {
|
||||
editorRef.current?.clearHistory();
|
||||
}
|
||||
}, [props.content, props.noteId]);
|
||||
const initialCursorLocationRef = useRef(0);
|
||||
initialCursorLocationRef.current = props.initialCursorLocation.markdown ?? 0;
|
||||
|
||||
useSyncEditorValue({
|
||||
content: props.content,
|
||||
visiblePanes: props.visiblePanes,
|
||||
onMessage: props.onMessage,
|
||||
editorRef,
|
||||
noteId: props.noteId,
|
||||
initialCursorLocationRef,
|
||||
});
|
||||
|
||||
const renderEditor = () => {
|
||||
return (
|
||||
@@ -416,6 +419,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
<Editor
|
||||
style={styles.editor}
|
||||
initialText={props.content}
|
||||
initialSelectionRef={initialCursorLocationRef}
|
||||
initialNoteId={props.noteId}
|
||||
ref={editorRef}
|
||||
settings={editorSettings}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { ForwardedRef } from 'react';
|
||||
import { ForwardedRef, RefObject } from 'react';
|
||||
import { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
import { EditorProps, LogMessageCallback, OnEventCallback, ContentScriptData } from '@joplin/editor/types';
|
||||
import createEditor from '@joplin/editor/CodeMirror/createEditor';
|
||||
@@ -11,7 +11,6 @@ import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import setupVim from '@joplin/editor/CodeMirror/utils/setupVim';
|
||||
import { dirname } from 'path';
|
||||
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';
|
||||
@@ -23,6 +22,7 @@ import getResourceBaseUrl from '../../../utils/getResourceBaseUrl';
|
||||
interface Props extends EditorProps {
|
||||
style: React.CSSProperties;
|
||||
pluginStates: PluginStates;
|
||||
initialSelectionRef: RefObject<number>;
|
||||
|
||||
onEditorPaste: (event: Event)=> void;
|
||||
externalSearch: SearchMarkers;
|
||||
@@ -43,8 +43,6 @@ const Editor = (props: Props, ref: ForwardedRef<CodeMirrorControl>) => {
|
||||
onLogMessageRef.current = props.onLogMessage;
|
||||
}, [props.onEvent, props.onLogMessage]);
|
||||
|
||||
useEditorSearch(editor);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) {
|
||||
return () => {};
|
||||
@@ -127,6 +125,9 @@ const Editor = (props: Props, ref: ForwardedRef<CodeMirrorControl>) => {
|
||||
direction: 'unset',
|
||||
},
|
||||
});
|
||||
const cursor = props.initialSelectionRef.current;
|
||||
editor.select(cursor, cursor);
|
||||
|
||||
setEditor(editor);
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { useEffect, useRef, RefObject } from 'react';
|
||||
import { OnMessage } from '../../../../utils/types';
|
||||
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
|
||||
|
||||
interface Props {
|
||||
content: string;
|
||||
|
||||
visiblePanes: string[];
|
||||
onMessage: OnMessage;
|
||||
editorRef: RefObject<CodeMirrorControl>;
|
||||
noteId: string;
|
||||
initialCursorLocationRef: RefObject<number>;
|
||||
}
|
||||
|
||||
// Updates the editor's value as necessary
|
||||
const useSyncEditorValue = ({ content, visiblePanes, onMessage, editorRef, noteId, initialCursorLocationRef }: Props) => {
|
||||
const visiblePanesRef = useRef(visiblePanes);
|
||||
visiblePanesRef.current = visiblePanes;
|
||||
const onMessageRef = useRef(onMessage);
|
||||
onMessageRef.current = onMessage;
|
||||
|
||||
const lastNoteIdRef = useRef(noteId);
|
||||
|
||||
useEffect(() => {
|
||||
// Include the noteId in the update props to give plugins access
|
||||
// to the current note ID.
|
||||
const updateProps = { noteId: noteId };
|
||||
if (editorRef.current?.updateBody(content, updateProps)) {
|
||||
editorRef.current?.clearHistory();
|
||||
|
||||
// Only reset the cursor location when switching notes. If, for example,
|
||||
// the note is updated from a secondary window, the cursor location shouldn't
|
||||
// reset.
|
||||
const noteChanged = lastNoteIdRef.current !== noteId;
|
||||
if (noteChanged) {
|
||||
const cursorLocation = initialCursorLocationRef.current;
|
||||
editorRef.current?.select(cursorLocation, cursorLocation);
|
||||
}
|
||||
lastNoteIdRef.current = noteId;
|
||||
|
||||
// If the viewer isn't visible, the content should be considered rendered
|
||||
// after the editor has finished updating:
|
||||
if (!visiblePanesRef.current.includes('viewer')) {
|
||||
onMessageRef.current({ channel: 'noteRenderComplete' });
|
||||
}
|
||||
}
|
||||
}, [content, noteId, editorRef, initialCursorLocationRef]);
|
||||
};
|
||||
|
||||
export default useSyncEditorValue;
|
||||
@@ -23,7 +23,7 @@ import { themeStyle } from '@joplin/lib/theme';
|
||||
import { loadScript } from '../../../utils/loadScript';
|
||||
import bridge from '../../../../services/bridge';
|
||||
import { TinyMceEditorEvents } from './utils/types';
|
||||
import type { Editor, EditorEvent } from 'tinymce';
|
||||
import type { Bookmark, Editor, EditorEvent } from 'tinymce';
|
||||
import { joplinCommandToTinyMceCommands, TinyMceCommand } from './utils/joplinCommandToTinyMceCommands';
|
||||
import shouldPasteResources from './utils/shouldPasteResources';
|
||||
import lightTheme from '@joplin/lib/themes/light';
|
||||
@@ -47,6 +47,7 @@ import Setting from '@joplin/lib/models/Setting';
|
||||
import useTextPatternsLookup, { TextPatternContext } from './utils/useTextPatternsLookup';
|
||||
import { toFileProtocolPath } from '@joplin/utils/path';
|
||||
import { RenderResultPluginAsset } from '@joplin/renderer/types';
|
||||
import useCursorPositioning from './utils/useCursorPositioning';
|
||||
|
||||
const logger = Logger.create('TinyMCE');
|
||||
|
||||
@@ -918,8 +919,6 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
||||
|
||||
editor.on('SetContent', () => {
|
||||
preprocessContent();
|
||||
|
||||
props_onMessage.current({ channel: 'noteRenderComplete' });
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -1046,6 +1045,12 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { onRestoreCursorPosition } = useCursorPositioning({
|
||||
initialCursorLocation: props.initialCursorLocation.richText as Bookmark,
|
||||
onCursorUpdate: props.onCursorMotion,
|
||||
editor,
|
||||
});
|
||||
|
||||
const lastNoteIdRef = useRef(props.noteId);
|
||||
useEffect(() => {
|
||||
if (!editor) return () => {};
|
||||
@@ -1113,8 +1118,12 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
||||
// times would result in an empty note.
|
||||
// https://github.com/laurent22/joplin/issues/3534
|
||||
editor.undoManager.reset();
|
||||
|
||||
// Only restore the cursor position from the global state when switching notes.
|
||||
// See https://github.com/laurent22/joplin/issues/13579
|
||||
onRestoreCursorPosition();
|
||||
} else {
|
||||
// Restore the cursor location
|
||||
// Restore the cursor location from the current note
|
||||
editor.selection.bookmarkManager.moveToBookmark(bookmark);
|
||||
}
|
||||
|
||||
@@ -1123,6 +1132,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
||||
resourceInfos: props.resourceInfos,
|
||||
contentKey: props.contentKey,
|
||||
};
|
||||
props_onMessage.current({ channel: 'noteRenderComplete' });
|
||||
}
|
||||
|
||||
const allAssetsOptions: NoteStyleOptions = {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { Bookmark, Editor } from 'tinymce';
|
||||
import { OnCursorMotion } from '../../../utils/types';
|
||||
|
||||
interface Props {
|
||||
initialCursorLocation: Bookmark;
|
||||
editor: Editor;
|
||||
onCursorUpdate: OnCursorMotion;
|
||||
}
|
||||
|
||||
const useCursorPositioning = ({ initialCursorLocation, editor, onCursorUpdate }: Props) => {
|
||||
const initialCursorLocationRef = useRef(initialCursorLocation);
|
||||
initialCursorLocationRef.current = initialCursorLocation;
|
||||
|
||||
const appliedInitialCursorLocationRef = useRef(false);
|
||||
const onRestoreCursorPosition = useCallback(() => {
|
||||
if (editor) {
|
||||
if (initialCursorLocationRef.current) {
|
||||
editor.selection.moveToBookmark(initialCursorLocationRef.current);
|
||||
}
|
||||
|
||||
appliedInitialCursorLocationRef.current = true;
|
||||
}
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return () => {};
|
||||
|
||||
const onSelectionChange = () => {
|
||||
// Wait until the initial cursor position has been set. This avoids resetting
|
||||
// the initial cursor position to zero when the editor first loads.
|
||||
if (!appliedInitialCursorLocationRef.current) return;
|
||||
|
||||
// Use an offset bookmark -- the default bookmark type is not preserved after unloading
|
||||
// and reloading the editor.
|
||||
const offsetBookmarkId = 2;
|
||||
onCursorUpdate({
|
||||
richText: editor.selection.getBookmark(offsetBookmarkId, true),
|
||||
});
|
||||
};
|
||||
|
||||
editor.on('SelectionChange', onSelectionChange);
|
||||
|
||||
return () => {
|
||||
editor.off('SelectionChange', onSelectionChange);
|
||||
};
|
||||
}, [editor, onCursorUpdate, onRestoreCursorPosition]);
|
||||
|
||||
return { onRestoreCursorPosition };
|
||||
};
|
||||
|
||||
export default useCursorPositioning;
|
||||
@@ -18,7 +18,7 @@ import { NoteEditorProps, FormNote, OnChangeEvent, AllAssetsOptions, NoteBodyEdi
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import Button, { ButtonLevel } from '../Button/Button';
|
||||
import eventManager, { EventName } from '@joplin/lib/eventManager';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { AppState, EditorCursorLocations } from '../../app.reducer';
|
||||
import ToolbarButtonUtils, { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||
import { _, _n } from '@joplin/lib/locale';
|
||||
import NoteTitleBar from './NoteTitle/NoteTitleBar';
|
||||
@@ -57,6 +57,7 @@ import StatusBar from './StatusBar';
|
||||
import useVisiblePluginEditorViewIds from '@joplin/lib/hooks/plugins/useVisiblePluginEditorViewIds';
|
||||
import useConnectToEditorPlugin from './utils/useConnectToEditorPlugin';
|
||||
import getResourceBaseUrl from './utils/getResourceBaseUrl';
|
||||
import useInitialCursorLocation from './utils/useInitialCursorLocation';
|
||||
|
||||
const debounce = require('debounce');
|
||||
|
||||
@@ -329,13 +330,14 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
});
|
||||
}, [formNote, setFormNote, handleProvisionalFlag, props.dispatch]);
|
||||
|
||||
const { scrollWhenReady, clearScrollWhenReady } = useScrollWhenReadyOptions({
|
||||
const { scrollWhenReadyRef, clearScrollWhenReady } = useScrollWhenReadyOptions({
|
||||
noteId: formNote.id,
|
||||
selectedNoteHash: props.selectedNoteHash,
|
||||
lastEditorScrollPercents: props.lastEditorScrollPercents,
|
||||
editorRef,
|
||||
editorName: props.bodyEditor,
|
||||
});
|
||||
const onMessage = useMessageHandler(scrollWhenReady, clearScrollWhenReady, windowId, editorRef, setLocalSearchResultCount, props.dispatch, formNote, htmlToMarkdown, markupToHtml);
|
||||
const onMessage = useMessageHandler(scrollWhenReadyRef, clearScrollWhenReady, windowId, editorRef, setLocalSearchResultCount, props.dispatch, formNote, htmlToMarkdown, markupToHtml);
|
||||
|
||||
useResourceUnwatcher({ noteId: formNote.id, windowId });
|
||||
|
||||
@@ -409,6 +411,14 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
});
|
||||
}, [props.dispatch]);
|
||||
|
||||
const onCursorMotion = useCallback((location: EditorCursorLocations) => {
|
||||
props.dispatch({
|
||||
type: 'EDITOR_CURSOR_POSITION_SET',
|
||||
noteId: formNoteRef.current.id,
|
||||
location,
|
||||
});
|
||||
}, [props.dispatch]);
|
||||
|
||||
function renderNoNotes(rootStyle: React.CSSProperties) {
|
||||
const emptyDivStyle = {
|
||||
backgroundColor: 'black',
|
||||
@@ -419,6 +429,9 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
}
|
||||
|
||||
const searchMarkers = useSearchMarkers(showLocalSearch, localSearchMarkerOptions, props.searches, props.selectedSearchId, props.highlightedWords);
|
||||
const initialCursorLocation = useInitialCursorLocation({
|
||||
lastEditorCursorLocations: props.lastEditorCursorLocations, noteId: props.noteId,
|
||||
});
|
||||
|
||||
const markupLanguage = formNote.markup_language;
|
||||
const editorProps: NoteBodyEditorPropsAndRef = {
|
||||
@@ -432,6 +445,7 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
content: formNote.body,
|
||||
contentMarkupLanguage: markupLanguage,
|
||||
contentOriginalCss: formNote.originalCss,
|
||||
initialCursorLocation,
|
||||
resourceInfos: resourceInfos,
|
||||
resourceDirectory: Setting.value('resourceDir'),
|
||||
htmlToMarkdown: htmlToMarkdown,
|
||||
@@ -442,6 +456,7 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
dispatch: props.dispatch,
|
||||
noteToolbar: null,
|
||||
onScroll: onScroll,
|
||||
onCursorMotion,
|
||||
setLocalSearchResultCount: setLocalSearchResultCount,
|
||||
setLocalSearch: localSearch_change,
|
||||
setShowLocalSearch,
|
||||
@@ -729,6 +744,7 @@ const mapStateToProps = (state: AppState, ownProps: ConnectProps) => {
|
||||
notesParentType: windowState.notesParentType,
|
||||
selectedNoteTags: windowState.selectedNoteTags,
|
||||
lastEditorScrollPercents: state.lastEditorScrollPercents,
|
||||
lastEditorCursorLocations: state.lastEditorCursorLocations,
|
||||
selectedNoteHash: windowState.selectedNoteHash,
|
||||
searches: state.searches,
|
||||
selectedSearchId: windowState.selectedSearchId,
|
||||
|
||||
@@ -14,6 +14,7 @@ import { ScrollbarSize } from '@joplin/lib/models/settings/builtInMetadata';
|
||||
import { RefObject, SetStateAction } from 'react';
|
||||
import * as React from 'react';
|
||||
import { ResourceEntity, ResourceLocalStateEntity } from '@joplin/lib/services/database/types';
|
||||
import { EditorCursorLocations, NoteIdToEditorCursorLocations, NoteIdToScrollPercent } from '../../../app.reducer';
|
||||
|
||||
export interface AllAssetsOptions {
|
||||
contentMaxWidthTarget?: string;
|
||||
@@ -40,8 +41,8 @@ export interface NoteEditorProps {
|
||||
notesParentType: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
selectedNoteTags: any[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
lastEditorScrollPercents: any;
|
||||
lastEditorScrollPercents: NoteIdToScrollPercent;
|
||||
lastEditorCursorLocations: NoteIdToEditorCursorLocations;
|
||||
selectedNoteHash: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
searches: any[];
|
||||
@@ -83,6 +84,14 @@ export interface NoteBodyEditorRef {
|
||||
export { MarkupToHtmlOptions };
|
||||
export type MarkupToHtmlHandler = (markupLanguage: MarkupLanguage, markup: string, options: MarkupToHtmlOptions)=> Promise<RenderResult>;
|
||||
export type HtmlToMarkdownHandler = (markupLanguage: number, html: string, originalCss: string, parseOptions?: ParseOptions)=> Promise<string>;
|
||||
export type OnCursorMotion = (event: EditorCursorLocations)=> void;
|
||||
|
||||
export interface MessageEvent {
|
||||
channel: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Partially refactored old code before rule was applied
|
||||
args?: any[];
|
||||
}
|
||||
export type OnMessage = (event: MessageEvent)=> void;
|
||||
|
||||
export interface NoteBodyEditorProps {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
@@ -102,12 +111,13 @@ export interface NoteBodyEditorProps {
|
||||
contentKey: string;
|
||||
contentMarkupLanguage: number;
|
||||
contentOriginalCss: string;
|
||||
initialCursorLocation: EditorCursorLocations;
|
||||
onChange(event: OnChangeEvent): void;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
onWillChange(event: any): void;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
onMessage(event: any): void;
|
||||
onMessage: OnMessage;
|
||||
onScroll(event: { percent: number }): void;
|
||||
onCursorMotion: OnCursorMotion;
|
||||
markupToHtml: MarkupToHtmlHandler;
|
||||
htmlToMarkdown: HtmlToMarkdownHandler;
|
||||
allAssets: (markupLanguage: MarkupLanguage, options: AllAssetsOptions)=> Promise<RenderResultPluginAsset[]>;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useMemo } from 'react';
|
||||
import { EditorCursorLocations, NoteIdToEditorCursorLocations } from '../../../app.reducer';
|
||||
|
||||
interface Props {
|
||||
lastEditorCursorLocations: NoteIdToEditorCursorLocations;
|
||||
noteId: string;
|
||||
}
|
||||
|
||||
const useInitialCursorLocation = ({ noteId, lastEditorCursorLocations }: Props) => {
|
||||
const lastCursorLocation = lastEditorCursorLocations[noteId];
|
||||
|
||||
return useMemo((): EditorCursorLocations => {
|
||||
return lastCursorLocation ?? { };
|
||||
}, [lastCursorLocation]);
|
||||
};
|
||||
|
||||
export default useInitialCursorLocation;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback } from 'react';
|
||||
import { FormNote, HtmlToMarkdownHandler, MarkupToHtmlHandler, ScrollOptions } from './types';
|
||||
import { RefObject, useCallback } from 'react';
|
||||
import { FormNote, HtmlToMarkdownHandler, MarkupToHtmlHandler, ScrollOptions, MessageEvent } from './types';
|
||||
import contextMenu from './contextMenu';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import PostMessageService from '@joplin/lib/services/PostMessageService';
|
||||
@@ -8,7 +8,7 @@ import { reg } from '@joplin/lib/registry';
|
||||
import bridge from '../../../services/bridge';
|
||||
|
||||
export default function useMessageHandler(
|
||||
scrollWhenReady: ScrollOptions|null,
|
||||
scrollWhenReadyRef: RefObject<ScrollOptions|null>,
|
||||
clearScrollWhenReady: ()=> void,
|
||||
windowId: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
@@ -21,8 +21,7 @@ export default function useMessageHandler(
|
||||
htmlToMd: HtmlToMarkdownHandler,
|
||||
mdToHtml: MarkupToHtmlHandler,
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
return useCallback(async (event: any) => {
|
||||
return useCallback(async (event: MessageEvent) => {
|
||||
const msg = event.channel ? event.channel : '';
|
||||
const args = event.args;
|
||||
const arg0 = args && args.length >= 1 ? args[0] : null;
|
||||
@@ -35,8 +34,8 @@ export default function useMessageHandler(
|
||||
s.splice(0, 1);
|
||||
reg.logger().error(s.join(':'));
|
||||
} else if (msg === 'noteRenderComplete') {
|
||||
if (scrollWhenReady) {
|
||||
const options = { ...scrollWhenReady };
|
||||
if (scrollWhenReadyRef.current) {
|
||||
const options = { ...scrollWhenReadyRef.current };
|
||||
clearScrollWhenReady();
|
||||
editorRef.current.scrollTo(options);
|
||||
}
|
||||
@@ -78,5 +77,5 @@ export default function useMessageHandler(
|
||||
// bridge().showErrorMessageBox(_('Unsupported link or message: %s', msg));
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [dispatch, setLocalSearchResultCount, scrollWhenReady, formNote]);
|
||||
}, [dispatch, setLocalSearchResultCount, scrollWhenReadyRef, formNote]);
|
||||
}
|
||||
|
||||
@@ -1,41 +1,48 @@
|
||||
import { RefObject, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { RefObject, useCallback, useRef } from 'react';
|
||||
import { NoteBodyEditorRef, ScrollOptions, ScrollOptionTypes } from './types';
|
||||
import usePrevious from '@joplin/lib/hooks/usePrevious';
|
||||
import type { EditorScrollPercents } from '../../../app.reducer';
|
||||
import type { NoteIdToScrollPercent } from '../../../app.reducer';
|
||||
import useNowEffect from '@joplin/lib/hooks/useNowEffect';
|
||||
|
||||
interface Props {
|
||||
noteId: string;
|
||||
editorName: string;
|
||||
selectedNoteHash: string;
|
||||
lastEditorScrollPercents: EditorScrollPercents;
|
||||
lastEditorScrollPercents: NoteIdToScrollPercent;
|
||||
editorRef: RefObject<NoteBodyEditorRef>;
|
||||
}
|
||||
|
||||
const useScrollWhenReadyOptions = ({ noteId, selectedNoteHash, lastEditorScrollPercents, editorRef }: Props) => {
|
||||
const [scrollWhenReady, setScrollWhenReady] = useState<ScrollOptions|null>(null);
|
||||
const useScrollWhenReadyOptions = ({ noteId, editorName, selectedNoteHash, lastEditorScrollPercents, editorRef }: Props) => {
|
||||
const scrollWhenReadyRef = useRef<ScrollOptions|null>(null);
|
||||
|
||||
const previousNoteId = usePrevious(noteId);
|
||||
const lastScrollPercentsRef = useRef<EditorScrollPercents>(null);
|
||||
const noteIdChanged = noteId !== previousNoteId;
|
||||
const previousEditor = usePrevious(editorName);
|
||||
const editorChanged = editorName !== previousEditor;
|
||||
const lastScrollPercentsRef = useRef<NoteIdToScrollPercent>(null);
|
||||
lastScrollPercentsRef.current = lastEditorScrollPercents;
|
||||
|
||||
useEffect(() => {
|
||||
if (noteId === previousNoteId) return;
|
||||
// This needs to be a nowEffect to prevent race conditions
|
||||
useNowEffect(() => {
|
||||
if (!editorChanged && !noteIdChanged) return () => {};
|
||||
|
||||
if (editorRef.current) {
|
||||
editorRef.current.resetScroll();
|
||||
}
|
||||
|
||||
const lastScrollPercent = lastScrollPercentsRef.current[noteId] || 0;
|
||||
setScrollWhenReady({
|
||||
scrollWhenReadyRef.current = {
|
||||
type: selectedNoteHash ? ScrollOptionTypes.Hash : ScrollOptionTypes.Percent,
|
||||
value: selectedNoteHash ? selectedNoteHash : lastScrollPercent,
|
||||
});
|
||||
}, [noteId, previousNoteId, selectedNoteHash, editorRef]);
|
||||
};
|
||||
return () => {};
|
||||
}, [editorChanged, noteIdChanged, noteId, selectedNoteHash, editorRef]);
|
||||
|
||||
const clearScrollWhenReady = useCallback(() => {
|
||||
setScrollWhenReady(null);
|
||||
scrollWhenReadyRef.current = null;
|
||||
}, []);
|
||||
|
||||
return { scrollWhenReady, clearScrollWhenReady };
|
||||
return { scrollWhenReadyRef, clearScrollWhenReady };
|
||||
};
|
||||
|
||||
export default useScrollWhenReadyOptions;
|
||||
|
||||
@@ -501,7 +501,12 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
<div role='table' aria-labelledby='note-properties-dialog-title'>
|
||||
{noteComps}
|
||||
</div>
|
||||
<DialogButtonRow themeId={this.props.themeId} okButtonShow={!this.isReadOnly()} okButtonRef={this.okButton} onClick={this.buttonRow_click}/>
|
||||
<DialogButtonRow
|
||||
themeId={this.props.themeId}
|
||||
okButtonShow={!this.isReadOnly()}
|
||||
okButtonRef={this.okButton}
|
||||
onClick={this.buttonRow_click}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ async function initialize() {
|
||||
panes: Setting.value('noteVisiblePanes'),
|
||||
});
|
||||
|
||||
InteropService.instance().document = document;
|
||||
InteropService.instance().domParser = new DOMParser();
|
||||
InteropService.instance().xmlSerializer = new XMLSerializer();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import Dialog from '../Dialog';
|
||||
import DialogButtonRow, { ClickEvent, ButtonSpec } from '../DialogButtonRow';
|
||||
import DialogButtonRow, { ClickEvent } from '../DialogButtonRow';
|
||||
import DialogTitle from '../DialogTitle';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { FolderEntity } from '@joplin/lib/services/database/types';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import ShareService, { ApiShare } from '@joplin/lib/services/share/ShareService';
|
||||
@@ -129,7 +129,6 @@ function ShareFolderDialog(props: Props) {
|
||||
const [share, setShare] = useState<StateShare>(null);
|
||||
const [shareUsers, setShareUsers] = useState<StateShareUser[]>([]);
|
||||
const [shareState, setShareState] = useState<ShareState>(ShareState.Idle);
|
||||
const [customButtons, setCustomButtons] = useState<ButtonSpec[]>([]);
|
||||
const [recipientsBeingUpdated, setRecipientsBeingUpdated] = useState<Record<string, boolean>>({});
|
||||
|
||||
async function synchronize(event: AsyncEffectEvent = null) {
|
||||
@@ -163,13 +162,6 @@ function ShareFolderDialog(props: Props) {
|
||||
void ShareService.instance().refreshShareUsers(share.id);
|
||||
}, [share]);
|
||||
|
||||
useEffect(() => {
|
||||
setCustomButtons(share ? [{
|
||||
name: 'unshare',
|
||||
label: _('Unshare'),
|
||||
}] : []);
|
||||
}, [share]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!share) return;
|
||||
const sus = props.shareUsers[share.id];
|
||||
@@ -177,10 +169,6 @@ function ShareFolderDialog(props: Props) {
|
||||
setShareUsers(sus);
|
||||
}, [share, props.shareUsers]);
|
||||
|
||||
useEffect(() => {
|
||||
void ShareService.instance().refreshShares();
|
||||
}, [props.folderId]);
|
||||
|
||||
const permissionsFromString = (p: string): SharePermissions => {
|
||||
return {
|
||||
can_read: 1,
|
||||
@@ -269,7 +257,7 @@ function ShareFolderDialog(props: Props) {
|
||||
}, []);
|
||||
|
||||
function renderAddRecipient() {
|
||||
const disabled = shareState !== ShareState.Idle;
|
||||
const disabled = shareState !== ShareState.Idle && shareState !== ShareState.Synchronizing;
|
||||
|
||||
const dropdown = !props.canUseSharePermissions ? null : <Dropdown className="permission-dropdown" options={permissionOptions} value={recipientPermissions} onChange={recipientPermissions_change}/>;
|
||||
|
||||
@@ -395,6 +383,17 @@ function ShareFolderDialog(props: Props) {
|
||||
props.onClose();
|
||||
}
|
||||
|
||||
const customButtons = useMemo(() => {
|
||||
return share ? [{
|
||||
name: 'unshare',
|
||||
label: _('Unshare'),
|
||||
// Don't allow unsharing the folder during the "create" action. Doing so might
|
||||
// be able to cause issues similar to #13518 (e.g. if the "unshare" action completes while
|
||||
// the "share" action is still in progress).
|
||||
disabled: shareState === ShareState.Creating || shareState === ShareState.Synchronizing,
|
||||
}] : [];
|
||||
}, [share, shareState]);
|
||||
|
||||
function renderContent() {
|
||||
return (
|
||||
<StyledRoot className="share-folder-dialog">
|
||||
|
||||
@@ -356,6 +356,7 @@ const useOnRenderItem = (props: Props) => {
|
||||
onClick={tagItem_click}
|
||||
onTagDrop={onTagDrop_}
|
||||
onContextMenu={onItemContextMenu}
|
||||
label={item.label}
|
||||
tag={tag}
|
||||
itemCount={itemCount}
|
||||
index={index}
|
||||
@@ -384,7 +385,7 @@ const useOnRenderItem = (props: Props) => {
|
||||
anchorRef={anchorRef}
|
||||
selected={selected}
|
||||
folderId={folder.id}
|
||||
folderTitle={Folder.displayTitle(folder)}
|
||||
folderTitle={item.label}
|
||||
folderIcon={Folder.unserializeIcon(folder.icon)}
|
||||
depth={item.depth}
|
||||
isExpanded={isExpanded}
|
||||
|
||||
@@ -49,6 +49,24 @@ const getParentOffset = (childIndex: number, listItems: ListItem[]): number|null
|
||||
return null;
|
||||
};
|
||||
|
||||
const findNextTypeAheadMatch = (selectedIndex: number, query: string, listItems: ListItem[]) => {
|
||||
const matches = (item: ListItem) => {
|
||||
return item.label.startsWith(query);
|
||||
};
|
||||
const indexBefore = listItems.slice(0, selectedIndex).findIndex(matches);
|
||||
// Search in all results **after** the current. This prevents the current item from
|
||||
// always being identified as the next match, if the user repeatedly presses the
|
||||
// same key.
|
||||
const startAfter = selectedIndex + 1;
|
||||
let indexAfter = listItems.slice(startAfter).findIndex(matches);
|
||||
if (indexAfter !== -1) {
|
||||
indexAfter += startAfter;
|
||||
}
|
||||
// Prefer jumping to the next match, rather than the previous
|
||||
const matchingIndex = indexAfter !== -1 ? indexAfter : indexBefore;
|
||||
return matchingIndex;
|
||||
};
|
||||
|
||||
const useOnSidebarKeyDownHandler = (props: Props) => {
|
||||
const { updateSelectedIndex, listItems, selectedIndex, collapsedFolderIds, dispatch } = props;
|
||||
|
||||
@@ -56,6 +74,8 @@ const useOnSidebarKeyDownHandler = (props: Props) => {
|
||||
const selectedItem = listItems[selectedIndex];
|
||||
let indexChange = 0;
|
||||
|
||||
const ctrlAltOrMeta = event.ctrlKey || event.altKey || event.metaKey;
|
||||
|
||||
if (selectedItem && isToggleShortcut(event.code, selectedItem, collapsedFolderIds)) {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -82,9 +102,22 @@ const useOnSidebarKeyDownHandler = (props: Props) => {
|
||||
indexChange = 1;
|
||||
} else if ((event.ctrlKey || event.metaKey) && event.code === 'KeyA') { // ctrl+a or cmd+a
|
||||
event.preventDefault();
|
||||
} else if (event.code === 'Home') {
|
||||
event.preventDefault();
|
||||
updateSelectedIndex(0);
|
||||
indexChange = 0;
|
||||
} else if (event.code === 'End') {
|
||||
event.preventDefault();
|
||||
updateSelectedIndex(listItems.length - 1);
|
||||
indexChange = 0;
|
||||
} else if (event.code === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
void CommandService.instance().execute('focusElement', 'noteList');
|
||||
} else if (selectedIndex && selectedIndex >= 0 && event.key.length === 1 && !ctrlAltOrMeta) {
|
||||
const nextMatch = findNextTypeAheadMatch(selectedIndex, event.key, listItems);
|
||||
if (nextMatch !== -1) {
|
||||
indexChange = nextMatch - selectedIndex;
|
||||
}
|
||||
}
|
||||
|
||||
if (indexChange !== 0) {
|
||||
|
||||
@@ -4,6 +4,8 @@ import { FolderEntity, TagsWithNoteCountEntity } from '@joplin/lib/services/data
|
||||
import { buildFolderTree, renderFolders, renderTags } from '@joplin/lib/components/shared/side-menu-shared';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import toggleHeader from './utils/toggleHeader';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import Tag from '@joplin/lib/models/Tag';
|
||||
|
||||
interface Props {
|
||||
tags: TagsWithNoteCountEntity[];
|
||||
@@ -18,6 +20,7 @@ const useSidebarListData = (props: Props): ListItem[] => {
|
||||
return renderTags<ListItem>(props.tags, (tag): TagListItem => {
|
||||
return {
|
||||
kind: ListItemType.Tag,
|
||||
label: Tag.displayTitle(tag),
|
||||
tag,
|
||||
key: tag.id,
|
||||
depth: 1,
|
||||
@@ -38,6 +41,7 @@ const useSidebarListData = (props: Props): ListItem[] => {
|
||||
return renderFolders<ListItem>(renderProps, (folder, hasChildren, depth): FolderListItem => {
|
||||
return {
|
||||
kind: ListItemType.Folder,
|
||||
label: Folder.displayTitle(folder),
|
||||
folder,
|
||||
hasChildren,
|
||||
// The toplevel headers have depth 1, so the toplevel notebook needs
|
||||
@@ -65,9 +69,9 @@ const useSidebarListData = (props: Props): ListItem[] => {
|
||||
hasChildren: folderItems.items.length > 0,
|
||||
};
|
||||
const foldersSectionContent: ListItem[] = props.folderHeaderIsExpanded ? [
|
||||
{ kind: ListItemType.AllNotes, key: 'all-notes', depth: 2, hasChildren: false },
|
||||
{ kind: ListItemType.AllNotes, label: _('All notes'), key: 'all-notes', depth: 2, hasChildren: false },
|
||||
...folderItems.items,
|
||||
{ kind: ListItemType.Spacer, key: 'after-folders-spacer', depth: 1, hasChildren: false },
|
||||
{ kind: ListItemType.Spacer, label: '', key: 'after-folders-spacer', depth: 1, hasChildren: false },
|
||||
] : [];
|
||||
|
||||
const tagsHeader: HeaderListItem = {
|
||||
|
||||
@@ -7,7 +7,6 @@ import Setting from '@joplin/lib/models/Setting';
|
||||
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import PerFolderSortOrderService from '../../../services/sortOrder/PerFolderSortOrderService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { connect } from 'react-redux';
|
||||
import EmptyExpandLink from './EmptyExpandLink';
|
||||
import ListItemWrapper, { ListItemRef } from './ListItemWrapper';
|
||||
@@ -70,7 +69,7 @@ const AllNotesItem: React.FC<Props> = props => {
|
||||
onClick={onAllNotesClick_}
|
||||
onContextMenu={toggleAllNotesContextMenu}
|
||||
>
|
||||
{_('All notes')}
|
||||
{props.item.label}
|
||||
</StyledListItemAnchor>
|
||||
</ListItemWrapper>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { StyledHeader, StyledHeaderIcon, StyledHeaderLabel } from '../styles';
|
||||
import { HeaderId, HeaderListItem } from '../types';
|
||||
import bridge from '../../../services/bridge';
|
||||
@@ -25,8 +25,6 @@ const HeaderItem: React.FC<Props> = props => {
|
||||
const item = props.item;
|
||||
const onItemClick = item.onClick;
|
||||
const itemId = item.id;
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const expanded = item.expanded;
|
||||
|
||||
const onClick: React.MouseEventHandler<HTMLElement> = useCallback(event => {
|
||||
if (onItemClick) {
|
||||
@@ -46,14 +44,6 @@ const HeaderItem: React.FC<Props> = props => {
|
||||
}
|
||||
}, [itemId]);
|
||||
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
setIsHovered(true);
|
||||
}, []);
|
||||
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
setIsHovered(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ListItemWrapper
|
||||
containerRef={props.anchorRef}
|
||||
@@ -70,10 +60,8 @@ const HeaderItem: React.FC<Props> = props => {
|
||||
>
|
||||
<StyledHeader
|
||||
onClick={onClick}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<StyledHeaderIcon aria-hidden='true' role='img' className={isHovered ? `fas ${expanded ? 'fa-caret-down' : 'fa-caret-right'}` : item.iconName}/>
|
||||
<StyledHeaderIcon aria-hidden='true' role='img' className={item.iconName}/>
|
||||
<StyledHeaderLabel>{item.label}</StyledHeaderLabel>
|
||||
</StyledHeader>
|
||||
</ListItemWrapper>
|
||||
|
||||
@@ -5,7 +5,6 @@ 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';
|
||||
|
||||
@@ -15,6 +14,7 @@ interface Props {
|
||||
anchorRef: ListItemRef;
|
||||
selected: boolean;
|
||||
tag: TagsWithNoteCountEntity;
|
||||
label: string;
|
||||
onTagDrop: React.DragEventHandler<HTMLElement>;
|
||||
onContextMenu: React.MouseEventHandler<HTMLElement>;
|
||||
onClick: (event: TagLinkClickEvent)=> void;
|
||||
@@ -58,7 +58,7 @@ const TagItem = (props: Props) => {
|
||||
onContextMenu={props.onContextMenu}
|
||||
onClick={onClickHandler}
|
||||
>
|
||||
<StyledSpanFix className="tag-label">{Tag.displayTitle(tag)}</StyledSpanFix>
|
||||
<StyledSpanFix className="tag-label">{props.label}</StyledSpanFix>
|
||||
{noteCount}
|
||||
</StyledListItemAnchor>
|
||||
</ListItemWrapper>
|
||||
|
||||
@@ -16,6 +16,8 @@ export enum ListItemType {
|
||||
|
||||
interface BaseListItem {
|
||||
key: string;
|
||||
// Used for typeahead
|
||||
label: string;
|
||||
depth: number;
|
||||
hasChildren: boolean;
|
||||
}
|
||||
@@ -26,7 +28,6 @@ interface ToplevelListItem extends BaseListItem {
|
||||
|
||||
export interface HeaderListItem extends ToplevelListItem {
|
||||
kind: ListItemType.Header;
|
||||
label: string;
|
||||
expanded: boolean;
|
||||
iconName: string;
|
||||
id: HeaderId;
|
||||
|
||||
@@ -38,7 +38,7 @@ describe('NoteListUtils', () => {
|
||||
const mockStore = {
|
||||
getState: () => {
|
||||
return {
|
||||
...createAppDefaultWindowState(),
|
||||
...createAppDefaultWindowState(null),
|
||||
settings: {},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ import getMainWindow from './util/getMainWindow';
|
||||
import setFilePickerResponse from './util/setFilePickerResponse';
|
||||
import setMessageBoxResponse from './util/setMessageBoxResponse';
|
||||
import getImageSourceSize from './util/getImageSourceSize';
|
||||
import setSettingValue from './util/setSettingValue';
|
||||
|
||||
|
||||
test.describe('main', () => {
|
||||
@@ -19,6 +20,13 @@ test.describe('main', () => {
|
||||
await mainPage.waitFor();
|
||||
});
|
||||
|
||||
test('app should support French localization', async ({ mainWindow, electronApp }) => {
|
||||
await setSettingValue(electronApp, mainWindow, 'locale', 'fr_FR');
|
||||
// The "Notebooks" header should be localized
|
||||
const localizedText = mainWindow.getByText('Carnets').first();
|
||||
await expect(localizedText).toBeAttached();
|
||||
});
|
||||
|
||||
test('should be able to create and edit a new note', async ({ mainWindow }) => {
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
const editor = await mainScreen.createNewNote('Test note');
|
||||
|
||||
@@ -180,8 +180,8 @@ test.describe('markdownEditor', () => {
|
||||
await expect(matches).toHaveCount(1);
|
||||
|
||||
// Should continue searching after switching to view-only mode
|
||||
await noteEditor.toggleEditorLayoutButton.click();
|
||||
await noteEditor.toggleEditorLayoutButton.click();
|
||||
await noteEditor.toggleEditorLayout();
|
||||
await noteEditor.toggleEditorLayout();
|
||||
await expect(noteEditor.codeMirrorEditor).not.toBeVisible();
|
||||
await expect(noteEditor.editorSearchInput).not.toBeVisible();
|
||||
await expect(noteEditor.viewerSearchInput).toBeVisible();
|
||||
@@ -194,7 +194,7 @@ test.describe('markdownEditor', () => {
|
||||
await expect(matches).toHaveCount(0);
|
||||
|
||||
// After showing the viewer again, search should still be hidden
|
||||
await noteEditor.toggleEditorLayoutButton.click();
|
||||
await noteEditor.toggleEditorLayout();
|
||||
await expect(noteEditor.codeMirrorEditor).toBeVisible();
|
||||
await expect(noteEditor.editorSearchInput).not.toBeVisible();
|
||||
});
|
||||
@@ -274,5 +274,68 @@ test.describe('markdownEditor', () => {
|
||||
expect(imageSize[0]).toBeGreaterThan(0);
|
||||
expect(imageSize[1]).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('ctrl-clicking on note links should open the linked note (when the viewer is hidden)', async ({ mainWindow }) => {
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
await mainScreen.createNewNote('Original');
|
||||
const noteEditor = mainScreen.noteEditor;
|
||||
await noteEditor.hideViewer();
|
||||
|
||||
await noteEditor.focusCodeMirrorEditor();
|
||||
await mainWindow.keyboard.type('# Test');
|
||||
await mainWindow.keyboard.press('Enter');
|
||||
await mainWindow.keyboard.type('## Test 2');
|
||||
await mainWindow.keyboard.press('Enter');
|
||||
await mainWindow.keyboard.type('### Test 3');
|
||||
|
||||
const editorContent = await noteEditor.contentLocator();
|
||||
|
||||
// Extract the note ID
|
||||
const note1Locator = mainScreen.noteList.getNoteItemByTitle('Original');
|
||||
await note1Locator.dragTo(editorContent);
|
||||
const linkExpression = /\[[^\]]*\]\(:\/([a-z0-9]{32})\)/;
|
||||
await noteEditor.expectToHaveText(linkExpression);
|
||||
const targetNoteId = (await editorContent.textContent()).match(linkExpression)[1];
|
||||
|
||||
await mainScreen.createNewNote('Test note links');
|
||||
|
||||
// Create a new link to a header
|
||||
await noteEditor.focusCodeMirrorEditor();
|
||||
await mainWindow.keyboard.press('Enter');
|
||||
await mainWindow.keyboard.press('Enter');
|
||||
await mainWindow.keyboard.type('[link](:/');
|
||||
await mainWindow.keyboard.type(targetNoteId);
|
||||
await mainWindow.keyboard.type('#test-2');
|
||||
await mainWindow.keyboard.type(')');
|
||||
await mainWindow.keyboard.press('Enter');
|
||||
|
||||
// Clicking the link should navigate to note1
|
||||
const link = editorContent.getByText(/\[?link\]?/);
|
||||
await link.click({ modifiers: ['ControlOrMeta'] });
|
||||
await expect(noteEditor.noteTitleInput).toHaveValue('Original');
|
||||
await noteEditor.expectToHaveText(/^# Test/);
|
||||
await expect.poll(() => editorContent.evaluate(async editor => {
|
||||
const selection = getSelection();
|
||||
return editor.contains(selection.anchorNode);
|
||||
})).toBe(true);
|
||||
|
||||
// The cursor should be positioned on the linked-to header
|
||||
await expect.poll(async () => {
|
||||
await mainWindow.keyboard.type('[[cursor]]');
|
||||
await noteEditor.expectToHaveText(/## Test 2\[\[cursor\]\]/);
|
||||
return true;
|
||||
}).toBe(true);
|
||||
});
|
||||
|
||||
test('should still support the legacy Markdown editor', async ({ electronApp, mainWindow }) => {
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
await mainScreen.waitFor();
|
||||
|
||||
await setSettingValue(electronApp, mainWindow, 'editor.legacyMarkdown', true);
|
||||
await mainScreen.createNewNote('Test');
|
||||
|
||||
// Should show the legacy editor
|
||||
await expect(mainWindow.locator('.rli-editor .CodeMirror5')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -65,12 +65,20 @@ export default class NoteEditorPage {
|
||||
}
|
||||
}
|
||||
|
||||
public async expectToHaveText(content: string) {
|
||||
public async expectToHaveText(expected: string|RegExp) {
|
||||
// expect(...).toHaveText can fail in the Rich Text Editor (perhaps due to frame locators).
|
||||
// Using expect.poll refreshes the locator on each attempt, which seems to prevent flakiness.
|
||||
await expect.poll(
|
||||
async () => (await this.contentLocator()).textContent(),
|
||||
).toBe(content);
|
||||
const expectResult = expect.poll(
|
||||
// Use .innerText: textContent doesn't handle line breaks correctly in the CodeMirror
|
||||
// editor.
|
||||
async () => (await this.contentLocator()).innerText(),
|
||||
);
|
||||
// Allow `expected` to be either an exact match (a string) or a pattern
|
||||
if (typeof expected === 'string') {
|
||||
await expectResult.toBe(expected);
|
||||
} else {
|
||||
await expectResult.toMatch(expected);
|
||||
}
|
||||
}
|
||||
|
||||
public getNoteViewerFrameLocator() {
|
||||
@@ -117,4 +125,14 @@ export default class NoteEditorPage {
|
||||
await expect(backButton).not.toBeDisabled();
|
||||
await backButton.click();
|
||||
}
|
||||
|
||||
public async toggleEditorLayout() {
|
||||
await this.toggleEditorLayoutButton.click();
|
||||
}
|
||||
|
||||
public async hideViewer() {
|
||||
await expect(this.noteViewerContainer).toBeVisible();
|
||||
await this.toggleEditorLayout();
|
||||
await expect(this.noteViewerContainer).not.toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ test.describe('pluginApi', () => {
|
||||
await mainScreen.createNewNote('First note');
|
||||
|
||||
const editor = mainScreen.noteEditor;
|
||||
await editor.expectToHaveText('');
|
||||
await editor.expectToHaveText('\n');
|
||||
|
||||
await mainScreen.goToAnything.runCommand(app, 'showTestDialog');
|
||||
// Wait for the iframe to load
|
||||
|
||||
@@ -44,6 +44,28 @@ test.describe('sidebar', () => {
|
||||
await expect(mainWindow.locator(':focus')).toHaveText('All notes');
|
||||
});
|
||||
|
||||
test('should allow changing the focused folder by pressing the first character of the title', async ({ electronApp, mainWindow }) => {
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
const sidebar = mainScreen.sidebar;
|
||||
|
||||
const folderAHeader = await sidebar.createNewFolder('1-Test A');
|
||||
await expect(folderAHeader).toBeVisible();
|
||||
|
||||
const folderBHeader = await sidebar.createNewFolder('Folder b');
|
||||
await expect(folderBHeader).toBeVisible();
|
||||
await folderBHeader.click();
|
||||
|
||||
await sidebar.forceUpdateSorting(electronApp);
|
||||
|
||||
await folderBHeader.click();
|
||||
await mainWindow.keyboard.type('1');
|
||||
await expect(mainWindow.locator(':focus')).toHaveText('1-Test A');
|
||||
await mainWindow.keyboard.type('F');
|
||||
await expect(mainWindow.locator(':focus')).toHaveText('Folder b');
|
||||
await mainWindow.keyboard.type('A');
|
||||
await expect(mainWindow.locator(':focus')).toHaveText('All notes');
|
||||
});
|
||||
|
||||
test('left/right arrow keys should expand/collapse notebooks', async ({ electronApp, mainWindow }) => {
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
const sidebar = mainScreen.sidebar;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "3.5.2",
|
||||
"version": "3.5.6",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.bundle.js",
|
||||
"private": true,
|
||||
@@ -12,10 +12,11 @@
|
||||
"electronRebuild": "gulp electronRebuild",
|
||||
"tsc": "tsc --project tsconfig.json",
|
||||
"watch": "tsc --watch --preserveWatchOutput --project tsconfig.json",
|
||||
"start": "gulp before-start && JOPLIN_SOURCE_MAP_ENABLED=1 electron . --env dev --log-level debug --open-dev-tools --no-welcome",
|
||||
"start": "gulp before-start && electron . --env dev --log-level debug --open-dev-tools --no-welcome",
|
||||
"test": "jest",
|
||||
"test-ui": "gulp before-start && playwright test",
|
||||
"test-ci": "yarn test",
|
||||
"resolve-sourcemap": "node tools/resolveSourceMap.js",
|
||||
"modifyReleaseAssets": "node tools/modifyReleaseAssets.js"
|
||||
},
|
||||
"repository": {
|
||||
@@ -46,6 +47,7 @@
|
||||
"asar": true,
|
||||
"asarUnpack": "./node_modules/node-notifier/vendor/**",
|
||||
"win": {
|
||||
"sign": "./sign.js",
|
||||
"rfc3161TimeStampServer": "http://timestamp.digicert.com",
|
||||
"icon": "../../Assets/ImageSources/Joplin.ico",
|
||||
"target": [
|
||||
@@ -142,17 +144,16 @@
|
||||
"@joplin/renderer": "~3.5",
|
||||
"@joplin/tools": "~3.5",
|
||||
"@joplin/utils": "~3.5",
|
||||
"@playwright/test": "1.52.0",
|
||||
"@playwright/test": "1.53.2",
|
||||
"@sentry/electron": "4.24.0",
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/mustache": "4.2.6",
|
||||
"@types/node": "18.19.115",
|
||||
"@types/node": "18.19.130",
|
||||
"@types/react": "18.3.23",
|
||||
"@types/react-dom": "18.3.7",
|
||||
"@types/react-redux": "7.1.33",
|
||||
"@types/styled-components": "5.1.32",
|
||||
"@types/tesseract.js": "2.0.0",
|
||||
"async-mutex": "0.5.0",
|
||||
"axios": "^1.7.7",
|
||||
"codemirror": "5.65.9",
|
||||
@@ -160,7 +161,7 @@
|
||||
"compare-versions": "6.1.1",
|
||||
"countable": "3.0.1",
|
||||
"debounce": "1.2.1",
|
||||
"electron": "37.4.0",
|
||||
"electron": "37.7.0",
|
||||
"electron-builder": "24.13.3",
|
||||
"electron-updater": "6.6.2",
|
||||
"electron-window-state": "5.0.3",
|
||||
@@ -187,7 +188,7 @@
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-redux": "8.1.3",
|
||||
"react-select": "5.10.1",
|
||||
"react-select": "5.10.2",
|
||||
"react-test-renderer": "18.3.1",
|
||||
"react-toggle-button": "2.2.0",
|
||||
"react-tooltip": "4.5.1",
|
||||
@@ -199,14 +200,14 @@
|
||||
"styled-components": "5.3.11",
|
||||
"styled-system": "5.1.5",
|
||||
"taboverride": "4.0.3",
|
||||
"tesseract.js": "5.1.1",
|
||||
"tesseract.js": "6.0.1",
|
||||
"tinymce": "6.8.5",
|
||||
"ts-jest": "29.3.4",
|
||||
"ts-jest": "29.4.1",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron/remote": "2.1.2",
|
||||
"@electron/remote": "2.1.3",
|
||||
"@joplin/onenote-converter": "~3.5",
|
||||
"fs-extra": "11.2.0",
|
||||
"keytar": "7.9.0",
|
||||
|
||||
109
packages/app-desktop/sign.js
Normal file
109
packages/app-desktop/sign.js
Normal file
@@ -0,0 +1,109 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const { chdir, cwd } = require('process');
|
||||
const { mkdirpSync, moveSync, pathExists } = require('fs-extra');
|
||||
const { readdirSync, writeFileSync } = require('fs');
|
||||
const { dirname } = require('path');
|
||||
|
||||
const signToolName = 'CodeSignTool.bat';
|
||||
|
||||
const getTempDir = () => {
|
||||
if (process.env.RUNNER_TEMP) return process.env.RUNNER_TEMP;
|
||||
if (process.env.GITHUB_WORKSPACE) return process.env.GITHUB_WORKSPACE;
|
||||
|
||||
const output = `${dirname(dirname(__dirname))}/temp`;
|
||||
mkdirpSync(output);
|
||||
return output;
|
||||
};
|
||||
|
||||
const tempDir = getTempDir();
|
||||
|
||||
const downloadSignTool = async () => {
|
||||
const signToolUrl = 'https://www.ssl.com/download/codesigntool-for-windows/';
|
||||
const downloadDir = `${tempDir}/signToolDownloadTemp`;
|
||||
const extractDir = `${tempDir}/signToolExtractTemp`;
|
||||
|
||||
if (await pathExists(`${extractDir}/${signToolName}`)) {
|
||||
console.info('sign.js: Sign tool has already been downloaded - skipping');
|
||||
return extractDir;
|
||||
}
|
||||
|
||||
mkdirpSync(downloadDir);
|
||||
mkdirpSync(extractDir);
|
||||
|
||||
const response = await fetch(signToolUrl);
|
||||
if (!response.ok) throw new Error(`sign.js: HTTP error ${response.status}: ${response.statusText}`);
|
||||
|
||||
const zipPath = `${downloadDir}/codeSignTool.zip`;
|
||||
|
||||
const buffer = Buffer.from(await response.arrayBuffer());
|
||||
writeFileSync(zipPath, buffer);
|
||||
|
||||
console.info('sign.js: Downloaded sign tool zip:', readdirSync(downloadDir));
|
||||
|
||||
mkdirpSync(extractDir);
|
||||
|
||||
execSync(
|
||||
`powershell -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${extractDir}' -Force"`,
|
||||
{ stdio: 'inherit' },
|
||||
);
|
||||
|
||||
console.info('sign.js: Extracted sign tool zip:', readdirSync(extractDir));
|
||||
|
||||
return extractDir;
|
||||
};
|
||||
|
||||
exports.default = async (configuration) => {
|
||||
const inputFilePath = configuration.path;
|
||||
|
||||
const {
|
||||
SSL_ESIGNER_USER_NAME,
|
||||
SSL_ESIGNER_USER_PASSWORD,
|
||||
SSL_ESIGNER_CREDENTIAL_ID,
|
||||
SSL_ESIGNER_USER_TOTP,
|
||||
SIGN_APPLICATION,
|
||||
} = process.env;
|
||||
|
||||
console.info('sign.js: File to sign:', inputFilePath);
|
||||
|
||||
console.info('sign.js: Using temp dir:', tempDir);
|
||||
|
||||
if (SIGN_APPLICATION !== '1') {
|
||||
console.info('sign.js: SIGN_APPLICATION != 1 - not signing application');
|
||||
return;
|
||||
}
|
||||
|
||||
console.info('sign.js: SIGN_APPLICATION = 1 - signing application');
|
||||
|
||||
const signToolDir = await downloadSignTool();
|
||||
const signToolOutDir = `${tempDir}/signedToolOutDir`;
|
||||
mkdirpSync(signToolOutDir);
|
||||
|
||||
const previousDir = cwd();
|
||||
chdir(signToolDir);
|
||||
|
||||
try {
|
||||
const cmd = [
|
||||
`${signToolName} sign`,
|
||||
`-input_file_path="${inputFilePath}"`,
|
||||
`-output_dir_path="${signToolOutDir}"`,
|
||||
`-credential_id="${SSL_ESIGNER_CREDENTIAL_ID}"`,
|
||||
`-username="${SSL_ESIGNER_USER_NAME}"`,
|
||||
`-password="${SSL_ESIGNER_USER_PASSWORD}"`,
|
||||
`-totp_secret="${SSL_ESIGNER_USER_TOTP}"`,
|
||||
];
|
||||
|
||||
execSync(cmd.join(' '));
|
||||
|
||||
const createdFiles = readdirSync(signToolOutDir);
|
||||
console.info('sign.js: Created files:', createdFiles);
|
||||
|
||||
moveSync(`${signToolOutDir}/${createdFiles[0]}`, inputFilePath, { overwrite: true });
|
||||
} catch (error) {
|
||||
console.error('sign.js: Could not sign file:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
chdir(previousDir);
|
||||
}
|
||||
};
|
||||
@@ -9,7 +9,7 @@ const baseNodeModules = join(baseDir, 'node_modules');
|
||||
|
||||
// Note: Roughly based on js-draw's use of esbuild:
|
||||
// https://github.com/personalizedrefrigerator/js-draw/blob/6fe6d6821402a08a8d17f15a8f48d95e5d7b084f/packages/build-tool/src/BundledFile.ts#L64
|
||||
const makeBuildContext = (entryPoint: string, renderer: boolean, computeFileSizeStats: boolean) => {
|
||||
const makeBuildContext = (entryPoint: string, renderer: boolean, addDebugStats: boolean) => {
|
||||
return esbuild.context({
|
||||
entryPoints: [entryPoint],
|
||||
outfile: `${filename(entryPoint)}.bundle.js`,
|
||||
@@ -19,7 +19,7 @@ const makeBuildContext = (entryPoint: string, renderer: boolean, computeFileSize
|
||||
format: 'iife', // Immediately invoked function expression
|
||||
sourcemap: true,
|
||||
sourcesContent: false, // Do not embed full source file content in the .map file
|
||||
metafile: computeFileSizeStats,
|
||||
metafile: addDebugStats,
|
||||
platform: 'node',
|
||||
target: ['node20.0'],
|
||||
mainFields: renderer ? ['browser', 'main'] : ['main'],
|
||||
@@ -92,26 +92,29 @@ const makeBuildContext = (entryPoint: string, renderer: boolean, computeFileSize
|
||||
{
|
||||
name: 'joplin--smaller-source-map-size',
|
||||
setup: build => {
|
||||
// Exclude dependencies from node_modules. This significantly reduces the size of the
|
||||
// Unless bundling with additional debug information, exclude 3rd-party
|
||||
// dependencies from source maps. This significantly reduces the size of the
|
||||
// source map, improving startup performance.
|
||||
//
|
||||
// See https://github.com/evanw/esbuild/issues/1685#issuecomment-944916409
|
||||
// and https://github.com/evanw/esbuild/issues/4130
|
||||
const emptyMapData = Buffer.from(
|
||||
JSON.stringify({ version: 3, sources: [null], mappings: 'AAAA' }),
|
||||
'utf-8',
|
||||
).toString('base64');
|
||||
const emptyMapUrl = `data:application/json;base64,${emptyMapData}`;
|
||||
if (!addDebugStats) {
|
||||
const emptyMapData = Buffer.from(
|
||||
JSON.stringify({ version: 3, sources: [null], mappings: 'AAAA' }),
|
||||
'utf-8',
|
||||
).toString('base64');
|
||||
const emptyMapUrl = `data:application/json;base64,${emptyMapData}`;
|
||||
|
||||
build.onLoad({ filter: /node_modules.*js$/ }, args => {
|
||||
return {
|
||||
contents: [
|
||||
readFileSync(args.path, 'utf8'),
|
||||
`//# sourceMappingURL=${emptyMapUrl}`,
|
||||
].join('\n'),
|
||||
loader: 'default',
|
||||
};
|
||||
});
|
||||
build.onLoad({ filter: /node_modules.*js$/ }, args => {
|
||||
return {
|
||||
contents: [
|
||||
readFileSync(args.path, 'utf8'),
|
||||
`//# sourceMappingURL=${emptyMapUrl}`,
|
||||
].join('\n'),
|
||||
loader: 'default',
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
55
packages/app-desktop/tools/resolveSourceMap.ts
Normal file
55
packages/app-desktop/tools/resolveSourceMap.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { dirname, relative } from 'path';
|
||||
import * as yargs from 'yargs';
|
||||
const { wrapCallSite } = require('source-map-support');
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const resolveLine = (lineNumber: number, columnNumber: number, filePath: string) => {
|
||||
// Note: This is an undocumented function provided by source-map-support. It
|
||||
// may change in the future:
|
||||
const frame = wrapCallSite({
|
||||
getFileName: () => filePath,
|
||||
isEval: ()=>false,
|
||||
isNative: ()=>false,
|
||||
getLineNumber: ()=>lineNumber,
|
||||
getColumnNumber: ()=>columnNumber,
|
||||
});
|
||||
|
||||
const baseDir = dirname(dirname(dirname(__dirname)));
|
||||
const relativeFilePath = relative(baseDir, frame.getFileName());
|
||||
return `${relativeFilePath}:${frame.getLineNumber()}`;
|
||||
};
|
||||
|
||||
const resolvePosition = (position: string, sourceMap: string) => {
|
||||
const match = /^(\d{1,10}):(\d{1,10})$/.exec(position.trim());
|
||||
if (!match) {
|
||||
throw new Error('Invalid format. Expected line:col');
|
||||
}
|
||||
|
||||
const lineNumber = Number(match[1]);
|
||||
const columnNumber = Number(match[2]);
|
||||
return resolveLine(lineNumber, columnNumber, sourceMap);
|
||||
};
|
||||
|
||||
void yargs
|
||||
.usage('$0 [args]')
|
||||
.command(
|
||||
'$0 <position>',
|
||||
'Resolves a position based on a source map. If resolving a position in a specific error message, be sure to use the source map generated by "yarn bundle" from that specific commit.',
|
||||
(yargs) => {
|
||||
return yargs.options({
|
||||
'position': { type: 'string', help: 'A line:col position (e.g. 123:4567)' },
|
||||
'sourcemap': {
|
||||
type: 'string',
|
||||
default: './main-html.bundle.js',
|
||||
help: 'The path to the source map. This source map should be a source map compiled from the commit/release that created the error.',
|
||||
},
|
||||
});
|
||||
},
|
||||
async (args) => {
|
||||
console.log(await resolvePosition(args.position, args.sourcemap));
|
||||
process.exit(0);
|
||||
},
|
||||
)
|
||||
.help()
|
||||
.argv;
|
||||
@@ -1,6 +1,8 @@
|
||||
|
||||
// source-map-support can add 1-3 seconds to the application startup
|
||||
// time -- disable it unless requested:
|
||||
if (process.env.JOPLIN_SOURCE_MAP_ENABLED) {
|
||||
// time. In the future, it may make sense to either:
|
||||
// - Use Sentry for resolving source maps: https://docs.sentry.io/platforms/javascript/guides/electron/sourcemaps/
|
||||
// - Use NodeJS source map support (if https://github.com/electron/electron/issues/38875 is resolved)
|
||||
if (!process.env.JOPLIN_SOURCE_MAP_DISABLED) {
|
||||
require('source-map-support').install();
|
||||
}
|
||||
|
||||
@@ -132,6 +132,7 @@ const EditorToolbar: React.FC<Props> = props => {
|
||||
style={styles.content}
|
||||
contentContainerStyle={styles.contentContainer}
|
||||
onLayout={onContainerLayout}
|
||||
keyboardShouldPersistTaps="always"
|
||||
>
|
||||
{buttonInfos.map(renderButton)}
|
||||
<View style={styles.spacer}/>
|
||||
|
||||
@@ -7,6 +7,7 @@ import createMockReduxStore from '../utils/testing/createMockReduxStore';
|
||||
import setupGlobalStore from '../utils/testing/setupGlobalStore';
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react-native';
|
||||
import FeedbackBanner from './FeedbackBanner';
|
||||
import { MobilePlatform } from '@joplin/lib/shim';
|
||||
|
||||
interface WrapperProps { }
|
||||
|
||||
@@ -84,7 +85,7 @@ describe('FeedbackBanner', () => {
|
||||
setupGlobalStore(store);
|
||||
|
||||
jest.useFakeTimers({ advanceTimers: true });
|
||||
mockMobilePlatform('web');
|
||||
mockMobilePlatform(MobilePlatform.Web);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -93,9 +94,9 @@ describe('FeedbackBanner', () => {
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ platform: 'android', shouldShow: false },
|
||||
{ platform: 'web', shouldShow: true },
|
||||
{ platform: 'ios', shouldShow: false },
|
||||
{ platform: MobilePlatform.Android, shouldShow: false },
|
||||
{ platform: MobilePlatform.Web, shouldShow: true },
|
||||
{ platform: MobilePlatform.Ios, shouldShow: false },
|
||||
])('should correctly show/hide the feedback banner on %s', ({ platform, shouldShow }) => {
|
||||
mockMobilePlatform(platform);
|
||||
|
||||
|
||||
@@ -48,6 +48,11 @@ const useStyles = (themeId: number) => {
|
||||
invisibleHeading: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
// Use compact mode on the button and expand the padding to match the original styling, to work around an Android issue #13120
|
||||
buttonStyle: {
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16,
|
||||
},
|
||||
});
|
||||
}, [themeId]);
|
||||
};
|
||||
@@ -55,15 +60,22 @@ const useStyles = (themeId: number) => {
|
||||
const ModalDialog: React.FC<Props> = props => {
|
||||
const styles = useStyles(props.themeId);
|
||||
const theme = themeStyle(props.themeId);
|
||||
const containerStyle = !props.modalProps.containerStyle ? styles.container : {
|
||||
...styles.container,
|
||||
...props.modalProps.containerStyle,
|
||||
};
|
||||
const modalProps = {
|
||||
...props.modalProps,
|
||||
containerStyle,
|
||||
} as Partial<ModalElementProps>;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
transparent={true}
|
||||
visible={true}
|
||||
onRequestClose={null}
|
||||
containerStyle={styles.container}
|
||||
backgroundColor={theme.backgroundColorTransparent2}
|
||||
{...props.modalProps}
|
||||
{...modalProps}
|
||||
>
|
||||
<View style={styles.contentWrapper}>{props.children}</View>
|
||||
<View style={styles.buttonRow}>
|
||||
@@ -76,8 +88,8 @@ const ModalDialog: React.FC<Props> = props => {
|
||||
accessible={true}
|
||||
style={styles.invisibleHeading}
|
||||
/>
|
||||
<Button disabled={!props.buttonBarEnabled} onPress={props.onCancelPress}>{props.cancelTitle}</Button>
|
||||
<PrimaryButton disabled={!props.buttonBarEnabled} onPress={props.onOkPress}>{props.okTitle}</PrimaryButton>
|
||||
<Button compact contentStyle={styles.buttonStyle} disabled={!props.buttonBarEnabled} onPress={props.onCancelPress}>{props.cancelTitle}</Button>
|
||||
<PrimaryButton compact contentStyle={styles.buttonStyle} disabled={!props.buttonBarEnabled} onPress={props.onOkPress}>{props.okTitle}</PrimaryButton>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { describe, it, beforeEach } from '@jest/globals';
|
||||
import { render, waitFor } from '../../utils/testing/testingLibrary';
|
||||
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
import TestProviderStack from '../testing/TestProviderStack';
|
||||
import createMockReduxStore from '../../utils/testing/createMockReduxStore';
|
||||
import createTestEditorProps from './testing/createTestEditorProps';
|
||||
import { EditorEvent, EditorEventType } from '@joplin/editor/events';
|
||||
import { RefObject, useCallback } from 'react';
|
||||
import { EditorCommandType, EditorControl } from '@joplin/editor/types';
|
||||
import MarkdownEditor from './MarkdownEditor';
|
||||
|
||||
|
||||
interface WrapperProps {
|
||||
ref?: RefObject<EditorControl>;
|
||||
onBodyChange: (newBody: string)=> void;
|
||||
noteBody: string;
|
||||
}
|
||||
|
||||
const defaultEditorProps = createTestEditorProps();
|
||||
const testStore = createMockReduxStore();
|
||||
const WrappedEditor: React.FC<WrapperProps> = (
|
||||
{
|
||||
noteBody,
|
||||
onBodyChange,
|
||||
ref,
|
||||
}: WrapperProps,
|
||||
) => {
|
||||
const onEvent = useCallback((event: EditorEvent) => {
|
||||
if (event.kind === EditorEventType.Change) {
|
||||
onBodyChange(event.value);
|
||||
}
|
||||
}, [onBodyChange]);
|
||||
|
||||
return <TestProviderStack store={testStore}>
|
||||
<MarkdownEditor
|
||||
{...defaultEditorProps}
|
||||
onEditorEvent={onEvent}
|
||||
initialText={noteBody}
|
||||
editorRef={ref ?? defaultEditorProps.editorRef}
|
||||
/>
|
||||
</TestProviderStack>;
|
||||
};
|
||||
|
||||
describe('MarkdownEditor', () => {
|
||||
beforeEach(async () => {
|
||||
await setupDatabaseAndSynchronizer(0);
|
||||
await switchClient(0);
|
||||
Setting.setValue('editor.codeView', true);
|
||||
});
|
||||
|
||||
// Regression test for #13193. This verifies that the editor can be reached
|
||||
// over IPC.
|
||||
it('should support the "textBold" command', async () => {
|
||||
let editorBody = 'test';
|
||||
const editorRef = React.createRef<EditorControl|null>();
|
||||
render(<WrappedEditor
|
||||
ref={editorRef}
|
||||
noteBody={editorBody}
|
||||
onBodyChange={newValue => { editorBody = newValue; }}
|
||||
/>);
|
||||
|
||||
// Should mark the command as supported
|
||||
expect(await editorRef.current.supportsCommand(EditorCommandType.ToggleBolded));
|
||||
|
||||
// Command should run
|
||||
await editorRef.current.execCommand(EditorCommandType.SelectAll);
|
||||
await editorRef.current.execCommand(EditorCommandType.ToggleBolded);
|
||||
await waitFor(() => {
|
||||
expect(editorBody).toBe('**test**');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -121,6 +121,7 @@ const useStyles = (theme: Theme) => {
|
||||
height: buttonSize,
|
||||
backgroundColor: theme.backgroundColor4,
|
||||
color: theme.color4,
|
||||
margin: 2,
|
||||
},
|
||||
buttonText: buttonTextStyle,
|
||||
activeButtonText: {
|
||||
@@ -352,7 +353,7 @@ export const SearchPanel = (props: SearchPanelProps) => {
|
||||
);
|
||||
|
||||
const advancedLayout = (
|
||||
<View style={{ flexDirection: 'column', alignItems: 'center' }}>
|
||||
<View style={{ flexDirection: 'column' }}>
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
{ closeButton }
|
||||
{ labeledSearchInput }
|
||||
|
||||
@@ -10,6 +10,7 @@ import JoplinCloudIcon from './JoplinCloudIcon';
|
||||
import NavService from '@joplin/lib/services/NavService';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import CardButton from '../buttons/CardButton';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
interface Props {
|
||||
dispatch: Dispatch;
|
||||
@@ -86,6 +87,11 @@ const SyncWizard: React.FC<Props> = ({ themeId, visible, dispatch }) => {
|
||||
});
|
||||
}, [dispatch]);
|
||||
|
||||
const onManualDismiss = useCallback(() => {
|
||||
Setting.setValue('sync.wizard.autoShowOnStartup', false);
|
||||
onDismiss();
|
||||
}, [onDismiss]);
|
||||
|
||||
const onSelectJoplinCloud = useCallback(async () => {
|
||||
onDismiss();
|
||||
await NavService.go('JoplinCloudLogin');
|
||||
@@ -99,7 +105,7 @@ const SyncWizard: React.FC<Props> = ({ themeId, visible, dispatch }) => {
|
||||
return <DismissibleDialog
|
||||
themeId={themeId}
|
||||
visible={visible}
|
||||
onDismiss={onDismiss}
|
||||
onDismiss={onManualDismiss}
|
||||
size={DialogVariant.SmallResize}
|
||||
scrollOverflow={true}
|
||||
heading={_('Sync')}
|
||||
|
||||
@@ -54,7 +54,6 @@ const useStyles = (themeId: number, headerStyle: TextStyle|undefined) => {
|
||||
},
|
||||
tagBoxRoot: {
|
||||
flexDirection: 'column',
|
||||
flexGrow: 0.5,
|
||||
flexShrink: 1,
|
||||
},
|
||||
tagBoxScrollView: {
|
||||
@@ -86,6 +85,7 @@ const useStyles = (themeId: number, headerStyle: TextStyle|undefined) => {
|
||||
backgroundColor: theme.dividerColor,
|
||||
},
|
||||
tagSearch: {
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
},
|
||||
noTagsLabel: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { Card, TouchableRipple } from 'react-native-paper';
|
||||
import { useMemo } from 'react';
|
||||
import { StyleSheet, View, ViewStyle } from 'react-native';
|
||||
import { Platform, StyleSheet, View, ViewStyle } from 'react-native';
|
||||
|
||||
export enum InstallState {
|
||||
NotInstalled,
|
||||
@@ -20,16 +20,25 @@ interface Props {
|
||||
const useStyles = (disabled: boolean) => {
|
||||
return useMemo(() => {
|
||||
// For the TouchableRipple to work on Android, the card needs a transparent background.
|
||||
const baseCard = { backgroundColor: 'transparent' };
|
||||
const borderRadius = 12;
|
||||
const baseCard = { backgroundColor: 'transparent', borderRadius };
|
||||
return StyleSheet.create({
|
||||
cardOuterWrapper: {
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
borderRadius: 12,
|
||||
borderRadius,
|
||||
overflow: 'hidden',
|
||||
// Accessibility: Prevent the 'overflow: hidden' from hiding the focus indicator
|
||||
// on web. Only apply to web, as this causes the touchable ripple
|
||||
// from being completely contained within the card on non-web platforms.
|
||||
...(Platform.OS === 'web' ? {
|
||||
margin: -2,
|
||||
padding: 2,
|
||||
} : {}),
|
||||
},
|
||||
cardInnerWrapper: {
|
||||
width: '100%',
|
||||
borderRadius,
|
||||
},
|
||||
card: disabled ? {
|
||||
...baseCard,
|
||||
|
||||
@@ -629,7 +629,7 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
|
||||
);
|
||||
}
|
||||
|
||||
addSettingLink('donate_link', _('Make a donation'), 'https://joplinapp.org/donate/');
|
||||
if (Platform.OS !== 'ios') addSettingLink('donate_link', _('Make a donation'), 'https://joplinapp.org/donate/');
|
||||
addSettingLink('website_link', _('Joplin website'), 'https://joplinapp.org/');
|
||||
addSettingLink('privacy_link', _('Privacy Policy'), 'https://joplinapp.org/privacy/');
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { act, fireEvent, render, screen, userEvent, waitFor } from '../../../../
|
||||
import PluginService, { PluginSettings, defaultPluginSetting } from '@joplin/lib/services/plugins/PluginService';
|
||||
import { writeFile } from 'fs-extra';
|
||||
import { join } from 'path';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import shim, { MobilePlatform } from '@joplin/lib/shim';
|
||||
import { resetRepoApi } from './utils/useRepoApi';
|
||||
import { Store } from 'redux';
|
||||
import { AppState } from '../../../../utils/types';
|
||||
@@ -59,7 +59,7 @@ describe('PluginStates.installed', () => {
|
||||
mockPluginServiceSetup(reduxStore);
|
||||
resetRepoApi();
|
||||
|
||||
await mockMobilePlatform('android');
|
||||
await mockMobilePlatform(MobilePlatform.Android);
|
||||
await mockRepositoryApiConstructor();
|
||||
|
||||
// Fake timers are necessary to prevent a warning.
|
||||
@@ -73,8 +73,8 @@ describe('PluginStates.installed', () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
'android',
|
||||
'ios',
|
||||
MobilePlatform.Android,
|
||||
MobilePlatform.Ios,
|
||||
])('should not allow updating a plugin that is not recommended on iOS, but should on Android (on %s)', async (platform) => {
|
||||
await mockMobilePlatform(platform);
|
||||
expect(shim.mobilePlatform()).toBe(platform);
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Store } from 'redux';
|
||||
import mockRepositoryApiConstructor from './testUtils/mockRepositoryApiConstructor';
|
||||
import { resetRepoApi } from './utils/useRepoApi';
|
||||
import mockPluginServiceSetup from '../../../../utils/testing/mockPluginServiceSetup';
|
||||
import { MobilePlatform } from '@joplin/lib/shim';
|
||||
|
||||
const expectSearchResultCountToBe = async (count: number) => {
|
||||
await waitFor(() => {
|
||||
@@ -38,7 +39,7 @@ describe('PluginStates.search', () => {
|
||||
await switchClient(0);
|
||||
reduxStore = createMockReduxStore();
|
||||
mockPluginServiceSetup(reduxStore);
|
||||
mockMobilePlatform('android');
|
||||
mockMobilePlatform(MobilePlatform.Android);
|
||||
resetRepoApi();
|
||||
|
||||
await mockRepositoryApiConstructor();
|
||||
@@ -70,7 +71,7 @@ describe('PluginStates.search', () => {
|
||||
|
||||
it('should only show recommended plugin search results on iOS-like environments', async () => {
|
||||
// iOS uses restricted install mode
|
||||
mockMobilePlatform('ios');
|
||||
mockMobilePlatform(MobilePlatform.Ios);
|
||||
await mockRepositoryApiConstructor();
|
||||
|
||||
const wrapper = render(<WrappedPluginStates initialPluginSettings={{}} store={reduxStore}/>);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { ItemEvent } from '@joplin/lib/components/shared/config/plugins/types';
|
||||
import { Linking } from 'react-native';
|
||||
import getPluginHelpUrl from '@joplin/lib/services/plugins/utils/getPluginHelpUrl';
|
||||
|
||||
const openWebsiteForPlugin = ({ item }: ItemEvent) => {
|
||||
return Linking.openURL(`https://joplinapp.org/plugins/plugin/${item.manifest.id}`);
|
||||
return Linking.openURL(getPluginHelpUrl(item.manifest.id));
|
||||
};
|
||||
|
||||
export default openWebsiteForPlugin;
|
||||
|
||||
@@ -17,6 +17,7 @@ import { Portal, ProgressBar, Snackbar } from 'react-native-paper';
|
||||
import useBackHandler from '../../../utils/hooks/useBackHandler';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import NavService from '@joplin/lib/services/NavService';
|
||||
import { ResourceOcrDriverId, ResourceOcrStatus } from '@joplin/lib/services/database/types';
|
||||
|
||||
const logger = Logger.create('DocumentScanner');
|
||||
|
||||
@@ -77,13 +78,24 @@ const DocumentScanner: React.FC<Props> = ({ themeId, dispatch }) => {
|
||||
const onCreateNote = useCallback(async (event: CreateNoteEvent) => {
|
||||
setSnackbarMessage(_('Creating note "%s"...', event.title));
|
||||
setCreatingNote(true);
|
||||
logger.info('Creating note', event.queueForTranscription ? '(with transcription)' : '');
|
||||
|
||||
try {
|
||||
const resources = [];
|
||||
for (const image of photos) {
|
||||
resources.push(await shim.createResourceFromPath(
|
||||
image.uri,
|
||||
{ title: event.title, mime: image.type },
|
||||
{
|
||||
...(event.queueForTranscription ? {
|
||||
ocr_status: ResourceOcrStatus.Todo,
|
||||
ocr_driver_id: ResourceOcrDriverId.HandwrittenText,
|
||||
ocr_details: '',
|
||||
ocr_error: '',
|
||||
ocr_text: '',
|
||||
} : {}),
|
||||
title: event.title,
|
||||
mime: image.type,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { useMemo, useState, useCallback, useEffect } from 'react';
|
||||
import { themeStyle } from '../../global-style';
|
||||
import { ScrollView, StyleSheet, View } from 'react-native';
|
||||
import { ScrollView, StyleSheet, TextStyle, View } from 'react-native';
|
||||
import { CameraResult } from '../../CameraView/types';
|
||||
import TextInput from '../../TextInput';
|
||||
import PhotoPreview from '../../CameraView/PhotoPreview';
|
||||
@@ -15,11 +15,14 @@ import Folder from '@joplin/lib/models/Folder';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { formatMsToLocal } from '@joplin/utils/time';
|
||||
import { PrimaryButton } from '../../buttons';
|
||||
import { Switch, Text } from 'react-native-paper';
|
||||
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
|
||||
|
||||
export interface CreateNoteEvent {
|
||||
title: string;
|
||||
tags: string[];
|
||||
parentId: string;
|
||||
queueForTranscription: boolean;
|
||||
}
|
||||
|
||||
type OnCreateNote = (event: CreateNoteEvent)=> void;
|
||||
@@ -31,6 +34,8 @@ interface Props {
|
||||
allTags: TagEntity[];
|
||||
allFolders: FolderEntity[];
|
||||
selectedFolderId: string;
|
||||
isJoplinServer: boolean;
|
||||
queueForTranscriptionDefault: boolean;
|
||||
|
||||
onCreateNote: null|OnCreateNote;
|
||||
}
|
||||
@@ -38,6 +43,11 @@ interface Props {
|
||||
const useStyles = (themeId: number) => {
|
||||
return useMemo(() => {
|
||||
const theme = themeStyle(themeId);
|
||||
const headerStyle: TextStyle = {
|
||||
...theme.headerStyle,
|
||||
fontSize: theme.fontSize,
|
||||
fontWeight: 'normal',
|
||||
};
|
||||
return StyleSheet.create({
|
||||
titleInput: {
|
||||
color: theme.color,
|
||||
@@ -58,9 +68,7 @@ const useStyles = (themeId: number) => {
|
||||
tagEditor: {
|
||||
marginHorizontal: theme.margin,
|
||||
},
|
||||
tagEditorHeader: {
|
||||
fontWeight: 'normal',
|
||||
},
|
||||
tagEditorHeader: headerStyle,
|
||||
folderPickerLine: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
@@ -75,6 +83,24 @@ const useStyles = (themeId: number) => {
|
||||
alignSelf: 'flex-end',
|
||||
margin: theme.margin,
|
||||
},
|
||||
transcriptionCheckboxContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
marginHorizontal: theme.margin,
|
||||
marginTop: theme.margin * 2,
|
||||
marginBottom: theme.margin * 2,
|
||||
gap: 6,
|
||||
},
|
||||
transcriptionCheckbox: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
transcriptionLabel: headerStyle,
|
||||
transcriptionHelp: {
|
||||
color: theme.colorFaded,
|
||||
gap: 8,
|
||||
},
|
||||
});
|
||||
}, [themeId]);
|
||||
};
|
||||
@@ -85,12 +111,13 @@ const tagSearchResultsProps = {
|
||||
};
|
||||
|
||||
const NotePreview: React.FC<Props> = ({
|
||||
themeId, lastImage, imageCount, allTags, onCreateNote, allFolders, selectedFolderId: propsSelectedFolderId,
|
||||
themeId, lastImage, imageCount, allTags, onCreateNote, allFolders, selectedFolderId: propsSelectedFolderId, isJoplinServer, queueForTranscriptionDefault,
|
||||
}) => {
|
||||
const styles = useStyles(themeId);
|
||||
const [title, setTitle] = useState('');
|
||||
const [tags, setTags] = useState([]);
|
||||
const [selectedFolderId, setSelectedFolderId] = useState(propsSelectedFolderId);
|
||||
const [queueForTranscription, setQueueForTranscription] = useState(queueForTranscriptionDefault);
|
||||
|
||||
const realFolders = useMemo(() => {
|
||||
return Folder.getRealFolders(allFolders);
|
||||
@@ -115,18 +142,33 @@ const NotePreview: React.FC<Props> = ({
|
||||
const onNewNote = useCallback(() => {
|
||||
if (!onCreateNote) return;
|
||||
|
||||
Setting.setValue('scanner.requestTranscription', queueForTranscription);
|
||||
|
||||
onCreateNote({
|
||||
tags,
|
||||
title,
|
||||
parentId: selectedFolderId ?? '',
|
||||
queueForTranscription,
|
||||
});
|
||||
}, [onCreateNote, tags, title, selectedFolderId]);
|
||||
}, [onCreateNote, tags, title, selectedFolderId, queueForTranscription]);
|
||||
|
||||
const onNewFolder = useCallback(async (title: string) => {
|
||||
const folder = await Folder.save({ title });
|
||||
setSelectedFolderId(folder.id);
|
||||
}, []);
|
||||
|
||||
const transcriptionCheckbox = <View style={styles.transcriptionCheckboxContainer}>
|
||||
<View style={styles.transcriptionCheckbox}>
|
||||
<Text nativeID='transcriptionLabel' style={styles.transcriptionLabel}>{_('Recognise text:')}</Text>
|
||||
<Switch accessibilityLabelledBy='transcriptionLabel' value={queueForTranscription} onValueChange={setQueueForTranscription} />
|
||||
</View>
|
||||
<View>
|
||||
<Text style={styles.transcriptionHelp}>{
|
||||
_('When enabled, requests that the images in the note be transcribed with a higher-quality on-server transcription service. Requires sync with a copy of the desktop app.')
|
||||
}</Text>
|
||||
</View>
|
||||
</View>;
|
||||
|
||||
return <ScrollView style={styles.rootScrollView}>
|
||||
<TextInput
|
||||
style={styles.titleInput}
|
||||
@@ -162,6 +204,7 @@ const NotePreview: React.FC<Props> = ({
|
||||
headerStyle={styles.tagEditorHeader}
|
||||
searchResultProps={tagSearchResultsProps}
|
||||
/>
|
||||
{isJoplinServer ? transcriptionCheckbox : null}
|
||||
<PrimaryButton
|
||||
onPress={onNewNote}
|
||||
style={styles.actionButton}
|
||||
@@ -174,5 +217,7 @@ export default connect((state: AppState) => ({
|
||||
allTags: state.tags,
|
||||
allFolders: state.folders,
|
||||
selectedFolderId: state.selectedFolderId,
|
||||
isJoplinServer: SyncTargetRegistry.isJoplinServerOrCloud(state.settings['sync.target']),
|
||||
themeId: state.settings.theme,
|
||||
queueForTranscriptionDefault: state.settings['scanner.requestTranscription'],
|
||||
}))(NotePreview);
|
||||
|
||||
@@ -515,6 +515,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
paddingLeft: theme.marginLeft,
|
||||
borderBottomColor: theme.dividerColor,
|
||||
borderBottomWidth: 1,
|
||||
maxHeight: '40%',
|
||||
};
|
||||
|
||||
styles.titleContainerTodo = { ...styles.titleContainer };
|
||||
@@ -659,6 +660,17 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
});
|
||||
}
|
||||
|
||||
// Reset undo/redo button state when switching to edit mode or when switching between markdown and rich text editors, since the editor is
|
||||
// recreated and loses its undo/redo history
|
||||
if (this.state.mode === 'edit' && (prevState.mode !== this.state.mode || prevProps.editorType !== this.props.editorType)) {
|
||||
this.setState({
|
||||
undoRedoButtonState: {
|
||||
canUndo: false,
|
||||
canRedo: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (prevProps.noteId && this.props.noteId && prevProps.noteId !== this.props.noteId) {
|
||||
// Easier to just go back, then go to the note since
|
||||
// the Note screen doesn't handle reloading a different note
|
||||
@@ -690,6 +702,10 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
if (prevState.note.body !== this.state.note.body) {
|
||||
this.emitEditorPluginUpdate_();
|
||||
}
|
||||
|
||||
if (prevState.multiline !== this.state.multiline && this.titleTextFieldRef.current) {
|
||||
focus('Note::focusUpdate::title', this.titleTextFieldRef.current);
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
@@ -1703,6 +1719,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
<View style={titleContainerStyle}>
|
||||
{isTodo && <Checkbox style={this.styles().checkbox} checked={!!Number(note.todo_completed)} onChange={this.todoCheckbox_change} />}
|
||||
<TextInput
|
||||
key={this.state.multiline ? 'multiLine' : 'singleLine'}
|
||||
ref={this.titleTextFieldRef}
|
||||
underlineColorAndroid="#ffffff00"
|
||||
autoCapitalize="sentences"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { View, StyleSheet, TextInput, Platform } from 'react-native';
|
||||
import { View, StyleSheet, TextInput, Platform, ScrollView, Text as TextNative } from 'react-native';
|
||||
import { AppState } from '../../utils/types';
|
||||
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import Revision from '@joplin/lib/models/Revision';
|
||||
@@ -111,6 +111,7 @@ const useStyles = (themeId: number) => {
|
||||
flex: 0,
|
||||
flexDirection: 'row',
|
||||
flexBasis: 'auto',
|
||||
maxHeight: '40%',
|
||||
},
|
||||
titleText: {
|
||||
flex: 1,
|
||||
@@ -224,12 +225,25 @@ const NoteRevisionViewer: React.FC<Props> = props => {
|
||||
|
||||
const titleComponent = (
|
||||
<View style={styles.titleViewContainer}>
|
||||
<TextInput
|
||||
style={styles.titleText}
|
||||
value={note?.title ?? ''}
|
||||
editable={false}
|
||||
multiline={multiline}
|
||||
/>
|
||||
{
|
||||
multiline ?
|
||||
<ScrollView
|
||||
style={{ flex: 1 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<TextNative
|
||||
selectable
|
||||
style={styles.titleText}
|
||||
>
|
||||
{note?.title ?? ''}
|
||||
</TextNative>
|
||||
</ScrollView> :
|
||||
<TextInput
|
||||
style={styles.titleText}
|
||||
value={note?.title ?? ''}
|
||||
editable={false}
|
||||
/>
|
||||
}
|
||||
{ titleToggleButton }
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@ import TagEditor, { TagEditorMode } from '../TagEditor';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import { ViewStyle } from 'react-native';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
@@ -22,6 +23,9 @@ const modalPropOverrides = {
|
||||
// Prevent the keyboard from auto-dismissing when tapping outside the search input
|
||||
keyboardShouldPersistTaps: true,
|
||||
},
|
||||
containerStyle: {
|
||||
height: '100%',
|
||||
} as ViewStyle,
|
||||
};
|
||||
|
||||
const NoteTagsDialogComponent: React.FC<Props> = props => {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import * as React from 'react';
|
||||
import { AppState } from '../../../utils/types';
|
||||
import { Store } from 'redux';
|
||||
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
import createMockReduxStore from '../../../utils/testing/createMockReduxStore';
|
||||
import setupGlobalStore from '../../../utils/testing/setupGlobalStore';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import { render, screen } from '../../../utils/testing/testingLibrary';
|
||||
import SearchResults from './SearchResults';
|
||||
import SearchEngine from '@joplin/lib/services/search/SearchEngine';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import TestProviderStack from '../../testing/TestProviderStack';
|
||||
|
||||
const createNotes = async (count: number) => {
|
||||
const folder = await Folder.save({ title: 'Test Note' });
|
||||
for (let i = 0; i < count; i++) {
|
||||
await Note.save({ title: `abcd ${i}`, body: 'body', parent_id: folder.id });
|
||||
}
|
||||
await SearchEngine.instance().syncTables();
|
||||
};
|
||||
|
||||
let store: Store<AppState>;
|
||||
|
||||
interface WrapperProps {
|
||||
query: string;
|
||||
paused: boolean;
|
||||
}
|
||||
const WrappedSearchResults: React.FC<WrapperProps> = props => (
|
||||
<TestProviderStack store={store}>
|
||||
<SearchResults paused={props.paused} query={props.query} onHighlightedWordsChange={() => { }} ftsEnabled={1} />
|
||||
</TestProviderStack>
|
||||
);
|
||||
|
||||
describe('SearchResult', () => {
|
||||
beforeEach(async () => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
store = createMockReduxStore();
|
||||
setupGlobalStore(store);
|
||||
});
|
||||
|
||||
test('should show results when unpaused', async () => {
|
||||
const noteCount = 8;
|
||||
await createNotes(noteCount);
|
||||
|
||||
render(<WrappedSearchResults query='abcd' paused={false}/>);
|
||||
const items = await screen.findAllByText(/abcd \d\d?\d?/);
|
||||
expect(items.length).toBe(noteCount);
|
||||
});
|
||||
});
|
||||
@@ -13,6 +13,7 @@ import shim from '@joplin/lib/shim';
|
||||
|
||||
interface Props {
|
||||
query: string;
|
||||
paused: boolean;
|
||||
onHighlightedWordsChange: (highlightedWords: (ComplexTerm | string)[])=> void;
|
||||
|
||||
ftsEnabled: number;
|
||||
@@ -28,7 +29,7 @@ const useResults = (props: Props) => {
|
||||
let notes: NoteEntity[] = [];
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
if (query) {
|
||||
if (query && !props.paused) {
|
||||
if (ftsEnabled) {
|
||||
const r = await SearchEngineUtils.notesForQuery(query, true, { appendWildCards: true });
|
||||
notes = r.notes;
|
||||
@@ -57,7 +58,7 @@ const useResults = (props: Props) => {
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
}, [query, ftsEnabled], { interval: 200 });
|
||||
}, [query, props.paused, ftsEnabled], { interval: 200 });
|
||||
|
||||
return {
|
||||
notes,
|
||||
|
||||
@@ -53,11 +53,36 @@ const useStyles = (theme: ThemeStyle, visible: boolean) => {
|
||||
}, [theme, visible]);
|
||||
};
|
||||
|
||||
// Workaround for https://github.com/laurent22/joplin/issues/12823:
|
||||
// Disable search-as-you-type for short 0-2 character searches that
|
||||
// are likely to match the start of a large number of words.
|
||||
const useSearchPaused = (query: string) => {
|
||||
const [pauseDisabled, setPauseDisabled] = useState(false);
|
||||
// Only disable search-as-you-type for a subset of all characters.
|
||||
// This is, for example, to ensure that search-as-you-type remains
|
||||
// enabled for CJK characters (e.g. U+6570 has length 1).
|
||||
const paused = query.match(/^[a-z0-9]{0,2}$/i);
|
||||
|
||||
const onOverridePause = useCallback(() => {
|
||||
setPauseDisabled(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setPauseDisabled(false);
|
||||
}, [query]);
|
||||
|
||||
return {
|
||||
paused: paused && !pauseDisabled,
|
||||
onOverridePause,
|
||||
};
|
||||
};
|
||||
|
||||
const SearchScreenComponent: React.FC<Props> = props => {
|
||||
const theme = themeStyle(props.themeId);
|
||||
const styles = useStyles(theme, props.visible);
|
||||
|
||||
const [query, setQuery] = useState(props.query);
|
||||
const { paused, onOverridePause } = useSearchPaused(query);
|
||||
|
||||
const globalQueryRef = useRef(props.query);
|
||||
globalQueryRef.current = props.query;
|
||||
@@ -99,6 +124,7 @@ const SearchScreenComponent: React.FC<Props> = props => {
|
||||
autoFocus={props.visible}
|
||||
underlineColorAndroid="#ffffff00"
|
||||
onChangeText={setQuery}
|
||||
onSubmitEditing={onOverridePause}
|
||||
value={query}
|
||||
selectionColor={theme.textSelectionColor}
|
||||
keyboardAppearance={theme.keyboardAppearance}
|
||||
@@ -114,6 +140,7 @@ const SearchScreenComponent: React.FC<Props> = props => {
|
||||
|
||||
<SearchResults
|
||||
query={query}
|
||||
paused={paused}
|
||||
ftsEnabled={props.ftsEnabled}
|
||||
onHighlightedWordsChange={onHighlightedWordsChange}
|
||||
/>
|
||||
|
||||
@@ -96,6 +96,10 @@ const useStyles = (themeId: number) => {
|
||||
...buttonStyle,
|
||||
flex: 0,
|
||||
};
|
||||
const folderButtonTextStyle: ViewStyle = {
|
||||
...buttonTextStyle,
|
||||
paddingLeft: 0,
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
menu: {
|
||||
@@ -113,9 +117,10 @@ const useStyles = (themeId: number) => {
|
||||
},
|
||||
sidebarIcon: sidebarIconStyle,
|
||||
folderButton: folderButtonStyle,
|
||||
folderButtonText: {
|
||||
...buttonTextStyle,
|
||||
paddingLeft: 0,
|
||||
folderButtonText: folderButtonTextStyle,
|
||||
conflictFolderButtonText: {
|
||||
...folderButtonTextStyle,
|
||||
color: theme.colorError,
|
||||
},
|
||||
folderButtonSelected: {
|
||||
...folderButtonStyle,
|
||||
@@ -264,6 +269,7 @@ const FolderItem: React.FC<FolderItemProps> = props => {
|
||||
// depth is specified with an accessibilityLabel:
|
||||
const folderDepthDescription = props.depth > 0 ? _('(level %d)', props.depth) : '';
|
||||
const accessibilityLabel = `${folderTitle} ${folderDepthDescription}`.trim();
|
||||
const folderButtonTextStyle = props.folder.id === Folder.conflictFolderId() ? baseStyles.conflictFolderButtonText : baseStyles.folderButtonText;
|
||||
return (
|
||||
<View key={props.folder.id} style={styles.buttonWrapper}>
|
||||
<TouchableRipple
|
||||
@@ -279,7 +285,7 @@ const FolderItem: React.FC<FolderItemProps> = props => {
|
||||
{renderFolderIcon(props.folder.id, folderIcon)}
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={baseStyles.folderButtonText}
|
||||
style={folderButtonTextStyle}
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
>
|
||||
{folderTitle}
|
||||
|
||||
@@ -88,7 +88,7 @@ const useWebViewSetup = ({
|
||||
`;
|
||||
|
||||
const injectedJavaScript = useMemo(() => `
|
||||
if (typeof markdownEditorBundle === 'undefined') {
|
||||
if (typeof window.markdownEditorBundle === 'undefined') {
|
||||
${shim.injectedJs('markdownEditorBundle')};
|
||||
window.markdownEditorBundle = markdownEditorBundle;
|
||||
markdownEditorBundle.setUpLogger();
|
||||
|
||||
@@ -535,7 +535,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 145;
|
||||
CURRENT_PROJECT_VERSION = 146;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Joplin/Info.plist;
|
||||
@@ -570,7 +570,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 145;
|
||||
CURRENT_PROJECT_VERSION = 146;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
INFOPLIST_FILE = Joplin/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
|
||||
@@ -771,7 +771,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 145;
|
||||
CURRENT_PROJECT_VERSION = 146;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
@@ -814,7 +814,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 145;
|
||||
CURRENT_PROJECT_VERSION = 146;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
|
||||
@@ -6,7 +6,7 @@ PODS:
|
||||
- ReactCommon/turbomodule/core
|
||||
- EXConstants (17.1.7):
|
||||
- ExpoModulesCore
|
||||
- Expo (53.0.19):
|
||||
- Expo (53.0.20):
|
||||
- DoubleConversion
|
||||
- ExpoModulesCore
|
||||
- glog
|
||||
@@ -35,7 +35,7 @@ PODS:
|
||||
- Yoga
|
||||
- ExpoAsset (11.1.7):
|
||||
- ExpoModulesCore
|
||||
- ExpoCamera (16.1.10):
|
||||
- ExpoCamera (16.1.11):
|
||||
- ExpoModulesCore
|
||||
- ZXingObjC/OneD
|
||||
- ZXingObjC/PDF417
|
||||
@@ -45,7 +45,7 @@ PODS:
|
||||
- ExpoModulesCore
|
||||
- ExpoLocalAuthentication (16.0.5):
|
||||
- ExpoModulesCore
|
||||
- ExpoModulesCore (2.4.2):
|
||||
- ExpoModulesCore (2.5.0):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
@@ -1408,7 +1408,7 @@ PODS:
|
||||
- ReactCommon/turbomodule/core
|
||||
- react-native-alarm-notification (3.5.0):
|
||||
- React
|
||||
- react-native-document-picker (10.1.3):
|
||||
- react-native-document-picker (10.1.5):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
@@ -1522,7 +1522,7 @@ PODS:
|
||||
- React-Core
|
||||
- react-native-version-info (1.1.1):
|
||||
- React-Core
|
||||
- react-native-webview (13.14.2):
|
||||
- react-native-webview (13.15.0):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
@@ -1890,7 +1890,7 @@ PODS:
|
||||
- React
|
||||
- RNSecureRandom (1.0.1):
|
||||
- React
|
||||
- RNShare (12.0.11):
|
||||
- RNShare (12.1.0):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
@@ -2293,13 +2293,13 @@ SPEC CHECKSUMS:
|
||||
DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5
|
||||
EXAV: ae28256069c4cdde93d185c007d8f68d92902c2e
|
||||
EXConstants: 98bcf0f22b820f9b28f9fee55ff2daededadd2f8
|
||||
Expo: 4b1c6de7c441e1caa1918671ae0aa34d51f019a5
|
||||
Expo: b527631da3b11e085809e877b845f9e6cdd68f9c
|
||||
ExpoAsset: ef06e880126c375f580d4923fdd1cdf4ee6ee7d6
|
||||
ExpoCamera: 7edf99216d92e40b991d4e7ed69eba9527c94cda
|
||||
ExpoCamera: e1879906d41184e84b57d7643119f8509414e318
|
||||
ExpoFileSystem: 7f92f7be2f5c5ed40a7c9efc8fa30821181d9d63
|
||||
ExpoFont: cf508bc2e6b70871e05386d71cab927c8524cc8e
|
||||
ExpoLocalAuthentication: c35f18692dcb35775a1be0f37b2131096951a6bd
|
||||
ExpoModulesCore: e2c98670a94932b744f5bc4e394520e1c63b5462
|
||||
ExpoModulesCore: f55e7872391bae03ee5547c83152c81750d89508
|
||||
fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6
|
||||
FBLazyVector: 84b955f7b4da8b895faf5946f73748267347c975
|
||||
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
|
||||
@@ -2340,7 +2340,7 @@ SPEC CHECKSUMS:
|
||||
React-Mapbuffer: c3f4b608e4a59dd2f6a416ef4d47a14400194468
|
||||
React-microtasksnativemodule: 054f34e9b82f02bd40f09cebd4083828b5b2beb6
|
||||
react-native-alarm-notification: a4326a743df72a94d361a4c3a21515556f650341
|
||||
react-native-document-picker: da39c5e4f279d39c0356dca157b98f9dc349e5bb
|
||||
react-native-document-picker: d7580f6e287bbf2c31c071d6b3f252ae1c6586f1
|
||||
react-native-geolocation: ec15ffebc53790314885eb9e5f2132132fbc2600
|
||||
react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba
|
||||
react-native-image-picker: 7babe45e727db306b3f00d08c72eda3586d6e9c1
|
||||
@@ -2352,7 +2352,7 @@ SPEC CHECKSUMS:
|
||||
react-native-safe-area-context: dde2052b903c11d677c320b599c3244021c34ce8
|
||||
react-native-sqlite-storage: 0c84826214baaa498796c7e46a5ccc9a82e114ed
|
||||
react-native-version-info: f0b04e16111c4016749235ff6d9a757039189141
|
||||
react-native-webview: 2d9ffd72b87cf905cdf8821d7d27d551188bac70
|
||||
react-native-webview: 0dceb35a9d050f5fa55f7fe2d8c4d1903651eb7d
|
||||
React-NativeModulesApple: 2c4377e139522c3d73f5df582e4f051a838ff25e
|
||||
React-oscompat: ef5df1c734f19b8003e149317d041b8ce1f7d29c
|
||||
React-perflogger: 9a151e0b4c933c9205fd648c246506a83f31395d
|
||||
@@ -2395,7 +2395,7 @@ SPEC CHECKSUMS:
|
||||
RNLocalize: 6a87f0490f1793d7a70042e4c55eb9a1ba6dd5b4
|
||||
RNQuickAction: c2c8f379e614428be0babe4d53a575739667744d
|
||||
RNSecureRandom: b64d263529492a6897e236a22a2c4249aa1b53dc
|
||||
RNShare: 675e8e4a84f0137baf33057cac8f7334b0bb4b98
|
||||
RNShare: 9528acd4e374d3cb76b994b9e167d4a75cd8f452
|
||||
RNSVG: 295a96bc43f2baa5958d64aeec9847a1d8ca7a3d
|
||||
RNVectorIcons: d53917643fddb261b22bd6d889776f336893622b
|
||||
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
||||
|
||||
@@ -29,11 +29,11 @@
|
||||
"@joplin/renderer": "~3.5",
|
||||
"@joplin/utils": "~3.5",
|
||||
"@react-native-clipboard/clipboard": "1.16.3",
|
||||
"@react-native-community/datetimepicker": "8.4.2",
|
||||
"@react-native-community/datetimepicker": "8.4.3",
|
||||
"@react-native-community/geolocation": "3.4.0",
|
||||
"@react-native-community/netinfo": "11.4.1",
|
||||
"@react-native-community/push-notification-ios": "1.11.0",
|
||||
"@react-native-documents/picker": "10.1.3",
|
||||
"@react-native-documents/picker": "10.1.5",
|
||||
"assert-browserify": "2.0.0",
|
||||
"buffer": "6.0.3",
|
||||
"color": "3.2.1",
|
||||
@@ -41,9 +41,9 @@
|
||||
"crypto-browserify": "3.12.1",
|
||||
"deprecated-react-native-prop-types": "5.0.0",
|
||||
"events": "3.3.0",
|
||||
"expo": "53.0.19",
|
||||
"expo": "53.0.20",
|
||||
"expo-av": "15.1.7",
|
||||
"expo-camera": "16.1.10",
|
||||
"expo-camera": "16.1.11",
|
||||
"expo-local-authentication": "16.0.5",
|
||||
"lodash": "4.17.21",
|
||||
"md5": "2.3.0",
|
||||
@@ -66,15 +66,15 @@
|
||||
"react-native-quick-actions": "0.3.13",
|
||||
"react-native-quick-crypto": "0.7.17",
|
||||
"react-native-rsa-native": "2.0.5",
|
||||
"react-native-safe-area-context": "5.4.1",
|
||||
"react-native-safe-area-context": "5.5.2",
|
||||
"react-native-securerandom": "1.0.1",
|
||||
"react-native-share": "12.0.11",
|
||||
"react-native-share": "12.1.2",
|
||||
"react-native-sqlite-storage": "6.0.1",
|
||||
"react-native-svg": "15.13.0",
|
||||
"react-native-url-polyfill": "2.0.0",
|
||||
"react-native-vector-icons": "10.2.0",
|
||||
"react-native-version-info": "1.1.1",
|
||||
"react-native-webview": "13.14.2",
|
||||
"react-native-webview": "13.15.0",
|
||||
"react-native-zip-archive": "7.0.2",
|
||||
"react-redux": "8.1.3",
|
||||
"redux": "4.2.1",
|
||||
@@ -99,23 +99,23 @@
|
||||
"@react-native-community/cli": "16.0.3",
|
||||
"@react-native-community/cli-platform-android": "16.0.3",
|
||||
"@react-native-community/cli-platform-ios": "16.0.3",
|
||||
"@react-native/babel-preset": "0.79.5",
|
||||
"@react-native/babel-preset": "0.80.1",
|
||||
"@react-native/metro-config": "0.79.5",
|
||||
"@react-native/typescript-config": "0.79.5",
|
||||
"@react-native/typescript-config": "0.80.2",
|
||||
"@sqlite.org/sqlite-wasm": "3.46.0-build2",
|
||||
"@testing-library/react-native": "13.2.0",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/node": "18.19.115",
|
||||
"@types/node": "18.19.130",
|
||||
"@types/react": "19.0.14",
|
||||
"@types/react-redux": "7.1.33",
|
||||
"@types/serviceworker": "0.0.140",
|
||||
"@types/serviceworker": "0.0.149",
|
||||
"@types/tar-stream": "3.1.4",
|
||||
"babel-jest": "29.7.0",
|
||||
"babel-loader": "9.1.3",
|
||||
"babel-plugin-module-resolver": "4.1.0",
|
||||
"babel-plugin-react-native-web": "0.20.0",
|
||||
"esbuild": "0.25.5",
|
||||
"esbuild": "0.25.8",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"fs-extra": "11.2.0",
|
||||
"gulp": "4.0.2",
|
||||
@@ -130,10 +130,10 @@
|
||||
"react-native-web": "0.20.0",
|
||||
"react-refresh": "0.17.0",
|
||||
"react-test-renderer": "19.0.0",
|
||||
"sharp": "0.34.2",
|
||||
"sharp": "0.34.3",
|
||||
"sqlite3": "5.1.6",
|
||||
"timers-browserify": "2.0.12",
|
||||
"ts-jest": "29.3.4",
|
||||
"ts-jest": "29.4.1",
|
||||
"ts-loader": "9.5.2",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.8.3",
|
||||
|
||||
@@ -144,7 +144,7 @@ const generalMiddleware = (store: any) => (next: any) => async (action: any) =>
|
||||
if (action.type === 'NAV_GO') Keyboard.dismiss();
|
||||
|
||||
if (['NOTE_UPDATE_ONE', 'NOTE_DELETE', 'FOLDER_UPDATE_ONE', 'FOLDER_DELETE'].indexOf(action.type) >= 0) {
|
||||
if (!await reg.syncTarget().syncStarted()) void reg.scheduleSync(1000, { syncSteps: ['update_remote', 'delete_remote'] }, true);
|
||||
if (!await reg.syncTarget().syncStarted()) void reg.scheduleSync(reg.syncAsYouTypeInterval(), { syncSteps: ['update_remote', 'delete_remote'] }, true);
|
||||
SearchEngine.instance().scheduleSyncTables();
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
// files: First here we convert the JS file to a plain string, and that string
|
||||
// is then loaded by eg. the Mermaid plugin, and finally injected in the WebView.
|
||||
|
||||
import { dirname, extname, basename } from 'path';
|
||||
import { dirname, extname, basename, resolve } from 'path';
|
||||
|
||||
import * as esbuild from 'esbuild';
|
||||
import copyAssets from './copyAssets';
|
||||
import { writeFile } from 'fs-extra';
|
||||
import { writeFile, readFile } from 'fs-extra';
|
||||
|
||||
export default class BundledFile {
|
||||
private readonly bundleOutputPathBase_: string;
|
||||
@@ -54,6 +54,32 @@ export default class BundledFile {
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
// Supports require(...)ing SVG images
|
||||
name: 'joplin--require-svg',
|
||||
setup: build => {
|
||||
// A relative path to an SVG:
|
||||
build.onResolve({ filter: /^\.{1,2}\/.*\.svg$/ }, args => ({
|
||||
path: resolve(args.resolveDir, args.path),
|
||||
namespace: 'joplin-require-svg',
|
||||
}));
|
||||
|
||||
build.onLoad({ filter: /^.*$/, namespace: 'joplin-require-svg' }, async args => {
|
||||
const fileContent = await readFile(args.path, 'utf-8');
|
||||
return { contents: `
|
||||
let svg = null;
|
||||
export default () => {
|
||||
svg ??= (() => {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(${JSON.stringify(fileContent)}, 'image/svg+xml');
|
||||
return doc.querySelector('svg');
|
||||
})();
|
||||
return svg.cloneNode(true);
|
||||
};
|
||||
` };
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'joplin--copy-final',
|
||||
setup: build => {
|
||||
|
||||
@@ -67,6 +67,11 @@ const appReducer = (state = appDefaultState, action: any) => {
|
||||
|
||||
newState.selectedNoteHash = '';
|
||||
|
||||
if (currentRoute.routeName === 'Search' && action.routeName === 'Notes') {
|
||||
// Force a reload of the note list
|
||||
newState.notesSource = '';
|
||||
}
|
||||
|
||||
if (action.routeName === 'Search') {
|
||||
newState.notesParentType = 'Search';
|
||||
}
|
||||
|
||||
@@ -488,6 +488,14 @@ const buildStartupTasks = (
|
||||
|
||||
// await printTestData();
|
||||
});
|
||||
addTask('buildStartupTasks/optionally show sync wizard', async () => {
|
||||
if (Setting.value('sync.wizard.autoShowOnStartup') && Setting.value('sync.target') === 0) {
|
||||
dispatch({
|
||||
type: 'SYNC_WIZARD_VISIBLE_CHANGE',
|
||||
visible: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return startupTasks;
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ const useSafeAreaPadding = () => {
|
||||
paddingRight: safeAreaInsets.right,
|
||||
paddingLeft: safeAreaInsets.left,
|
||||
paddingTop: safeAreaInsets.top,
|
||||
paddingBottom: 0,
|
||||
paddingBottom: safeAreaInsets.bottom,
|
||||
} : {
|
||||
paddingTop: safeAreaInsets.top,
|
||||
paddingBottom: safeAreaInsets.bottom,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import shimInitShared from './shimInitShared';
|
||||
|
||||
import shim from '@joplin/lib/shim';
|
||||
import shim, { MobilePlatform } from '@joplin/lib/shim';
|
||||
const { GeolocationReact } = require('../geolocation-react.js');
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
import { generateSecureRandom } from 'react-native-securerandom';
|
||||
@@ -165,7 +165,7 @@ export default function shimInit() {
|
||||
};
|
||||
|
||||
shim.mobilePlatform = () => {
|
||||
return Platform.OS;
|
||||
return Platform.OS as MobilePlatform;
|
||||
};
|
||||
|
||||
shim.isAppleSilicon = () => {
|
||||
|
||||
@@ -9,7 +9,7 @@ import * as mimeUtils from '@joplin/lib/mime-utils';
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
import { getLocales } from 'react-native-localize';
|
||||
import type Setting from '@joplin/lib/models/Setting';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import shim, { MobilePlatform } from '@joplin/lib/shim';
|
||||
import { closestSupportedLocale, defaultLocale, setLocale } from '@joplin/lib/locale';
|
||||
|
||||
const shimInitShared = () => {
|
||||
@@ -76,7 +76,7 @@ const shimInitShared = () => {
|
||||
};
|
||||
|
||||
shim.mobilePlatform = () => {
|
||||
return Platform.OS;
|
||||
return Platform.OS as MobilePlatform;
|
||||
};
|
||||
|
||||
shim.platformArch = () => {
|
||||
|
||||
@@ -29,10 +29,14 @@ interface CssDecorationSpec extends DecorationRange {
|
||||
id?: number;
|
||||
}
|
||||
|
||||
interface RemoveMarkDecorationSpec {
|
||||
id: number;
|
||||
}
|
||||
|
||||
const addLineDecorationEffect = StateEffect.define<CssDecorationSpec>(mapRangeConfig);
|
||||
const removeLineDecorationEffect = StateEffect.define<CssDecorationSpec>(mapRangeConfig);
|
||||
const addMarkDecorationEffect = StateEffect.define<CssDecorationSpec>(mapRangeConfig);
|
||||
const removeMarkDecorationEffect = StateEffect.define<CssDecorationSpec>(mapRangeConfig);
|
||||
const removeMarkDecorationEffect = StateEffect.define<RemoveMarkDecorationSpec>();
|
||||
const refreshOverlaysEffect = StateEffect.define();
|
||||
|
||||
export interface LineWidgetOptions {
|
||||
@@ -190,12 +194,13 @@ export default class Decorator {
|
||||
decorations = decorations.update({
|
||||
add: [decoration.range(from, to)],
|
||||
});
|
||||
} else if (effect.is(removeLineDecorationEffect) || effect.is(removeMarkDecorationEffect)) {
|
||||
} else if (effect.is(removeLineDecorationEffect)) {
|
||||
const doc = transaction.state.doc;
|
||||
const targetFrom = doc.lineAt(effect.value.from).from;
|
||||
const targetTo = doc.lineAt(effect.value.to).to;
|
||||
const { from, to } = effect.value;
|
||||
// Handle the case where { from, to } point to an outdated document
|
||||
const targetFrom = doc.lineAt(from).from;
|
||||
const targetTo = doc.lineAt(to).to;
|
||||
|
||||
const targetId = effect.value.id;
|
||||
const targetDecoration = this.classNameToCssDecoration(
|
||||
effect.value.cssClass, effect.is(removeLineDecorationEffect),
|
||||
);
|
||||
@@ -203,12 +208,15 @@ export default class Decorator {
|
||||
decorations = decorations.update({
|
||||
// Returns true only for decorations that should be kept.
|
||||
filter: (from, to, value) => {
|
||||
if (targetId !== undefined) {
|
||||
return value.spec.id !== effect.value.id;
|
||||
}
|
||||
|
||||
const isInRange = from >= targetFrom && to <= targetTo;
|
||||
return isInRange && value.eq(targetDecoration);
|
||||
return !isInRange || !value.eq(targetDecoration);
|
||||
},
|
||||
});
|
||||
} else if (effect.is(removeMarkDecorationEffect)) {
|
||||
decorations = decorations.update({
|
||||
// Returns true only for decorations that should be kept.
|
||||
filter: (_from, _to, value) => {
|
||||
return value.spec.id !== effect.value.id;
|
||||
},
|
||||
});
|
||||
} else if (effect.is(addLineWidgetEffect)) {
|
||||
@@ -384,9 +392,10 @@ export default class Decorator {
|
||||
}
|
||||
|
||||
public markText(from: number, to: number, options?: MarkTextOptions) {
|
||||
const id = this._nextLineWidgetId++;
|
||||
const effectOptions: CssDecorationSpec = {
|
||||
cssClass: options.className ?? '',
|
||||
id: this._nextLineWidgetId++,
|
||||
id,
|
||||
from,
|
||||
to,
|
||||
};
|
||||
@@ -398,7 +407,7 @@ export default class Decorator {
|
||||
return {
|
||||
clear: () => {
|
||||
this.editor.dispatch({
|
||||
effects: removeMarkDecorationEffect.of(effectOptions),
|
||||
effects: removeMarkDecorationEffect.of({ id }),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -89,8 +89,14 @@ export default class CodeMirrorControl extends CodeMirror5Emulation implements E
|
||||
}
|
||||
|
||||
public select(anchor: number, head: number) {
|
||||
const maximumPosition = this.editor.state.doc.length;
|
||||
this.editor.dispatch(this.editor.state.update({
|
||||
selection: { anchor, head },
|
||||
selection: {
|
||||
// Ensure that (anchor, head) are in range.
|
||||
// (CodeMirror throws when (anchor, head) are out-of-range.)
|
||||
anchor: Math.min(anchor, maximumPosition),
|
||||
head: Math.min(head, maximumPosition),
|
||||
},
|
||||
scrollIntoView: true,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { EditorSelection } from '@codemirror/state';
|
||||
import createTestEditor from '../testing/createTestEditor';
|
||||
import { getSearchQuery, openSearchPanel, SearchQuery, setSearchQuery } from '@codemirror/search';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import searchExtension from './searchExtension';
|
||||
import createEditorSettings from '../../testing/createEditorSettings';
|
||||
import { Second } from '@joplin/utils/time';
|
||||
|
||||
const setSearchText = (text: string, view: EditorView) => {
|
||||
const oldQuery = getSearchQuery(view.state);
|
||||
const query = new SearchQuery({
|
||||
search: text,
|
||||
caseSensitive: oldQuery.caseSensitive,
|
||||
regexp: oldQuery.regexp,
|
||||
replace: oldQuery.replace,
|
||||
});
|
||||
view.dispatch({
|
||||
effects: [
|
||||
setSearchQuery.of(query),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const setSearchTextAndWait = async (text: string, view: EditorView) => {
|
||||
setSearchText(text, view);
|
||||
await jest.advanceTimersByTimeAsync(Second);
|
||||
};
|
||||
|
||||
const createEditor = async (initialText: string, cursorPosition: number) => {
|
||||
const view = await createTestEditor(initialText, EditorSelection.cursor(cursorPosition), [], [
|
||||
searchExtension(()=>{}, createEditorSettings(1)),
|
||||
]);
|
||||
openSearchPanel(view);
|
||||
|
||||
return view;
|
||||
};
|
||||
|
||||
const getSelectionFrom = (view: EditorView) => {
|
||||
return view.state.selection.main.from;
|
||||
};
|
||||
|
||||
describe('searchExtension', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
test('should auto-scroll to a match when the search changes', async () => {
|
||||
const view = await createEditor('Line 1\n\nLine 3\nLine 4', 0);
|
||||
const docText = view.state.doc.toString();
|
||||
|
||||
await setSearchTextAndWait('Line 3', view);
|
||||
expect(getSelectionFrom(view)).toBe(docText.indexOf('Line 3'));
|
||||
|
||||
await setSearchTextAndWait('Line 4', view);
|
||||
expect(getSelectionFrom(view)).toBe(docText.indexOf('Line 4'));
|
||||
});
|
||||
|
||||
test('should advance to the next match on change', async () => {
|
||||
const view = await createEditor('Match\nMatch2\nMatch23', 0);
|
||||
const docText = view.state.doc.toString();
|
||||
|
||||
await setSearchTextAndWait('Match', view);
|
||||
expect(getSelectionFrom(view)).toBe(0);
|
||||
|
||||
await setSearchTextAndWait('Match2', view);
|
||||
expect(getSelectionFrom(view)).toBe(docText.indexOf('Match2'));
|
||||
|
||||
await setSearchTextAndWait('Match23', view);
|
||||
expect(getSelectionFrom(view)).toBe(docText.indexOf('Match23'));
|
||||
});
|
||||
|
||||
test('should preserve auto-match start position until selection is manually changed', async () => {
|
||||
const view = await createEditor('Match1\nMatch2\nMatch23', 0);
|
||||
const docText = view.state.doc.toString();
|
||||
|
||||
await setSearchTextAndWait('Match2', view);
|
||||
expect(getSelectionFrom(view)).toBe(docText.indexOf('Match2'));
|
||||
|
||||
await setSearchTextAndWait('Match', view);
|
||||
expect(getSelectionFrom(view)).toBe(docText.indexOf('Match'));
|
||||
|
||||
await setSearchTextAndWait('Match23', view);
|
||||
expect(getSelectionFrom(view)).toBe(docText.indexOf('Match23'));
|
||||
|
||||
// Manually setting the selection should change the match start location
|
||||
view.dispatch({
|
||||
selection: EditorSelection.single('Match1\n'.length),
|
||||
});
|
||||
await setSearchTextAndWait('Match2', view);
|
||||
expect(getSelectionFrom(view)).toBe(docText.indexOf('Match2'));
|
||||
});
|
||||
|
||||
test('search should wrap to the top when not found after the cursor', async () => {
|
||||
const view = await createEditor('Before\nOther\nOther 2', 5);
|
||||
|
||||
await setSearchTextAndWait('Before', view);
|
||||
expect(getSelectionFrom(view)).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,152 @@
|
||||
import { EditorState, Extension, StateEffect } from '@codemirror/state';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { EditorSelection, EditorState, Extension, StateEffect, StateField } from '@codemirror/state';
|
||||
import { EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view';
|
||||
import { EditorSettings, OnEventCallback } from '../../types';
|
||||
import getSearchState from '../utils/getSearchState';
|
||||
import { EditorEventType } from '../../events';
|
||||
import { search, searchPanelOpen, setSearchQuery } from '@codemirror/search';
|
||||
import { search, searchPanelOpen, SearchQuery, setSearchQuery } from '@codemirror/search';
|
||||
import announceSearchMatch from '../vendor/announceSearchMatch';
|
||||
|
||||
type CancelEvent = { cancelled: boolean };
|
||||
|
||||
const scanForFirstMatch = async (
|
||||
state: EditorState,
|
||||
query: SearchQuery,
|
||||
startPosition: number,
|
||||
delayFunction: ()=> Promise<void>,
|
||||
cancelEvent: CancelEvent,
|
||||
) => {
|
||||
if (cancelEvent.cancelled) return null;
|
||||
|
||||
const pageSizeChars = 40_000;
|
||||
let nextStartPosition = startPosition;
|
||||
|
||||
const nextCursor = () => {
|
||||
if (nextStartPosition >= state.doc.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let endPosition = Math.min(nextStartPosition + pageSizeChars, state.doc.length);
|
||||
const endPositionLine = state.doc.lineAt(endPosition);
|
||||
// Always search up to the end of the current line to avoid getting partial matches.
|
||||
endPosition = endPositionLine.to;
|
||||
|
||||
const cursor = query.getCursor(state, nextStartPosition, endPosition);
|
||||
nextStartPosition = endPosition;
|
||||
return cursor;
|
||||
};
|
||||
|
||||
const nextCursorAndWait = async () => {
|
||||
const result = nextCursor();
|
||||
await delayFunction();
|
||||
return result;
|
||||
};
|
||||
|
||||
for (let cursor = nextCursor(); !!cursor && !cancelEvent.cancelled; cursor = await nextCursorAndWait()) {
|
||||
const match = cursor.next();
|
||||
if (match?.value && match.value.to && match.value.from !== match.value.to) {
|
||||
return match.value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Included in a transaction if it was caused by the auto-scroll-to-next-match logic
|
||||
const autoMatchAnnotation = StateEffect.define<boolean>();
|
||||
|
||||
const autoMatchSearchStartField = StateField.define<number>({
|
||||
create: (state) => state.selection.main.from,
|
||||
update: (lastValue, viewUpdate) => {
|
||||
const changedByAutoMatch = viewUpdate.effects.some(effect => effect.is(autoMatchAnnotation));
|
||||
const sameSelection = viewUpdate.startState.selection.eq(viewUpdate.newSelection);
|
||||
const noSignificantChanges = sameSelection && !viewUpdate.docChanged;
|
||||
|
||||
if (changedByAutoMatch || noSignificantChanges) {
|
||||
return lastValue;
|
||||
}
|
||||
return viewUpdate.newSelection.main.from;
|
||||
},
|
||||
});
|
||||
|
||||
const autoScrollToMatchPlugin = ViewPlugin.fromClass(class {
|
||||
private _lastCancelEvent: CancelEvent = { cancelled: false };
|
||||
public constructor(private _view: EditorView) { }
|
||||
|
||||
private async handleScrollOnQueryChange_(
|
||||
query: SearchQuery,
|
||||
state: EditorState,
|
||||
startState: EditorState,
|
||||
cancelEvent: CancelEvent,
|
||||
) {
|
||||
const isOpenSearchPanelEvent = () => searchPanelOpen(startState) && !searchPanelOpen(state);
|
||||
if (
|
||||
!query || query.search.length === 0
|
||||
// Avoid auto-scrolling to the search result when first opening the search panel
|
||||
|| isOpenSearchPanelEvent()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const getFirstMatchAfter = async (pos: number) => {
|
||||
const delayFunction = () => {
|
||||
return new Promise<void>(resolve => {
|
||||
requestAnimationFrame(() => resolve());
|
||||
});
|
||||
};
|
||||
return await scanForFirstMatch(
|
||||
state, query, pos, delayFunction, cancelEvent,
|
||||
);
|
||||
};
|
||||
|
||||
const searchStart = state.field(autoMatchSearchStartField);
|
||||
const firstMatchAfterSelection = await getFirstMatchAfter(searchStart);
|
||||
const targetMatch = firstMatchAfterSelection ?? await getFirstMatchAfter(0);
|
||||
|
||||
if (targetMatch && targetMatch.from >= 0 && !cancelEvent.cancelled) {
|
||||
this._view.dispatch({
|
||||
selection: EditorSelection.single(targetMatch.from, targetMatch.to),
|
||||
effects: [
|
||||
// Mark this transaction as an auto-match. This allows listeners to
|
||||
// process the transaction differently.
|
||||
autoMatchAnnotation.of(true),
|
||||
|
||||
EditorView.scrollIntoView(targetMatch.from),
|
||||
announceSearchMatch(state, targetMatch),
|
||||
],
|
||||
userEvent: 'select.search',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async update(update: ViewUpdate) {
|
||||
let lastQueryUpdate: StateEffect<SearchQuery>|null = null;
|
||||
for (const tr of update.transactions) {
|
||||
const queryUpdate = tr.effects.find(e => e.is(setSearchQuery));
|
||||
if (queryUpdate) {
|
||||
lastQueryUpdate = queryUpdate;
|
||||
}
|
||||
}
|
||||
|
||||
const cancelOngoingSearch = () => {
|
||||
this._lastCancelEvent.cancelled = true;
|
||||
};
|
||||
|
||||
const newCancelEvent = () => {
|
||||
cancelOngoingSearch();
|
||||
const cancelEvent = { cancelled: false };
|
||||
this._lastCancelEvent = cancelEvent;
|
||||
return cancelEvent;
|
||||
};
|
||||
|
||||
if (!searchPanelOpen(update.state)) {
|
||||
cancelOngoingSearch();
|
||||
} else if (lastQueryUpdate) {
|
||||
void this.handleScrollOnQueryChange_(
|
||||
lastQueryUpdate.value, update.state, update.startState, newCancelEvent(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
provide: () => autoMatchSearchStartField,
|
||||
});
|
||||
|
||||
export const searchChangeSourceEffect = StateEffect.define<string>();
|
||||
|
||||
@@ -31,6 +174,8 @@ const searchExtension = (onEvent: OnEventCallback, settings: EditorSettings): Ex
|
||||
},
|
||||
} : undefined),
|
||||
|
||||
autoScrollToMatchPlugin,
|
||||
|
||||
EditorState.transactionExtender.of((tr) => {
|
||||
if (tr.effects.some(e => e.is(setSearchQuery)) || searchPanelOpen(tr.state) !== searchPanelOpen(tr.startState)) {
|
||||
const changeSourceEffects = tr.effects.filter(effect => effect.is(searchChangeSourceEffect));
|
||||
|
||||
53
packages/editor/CodeMirror/vendor/announceSearchMatch.ts
vendored
Normal file
53
packages/editor/CodeMirror/vendor/announceSearchMatch.ts
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
import { EditorState } from "@codemirror/state"
|
||||
import { EditorView } from "@codemirror/view"
|
||||
|
||||
// From https://github.com/codemirror/search/blob/3fd68b965a1a149bb65a268ef52c10b36c080538/src/search.ts
|
||||
// Modified to accept an EditorState instead of an EditorView.
|
||||
//!
|
||||
//! MIT License
|
||||
//!
|
||||
//! Copyright (C) 2018-2021 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
|
||||
//!
|
||||
//! Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
//! of this software and associated documentation files (the "Software"), to deal
|
||||
//! in the Software without restriction, including without limitation the rights
|
||||
//! to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
//! copies of the Software, and to permit persons to whom the Software is
|
||||
//! furnished to do so, subject to the following conditions:
|
||||
//!
|
||||
//! The above copyright notice and this permission notice shall be included in
|
||||
//! all copies or substantial portions of the Software.
|
||||
//!
|
||||
//! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
//! IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
//! FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
//! AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
//! LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
//! OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
//! THE SOFTWARE.
|
||||
//!
|
||||
|
||||
const AnnounceMargin = 30
|
||||
|
||||
const Break = /[\s\.,:;?!]/
|
||||
|
||||
export default function announceSearchMatch(state: EditorState, {from, to}: {from: number, to: number}) {
|
||||
let line = state.doc.lineAt(from), lineEnd = state.doc.lineAt(to).to
|
||||
let start = Math.max(line.from, from - AnnounceMargin), end = Math.min(lineEnd, to + AnnounceMargin)
|
||||
let text = state.sliceDoc(start, end)
|
||||
if (start != line.from) {
|
||||
for (let i = 0; i < AnnounceMargin; i++) if (!Break.test(text[i + 1]) && Break.test(text[i])) {
|
||||
text = text.slice(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
if (end != lineEnd) {
|
||||
for (let i = text.length - 1; i > text.length - AnnounceMargin; i--) if (!Break.test(text[i - 1]) && Break.test(text[i])) {
|
||||
text = text.slice(0, i)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return EditorView.announce.of(
|
||||
`${state.phrase("current match")}. ${text} ${state.phrase("on line")} ${line.number}.`)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { EditorView } from 'prosemirror-view';
|
||||
import { EditorCommandType } from '../types';
|
||||
import { EditorCommandType } from '../../types';
|
||||
import commands from './commands';
|
||||
import createTestEditor from './testing/createTestEditor';
|
||||
import createTestEditor from '../testing/createTestEditor';
|
||||
|
||||
const selectAll = (editor: EditorView) => {
|
||||
commands[EditorCommandType.SelectAll](editor.state, editor.dispatch, editor);
|
||||
@@ -1,19 +1,19 @@
|
||||
import { Command, EditorState, Transaction } from 'prosemirror-state';
|
||||
import { EditorCommandType } from '../types';
|
||||
import { EditorCommandType } from '../../types';
|
||||
import { redo, undo } from 'prosemirror-history';
|
||||
import { autoJoin, selectAll, setBlockType, toggleMark } from 'prosemirror-commands';
|
||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
import schema from './schema';
|
||||
import schema from '../schema';
|
||||
import { liftListItem, sinkListItem, wrapRangeInList } from 'prosemirror-schema-list';
|
||||
import { NodeType } from 'prosemirror-model';
|
||||
import { getSearchVisible, setSearchVisible } from './plugins/searchPlugin';
|
||||
import { getSearchVisible, setSearchVisible } from '../plugins/searchPlugin';
|
||||
import { findNext, findPrev, replaceAll, replaceNext } from 'prosemirror-search';
|
||||
import { getEditorApi } from './plugins/joplinEditorApiPlugin';
|
||||
import { EditorEventType } from '../events';
|
||||
import extractSelectedLinesTo from './utils/extractSelectedLinesTo';
|
||||
import { getEditorApi } from '../plugins/joplinEditorApiPlugin';
|
||||
import { EditorEventType } from '../../events';
|
||||
import extractSelectedLinesTo from '../utils/extractSelectedLinesTo';
|
||||
import { EditorView } from 'prosemirror-view';
|
||||
import jumpToHash from './utils/jumpToHash';
|
||||
import canReplaceSelectionWith from './utils/canReplaceSelectionWith';
|
||||
import jumpToHash from '../utils/jumpToHash';
|
||||
import canReplaceSelectionWith from '../utils/canReplaceSelectionWith';
|
||||
import focusEditor from './focusEditor';
|
||||
|
||||
type Dispatch = (tr: Transaction)=> void;
|
||||
type ExtendedCommand = (state: EditorState, dispatch: Dispatch, view?: EditorView, options?: string[])=> boolean;
|
||||
@@ -81,12 +81,7 @@ const commands: Record<EditorCommandType, ExtendedCommand|null> = {
|
||||
[EditorCommandType.Undo]: undo,
|
||||
[EditorCommandType.Redo]: redo,
|
||||
[EditorCommandType.SelectAll]: selectAll,
|
||||
[EditorCommandType.Focus]: (_state, _dispatch?, view?) => {
|
||||
if (view) {
|
||||
focus('commands::focus', view);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
[EditorCommandType.Focus]: focusEditor,
|
||||
[EditorCommandType.ToggleBolded]: toggleMark(schema.marks.strong),
|
||||
[EditorCommandType.ToggleItalicized]: toggleMark(schema.marks.emphasis),
|
||||
[EditorCommandType.ToggleCode]: toggleCode,
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user