You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-12-29 23:48:19 +02:00
Compare commits
159 Commits
v3.5.3
...
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 | ||
|
|
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
|
||||
@@ -1097,8 +1103,9 @@ packages/editor/CodeMirror/utils/markdown/stripBlockquote.js
|
||||
packages/editor/CodeMirror/utils/markdown/toggleCheckboxAt.js
|
||||
packages/editor/CodeMirror/utils/setupVim.js
|
||||
packages/editor/CodeMirror/vendor/announceSearchMatch.js
|
||||
packages/editor/ProseMirror/commands.test.js
|
||||
packages/editor/ProseMirror/commands.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
|
||||
@@ -1117,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
|
||||
@@ -1146,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
|
||||
@@ -1414,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
|
||||
@@ -1629,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
|
||||
@@ -1742,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
|
||||
|
||||
21
.gitignore
vendored
21
.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
|
||||
@@ -1070,8 +1076,9 @@ packages/editor/CodeMirror/utils/markdown/stripBlockquote.js
|
||||
packages/editor/CodeMirror/utils/markdown/toggleCheckboxAt.js
|
||||
packages/editor/CodeMirror/utils/setupVim.js
|
||||
packages/editor/CodeMirror/vendor/announceSearchMatch.js
|
||||
packages/editor/ProseMirror/commands.test.js
|
||||
packages/editor/ProseMirror/commands.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
|
||||
@@ -1090,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
|
||||
@@ -1119,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
|
||||
@@ -1387,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
|
||||
@@ -1602,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
|
||||
@@ -1715,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
|
||||
|
||||
@@ -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"
|
||||
@@ -73,7 +73,7 @@
|
||||
"@joplin/tools": "~3.5",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/node": "18.19.118",
|
||||
"@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>
|
||||
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'),
|
||||
},
|
||||
|
||||
@@ -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: {},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -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.3",
|
||||
"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": {
|
||||
@@ -148,7 +149,7 @@
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/mustache": "4.2.6",
|
||||
"@types/node": "18.19.118",
|
||||
"@types/node": "18.19.130",
|
||||
"@types/react": "18.3.23",
|
||||
"@types/react-dom": "18.3.7",
|
||||
"@types/react-redux": "7.1.33",
|
||||
@@ -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",
|
||||
@@ -201,12 +202,12 @@
|
||||
"taboverride": "4.0.3",
|
||||
"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",
|
||||
|
||||
@@ -4,13 +4,25 @@ 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 = `${__dirname}/signToolDownloadTemp`;
|
||||
const extractDir = `${__dirname}/signToolExtractTemp`;
|
||||
const downloadDir = `${tempDir}/signToolDownloadTemp`;
|
||||
const extractDir = `${tempDir}/signToolExtractTemp`;
|
||||
|
||||
if (await pathExists(`${extractDir}/${signToolName}`)) {
|
||||
console.info('sign.js: Sign tool has already been downloaded - skipping');
|
||||
@@ -55,6 +67,8 @@ exports.default = async (configuration) => {
|
||||
|
||||
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;
|
||||
@@ -63,9 +77,8 @@ exports.default = async (configuration) => {
|
||||
console.info('sign.js: SIGN_APPLICATION = 1 - signing application');
|
||||
|
||||
const signToolDir = await downloadSignTool();
|
||||
const tempDir = `${__dirname}/temp`;
|
||||
|
||||
mkdirpSync(tempDir);
|
||||
const signToolOutDir = `${tempDir}/signedToolOutDir`;
|
||||
mkdirpSync(signToolOutDir);
|
||||
|
||||
const previousDir = cwd();
|
||||
chdir(signToolDir);
|
||||
@@ -74,7 +87,7 @@ exports.default = async (configuration) => {
|
||||
const cmd = [
|
||||
`${signToolName} sign`,
|
||||
`-input_file_path="${inputFilePath}"`,
|
||||
`-output_dir_path="${tempDir}"`,
|
||||
`-output_dir_path="${signToolOutDir}"`,
|
||||
`-credential_id="${SSL_ESIGNER_CREDENTIAL_ID}"`,
|
||||
`-username="${SSL_ESIGNER_USER_NAME}"`,
|
||||
`-password="${SSL_ESIGNER_USER_PASSWORD}"`,
|
||||
@@ -83,10 +96,10 @@ exports.default = async (configuration) => {
|
||||
|
||||
execSync(cmd.join(' '));
|
||||
|
||||
const createdFiles = readdirSync(tempDir);
|
||||
const createdFiles = readdirSync(signToolOutDir);
|
||||
console.info('sign.js: Created files:', createdFiles);
|
||||
|
||||
moveSync(`${tempDir}/${createdFiles[0]}`, inputFilePath, { overwrite: true });
|
||||
moveSync(`${signToolOutDir}/${createdFiles[0]}`, inputFilePath, { overwrite: true });
|
||||
} catch (error) {
|
||||
console.error('sign.js: Could not sign file:', error);
|
||||
process.exit(1);
|
||||
|
||||
@@ -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}/>
|
||||
|
||||
@@ -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/');
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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,7 +29,7 @@
|
||||
"@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",
|
||||
@@ -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,9 +66,9 @@
|
||||
"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",
|
||||
@@ -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.118",
|
||||
"@types/node": "18.19.130",
|
||||
"@types/react": "19.0.14",
|
||||
"@types/react-redux": "7.1.33",
|
||||
"@types/serviceworker": "0.0.141",
|
||||
"@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.6",
|
||||
"esbuild": "0.25.8",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"fs-extra": "11.2.0",
|
||||
"gulp": "4.0.2",
|
||||
@@ -133,7 +133,7 @@
|
||||
"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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
11
packages/editor/ProseMirror/commands/focusEditor.ts
Normal file
11
packages/editor/ProseMirror/commands/focusEditor.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Command } from 'prosemirror-state';
|
||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
|
||||
const focusEditor: Command = (_state, _dispatch?, view?) => {
|
||||
if (view) {
|
||||
focus('commands::focus', view);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export default focusEditor;
|
||||
@@ -4,7 +4,7 @@ import { EditorState, TextSelection, Transaction } from 'prosemirror-state';
|
||||
import { EditorView } from 'prosemirror-view';
|
||||
import { DOMParser as ProseMirrorDomParser } from 'prosemirror-model';
|
||||
import { history } from 'prosemirror-history';
|
||||
import commands from './commands';
|
||||
import commands from './commands/commands';
|
||||
import schema from './schema';
|
||||
import { gapCursor } from 'prosemirror-gapcursor';
|
||||
import { dropCursor } from 'prosemirror-dropcursor';
|
||||
@@ -16,7 +16,6 @@ import joplinEditablePlugin from './plugins/joplinEditablePlugin/joplinEditableP
|
||||
import keymapExtension from './plugins/keymapPlugin';
|
||||
import inputRulesExtension from './plugins/inputRulesPlugin';
|
||||
import originalMarkupPlugin from './plugins/originalMarkupPlugin';
|
||||
import { tableEditing } from 'prosemirror-tables';
|
||||
import preprocessEditorInput from './utils/preprocessEditorInput';
|
||||
import listPlugin from './plugins/listPlugin';
|
||||
import searchExtension from './plugins/searchPlugin';
|
||||
@@ -28,6 +27,7 @@ import getFileFromPasteEvent from '../utils/getFileFromPasteEvent';
|
||||
import { RenderResult } from '../../renderer/types';
|
||||
import postprocessEditorOutput from './utils/postprocessEditorOutput';
|
||||
import detailsPlugin from './plugins/detailsPlugin';
|
||||
import tablePlugin from './plugins/tablePlugin';
|
||||
|
||||
interface ProseMirrorControl extends EditorControl {
|
||||
getSettings(): EditorSettings;
|
||||
@@ -90,7 +90,7 @@ const createEditor = async (
|
||||
markupTracker,
|
||||
listPlugin,
|
||||
linkTooltipPlugin,
|
||||
tableEditing({ allowTableNodeSelection: true }),
|
||||
tablePlugin,
|
||||
joplinEditorApiPlugin,
|
||||
imagePlugin,
|
||||
].flat(),
|
||||
|
||||
@@ -6,7 +6,7 @@ import { getEditorApi } from './joplinEditorApiPlugin';
|
||||
import showModal from '../utils/dom/showModal';
|
||||
import createTextArea from '../utils/dom/createTextArea';
|
||||
import createExternalEditorPlugin, { OnHide } from './utils/createExternalEditorPlugin';
|
||||
import createFloatingButtonPlugin, { ToolbarPosition } from './utils/createFloatingButtonPlugin';
|
||||
import createFloatingButtonPlugin, { ToolbarType } from './utils/createFloatingButtonPlugin';
|
||||
|
||||
// See the fold example for more information about
|
||||
// writing similar ProseMirror plugins:
|
||||
@@ -263,7 +263,7 @@ const imagePlugin = [
|
||||
}),
|
||||
createFloatingButtonPlugin('image', [
|
||||
{ label: _ => _('Label'), command: (_node, offset) => editAltTextAt(offset) },
|
||||
], ToolbarPosition.TopRightInside),
|
||||
], ToolbarType.AnchorTopRight),
|
||||
];
|
||||
|
||||
export default imagePlugin;
|
||||
|
||||
@@ -9,7 +9,7 @@ import postProcessRenderedHtml from './postProcessRenderedHtml';
|
||||
import makeLinksClickableInElement from '../../utils/makeLinksClickableInElement';
|
||||
import SelectableNodeView from '../../utils/SelectableNodeView';
|
||||
import createExternalEditorPlugin, { OnHide } from '../utils/createExternalEditorPlugin';
|
||||
import createFloatingButtonPlugin, { ToolbarPosition } from '../utils/createFloatingButtonPlugin';
|
||||
import createFloatingButtonPlugin, { ToolbarType } from '../utils/createFloatingButtonPlugin';
|
||||
|
||||
// See the fold example for more information about
|
||||
// writing similar ProseMirror plugins:
|
||||
@@ -245,6 +245,6 @@ export default [
|
||||
className: 'edit-button',
|
||||
command: (_node, offset) => editAt(offset),
|
||||
},
|
||||
], ToolbarPosition.TopRightInside)
|
||||
], ToolbarType.AnchorTopRight)
|
||||
)),
|
||||
];
|
||||
|
||||
@@ -3,7 +3,7 @@ import schema from '../schema';
|
||||
import { keymap } from 'prosemirror-keymap';
|
||||
import { baseKeymap, chainCommands, exitCode, liftEmptyBlock, newlineInCode } from 'prosemirror-commands';
|
||||
import { liftListItem, sinkListItem, splitListItem } from 'prosemirror-schema-list';
|
||||
import commands from '../commands';
|
||||
import commands from '../commands/commands';
|
||||
import { EditorCommandType } from '../../types';
|
||||
import { Command, EditorState, TextSelection, Plugin } from 'prosemirror-state';
|
||||
import splitBlockAs from '../vendor/splitBlockAs';
|
||||
|
||||
40
packages/editor/ProseMirror/plugins/tablePlugin.ts
Normal file
40
packages/editor/ProseMirror/plugins/tablePlugin.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { addColumnAfter, addRowAfter, deleteColumn, deleteRow, tableEditing } from 'prosemirror-tables';
|
||||
import createFloatingButtonPlugin, { ToolbarType } from './utils/createFloatingButtonPlugin';
|
||||
import addColumnRightIcon from '../vendor/icons/addColumnRight';
|
||||
import addRowBelowIcon from '../vendor/icons/addRowBelow';
|
||||
import removeRowIcon from '../vendor/icons/removeRow';
|
||||
import removeColumnIcon from '../vendor/icons/removeColumn';
|
||||
import focusEditor from '../commands/focusEditor';
|
||||
import { Command } from 'prosemirror-state';
|
||||
|
||||
const tableCommand = (command: Command): Command => (state, dispatch, view) => {
|
||||
return command(state, dispatch, view) && focusEditor(state, dispatch, view);
|
||||
};
|
||||
|
||||
const tablePlugin = [
|
||||
tableEditing({ allowTableNodeSelection: true }),
|
||||
createFloatingButtonPlugin('table', [
|
||||
{
|
||||
icon: addRowBelowIcon,
|
||||
label: (_) => _('Add row'),
|
||||
command: () => tableCommand(addRowAfter),
|
||||
},
|
||||
{
|
||||
icon: addColumnRightIcon,
|
||||
label: (_) => _('Add column'),
|
||||
command: () => tableCommand(addColumnAfter),
|
||||
},
|
||||
{
|
||||
icon: removeRowIcon,
|
||||
label: (_) => _('Delete row'),
|
||||
command: () => tableCommand(deleteRow),
|
||||
},
|
||||
{
|
||||
icon: removeColumnIcon,
|
||||
label: (_) => _('Delete column'),
|
||||
command: () => tableCommand(deleteColumn),
|
||||
},
|
||||
], ToolbarType.FloatAboveBelow),
|
||||
];
|
||||
|
||||
export default tablePlugin;
|
||||
@@ -1,42 +1,158 @@
|
||||
import { Command, EditorState, Plugin } from 'prosemirror-state';
|
||||
import { Command, EditorState, Plugin, PluginView } from 'prosemirror-state';
|
||||
import { LocalizationResult, OnLocalize } from '../../../types';
|
||||
import { EditorView } from 'prosemirror-view';
|
||||
import createButton from '../../utils/dom/createButton';
|
||||
import { getEditorApi } from '../joplinEditorApiPlugin';
|
||||
import { Node } from 'prosemirror-model';
|
||||
import { Icon } from '../../vendor/icons/types';
|
||||
|
||||
type LocalizeFunction = (_: OnLocalize)=> LocalizationResult;
|
||||
|
||||
interface ButtonSpec {
|
||||
icon?: Icon;
|
||||
label: LocalizeFunction;
|
||||
command: (node: Node, offset: number)=> Command;
|
||||
showForNode?: (node: Node)=> boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export enum ToolbarPosition {
|
||||
TopLeftOutside,
|
||||
TopRightInside,
|
||||
export enum ToolbarType {
|
||||
// Attempts to keep the toolbar visible when the node
|
||||
// is visible. While showing the toolbar outside the node
|
||||
// is preferred, the toolbar will be shown inside the node
|
||||
// if insufficient outside space is available.
|
||||
FloatAboveBelow,
|
||||
// Anchors the toolbar to the top right corner of the
|
||||
// associated element.
|
||||
AnchorTopRight,
|
||||
}
|
||||
|
||||
class FloatingButtonBar {
|
||||
interface TargetNode {
|
||||
offset: number;
|
||||
node: Node;
|
||||
element: Element|null;
|
||||
}
|
||||
|
||||
class FloatingButtonBar implements PluginView {
|
||||
private container_: HTMLElement;
|
||||
private buttonRow_: ButtonRow;
|
||||
|
||||
private currentTarget_: TargetNode|null = null;
|
||||
private observer_: ElementObserver;
|
||||
|
||||
public constructor(
|
||||
view: EditorView, private targetNode_: string, private buttons_: ButtonSpec[], private position_: ToolbarPosition,
|
||||
private view_: EditorView,
|
||||
private targetNodeName_: string,
|
||||
buttons: ButtonSpec[],
|
||||
private type_: ToolbarType,
|
||||
) {
|
||||
this.container_ = document.createElement('div');
|
||||
this.container_.classList.add('floating-button-bar');
|
||||
|
||||
this.buttonRow_ = new ButtonRow(this.container_, buttons);
|
||||
|
||||
this.observer_ = new ElementObserver(
|
||||
() => this.repositionOverlay_(),
|
||||
);
|
||||
|
||||
// Prevent other elements (e.g. checkboxes, links) from being between the toolbar button and the
|
||||
// target element. If the toolbar is instead included **after** the Rich Text Editor's main content,
|
||||
// then all items included directly within the Rich Text Editor come before the toolbar in the focus
|
||||
// order.
|
||||
view.dom.parentElement.prepend(this.container_);
|
||||
this.update(view, null);
|
||||
view_.dom.parentElement.prepend(this.container_);
|
||||
this.update(view_, null);
|
||||
|
||||
if (this.type_ === ToolbarType.AnchorTopRight) {
|
||||
this.container_.classList.add('-anchored');
|
||||
} else if (this.type_ === ToolbarType.FloatAboveBelow) {
|
||||
this.container_.classList.add('-floating');
|
||||
} else {
|
||||
const unreachable_: never = this.type_;
|
||||
throw new Error(`Unknown toolbar type: ${unreachable_}`);
|
||||
}
|
||||
}
|
||||
|
||||
private repositionOverlay_() {
|
||||
if (!this.currentTarget_) return;
|
||||
|
||||
const overlay = this.container_;
|
||||
const view = this.view_;
|
||||
const target = this.currentTarget_;
|
||||
const position = this.view_.coordsAtPos(target.offset);
|
||||
const targetElement = view.nodeDOM(target.offset);
|
||||
|
||||
// Fall back to document.body to support testing environments:
|
||||
const parentBox = (this.container_.offsetParent ?? document.body).getBoundingClientRect();
|
||||
const tooltipBox = this.container_.getBoundingClientRect();
|
||||
const targetBox = targetElement instanceof HTMLElement ? targetElement.getBoundingClientRect() : {
|
||||
...position,
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
|
||||
this.container_.style.left = '';
|
||||
this.container_.style.right = '';
|
||||
|
||||
if (this.type_ === ToolbarType.FloatAboveBelow) {
|
||||
const padding = 10;
|
||||
const above = targetBox.top - tooltipBox.height - parentBox.top - padding;
|
||||
const below = targetBox.top + targetBox.height - parentBox.top + padding;
|
||||
const viewportTop = window.visualViewport?.pageTop;
|
||||
const viewportBottom = viewportTop + window.visualViewport?.height;
|
||||
const cursorTop = viewportTop + view.coordsAtPos(view.state.selection.head).top;
|
||||
|
||||
const getOffsetTop = () => {
|
||||
// If the toolbar must be displayed within the element to be visible, prefer
|
||||
// less movement:
|
||||
const previousTop = tooltipBox.top + viewportTop;
|
||||
const insideCandidates = [
|
||||
Math.max(viewportTop + padding, above),
|
||||
Math.min(viewportBottom - padding - tooltipBox.height, below),
|
||||
].sort((a, b) => {
|
||||
const distanceA = Math.abs(a - previousTop);
|
||||
const distanceB = Math.abs(b - previousTop);
|
||||
return distanceA - distanceB;
|
||||
}).filter(position => {
|
||||
return position >= above && position <= below;
|
||||
});
|
||||
|
||||
const positionCandidates = [
|
||||
// Always prefer showing the toolbar outside the element
|
||||
above, below,
|
||||
// Fall back to showing the toolbar inside
|
||||
...insideCandidates,
|
||||
];
|
||||
|
||||
const validCandidates = positionCandidates.filter((position) => {
|
||||
const candidateTop = position;
|
||||
const candidateBottom = position + tooltipBox.height;
|
||||
const candidateCenter = position + tooltipBox.height / 2;
|
||||
const distanceFromCursor = Math.abs(candidateCenter - cursorTop);
|
||||
|
||||
return candidateTop >= viewportTop
|
||||
// Avoid showing the toolbar off the bottom edge of the screen
|
||||
&& candidateBottom <= viewportBottom
|
||||
// Avoid showing the toolbar on the same line as the cursor
|
||||
&& distanceFromCursor > tooltipBox.height / 2 + padding;
|
||||
});
|
||||
return validCandidates[0] ?? positionCandidates[0];
|
||||
};
|
||||
|
||||
const targetCenter = targetBox.left + targetBox.width / 2;
|
||||
const currentCenter = parentBox.left + tooltipBox.width / 2;
|
||||
// Subtract (parentBox.left, parentBox.top): style.left and style.top
|
||||
// are relative to the parent, but the computed position is not.
|
||||
overlay.style.left = `${Math.max(targetCenter - currentCenter, 0)}px`;
|
||||
overlay.style.top = `${getOffsetTop()}px`;
|
||||
} else if (this.type_ === ToolbarType.AnchorTopRight) {
|
||||
overlay.style.right = `${parentBox.width - targetBox.width - (targetBox.left - parentBox.left)}px`;
|
||||
overlay.style.top = `${targetBox.top - parentBox.top}px`;
|
||||
}
|
||||
}
|
||||
|
||||
public update(view: EditorView, lastState: EditorState|null) {
|
||||
this.view_ = view;
|
||||
|
||||
const state = view.state;
|
||||
const sameSelection = lastState && state.selection.eq(lastState.selection);
|
||||
const sameDoc = lastState && state.doc.eq(lastState.doc);
|
||||
@@ -45,11 +161,12 @@ class FloatingButtonBar {
|
||||
}
|
||||
|
||||
const findTargetNode = () => {
|
||||
type TargetNode = { offset: number; node: Node };
|
||||
let target: TargetNode = null;
|
||||
let target: TargetNode|null = null;
|
||||
state.doc.nodesBetween(state.selection.from, state.selection.to, (node, offset) => {
|
||||
if (node.type.name === this.targetNode_) {
|
||||
target = { node, offset };
|
||||
if (node.type.name === this.targetNodeName_) {
|
||||
const dom = view.nodeDOM(offset);
|
||||
const domElement = dom instanceof HTMLElement ? dom : dom.parentElement;
|
||||
target = { node, offset, element: domElement };
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -59,69 +176,109 @@ class FloatingButtonBar {
|
||||
};
|
||||
|
||||
const target = findTargetNode();
|
||||
this.observer_.setElement(target?.element);
|
||||
this.currentTarget_ = target;
|
||||
|
||||
if (!target) {
|
||||
this.container_.classList.add('-hidden');
|
||||
} else {
|
||||
this.container_.classList.remove('-hidden');
|
||||
|
||||
const hasCreatedButtons = this.container_.children.length === this.buttons_.length;
|
||||
if (!hasCreatedButtons) {
|
||||
const { localize } = getEditorApi(view.state);
|
||||
this.container_.replaceChildren(...this.buttons_.map(buttonSpec => {
|
||||
const button = createButton(
|
||||
buttonSpec.label(localize),
|
||||
() => { },
|
||||
);
|
||||
this.buttonRow_.updateButtons(view, target);
|
||||
this.repositionOverlay_();
|
||||
}
|
||||
}
|
||||
|
||||
button.classList.add('action');
|
||||
if (buttonSpec.className) {
|
||||
button.classList.add(buttonSpec.className);
|
||||
}
|
||||
public destroy() {
|
||||
this.observer_.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
return button;
|
||||
}));
|
||||
}
|
||||
// Emits changes when the element's position changes.
|
||||
class ElementObserver {
|
||||
private intersectionObserver_: IntersectionObserver|null;
|
||||
private lastElement_: Element|null = null;
|
||||
|
||||
for (let i = 0; i < this.buttons_.length; i++) {
|
||||
const button = this.container_.children[i] as HTMLButtonElement;
|
||||
const buttonSpec = this.buttons_[i];
|
||||
public constructor(private onNodeUpdate_: ()=> void) {
|
||||
if (typeof IntersectionObserver !== 'undefined') {
|
||||
this.intersectionObserver_ = new IntersectionObserver(() => {
|
||||
this.onNodeUpdate_();
|
||||
});
|
||||
}
|
||||
document.addEventListener('scroll', this.onNodeUpdate_);
|
||||
window.addEventListener('resize', this.onNodeUpdate_);
|
||||
}
|
||||
|
||||
const command = buttonSpec.command(target.node, target.offset);
|
||||
button.onclick = () => {
|
||||
command(view.state, view.dispatch, view);
|
||||
};
|
||||
public setElement(element: Element|null) {
|
||||
if (element === this.lastElement_) return;
|
||||
|
||||
button.disabled = !command(view.state);
|
||||
}
|
||||
if (this.lastElement_) {
|
||||
this.intersectionObserver_?.unobserve(this.lastElement_);
|
||||
}
|
||||
|
||||
const position = view.coordsAtPos(target.offset);
|
||||
// Fall back to document.body to support testing environments:
|
||||
const parentBox = (this.container_.offsetParent ?? document.body).getBoundingClientRect();
|
||||
const tooltipBox = this.container_.getBoundingClientRect();
|
||||
if (element) {
|
||||
this.intersectionObserver_?.observe(element);
|
||||
}
|
||||
|
||||
this.container_.style.left = '';
|
||||
this.container_.style.right = '';
|
||||
this.lastElement_ = element;
|
||||
}
|
||||
|
||||
const nodeElement = view.nodeDOM(target.offset);
|
||||
const nodeBbox = nodeElement instanceof HTMLElement ? nodeElement.getBoundingClientRect() : {
|
||||
...position,
|
||||
width: 0,
|
||||
height: 0,
|
||||
public destroy() {
|
||||
this.intersectionObserver_?.disconnect();
|
||||
this.intersectionObserver_ = null;
|
||||
|
||||
document.removeEventListener('scroll', this.onNodeUpdate_);
|
||||
window.removeEventListener('resize', this.onNodeUpdate_);
|
||||
}
|
||||
}
|
||||
|
||||
class ButtonRow {
|
||||
private created_ = false;
|
||||
public constructor(private container_: HTMLElement, private buttons_: ButtonSpec[]) { }
|
||||
|
||||
public updateButtons(view: EditorView, targetNode: TargetNode) {
|
||||
// Late-init the buttons to allow accessing `view`:
|
||||
if (!this.created_) {
|
||||
this.created_ = true;
|
||||
|
||||
const { localize } = getEditorApi(view.state);
|
||||
this.container_.replaceChildren(...this.buttons_.map(buttonSpec => {
|
||||
const label = buttonSpec.label(localize);
|
||||
const button = createButton(
|
||||
buttonSpec.icon ? { label, icon: buttonSpec.icon() } : label,
|
||||
() => { },
|
||||
);
|
||||
|
||||
button.classList.add('action', 'action-button');
|
||||
if (buttonSpec.icon) {
|
||||
button.classList.add('-icon');
|
||||
}
|
||||
|
||||
if (buttonSpec.className) {
|
||||
button.classList.add(buttonSpec.className);
|
||||
}
|
||||
|
||||
return button;
|
||||
}));
|
||||
}
|
||||
|
||||
// Update the button listeners and states based on the current view and
|
||||
// target node
|
||||
for (let i = 0; i < this.buttons_.length; i++) {
|
||||
const button = this.container_.children[i] as HTMLButtonElement;
|
||||
const buttonSpec = this.buttons_[i];
|
||||
|
||||
const command = buttonSpec.command(targetNode.node, targetNode.offset);
|
||||
button.onclick = () => {
|
||||
command(view.state, view.dispatch, view);
|
||||
};
|
||||
|
||||
let top = nodeBbox.top - parentBox.top;
|
||||
if (this.position_ === ToolbarPosition.TopLeftOutside) {
|
||||
top -= tooltipBox.height;
|
||||
this.container_.style.left = `${Math.max(nodeBbox.left - parentBox.left, 0)}px`;
|
||||
} else if (this.position_ === ToolbarPosition.TopRightInside) {
|
||||
this.container_.style.right = `${parentBox.width - nodeBbox.width - (nodeBbox.left - parentBox.left)}px`;
|
||||
}
|
||||
this.container_.style.top = `${top}px`;
|
||||
button.disabled = !command(view.state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const createFloatingButtonPlugin = (nodeName: string, actions: ButtonSpec[], position: ToolbarPosition) => {
|
||||
const createFloatingButtonPlugin = (nodeName: string, actions: ButtonSpec[], position: ToolbarType) => {
|
||||
return new Plugin({
|
||||
view: (view) => new FloatingButtonBar(view, nodeName, actions, position),
|
||||
});
|
||||
|
||||
@@ -11,4 +11,5 @@ import './styles/link-tooltip.css';
|
||||
import './styles/joplin-image-view.css';
|
||||
import './styles/alt-text-editor.css';
|
||||
import './styles/floating-button-bar.css';
|
||||
import './styles/action-button.css';
|
||||
|
||||
|
||||
20
packages/editor/ProseMirror/styles/action-button.css
Normal file
20
packages/editor/ProseMirror/styles/action-button.css
Normal file
@@ -0,0 +1,20 @@
|
||||
|
||||
.action-button {
|
||||
background-color: transparent;
|
||||
color: currentColor;
|
||||
border-radius: 48px;
|
||||
transition: background-color 0.2s ease, opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
background-color: var(--joplin-background-color-hover3);
|
||||
}
|
||||
|
||||
.action-button.-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.action-button > .icon {
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -2,6 +2,15 @@
|
||||
.floating-button-bar {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
box-sizing: border-box;
|
||||
|
||||
background-color: var(--joplin-background-color3);
|
||||
color: var(--joplin-color);
|
||||
|
||||
box-shadow: 0px 0px 2px var(--joplin-color);
|
||||
}
|
||||
|
||||
.floating-button-bar.-hidden {
|
||||
@@ -9,5 +18,32 @@
|
||||
}
|
||||
|
||||
.floating-button-bar > .action {
|
||||
opacity: 0.9;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
border: none;
|
||||
}
|
||||
|
||||
|
||||
.floating-button-bar.-floating {
|
||||
border-radius: 64px;
|
||||
height: 64px;
|
||||
padding: 8px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.floating-button-bar.-anchored {
|
||||
border-radius: 48px;
|
||||
padding: 0px;
|
||||
gap: 4px;
|
||||
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.floating-button-bar.-anchored > .action {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.floating-button-bar.-anchored:focus-within, .floating-button-bar.-anchored:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
background-color: var(--joplin-background-color);
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0px 0px 2px var(--joplin-color);
|
||||
box-shadow: 0px 0px 3px var(--joplin-color);
|
||||
color: var(--joplin-color);
|
||||
|
||||
width: min(80vw, 600px);
|
||||
|
||||
@@ -2,3 +2,10 @@
|
||||
table .selectedCell {
|
||||
outline: 2px solid var(--joplin-text-selection-color);
|
||||
}
|
||||
|
||||
/* Prevent table entries from having more spacing in the Rich Text Editor
|
||||
than in the note viewer. Unlike the note viewer, the Rich Text Editor
|
||||
always adds a <p> wrapper element around the table cells. */
|
||||
th > p:only-child, td > p:only-child {
|
||||
margin: unset;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,28 @@ import createTextNode from './createTextNode';
|
||||
|
||||
type OnClick = ()=> void;
|
||||
|
||||
const createButton = (label: LocalizationResult, onClick: OnClick) => {
|
||||
type Content = LocalizationResult|{
|
||||
icon: Element;
|
||||
label: LocalizationResult;
|
||||
};
|
||||
|
||||
const isLocalizationResult = (content: Content): content is LocalizationResult => {
|
||||
return typeof content === 'string' || !('icon' in content);
|
||||
};
|
||||
|
||||
const createButton = (content: Content, onClick: OnClick) => {
|
||||
const button = document.createElement('button');
|
||||
button.appendChild(createTextNode(label));
|
||||
if (isLocalizationResult(content)) {
|
||||
button.appendChild(createTextNode(content));
|
||||
} else {
|
||||
button.appendChild(content.icon);
|
||||
|
||||
void (async () => {
|
||||
const label = await content.label;
|
||||
button.ariaLabel = label;
|
||||
button.title = label;
|
||||
})();
|
||||
}
|
||||
|
||||
button.onclick = onClick;
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ const removeListItemWrapperParagraphs = (container: HTMLElement) => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const restoreOriginalLinks = (container: HTMLElement) => {
|
||||
// Restore HREFs
|
||||
const links = container.querySelectorAll<HTMLAnchorElement>('a[href="#"][data-original-href]');
|
||||
@@ -32,6 +31,18 @@ const restoreOriginalLinks = (container: HTMLElement) => {
|
||||
}
|
||||
};
|
||||
|
||||
const removeTableItemExtraPadding = (container: HTMLElement) => {
|
||||
const cells = container.querySelectorAll<HTMLTableCellElement>('th, td');
|
||||
for (const cell of cells) {
|
||||
// Table cells can exist in Markdown without the need for invisible
|
||||
// content.
|
||||
// Remove single nonbreaking space padding:
|
||||
if (cell.textContent === '\u00A0') {
|
||||
cell.textContent = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const postprocessEditorOutput = (node: Node|DocumentFragment) => {
|
||||
// By default, if `src` is specified on an image, the browser will try to load the image, even if it isn't added
|
||||
// to the DOM. (A similar problem is described here: https://stackoverflow.com/q/62019538).
|
||||
@@ -52,6 +63,7 @@ const postprocessEditorOutput = (node: Node|DocumentFragment) => {
|
||||
fixResourceUrls(html);
|
||||
restoreOriginalLinks(html);
|
||||
removeListItemWrapperParagraphs(html);
|
||||
removeTableItemExtraPadding(html);
|
||||
|
||||
return html;
|
||||
};
|
||||
|
||||
208
packages/editor/ProseMirror/vendor/icons/LICENSE
vendored
Normal file
208
packages/editor/ProseMirror/vendor/icons/LICENSE
vendored
Normal file
@@ -0,0 +1,208 @@
|
||||
The icons included in this folder are vendored from https://fonts.google.com/icons.
|
||||
Changes made:
|
||||
- File names have been changed.
|
||||
|
||||
|
||||
License:
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
1
packages/editor/ProseMirror/vendor/icons/addColumnRight.svg
vendored
Normal file
1
packages/editor/ProseMirror/vendor/icons/addColumnRight.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M160-760v560h240v-560H160ZM80-120v-720h720v160h-80v-80H480v560h240v-80h80v160H80Zm400-360Zm-80 0h80-80Zm0 0Zm320 120v-80h-80v-80h80v-80h80v80h80v80h-80v80h-80Z"/></svg>
|
||||
|
After Width: | Height: | Size: 284 B |
3
packages/editor/ProseMirror/vendor/icons/addColumnRight.ts
vendored
Normal file
3
packages/editor/ProseMirror/vendor/icons/addColumnRight.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import icon from "./icon";
|
||||
|
||||
export default icon(require('./addColumnRight.svg'));
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user