You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-12-26 23:38:08 +02:00
Compare commits
24 Commits
v2.13.3
...
note_list_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
831a0480c9 | ||
|
|
fc8e54319d | ||
|
|
eb24e5328d | ||
|
|
aacec5ad84 | ||
|
|
0a61fc6d1a | ||
|
|
4908bcf226 | ||
|
|
da9b36df77 | ||
|
|
0228da1131 | ||
|
|
829b0b60f2 | ||
|
|
79b79d68fd | ||
|
|
6d72166eb3 | ||
|
|
b14c85771c | ||
|
|
073fd5f9fa | ||
|
|
f4490ef3ed | ||
|
|
517788a6c6 | ||
|
|
96b2929846 | ||
|
|
eb1915e495 | ||
|
|
966d3a0535 | ||
|
|
dd2212b559 | ||
|
|
46a04bf5e7 | ||
|
|
6034c0ba25 | ||
|
|
99cf53e971 | ||
|
|
1a4418ab70 | ||
|
|
cdece235ab |
101
.eslintignore
101
.eslintignore
@@ -38,10 +38,6 @@ packages/app-clipper/popup/config/webpack.config.js
|
||||
packages/app-clipper/popup/node_modules
|
||||
packages/app-clipper/popup/scripts/build.js
|
||||
packages/app-desktop/build/
|
||||
packages/app-desktop/test-results/
|
||||
packages/app-desktop/playwright-report/
|
||||
packages/app-desktop/playwright/.cache/
|
||||
packages/app-desktop/integration-tests/test-profile/
|
||||
packages/app-desktop/dist
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/plugins/lists.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/supportedLocales.js
|
||||
@@ -51,7 +47,7 @@ packages/app-desktop/packageInfo.js
|
||||
packages/app-desktop/services/electron-context-menu.js
|
||||
packages/app-desktop/vendor/lib/
|
||||
packages/app-mobile/android
|
||||
packages/app-mobile/components/NoteEditor/**/*.bundle.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.bundle.js
|
||||
packages/app-mobile/ios
|
||||
packages/app-mobile/lib/rnInjectedJs/
|
||||
packages/app-mobile/locales
|
||||
@@ -219,11 +215,12 @@ packages/app-desktop/gui/MenuBar.js
|
||||
packages/app-desktop/gui/MultiNoteActions.js
|
||||
packages/app-desktop/gui/Navigator.js
|
||||
packages/app-desktop/gui/NoteContentPropertiesDialog.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Editor.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Toolbar.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/styles/index.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/setupVim.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useContextMenu.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.test.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearch.js
|
||||
@@ -235,14 +232,6 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useLineSorting.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useListIdent.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useScrollHandler.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useScrollUtils.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useStyles.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useWebviewIpcMessage.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/CodeMirror.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/Editor.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/CodeMirror.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/PlainEditor/PlainEditor.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/joplinCommandToTinyMceCommands.js
|
||||
@@ -376,13 +365,6 @@ packages/app-desktop/gui/style/StyledTextInput.js
|
||||
packages/app-desktop/gui/utils/NoteListUtils.js
|
||||
packages/app-desktop/gui/utils/convertToScreenCoordinates.js
|
||||
packages/app-desktop/gui/utils/loadScript.js
|
||||
packages/app-desktop/integration-tests/main.spec.js
|
||||
packages/app-desktop/integration-tests/models/MainScreen.js
|
||||
packages/app-desktop/integration-tests/models/NoteEditorScreen.js
|
||||
packages/app-desktop/integration-tests/models/SettingsScreen.js
|
||||
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
|
||||
packages/app-desktop/integration-tests/util/test.js
|
||||
packages/app-desktop/playwright.config.js
|
||||
packages/app-desktop/plugins/GotoAnything.js
|
||||
packages/app-desktop/services/bridge.js
|
||||
packages/app-desktop/services/commands/stateToWhenClauseContext.js
|
||||
@@ -425,24 +407,28 @@ packages/app-mobile/components/ExtendedWebView.js
|
||||
packages/app-mobile/components/FolderPicker.js
|
||||
packages/app-mobile/components/Modal.js
|
||||
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useEditPopup.test.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useEditPopup.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useOnMessage.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useOnResourceLongPress.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useSource.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.test.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/decoratorExtension.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.bulletedVsChecklist.test.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.test.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleList.test.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.test.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownReformatter.test.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownReformatter.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/syntaxHighlightingLanguages.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/testUtil/createEditor.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/testUtil/forceFullParse.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/testUtil/loadLanguages.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/theme.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/types.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/webviewLogger.js
|
||||
packages/app-mobile/components/NoteEditor/EditLinkDialog.js
|
||||
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/js-draw/applyTemplateToEditor.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.test.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/startAutosaveLoop.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/types.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/watchEditorForTemplateChanges.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/promptRestoreAutosave.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleSpaceButton.js
|
||||
@@ -453,6 +439,7 @@ packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.test.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.js
|
||||
packages/app-mobile/components/NoteEditor/SearchPanel.js
|
||||
packages/app-mobile/components/NoteEditor/SelectionFormatting.js
|
||||
packages/app-mobile/components/NoteEditor/types.js
|
||||
packages/app-mobile/components/NoteList.js
|
||||
packages/app-mobile/components/ProfileSwitcher/ProfileEditor.js
|
||||
@@ -474,7 +461,6 @@ packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExport
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/exportAllFolders.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js
|
||||
packages/app-mobile/components/screens/LogScreen.js
|
||||
packages/app-mobile/components/screens/Note.js
|
||||
packages/app-mobile/components/screens/Notes.js
|
||||
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
|
||||
@@ -493,52 +479,16 @@ packages/app-mobile/services/voiceTyping/vosk.ios.js
|
||||
packages/app-mobile/setupQuickActions.js
|
||||
packages/app-mobile/tools/buildInjectedJs.js
|
||||
packages/app-mobile/utils/ShareExtension.js
|
||||
packages/app-mobile/utils/ShareUtils.test.js
|
||||
packages/app-mobile/utils/ShareUtils.js
|
||||
packages/app-mobile/utils/TlsUtils.js
|
||||
packages/app-mobile/utils/autodetectTheme.js
|
||||
packages/app-mobile/utils/checkPermissions.js
|
||||
packages/app-mobile/utils/createRootStyle.js
|
||||
packages/app-mobile/utils/debounce.js
|
||||
packages/app-mobile/utils/fs-driver/fs-driver-rn.js
|
||||
packages/app-mobile/utils/fs-driver/runOnDeviceTests.js
|
||||
packages/app-mobile/utils/fs-driver-rn.js
|
||||
packages/app-mobile/utils/setupNotifications.js
|
||||
packages/app-mobile/utils/shareHandler.js
|
||||
packages/app-mobile/utils/types.js
|
||||
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.test.js
|
||||
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.js
|
||||
packages/editor/CodeMirror/CodeMirror5Emulation/Decorator.js
|
||||
packages/editor/CodeMirror/CodeMirrorControl.test.js
|
||||
packages/editor/CodeMirror/CodeMirrorControl.js
|
||||
packages/editor/CodeMirror/PluginLoader.js
|
||||
packages/editor/CodeMirror/configFromSettings.js
|
||||
packages/editor/CodeMirror/createEditor.test.js
|
||||
packages/editor/CodeMirror/createEditor.js
|
||||
packages/editor/CodeMirror/editorCommands/editorCommands.js
|
||||
packages/editor/CodeMirror/editorCommands/supportsCommand.js
|
||||
packages/editor/CodeMirror/editorCommands/swapLine.js
|
||||
packages/editor/CodeMirror/getScrollFraction.js
|
||||
packages/editor/CodeMirror/markdown/computeSelectionFormatting.test.js
|
||||
packages/editor/CodeMirror/markdown/computeSelectionFormatting.js
|
||||
packages/editor/CodeMirror/markdown/decoratorExtension.js
|
||||
packages/editor/CodeMirror/markdown/markdownCommands.bulletedVsChecklist.test.js
|
||||
packages/editor/CodeMirror/markdown/markdownCommands.test.js
|
||||
packages/editor/CodeMirror/markdown/markdownCommands.toggleList.test.js
|
||||
packages/editor/CodeMirror/markdown/markdownCommands.js
|
||||
packages/editor/CodeMirror/markdown/markdownMathParser.test.js
|
||||
packages/editor/CodeMirror/markdown/markdownMathParser.js
|
||||
packages/editor/CodeMirror/markdown/markdownReformatter.test.js
|
||||
packages/editor/CodeMirror/markdown/markdownReformatter.js
|
||||
packages/editor/CodeMirror/markdown/syntaxHighlightingLanguages.js
|
||||
packages/editor/CodeMirror/testUtil/createEditorSettings.js
|
||||
packages/editor/CodeMirror/testUtil/createTestEditor.js
|
||||
packages/editor/CodeMirror/testUtil/forceFullParse.js
|
||||
packages/editor/CodeMirror/testUtil/loadLanguages.js
|
||||
packages/editor/CodeMirror/theme.js
|
||||
packages/editor/CodeMirror/util/isInSyntaxNode.js
|
||||
packages/editor/SelectionFormatting.js
|
||||
packages/editor/events.js
|
||||
packages/editor/types.js
|
||||
packages/fork-htmlparser2/src/CollectingHandler.js
|
||||
packages/fork-htmlparser2/src/FeedHandler.spec.js
|
||||
packages/fork-htmlparser2/src/FeedHandler.js
|
||||
@@ -589,7 +539,6 @@ packages/lib/WelcomeUtils.js
|
||||
packages/lib/array.js
|
||||
packages/lib/callbackUrlUtils.test.js
|
||||
packages/lib/callbackUrlUtils.js
|
||||
packages/lib/clipperUtils.js
|
||||
packages/lib/commands/historyBackward.js
|
||||
packages/lib/commands/historyForward.js
|
||||
packages/lib/commands/index.js
|
||||
@@ -708,10 +657,6 @@ packages/lib/services/commands/isEditorCommand.js
|
||||
packages/lib/services/commands/propsHaveChanged.js
|
||||
packages/lib/services/commands/stateToWhenClauseContext.js
|
||||
packages/lib/services/contextkey/contextkey.js
|
||||
packages/lib/services/database/addMigrationFile.js
|
||||
packages/lib/services/database/migrations/42.js
|
||||
packages/lib/services/database/migrations/43.js
|
||||
packages/lib/services/database/migrations/44.js
|
||||
packages/lib/services/database/types.js
|
||||
packages/lib/services/debug/populateDatabase.js
|
||||
packages/lib/services/e2ee/EncryptionService.test.js
|
||||
@@ -822,7 +767,6 @@ packages/lib/services/rest/routes/master_keys.js
|
||||
packages/lib/services/rest/routes/notes.js
|
||||
packages/lib/services/rest/routes/ping.js
|
||||
packages/lib/services/rest/routes/resources.js
|
||||
packages/lib/services/rest/routes/revisions.js
|
||||
packages/lib/services/rest/routes/search.js
|
||||
packages/lib/services/rest/routes/tags.js
|
||||
packages/lib/services/rest/utils/collectionToPaginatedResults.js
|
||||
@@ -974,7 +918,6 @@ packages/renderer/noteStyle.js
|
||||
packages/renderer/pathUtils.js
|
||||
packages/renderer/utils.js
|
||||
packages/tools/build-release-stats.js
|
||||
packages/tools/build-translation.js
|
||||
packages/tools/build-welcome.js
|
||||
packages/tools/buildServerDocker.test.js
|
||||
packages/tools/buildServerDocker.js
|
||||
|
||||
2
.github/scripts/run_ci.sh
vendored
2
.github/scripts/run_ci.sh
vendored
@@ -86,7 +86,7 @@ if [ "$IS_PULL_REQUEST" == "1" ] || [ "$IS_DEV_BRANCH" = "1" ]; then
|
||||
# Allocation failed - JavaScript heap out of memory
|
||||
#
|
||||
# https://stackoverflow.com/questions/38558989
|
||||
export NODE_OPTIONS="--max-old-space-size=32768"
|
||||
export NODE_OPTIONS="--max-old-space-size=4096"
|
||||
yarn run test-ci
|
||||
testResult=$?
|
||||
if [ $testResult -ne 0 ]; then
|
||||
|
||||
2
.github/workflows/cla.yml
vendored
2
.github/workflows/cla.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
- name: "CLA Assistant"
|
||||
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
|
||||
# Beta Release
|
||||
uses: contributor-assistant/github-action@v2.3.1
|
||||
uses: contributor-assistant/github-action@v2.3.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# the below token should have repo scope and must be manually added by you in the repository's secret
|
||||
|
||||
15
.github/workflows/github-actions-main.yml
vendored
15
.github/workflows/github-actions-main.yml
vendored
@@ -3,9 +3,7 @@ on: [push, pull_request]
|
||||
jobs:
|
||||
pre_job:
|
||||
if: github.repository == 'laurent22/joplin'
|
||||
# Do not use unbuntu-latest because it causes `The operation was canceled` failures:
|
||||
# https://github.com/actions/runner-images/issues/6709
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_skip: ${{ steps.skip_check.outputs.should_skip }}
|
||||
steps:
|
||||
@@ -21,9 +19,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
# Do not use unbuntu-latest because it causes `The operation was canceled` failures:
|
||||
# https://github.com/actions/runner-images/issues/6709
|
||||
os: [macos-latest, ubuntu-20.04, windows-2019]
|
||||
os: [macos-latest, ubuntu-latest, windows-2019]
|
||||
steps:
|
||||
|
||||
# Trying to fix random networking issues on Windows
|
||||
@@ -55,9 +51,6 @@ jobs:
|
||||
sudo apt-get install -y libsecret-1-dev
|
||||
sudo apt-get install -y translate-toolkit
|
||||
sudo apt-get install -y rsync
|
||||
# Provides a virtual display on Linux. Used for Playwright integration
|
||||
# testing.
|
||||
sudo apt-get install -y xvfb
|
||||
|
||||
- name: Install Docker Engine
|
||||
# if: runner.os == 'Linux' && startsWith(github.ref, 'refs/tags/server-v')
|
||||
@@ -146,9 +139,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
# Do not use unbuntu-latest because it causes `The operation was canceled` failures:
|
||||
# https://github.com/actions/runner-images/issues/6709
|
||||
os: [ubuntu-20.04]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
|
||||
- name: Install Docker Engine
|
||||
|
||||
95
.gitignore
vendored
95
.gitignore
vendored
@@ -201,11 +201,12 @@ packages/app-desktop/gui/MenuBar.js
|
||||
packages/app-desktop/gui/MultiNoteActions.js
|
||||
packages/app-desktop/gui/Navigator.js
|
||||
packages/app-desktop/gui/NoteContentPropertiesDialog.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Editor.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Toolbar.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/styles/index.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/setupVim.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useContextMenu.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.test.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearch.js
|
||||
@@ -217,14 +218,6 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useLineSorting.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useListIdent.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useScrollHandler.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useScrollUtils.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useStyles.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useWebviewIpcMessage.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/CodeMirror.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/Editor.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/CodeMirror.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/PlainEditor/PlainEditor.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/joplinCommandToTinyMceCommands.js
|
||||
@@ -358,13 +351,6 @@ packages/app-desktop/gui/style/StyledTextInput.js
|
||||
packages/app-desktop/gui/utils/NoteListUtils.js
|
||||
packages/app-desktop/gui/utils/convertToScreenCoordinates.js
|
||||
packages/app-desktop/gui/utils/loadScript.js
|
||||
packages/app-desktop/integration-tests/main.spec.js
|
||||
packages/app-desktop/integration-tests/models/MainScreen.js
|
||||
packages/app-desktop/integration-tests/models/NoteEditorScreen.js
|
||||
packages/app-desktop/integration-tests/models/SettingsScreen.js
|
||||
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
|
||||
packages/app-desktop/integration-tests/util/test.js
|
||||
packages/app-desktop/playwright.config.js
|
||||
packages/app-desktop/plugins/GotoAnything.js
|
||||
packages/app-desktop/services/bridge.js
|
||||
packages/app-desktop/services/commands/stateToWhenClauseContext.js
|
||||
@@ -407,24 +393,28 @@ packages/app-mobile/components/ExtendedWebView.js
|
||||
packages/app-mobile/components/FolderPicker.js
|
||||
packages/app-mobile/components/Modal.js
|
||||
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useEditPopup.test.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useEditPopup.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useOnMessage.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useOnResourceLongPress.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useSource.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.test.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/decoratorExtension.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.bulletedVsChecklist.test.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.test.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleList.test.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.test.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownReformatter.test.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownReformatter.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/syntaxHighlightingLanguages.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/testUtil/createEditor.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/testUtil/forceFullParse.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/testUtil/loadLanguages.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/theme.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/types.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/webviewLogger.js
|
||||
packages/app-mobile/components/NoteEditor/EditLinkDialog.js
|
||||
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/js-draw/applyTemplateToEditor.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.test.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/startAutosaveLoop.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/types.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/watchEditorForTemplateChanges.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/promptRestoreAutosave.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleSpaceButton.js
|
||||
@@ -435,6 +425,7 @@ packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.test.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.js
|
||||
packages/app-mobile/components/NoteEditor/SearchPanel.js
|
||||
packages/app-mobile/components/NoteEditor/SelectionFormatting.js
|
||||
packages/app-mobile/components/NoteEditor/types.js
|
||||
packages/app-mobile/components/NoteList.js
|
||||
packages/app-mobile/components/ProfileSwitcher/ProfileEditor.js
|
||||
@@ -456,7 +447,6 @@ packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExport
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/exportAllFolders.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js
|
||||
packages/app-mobile/components/screens/LogScreen.js
|
||||
packages/app-mobile/components/screens/Note.js
|
||||
packages/app-mobile/components/screens/Notes.js
|
||||
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
|
||||
@@ -475,52 +465,16 @@ packages/app-mobile/services/voiceTyping/vosk.ios.js
|
||||
packages/app-mobile/setupQuickActions.js
|
||||
packages/app-mobile/tools/buildInjectedJs.js
|
||||
packages/app-mobile/utils/ShareExtension.js
|
||||
packages/app-mobile/utils/ShareUtils.test.js
|
||||
packages/app-mobile/utils/ShareUtils.js
|
||||
packages/app-mobile/utils/TlsUtils.js
|
||||
packages/app-mobile/utils/autodetectTheme.js
|
||||
packages/app-mobile/utils/checkPermissions.js
|
||||
packages/app-mobile/utils/createRootStyle.js
|
||||
packages/app-mobile/utils/debounce.js
|
||||
packages/app-mobile/utils/fs-driver/fs-driver-rn.js
|
||||
packages/app-mobile/utils/fs-driver/runOnDeviceTests.js
|
||||
packages/app-mobile/utils/fs-driver-rn.js
|
||||
packages/app-mobile/utils/setupNotifications.js
|
||||
packages/app-mobile/utils/shareHandler.js
|
||||
packages/app-mobile/utils/types.js
|
||||
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.test.js
|
||||
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.js
|
||||
packages/editor/CodeMirror/CodeMirror5Emulation/Decorator.js
|
||||
packages/editor/CodeMirror/CodeMirrorControl.test.js
|
||||
packages/editor/CodeMirror/CodeMirrorControl.js
|
||||
packages/editor/CodeMirror/PluginLoader.js
|
||||
packages/editor/CodeMirror/configFromSettings.js
|
||||
packages/editor/CodeMirror/createEditor.test.js
|
||||
packages/editor/CodeMirror/createEditor.js
|
||||
packages/editor/CodeMirror/editorCommands/editorCommands.js
|
||||
packages/editor/CodeMirror/editorCommands/supportsCommand.js
|
||||
packages/editor/CodeMirror/editorCommands/swapLine.js
|
||||
packages/editor/CodeMirror/getScrollFraction.js
|
||||
packages/editor/CodeMirror/markdown/computeSelectionFormatting.test.js
|
||||
packages/editor/CodeMirror/markdown/computeSelectionFormatting.js
|
||||
packages/editor/CodeMirror/markdown/decoratorExtension.js
|
||||
packages/editor/CodeMirror/markdown/markdownCommands.bulletedVsChecklist.test.js
|
||||
packages/editor/CodeMirror/markdown/markdownCommands.test.js
|
||||
packages/editor/CodeMirror/markdown/markdownCommands.toggleList.test.js
|
||||
packages/editor/CodeMirror/markdown/markdownCommands.js
|
||||
packages/editor/CodeMirror/markdown/markdownMathParser.test.js
|
||||
packages/editor/CodeMirror/markdown/markdownMathParser.js
|
||||
packages/editor/CodeMirror/markdown/markdownReformatter.test.js
|
||||
packages/editor/CodeMirror/markdown/markdownReformatter.js
|
||||
packages/editor/CodeMirror/markdown/syntaxHighlightingLanguages.js
|
||||
packages/editor/CodeMirror/testUtil/createEditorSettings.js
|
||||
packages/editor/CodeMirror/testUtil/createTestEditor.js
|
||||
packages/editor/CodeMirror/testUtil/forceFullParse.js
|
||||
packages/editor/CodeMirror/testUtil/loadLanguages.js
|
||||
packages/editor/CodeMirror/theme.js
|
||||
packages/editor/CodeMirror/util/isInSyntaxNode.js
|
||||
packages/editor/SelectionFormatting.js
|
||||
packages/editor/events.js
|
||||
packages/editor/types.js
|
||||
packages/fork-htmlparser2/src/CollectingHandler.js
|
||||
packages/fork-htmlparser2/src/FeedHandler.spec.js
|
||||
packages/fork-htmlparser2/src/FeedHandler.js
|
||||
@@ -571,7 +525,6 @@ packages/lib/WelcomeUtils.js
|
||||
packages/lib/array.js
|
||||
packages/lib/callbackUrlUtils.test.js
|
||||
packages/lib/callbackUrlUtils.js
|
||||
packages/lib/clipperUtils.js
|
||||
packages/lib/commands/historyBackward.js
|
||||
packages/lib/commands/historyForward.js
|
||||
packages/lib/commands/index.js
|
||||
@@ -690,10 +643,6 @@ packages/lib/services/commands/isEditorCommand.js
|
||||
packages/lib/services/commands/propsHaveChanged.js
|
||||
packages/lib/services/commands/stateToWhenClauseContext.js
|
||||
packages/lib/services/contextkey/contextkey.js
|
||||
packages/lib/services/database/addMigrationFile.js
|
||||
packages/lib/services/database/migrations/42.js
|
||||
packages/lib/services/database/migrations/43.js
|
||||
packages/lib/services/database/migrations/44.js
|
||||
packages/lib/services/database/types.js
|
||||
packages/lib/services/debug/populateDatabase.js
|
||||
packages/lib/services/e2ee/EncryptionService.test.js
|
||||
@@ -804,7 +753,6 @@ packages/lib/services/rest/routes/master_keys.js
|
||||
packages/lib/services/rest/routes/notes.js
|
||||
packages/lib/services/rest/routes/ping.js
|
||||
packages/lib/services/rest/routes/resources.js
|
||||
packages/lib/services/rest/routes/revisions.js
|
||||
packages/lib/services/rest/routes/search.js
|
||||
packages/lib/services/rest/routes/tags.js
|
||||
packages/lib/services/rest/utils/collectionToPaginatedResults.js
|
||||
@@ -956,7 +904,6 @@ packages/renderer/noteStyle.js
|
||||
packages/renderer/pathUtils.js
|
||||
packages/renderer/utils.js
|
||||
packages/tools/build-release-stats.js
|
||||
packages/tools/build-translation.js
|
||||
packages/tools/build-welcome.js
|
||||
packages/tools/buildServerDocker.test.js
|
||||
packages/tools/buildServerDocker.js
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"exceptions": [
|
||||
"@joplin/lib",
|
||||
"@joplin/renderer",
|
||||
"@joplin/editor",
|
||||
"@joplin/pdf-viewer",
|
||||
"@joplin/fork-htmlparser2",
|
||||
"@joplin/fork-sax",
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<svg width="84" height="84" viewBox="0 0 84 84" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_52_28)">
|
||||
<rect width="84" height="84" fill="url(#paint0_linear_52_28)"/>
|
||||
<path d="M66 48.5424C66 51.4195 64.8173 54.0531 62.9108 55.9371C61.0108 57.8274 58.3546 59 55.4529 59H49.1324V53.5533H55.4529C56.8553 53.5533 58.1026 52.9959 59.0268 52.086C59.9445 51.1696 60.5067 49.9393 60.5067 48.5424C60.5067 47.1519 59.9445 45.9152 59.0268 44.9989C58.1026 44.089 56.8618 43.5315 55.4529 43.5315H50.1083L50.4056 40.539C50.4379 40.2314 50.4508 39.9303 50.4508 39.6163C50.4508 37.0724 49.4168 34.7976 47.7429 33.1316C46.0626 31.4655 43.7684 30.4467 41.2027 30.4467C38.637 30.4467 36.3428 31.4719 34.6625 33.1316C32.9822 34.7976 31.9546 37.0724 31.9546 39.6163C31.9546 39.9239 31.9675 40.2314 31.9999 40.539L32.2971 43.5315H27.5535C26.1511 43.5315 24.9039 44.089 23.9797 44.9989C23.062 45.9152 22.4997 47.1455 22.4997 48.5424C22.4997 49.9329 23.062 51.1696 23.9797 52.086C24.9039 52.9959 26.1447 53.5533 27.5535 53.5533H35.2118C36.6077 53.3291 37.9003 52.6755 38.9278 51.663C40.2139 50.3815 40.9248 48.6834 40.9248 46.8828V41.0837H35.3605V38.9819C35.3605 37.1941 36.8275 35.7331 38.637 35.7331H46.3211V46.8828C46.3211 50.0739 45.0222 53.1945 42.7473 55.4437C40.7439 57.4365 38.0619 58.6732 35.2441 58.9359L34.5332 59H27.5471C24.6453 59 21.9892 57.8274 20.0892 55.9371C18.1827 54.0531 17 51.4195 17 48.5424C17 45.6653 18.1827 43.0317 20.0892 41.1478C21.763 39.4817 24.0249 38.3732 26.526 38.1361C26.875 34.6951 28.4389 31.5937 30.772 29.2805C33.4346 26.6404 37.1377 24.9936 41.1963 25C45.2613 25 48.9644 26.6404 51.6205 29.2805C53.9471 31.5809 55.5046 34.6759 55.8601 38.1041C58.6002 38.213 61.0884 39.3472 62.8979 41.1478C64.8173 43.0317 66 45.6653 66 48.5424Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_52_28" x1="3" y1="4" x2="78" y2="79" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.14" stop-color="#3873DB"/>
|
||||
<stop offset="0.946667" stop-color="#163467"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_52_28">
|
||||
<rect width="84" height="84" rx="20" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.1 KiB |
@@ -1,75 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="84"
|
||||
height="84"
|
||||
viewBox="0 0 84 84"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg938"
|
||||
sodipodi:docname="JoplinCloudIcon copie.svg"
|
||||
inkscape:version="1.1.1 (c3084ef, 2021-09-22)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview940"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="3.5021331"
|
||||
inkscape:cx="-6.1391156"
|
||||
inkscape:cy="29.838957"
|
||||
inkscape:window-width="1310"
|
||||
inkscape:window-height="969"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="g926" />
|
||||
<g
|
||||
clip-path="url(#clip0_52_28)"
|
||||
id="g926">
|
||||
<rect
|
||||
width="84"
|
||||
height="84"
|
||||
fill="url(#paint0_linear_52_28)"
|
||||
id="rect922" />
|
||||
<path
|
||||
d="m 73.705932,49.824628 c 0,3.73216 -1.534192,7.148453 -4.007294,9.592368 -2.464671,2.452087 -5.91028,3.973178 -9.674351,3.973178 h -8.198921 v -7.065432 h 8.198921 c 1.819186,0 3.437178,-0.723056 4.636045,-1.903374 1.190436,-1.188749 1.919719,-2.784688 1.919719,-4.59674 0,-1.803749 -0.729283,-3.40799 -1.919719,-4.59661 -1.198867,-1.180318 -2.808427,-1.903504 -4.636045,-1.903504 h -6.932988 l 0.385656,-3.881856 c 0.0419,-0.399017 0.05863,-0.789603 0.05863,-1.196922 0,-3.299934 -1.341299,-6.250793 -3.512674,-8.41192 -2.179677,-2.161256 -5.155701,-3.482839 -8.483914,-3.482839 -3.328214,0 -6.304238,1.329885 -8.483915,3.482839 -2.179677,2.161127 -3.512674,5.111986 -3.512674,8.41192 0,0.399017 0.01673,0.797905 0.05876,1.196922 l 0.385526,3.881856 H 23.83332 c -1.819186,0 -3.437048,0.723186 -4.635915,1.903504 -1.190436,1.18862 -1.919849,2.784559 -1.919849,4.59661 0,1.80375 0.729413,3.407991 1.919849,4.59674 1.198867,1.180318 2.808427,1.903374 4.635915,1.903374 h 9.934308 c 1.810755,-0.290831 3.487509,-1.138677 4.820377,-2.452087 1.668322,-1.662356 2.590498,-3.865122 2.590498,-6.200852 V 40.14924 h -7.217982 v -2.726444 c 0,-2.319125 1.902985,-4.214327 4.250259,-4.214327 h 9.967776 v 14.463334 c 0,4.139479 -1.684926,8.187506 -4.635915,11.105157 -2.598801,2.58505 -6.077877,4.189291 -9.733113,4.530064 l -0.922176,0.08315 h -9.062334 c -3.7642,0 -7.20968,-1.521091 -9.67435,-3.973178 -2.473097,-2.443915 -4.007289,-5.860208 -4.007289,-9.592368 0,-3.732159 1.534192,-7.148452 4.007295,-9.592238 2.171245,-2.161256 5.10537,-3.599197 8.349784,-3.906762 0.452721,-4.463648 2.481404,-8.486768 5.50789,-11.487439 3.453911,-3.424725 8.257554,-5.560946 13.522349,-5.552643 5.273098,0 10.076741,2.127918 13.52222,5.552643 3.018054,2.984067 5.038435,6.998885 5.499588,11.445929 3.554444,0.141265 6.782124,1.612543 9.129399,3.948272 2.489836,2.443786 4.024028,5.860079 4.024028,9.592238 z"
|
||||
fill="#ffffff"
|
||||
id="path924"
|
||||
style="stroke-width:1.29719" />
|
||||
</g>
|
||||
<defs
|
||||
id="defs936">
|
||||
<linearGradient
|
||||
id="paint0_linear_52_28"
|
||||
x1="3"
|
||||
y1="4"
|
||||
x2="78"
|
||||
y2="79"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
offset="0.14"
|
||||
stop-color="#3873DB"
|
||||
id="stop928" />
|
||||
<stop
|
||||
offset="0.946667"
|
||||
stop-color="#163467"
|
||||
id="stop930" />
|
||||
</linearGradient>
|
||||
<clipPath
|
||||
id="clip0_52_28">
|
||||
<rect
|
||||
width="84"
|
||||
height="84"
|
||||
rx="20"
|
||||
fill="white"
|
||||
id="rect933" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.6 KiB |
@@ -1,16 +0,0 @@
|
||||
<svg width="84" height="84" viewBox="0 0 84 84" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_309_806)">
|
||||
<rect width="84" height="84" fill="url(#paint0_linear_309_806)"/>
|
||||
<path d="M42.5797 45.6916H33.6235V36.276C33.6235 33.3538 34.7629 30.6033 36.8396 28.5204C38.4998 26.8725 40.5826 25.8127 42.8431 25.4513V16.7033C38.2854 17.1138 33.9359 19.1354 30.6891 22.3822C27.0258 26.0333 24.9307 31.0995 24.9307 36.2821V54.3845H37.3052C40.2151 54.3845 42.5797 52.0198 42.5797 49.11V45.6916Z" fill="#1883E6"/>
|
||||
<path d="M42.4204 35.89V39.3022H51.3767V48.7179C51.3767 51.64 50.2372 54.3906 48.1605 56.4735C46.5003 58.1214 44.4175 59.1812 42.157 59.5426V68.2906C46.7147 67.8801 51.0642 65.8585 54.311 62.6117C57.9744 58.9606 60.0695 53.8944 60.0695 48.7118V30.6155H47.6949C44.7851 30.6155 42.4204 32.9863 42.4204 35.89Z" fill="#1883E6"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_309_806" x1="8.5" y1="3" x2="76.5" y2="81.5" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#B6D3F9"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_309_806">
|
||||
<rect width="84" height="84" rx="20" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -667,30 +667,6 @@ footer .bottom-links-row p {
|
||||
color: #0557ba;
|
||||
}
|
||||
|
||||
.joplin-cloud-feature-list .feature-description {
|
||||
max-width: 600px;
|
||||
font-size: .8em;
|
||||
color: #555555;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.joplin-cloud-feature-list .feature-title {
|
||||
text-decoration: none;
|
||||
color: #000000;
|
||||
margin-left: 10px;
|
||||
border: 2px solid black;
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 100px;
|
||||
height: 20px;
|
||||
font-weight: bold;
|
||||
font-size: 0.8em;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/*****************************************************************
|
||||
WHAT'S NEW PAGE
|
||||
*****************************************************************/
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230825-share-permissions.png" alt=""></p>
|
||||
<h2>Email to Note<a name="email-to-note" href="#email-to-note" class="heading-anchor">🔗</a></h2>
|
||||
<p>Joplin Cloud Pro and Teams also now include the Email to Note feature, allowing you to conveniently store your emails within Joplin Cloud. By simply forwarding your emails to your Joplin Cloud address, you can transform them into notes. The email's subject will serve as the note title, while the body of the email will be the note's content. These notes will be organized within a notebook named "Inbox."</p>
|
||||
<p>More information in the <a href="https://joplinapp.org/email_to_note/">Email to Note documentation</a>.</p>
|
||||
<p>More information in the <a href="https://joplinapp.org/email%5C_to%5C_note/">Email to Note documentation</a>.</p>
|
||||
<h2>Choose to resize an image or not<a name="choose-to-resize-an-image-or-not" href="#choose-to-resize-an-image-or-not" class="heading-anchor">🔗</a></h2>
|
||||
<p>By default, when you add a large image, Joplin will ask you if you would like to shrink it down or not. With this new release, you now have the option to always ask, to always resize, or to never resize the image, giving you more flexibility and reducing the number of prompts in the app.</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230825-resize-note.png" alt=""></p>
|
||||
|
||||
@@ -86,7 +86,11 @@ https://github.com/laurent22/joplin/blob/dev/{{{sourceMarkdownFile}}}
|
||||
{{> footer}}
|
||||
</div>
|
||||
|
||||
<script src="{{jsBaseUrl}}/bootstrap5.0.2.bundle.min.js" rel="preload" as="script"></script>
|
||||
<script
|
||||
src="{{jsBaseUrl}}/bootstrap5.0.2.bundle.min.js"
|
||||
rel="preload"
|
||||
as="script"
|
||||
></script>
|
||||
<script src="{{{assetUrls.js.script}}}"></script>
|
||||
|
||||
{{> analytics}}
|
||||
|
||||
@@ -138,14 +138,6 @@
|
||||
} else {
|
||||
applyPeriod('yearly');
|
||||
}
|
||||
|
||||
$('.feature-description').hide();
|
||||
|
||||
$('.feature-title').click((event) => {
|
||||
event.preventDefault();
|
||||
const featureId = event.currentTarget.getAttribute('data-id');
|
||||
$('.feature-description-' + featureId).toggle(200);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
|
||||
80
README.md
80
README.md
@@ -22,11 +22,11 @@ Three types of applications are available: for **desktop** (Windows, macOS and L
|
||||
|
||||
Operating System | Download
|
||||
---|---
|
||||
Windows (32 and 64-bit) | <a href='https://objects.joplinusercontent.com/v2.12.19/Joplin-Setup-2.12.19.exe?source=JoplinWebsite&type=New'><img alt='Get it on Windows' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeWindows.png'/></a>
|
||||
macOS | <a href='https://objects.joplinusercontent.com/v2.12.19/Joplin-2.12.19.dmg?source=JoplinWebsite&type=New'><img alt='Get it on macOS' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeMacOS.png'/></a>
|
||||
Linux | <a href='https://objects.joplinusercontent.com/v2.12.19/Joplin-2.12.19.AppImage?source=JoplinWebsite&type=New'><img alt='Get it on Linux' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeLinux.png'/></a>
|
||||
Windows (32 and 64-bit) | <a href='https://objects.joplinusercontent.com/v2.12.17/Joplin-Setup-2.12.17.exe?source=JoplinWebsite&type=New'><img alt='Get it on Windows' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeWindows.png'/></a>
|
||||
macOS | <a href='https://objects.joplinusercontent.com/v2.12.17/Joplin-2.12.17.dmg?source=JoplinWebsite&type=New'><img alt='Get it on macOS' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeMacOS.png'/></a>
|
||||
Linux | <a href='https://objects.joplinusercontent.com/v2.12.17/Joplin-2.12.17.AppImage?source=JoplinWebsite&type=New'><img alt='Get it on Linux' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeLinux.png'/></a>
|
||||
|
||||
**On Windows**, you may also use the <a href='https://objects.joplinusercontent.com/v2.12.19/JoplinPortable.exe?source=JoplinWebsite&type=New'>Portable version</a>. The [portable application](https://en.wikipedia.org/wiki/Portable_application) allows installing the software on a portable device such as a USB key. Simply copy the file JoplinPortable.exe in any directory on that USB key ; the application will then create a directory called "JoplinProfile" next to the executable file.
|
||||
**On Windows**, you may also use the <a href='https://objects.joplinusercontent.com/v2.12.17/JoplinPortable.exe?source=JoplinWebsite&type=New'>Portable version</a>. The [portable application](https://en.wikipedia.org/wiki/Portable_application) allows installing the software on a portable device such as a USB key. Simply copy the file JoplinPortable.exe in any directory on that USB key ; the application will then create a directory called "JoplinProfile" next to the executable file.
|
||||
|
||||
**On Linux**, the recommended way is to use the following installation script as it will handle the desktop icon too:
|
||||
|
||||
@@ -537,47 +537,47 @@ Current translations:
|
||||
<!-- LOCALE-TABLE-AUTO-GENERATED -->
|
||||
| Language | Po File | Last translator | Percent done
|
||||
---|---|---|---|---
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/arableague.png" width="16px"/> | Arabic | [ar](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ar.po) | [Whaell O](mailto:Whaell@protonmail.com) | 84%
|
||||
<img src="https://joplinapp.org/images/flags/es/basque_country.png" width="16px"/> | Basque | [eu](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eu.po) | juan.abasolo@ehu.eus | 21%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ba.png" width="16px"/> | Bosnian (Bosna i Hercegovina) | [bs_BA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bs_BA.po) | [Derviš T.](mailto:dervis.t@pm.me) | 54%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/bg.png" width="16px"/> | Bulgarian (България) | [bg_BG](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bg_BG.po) | | 42%
|
||||
<img src="https://joplinapp.org/images/flags/es/catalonia.png" width="16px"/> | Catalan | [ca](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ca.po) | [Xavi Ivars](mailto:xavi.ivars@gmail.com) | 92%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/hr.png" width="16px"/> | Croatian (Hrvatska) | [hr_HR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hr_HR.po) | [Milo Ivir](mailto:mail@milotype.de) | 97%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/cz.png" width="16px"/> | Czech (Česká republika) | [cs_CZ](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/cs_CZ.po) | Fejby | 94%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/dk.png" width="16px"/> | Dansk (Danmark) | [da_DK](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/da_DK.po) | ERYpTION | 97%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/de.png" width="16px"/> | Deutsch (Deutschland) | [de_DE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/de_DE.po) | [Mr-Kanister](mailto:viger_gtrc@simplelogin.com) | 97%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ee.png" width="16px"/> | Eesti Keel (Eesti) | [et_EE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/et_EE.po) | | 42%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/arableague.png" width="16px"/> | Arabic | [ar](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ar.po) | [Whaell O](mailto:Whaell@protonmail.com) | 77%
|
||||
<img src="https://joplinapp.org/images/flags/es/basque_country.png" width="16px"/> | Basque | [eu](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eu.po) | juan.abasolo@ehu.eus | 22%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ba.png" width="16px"/> | Bosnian (Bosna i Hercegovina) | [bs_BA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bs_BA.po) | [Derviš T.](mailto:dervis.t@pm.me) | 56%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/bg.png" width="16px"/> | Bulgarian (България) | [bg_BG](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bg_BG.po) | | 44%
|
||||
<img src="https://joplinapp.org/images/flags/es/catalonia.png" width="16px"/> | Catalan | [ca](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ca.po) | [Xavi Ivars](mailto:xavi.ivars@gmail.com) | 86%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/hr.png" width="16px"/> | Croatian (Hrvatska) | [hr_HR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hr_HR.po) | [Milo Ivir](mailto:mail@milotype.de) | 98%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/cz.png" width="16px"/> | Czech (Česká republika) | [cs_CZ](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/cs_CZ.po) | Fejby | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/dk.png" width="16px"/> | Dansk (Danmark) | [da_DK](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/da_DK.po) | ERYpTION | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/de.png" width="16px"/> | Deutsch (Deutschland) | [de_DE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/de_DE.po) | [Mr-Kanister](mailto:viger_gtrc@simplelogin.com) | 98%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ee.png" width="16px"/> | Eesti Keel (Eesti) | [et_EE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/et_EE.po) | | 43%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/gb.png" width="16px"/> | English (United Kingdom) | [en_GB](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_GB.po) | | 100%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/us.png" width="16px"/> | English (United States of America) | [en_US](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_US.po) | | 100%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/es.png" width="16px"/> | Español (España) | [es_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/es_ES.po) | [Francisco Villaverde](mailto:teko.gr@gmail.com) | 93%
|
||||
<img src="https://joplinapp.org/images/flags/esperanto.png" width="16px"/> | Esperanto | [eo](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eo.po) | Marton Paulo | 24%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/fi.png" width="16px"/> | Finnish (Suomi) | [fi_FI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fi_FI.po) | mrkaato0 | 94%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/es.png" width="16px"/> | Español (España) | [es_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/es_ES.po) | [Francisco Villaverde](mailto:teko.gr@gmail.com) | 95%
|
||||
<img src="https://joplinapp.org/images/flags/esperanto.png" width="16px"/> | Esperanto | [eo](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eo.po) | Marton Paulo | 25%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/fi.png" width="16px"/> | Finnish (Suomi) | [fi_FI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fi_FI.po) | mrkaato0 | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/fr.png" width="16px"/> | Français (France) | [fr_FR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fr_FR.po) | Laurent Cozic | 100%
|
||||
<img src="https://joplinapp.org/images/flags/es/galicia.png" width="16px"/> | Galician (España) | [gl_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/gl_ES.po) | [Marcos Lans](mailto:marcoslansgarza@gmail.com) | 27%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/id.png" width="16px"/> | Indonesian (Indonesia) | [id_ID](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/id_ID.po) | [Wisnu Adi Santoso](mailto:waditos@gmail.com) | 84%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/it.png" width="16px"/> | Italiano (Italia) | [it_IT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/it_IT.po) | [Manuel Tassi](mailto:mannivuwiki@gmail.com) | 76%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/hu.png" width="16px"/> | Magyar (Magyarország) | [hu_HU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hu_HU.po) | [Magyari Balázs](mailto:balmag@gmail.com) | 73%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/be.png" width="16px"/> | Nederlands (België, Belgique, Belgien) | [nl_BE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_BE.po) | | 74%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/nl.png" width="16px"/> | Nederlands (Nederland) | [nl_NL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_NL.po) | [MHolkamp](mailto:mholkamp@users.noreply.github.com) | 97%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/no.png" width="16px"/> | Norwegian (Norge, Noreg) | [nb_NO](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nb_NO.po) | [Mats Estensen](mailto:code@mxe.no) | 83%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ir.png" width="16px"/> | Persian | [fa](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fa.po) | [Kourosh Firoozbakht](mailto:kourox@protonmail.com) | 52%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/pl.png" width="16px"/> | Polski (Polska) | [pl_PL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pl_PL.po) | [X3NO](mailto:X3NO@disroot.org) | 85%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/br.png" width="16px"/> | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_BR.po) | [Fernando Nagase](mailto:nagase.fernando@gmail.com) | 97%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/pt.png" width="16px"/> | Português (Portugal) | [pt_PT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_PT.po) | [Diogo Caveiro](mailto:dcaveiro@yahoo.com) | 68%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ro.png" width="16px"/> | Română | [ro](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ro.po) | [Cristi Duluta](mailto:cristi.duluta@gmail.com) | 48%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/si.png" width="16px"/> | Slovenian (Slovenija) | [sl_SI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sl_SI.po) | [Martin Korelič](mailto:martin.korelic@protonmail.com) | 76%
|
||||
<img src="https://joplinapp.org/images/flags/es/galicia.png" width="16px"/> | Galician (España) | [gl_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/gl_ES.po) | [Marcos Lans](mailto:marcoslansgarza@gmail.com) | 28%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/id.png" width="16px"/> | Indonesian (Indonesia) | [id_ID](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/id_ID.po) | [Wisnu Adi Santoso](mailto:waditos@gmail.com) | 86%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/it.png" width="16px"/> | Italiano (Italia) | [it_IT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/it_IT.po) | [Manuel Tassi](mailto:mannivuwiki@gmail.com) | 78%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/hu.png" width="16px"/> | Magyar (Magyarország) | [hu_HU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hu_HU.po) | [Magyari Balázs](mailto:balmag@gmail.com) | 75%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/be.png" width="16px"/> | Nederlands (België, Belgique, Belgien) | [nl_BE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_BE.po) | | 76%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/nl.png" width="16px"/> | Nederlands (Nederland) | [nl_NL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_NL.po) | [MHolkamp](mailto:mholkamp@users.noreply.github.com) | 86%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/no.png" width="16px"/> | Norwegian (Norge, Noreg) | [nb_NO](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nb_NO.po) | [Mats Estensen](mailto:code@mxe.no) | 85%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ir.png" width="16px"/> | Persian | [fa](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fa.po) | [Kourosh Firoozbakht](mailto:kourox@protonmail.com) | 53%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/pl.png" width="16px"/> | Polski (Polska) | [pl_PL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pl_PL.po) | [X3NO](mailto:X3NO@disroot.org) | 88%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/br.png" width="16px"/> | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_BR.po) | [Douglas Leão](mailto:djlsplays@gmail.com) | 85%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/pt.png" width="16px"/> | Português (Portugal) | [pt_PT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_PT.po) | [Diogo Caveiro](mailto:dcaveiro@yahoo.com) | 70%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ro.png" width="16px"/> | Română | [ro](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ro.po) | [Cristi Duluta](mailto:cristi.duluta@gmail.com) | 49%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/si.png" width="16px"/> | Slovenian (Slovenija) | [sl_SI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sl_SI.po) | [Martin Korelič](mailto:martin.korelic@protonmail.com) | 78%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/se.png" width="16px"/> | Svenska | [sv](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sv.po) | [Jonatan Nyberg](mailto:jonatan@autistici.org) | 97%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/th.png" width="16px"/> | Thai (ประเทศไทย) | [th_TH](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/th_TH.po) | | 34%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/vn.png" width="16px"/> | Tiếng Việt | [vi](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po) | | 73%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/th.png" width="16px"/> | Thai (ประเทศไทย) | [th_TH](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/th_TH.po) | | 35%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/vn.png" width="16px"/> | Tiếng Việt | [vi](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po) | | 75%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/tr.png" width="16px"/> | Türkçe (Türkiye) | [tr_TR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/tr_TR.po) | [Arda Kılıçdağı](mailto:arda@kilicdagi.com) | 97%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ua.png" width="16px"/> | Ukrainian (Україна) | [uk_UA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/uk_UA.po) | [Vyacheslav Andreykiv](mailto:vandreykiv@gmail.com) | 95%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/gr.png" width="16px"/> | Ελληνικά (Ελλάδα) | [el_GR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/el_GR.po) | [Harris Arvanitis](mailto:xaris@tuta.io) | 94%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ru.png" width="16px"/> | Русский (Россия) | [ru_RU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ru_RU.po) | [Титан](mailto:fignin@ya.ru) | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/rs.png" width="16px"/> | српски језик (Србија) | [sr_RS](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sr_RS.po) | | 61%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/cn.png" width="16px"/> | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po) | [qx100](mailto:ztymaxwell@gmail.com) | 97%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/tw.png" width="16px"/> | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [Kevin Hsu](mailto:kevin.hsu.hws@gmail.com) | 84%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/jp.png" width="16px"/> | 日本語 (日本) | [ja_JP](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ja_JP.po) | [genneko](mailto:genneko217@gmail.com) | 97%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/kr.png" width="16px"/> | 한국어 | [ko](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ko.po) | [Ji-Hyeon Gim](mailto:potatogim@potatogim.net) | 84%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ua.png" width="16px"/> | Ukrainian (Україна) | [uk_UA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/uk_UA.po) | [Vyacheslav Andreykiv](mailto:vandreykiv@gmail.com) | 98%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/gr.png" width="16px"/> | Ελληνικά (Ελλάδα) | [el_GR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/el_GR.po) | [Harris Arvanitis](mailto:xaris@tuta.io) | 97%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ru.png" width="16px"/> | Русский (Россия) | [ru_RU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ru_RU.po) | [Dmitriy K](mailto:dmitry@atsip.ru) | 97%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/rs.png" width="16px"/> | српски језик (Србија) | [sr_RS](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sr_RS.po) | | 63%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/cn.png" width="16px"/> | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po) | [qx100](mailto:ztymaxwell@gmail.com) | 98%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/tw.png" width="16px"/> | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [Kevin Hsu](mailto:kevin.hsu.hws@gmail.com) | 86%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/jp.png" width="16px"/> | 日本語 (日本) | [ja_JP](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ja_JP.po) | [genneko](mailto:genneko217@gmail.com) | 88%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/kr.png" width="16px"/> | 한국어 | [ko](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ko.po) | [Ji-Hyeon Gim](mailto:potatogim@potatogim.net) | 86%
|
||||
<!-- LOCALE-TABLE-AUTO-GENERATED -->
|
||||
|
||||
# Contributors
|
||||
|
||||
18
package.json
18
package.json
@@ -68,10 +68,10 @@
|
||||
"devDependencies": {
|
||||
"@joplin/utils": "~2.12",
|
||||
"@seiyab/eslint-plugin-react-hooks": "4.5.1-beta.0",
|
||||
"@typescript-eslint/eslint-plugin": "6.0.0",
|
||||
"@typescript-eslint/parser": "6.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.62.0",
|
||||
"@typescript-eslint/parser": "5.62.0",
|
||||
"cspell": "5.21.2",
|
||||
"eslint": "8.47.0",
|
||||
"eslint": "8.46.0",
|
||||
"eslint-interactive": "10.8.0",
|
||||
"eslint-plugin-import": "2.28.1",
|
||||
"eslint-plugin-jest": "27.2.3",
|
||||
@@ -79,27 +79,27 @@
|
||||
"eslint-plugin-react": "7.33.2",
|
||||
"execa": "5.1.1",
|
||||
"fs-extra": "11.1.1",
|
||||
"glob": "10.3.10",
|
||||
"glob": "10.3.3",
|
||||
"gulp": "4.0.2",
|
||||
"husky": "3.1.0",
|
||||
"lerna": "3.22.1",
|
||||
"lint-staged": "13.3.0",
|
||||
"lint-staged": "13.2.3",
|
||||
"madge": "6.1.0",
|
||||
"npm-package-json-lint": "7.0.0",
|
||||
"npm-package-json-lint": "6.4.0",
|
||||
"typescript": "5.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/fs-extra": "11.0.2",
|
||||
"@types/fs-extra": "11.0.1",
|
||||
"eslint-plugin-github": "4.9.2",
|
||||
"http-server": "14.1.1",
|
||||
"node-gyp": "9.4.0",
|
||||
"nodemon": "3.0.1"
|
||||
"nodemon": "2.0.22"
|
||||
},
|
||||
"packageManager": "yarn@3.6.3",
|
||||
"resolutions": {
|
||||
"react-native-camera@4.2.1": "patch:react-native-camera@npm%3A4.2.1#./.yarn/patches/react-native-camera-npm-4.2.1-24b2600a7e.patch",
|
||||
"react-native-vosk@0.1.12": "patch:react-native-vosk@npm%3A0.1.12#./.yarn/patches/react-native-vosk-npm-0.1.12-76b1caaae8.patch",
|
||||
"eslint": "patch:eslint@8.47.0#./.yarn/patches/eslint-npm-8.39.0-d92bace04d.patch",
|
||||
"eslint": "patch:eslint@npm%3A8.39.0#./.yarn/patches/eslint-npm-8.39.0-d92bace04d.patch",
|
||||
"app-builder-lib@24.4.0": "patch:app-builder-lib@npm%3A24.4.0#./.yarn/patches/app-builder-lib-npm-24.4.0-05322ff057.patch",
|
||||
"react-native@0.71.10": "patch:react-native@npm%3A0.71.10#./.yarn/patches/react-native-animation-fix/react-native-npm-0.71.10-f9c32562d8.patch"
|
||||
}
|
||||
|
||||
@@ -50,9 +50,6 @@ class Command extends BaseCommand {
|
||||
{
|
||||
type: BaseModel.TYPE_TAG,
|
||||
},
|
||||
{
|
||||
type: BaseModel.TYPE_REVISION,
|
||||
},
|
||||
];
|
||||
|
||||
const lines = [];
|
||||
@@ -414,7 +411,7 @@ async function fetchAllNotes() {
|
||||
lines.push('');
|
||||
lines.push('If no `cursor` property is provided, the API will respond with the latest change ID. That can be used to retrieve future events later on.');
|
||||
lines.push('');
|
||||
lines.push('The results are paginated so you may need multiple calls to retrieve all the events. Use the `has_more` property to know if more can be retrieved.');
|
||||
lines.push('The results are paginated so will need to may multiple calls to retrieve all the events. Use the `has_more` property to know if more can be retrieved.');
|
||||
lines.push('');
|
||||
lines.push('## GET /events/:id');
|
||||
lines.push('');
|
||||
|
||||
@@ -57,8 +57,8 @@
|
||||
"proper-lockfile": "4.1.2",
|
||||
"read-chunk": "2.1.0",
|
||||
"server-destroy": "1.0.1",
|
||||
"sharp": "0.32.6",
|
||||
"sprintf-js": "1.1.3",
|
||||
"sharp": "0.32.5",
|
||||
"sprintf-js": "1.1.2",
|
||||
"sqlite3": "5.1.6",
|
||||
"string-padding": "1.0.2",
|
||||
"strip-ansi": "6.0.1",
|
||||
@@ -71,9 +71,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@joplin/tools": "~2.13",
|
||||
"@types/fs-extra": "11.0.2",
|
||||
"@types/fs-extra": "11.0.1",
|
||||
"@types/jest": "29.5.4",
|
||||
"@types/node": "18.17.19",
|
||||
"@types/node": "18.17.12",
|
||||
"@types/proper-lockfile": "^4.1.2",
|
||||
"gulp": "4.0.2",
|
||||
"jest": "29.6.4",
|
||||
|
||||
@@ -1,204 +0,0 @@
|
||||
<?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>Boomwhackers - Rio</title>
|
||||
<created>20230412T112328Z</created>
|
||||
<updated>20230524T133616Z</updated>
|
||||
<note-attributes>
|
||||
</note-attributes>
|
||||
<content>
|
||||
<![CDATA[<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd"><en-note><div><br/></div><en-media hash="b687cb1ab2d8f3f10b95c3a500841c66" type="application/zip" /><div><br/></div></en-note> ]]>
|
||||
</content>
|
||||
<resource>
|
||||
<data encoding="base64">
|
||||
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
|
||||
</data>
|
||||
<mime>application/zip</mime>
|
||||
<resource-attributes>
|
||||
<file-name>Boomwhackers - Rio.mscz</file-name>
|
||||
<source-url>en-cache://tokenKey%3D%22AuthToken%3AUser%3A75937676%22+32040eaf-3c97-1f46-f34c-1d4e8608c682+b687cb1ab2d8f3f10b95c3a500841c66+https://www.evernote.com/shard/s438/res/9521d2af-a8de-26f3-4f86-8f7f4deebb6b</source-url>
|
||||
</resource-attributes>
|
||||
</resource>
|
||||
</note>
|
||||
</en-export>
|
||||
File diff suppressed because one or more lines are too long
@@ -2,20 +2,5 @@ joplin.plugins.register({
|
||||
onStart: async function() {
|
||||
const folder = await joplin.data.post(['folders'], null, { title: "my plugin folder" });
|
||||
await joplin.data.post(['notes'], null, { parent_id: folder.id, title: "testing plugin!" });
|
||||
|
||||
// await joplin.commands.register({
|
||||
// name: 'updateCurrentNote',
|
||||
// label: 'Update current note via the data API',
|
||||
// iconName: 'fas fa-music',
|
||||
// execute: async () => {
|
||||
// const noteIds = await joplin.workspace.selectedNoteIds();
|
||||
// const noteId = noteIds.length === 1 ? noteIds[0] : null;
|
||||
// if (!noteId) return;
|
||||
// console.info('Modifying current note...');
|
||||
// await joplin.data.put(['notes', noteId], null, { body: "New note body " + Date.now() });
|
||||
// },
|
||||
// });
|
||||
|
||||
// await joplin.views.toolbarButtons.create('updateCurrentNoteButton', 'updateCurrentNote', 'editorToolbar');
|
||||
},
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
title: Example
|
||||
---
|
||||
|
||||
|
||||
|
||||
note body
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
title: Example
|
||||
---
|
||||
note body
|
||||
@@ -1,5 +1,6 @@
|
||||
// v0.4.4 - https://github.com/mozilla/readability/commit/49d345a455da1f4aa93f8b41e0f50422f9959c7c
|
||||
// v0.4.1 - https://github.com/mozilla/readability/commit/28843b6de84447dd6cef04058fda336938e628dc
|
||||
|
||||
/* eslint-env es6:false */
|
||||
/*
|
||||
* Copyright (c) 2010 Arc90 Inc
|
||||
*
|
||||
@@ -54,7 +55,7 @@ var REGEXPS = {
|
||||
var defaultOptions = { minScore: 20, minContentLength: 140, visibilityChecker: isNodeVisible };
|
||||
options = Object.assign(defaultOptions, options);
|
||||
|
||||
var nodes = doc.querySelectorAll("p, pre, article");
|
||||
var nodes = doc.querySelectorAll("p, pre");
|
||||
|
||||
// Get <div> nodes which have <br> node(s) and append them into the `nodes` variable.
|
||||
// Some articles' DOM structures might look like
|
||||
@@ -105,6 +106,5 @@ var REGEXPS = {
|
||||
}
|
||||
|
||||
if (typeof module === "object") {
|
||||
/* global module */
|
||||
module.exports = isProbablyReaderable;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
// v0.4.4 - https://github.com/mozilla/readability/commit/49d345a455da1f4aa93f8b41e0f50422f9959c7c
|
||||
// v0.4.1 - https://github.com/mozilla/readability/commit/28843b6de84447dd6cef04058fda336938e628dc
|
||||
|
||||
/*eslint-env es6:false*/
|
||||
/*
|
||||
* Copyright (c) 2010 Arc90 Inc
|
||||
*
|
||||
@@ -26,7 +27,7 @@
|
||||
* @param {HTMLDocument} doc The document to parse.
|
||||
* @param {Object} options The options object.
|
||||
*/
|
||||
function Readability(doc, options) {
|
||||
function Readability(doc, options) {
|
||||
// In some older versions, people passed a URI as the first argument. Cope:
|
||||
if (options && options.documentElement) {
|
||||
doc = options;
|
||||
@@ -55,7 +56,6 @@ function Readability(doc, options) {
|
||||
return el.innerHTML;
|
||||
};
|
||||
this._disableJSONLD = !!options.disableJSONLD;
|
||||
this._allowedVideoRegex = options.allowedVideoRegex || this.REGEXPS.videos;
|
||||
|
||||
// Start with all flags set
|
||||
this._flags = this.FLAG_STRIP_UNLIKELYS |
|
||||
@@ -75,7 +75,12 @@ function Readability(doc, options) {
|
||||
return `<${node.localName} ${attrPairs}>`;
|
||||
};
|
||||
this.log = function () {
|
||||
if (typeof console !== "undefined") {
|
||||
if (typeof dump !== "undefined") {
|
||||
var msg = Array.prototype.map.call(arguments, function(x) {
|
||||
return (x && x.nodeName) ? logNode(x) : x;
|
||||
}).join(" ");
|
||||
dump("Reader: (Readability) " + msg + "\n");
|
||||
} else if (typeof console !== "undefined") {
|
||||
let args = Array.from(arguments, arg => {
|
||||
if (arg && arg.nodeType == this.ELEMENT_NODE) {
|
||||
return logNode(arg);
|
||||
@@ -84,12 +89,6 @@ function Readability(doc, options) {
|
||||
});
|
||||
args.unshift("Reader: (Readability)");
|
||||
console.log.apply(console, args);
|
||||
} else if (typeof dump !== "undefined") {
|
||||
/* global dump */
|
||||
var msg = Array.prototype.map.call(arguments, function(x) {
|
||||
return (x && x.nodeName) ? logNode(x) : x;
|
||||
}).join(" ");
|
||||
dump("Reader: (Readability) " + msg + "\n");
|
||||
}
|
||||
};
|
||||
} else {
|
||||
@@ -143,9 +142,6 @@ function Readability(doc, options) {
|
||||
hashUrl: /^#.+/,
|
||||
srcsetUrl: /(\S+)(\s+[\d.]+[xw])?(\s*(?:,|$))/g,
|
||||
b64DataUrl: /^data:\s*([^\s;,]+)\s*;\s*base64\s*,/i,
|
||||
// Commas as used in Latin, Sindhi, Chinese and various other scripts.
|
||||
// see: https://en.wikipedia.org/wiki/Comma#Comma_variants
|
||||
commas: /\u002C|\u060C|\uFE50|\uFE10|\uFE11|\u2E41|\u2E34|\u2E32|\uFF0C/g,
|
||||
// See: https://schema.org/Article
|
||||
jsonLdArticleTypes: /^Article|AdvertiserContentArticle|NewsArticle|AnalysisNewsArticle|AskPublicNewsArticle|BackgroundNewsArticle|OpinionNewsArticle|ReportageNewsArticle|ReviewNewsArticle|Report|SatiricalArticle|ScholarlyArticle|MedicalScholarlyArticle|SocialMediaPosting|BlogPosting|LiveBlogPosting|DiscussionForumPosting|TechArticle|APIReference$/
|
||||
},
|
||||
@@ -902,11 +898,6 @@ function Readability(doc, options) {
|
||||
let shouldRemoveTitleHeader = true;
|
||||
|
||||
while (node) {
|
||||
|
||||
if (node.tagName === "HTML") {
|
||||
this._articleLang = node.getAttribute("lang");
|
||||
}
|
||||
|
||||
var matchString = node.className + " " + node.id;
|
||||
|
||||
if (!this._isProbablyVisible(node)) {
|
||||
@@ -915,12 +906,6 @@ function Readability(doc, options) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// User is not able to see elements applied with both "aria-modal = true" and "role = dialog"
|
||||
if (node.getAttribute("aria-modal") == "true" && node.getAttribute("role") == "dialog") {
|
||||
node = this._removeAndGetNext(node);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check to see if this node is a byline, and remove it if it is.
|
||||
if (this._checkByline(node, matchString)) {
|
||||
node = this._removeAndGetNext(node);
|
||||
@@ -1035,7 +1020,7 @@ function Readability(doc, options) {
|
||||
contentScore += 1;
|
||||
|
||||
// Add points for any commas within this paragraph.
|
||||
contentScore += innerText.split(this.REGEXPS.commas).length;
|
||||
contentScore += innerText.split(",").length;
|
||||
|
||||
// For every 100 characters in this paragraph, add another point. Up to 3 points.
|
||||
contentScore += Math.min(Math.floor(innerText.length / 100), 3);
|
||||
@@ -1374,88 +1359,72 @@ function Readability(doc, options) {
|
||||
_getJSONLD: function (doc) {
|
||||
var scripts = this._getAllNodesWithTag(doc, ["script"]);
|
||||
|
||||
var metadata;
|
||||
|
||||
this._forEachNode(scripts, function(jsonLdElement) {
|
||||
if (!metadata && jsonLdElement.getAttribute("type") === "application/ld+json") {
|
||||
try {
|
||||
// Strip CDATA markers if present
|
||||
var content = jsonLdElement.textContent.replace(/^\s*<!\[CDATA\[|\]\]>\s*$/g, "");
|
||||
var parsed = JSON.parse(content);
|
||||
if (
|
||||
!parsed["@context"] ||
|
||||
!parsed["@context"].match(/^https?\:\/\/schema\.org$/)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!parsed["@type"] && Array.isArray(parsed["@graph"])) {
|
||||
parsed = parsed["@graph"].find(function(it) {
|
||||
return (it["@type"] || "").match(
|
||||
this.REGEXPS.jsonLdArticleTypes
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!parsed ||
|
||||
!parsed["@type"] ||
|
||||
!parsed["@type"].match(this.REGEXPS.jsonLdArticleTypes)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
metadata = {};
|
||||
|
||||
if (typeof parsed.name === "string" && typeof parsed.headline === "string" && parsed.name !== parsed.headline) {
|
||||
// we have both name and headline element in the JSON-LD. They should both be the same but some websites like aktualne.cz
|
||||
// put their own name into "name" and the article title to "headline" which confuses Readability. So we try to check if either
|
||||
// "name" or "headline" closely matches the html title, and if so, use that one. If not, then we use "name" by default.
|
||||
|
||||
var title = this._getArticleTitle();
|
||||
var nameMatches = this._textSimilarity(parsed.name, title) > 0.75;
|
||||
var headlineMatches = this._textSimilarity(parsed.headline, title) > 0.75;
|
||||
|
||||
if (headlineMatches && !nameMatches) {
|
||||
metadata.title = parsed.headline;
|
||||
} else {
|
||||
metadata.title = parsed.name;
|
||||
}
|
||||
} else if (typeof parsed.name === "string") {
|
||||
metadata.title = parsed.name.trim();
|
||||
} else if (typeof parsed.headline === "string") {
|
||||
metadata.title = parsed.headline.trim();
|
||||
}
|
||||
if (parsed.author) {
|
||||
if (typeof parsed.author.name === "string") {
|
||||
metadata.byline = parsed.author.name.trim();
|
||||
} else if (Array.isArray(parsed.author) && parsed.author[0] && typeof parsed.author[0].name === "string") {
|
||||
metadata.byline = parsed.author
|
||||
.filter(function(author) {
|
||||
return author && typeof author.name === "string";
|
||||
})
|
||||
.map(function(author) {
|
||||
return author.name.trim();
|
||||
})
|
||||
.join(", ");
|
||||
}
|
||||
}
|
||||
if (typeof parsed.description === "string") {
|
||||
metadata.excerpt = parsed.description.trim();
|
||||
}
|
||||
if (
|
||||
parsed.publisher &&
|
||||
typeof parsed.publisher.name === "string"
|
||||
) {
|
||||
metadata.siteName = parsed.publisher.name.trim();
|
||||
}
|
||||
return;
|
||||
} catch (err) {
|
||||
this.log(err.message);
|
||||
}
|
||||
}
|
||||
var jsonLdElement = this._findNode(scripts, function(el) {
|
||||
return el.getAttribute("type") === "application/ld+json";
|
||||
});
|
||||
return metadata ? metadata : {};
|
||||
|
||||
if (jsonLdElement) {
|
||||
try {
|
||||
// Strip CDATA markers if present
|
||||
var content = jsonLdElement.textContent.replace(/^\s*<!\[CDATA\[|\]\]>\s*$/g, "");
|
||||
var parsed = JSON.parse(content);
|
||||
var metadata = {};
|
||||
if (
|
||||
!parsed["@context"] ||
|
||||
!parsed["@context"].match(/^https?\:\/\/schema\.org$/)
|
||||
) {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
if (!parsed["@type"] && Array.isArray(parsed["@graph"])) {
|
||||
parsed = parsed["@graph"].find(function(it) {
|
||||
return (it["@type"] || "").match(
|
||||
this.REGEXPS.jsonLdArticleTypes
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!parsed ||
|
||||
!parsed["@type"] ||
|
||||
!parsed["@type"].match(this.REGEXPS.jsonLdArticleTypes)
|
||||
) {
|
||||
return metadata;
|
||||
}
|
||||
if (typeof parsed.name === "string") {
|
||||
metadata.title = parsed.name.trim();
|
||||
} else if (typeof parsed.headline === "string") {
|
||||
metadata.title = parsed.headline.trim();
|
||||
}
|
||||
if (parsed.author) {
|
||||
if (typeof parsed.author.name === "string") {
|
||||
metadata.byline = parsed.author.name.trim();
|
||||
} else if (Array.isArray(parsed.author) && parsed.author[0] && typeof parsed.author[0].name === "string") {
|
||||
metadata.byline = parsed.author
|
||||
.filter(function(author) {
|
||||
return author && typeof author.name === "string";
|
||||
})
|
||||
.map(function(author) {
|
||||
return author.name.trim();
|
||||
})
|
||||
.join(", ");
|
||||
}
|
||||
}
|
||||
if (typeof parsed.description === "string") {
|
||||
metadata.excerpt = parsed.description.trim();
|
||||
}
|
||||
if (
|
||||
parsed.publisher &&
|
||||
typeof parsed.publisher.name === "string"
|
||||
) {
|
||||
metadata.siteName = parsed.publisher.name.trim();
|
||||
}
|
||||
return metadata;
|
||||
} catch (err) {
|
||||
this.log(err.message);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -1654,7 +1623,12 @@ function Readability(doc, options) {
|
||||
* @param Element
|
||||
**/
|
||||
_removeScripts: function(doc) {
|
||||
this._removeNodes(this._getAllNodesWithTag(doc, ["script", "noscript"]));
|
||||
this._removeNodes(this._getAllNodesWithTag(doc, ["script"]), function(scriptNode) {
|
||||
scriptNode.nodeValue = "";
|
||||
scriptNode.removeAttribute("src");
|
||||
return true;
|
||||
});
|
||||
this._removeNodes(this._getAllNodesWithTag(doc, ["noscript"]));
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -1844,13 +1818,13 @@ function Readability(doc, options) {
|
||||
if (isEmbed) {
|
||||
// First, check the elements attributes to see if any of them contain youtube or vimeo
|
||||
for (var i = 0; i < element.attributes.length; i++) {
|
||||
if (this._allowedVideoRegex.test(element.attributes[i].value)) {
|
||||
if (this.REGEXPS.videos.test(element.attributes[i].value)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// For embed with <object> tag, check inner HTML as well.
|
||||
if (element.tagName === "object" && this._allowedVideoRegex.test(element.innerHTML)) {
|
||||
if (element.tagName === "object" && this.REGEXPS.videos.test(element.innerHTML)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -2119,13 +2093,13 @@ function Readability(doc, options) {
|
||||
for (var i = 0; i < embeds.length; i++) {
|
||||
// If this embed has attribute that matches video regex, don't delete it.
|
||||
for (var j = 0; j < embeds[i].attributes.length; j++) {
|
||||
if (this._allowedVideoRegex.test(embeds[i].attributes[j].value)) {
|
||||
if (this.REGEXPS.videos.test(embeds[i].attributes[j].value)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// For embed with <object> tag, check inner HTML as well.
|
||||
if (embeds[i].tagName === "object" && this._allowedVideoRegex.test(embeds[i].innerHTML)) {
|
||||
if (embeds[i].tagName === "object" && this.REGEXPS.videos.test(embeds[i].innerHTML)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2143,21 +2117,6 @@ function Readability(doc, options) {
|
||||
(!isList && weight < 25 && linkDensity > 0.2) ||
|
||||
(weight >= 25 && linkDensity > 0.5) ||
|
||||
((embedCount === 1 && contentLength < 75) || embedCount > 1);
|
||||
// Allow simple lists of images to remain in pages
|
||||
if (isList && haveToRemove) {
|
||||
for (var x = 0; x < node.children.length; x++) {
|
||||
let child = node.children[x];
|
||||
// Don't filter in lists with li's that contain more than one child
|
||||
if (child.children.length > 1) {
|
||||
return haveToRemove;
|
||||
}
|
||||
}
|
||||
let li_count = node.getElementsByTagName("li").length;
|
||||
// Only allow the list to remain if every li contains an image
|
||||
if (img == li_count) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return haveToRemove;
|
||||
}
|
||||
return false;
|
||||
@@ -2290,7 +2249,6 @@ function Readability(doc, options) {
|
||||
title: this._articleTitle,
|
||||
byline: metadata.byline || this._articleByline,
|
||||
dir: this._articleDir,
|
||||
lang: this._articleLang,
|
||||
content: this._serializer(articleContent),
|
||||
textContent: textContent,
|
||||
length: textContent.length,
|
||||
@@ -2301,6 +2259,5 @@ function Readability(doc, options) {
|
||||
};
|
||||
|
||||
if (typeof module === "object") {
|
||||
/* global module */
|
||||
module.exports = Readability;
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
'use strict';
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
exports.getStyleSheets = exports.getImageSizes = void 0;
|
||||
function absoluteUrl(url) {
|
||||
if (!url) { return url; }
|
||||
const protocol = url.toLowerCase().split(':')[0];
|
||||
if (['http', 'https', 'file', 'data'].indexOf(protocol) >= 0) { return url; }
|
||||
if (url.indexOf('//') === 0) {
|
||||
return location.protocol + url;
|
||||
} else if (url[0] === '/') {
|
||||
return `${location.protocol}//${location.host}${url}`;
|
||||
} else {
|
||||
return `${baseUrl()}/${url}`;
|
||||
}
|
||||
}
|
||||
function pageLocationOrigin() {
|
||||
// location.origin normally returns the protocol + domain + port (eg. https://example.com:8080)
|
||||
// but for file:// protocol this is browser dependant and in particular Firefox returns "null"
|
||||
// in this case.
|
||||
if (location.protocol === 'file:') {
|
||||
return 'file://';
|
||||
} else {
|
||||
return location.origin;
|
||||
}
|
||||
}
|
||||
function baseUrl() {
|
||||
let output = pageLocationOrigin() + location.pathname;
|
||||
if (output[output.length - 1] !== '/') {
|
||||
const output2 = output.split('/');
|
||||
output2.pop();
|
||||
output = output2.join('/');
|
||||
}
|
||||
return output;
|
||||
}
|
||||
function getJoplinClipperSvgClassName(svg) {
|
||||
for (const className of svg.classList) {
|
||||
if (className.indexOf('joplin-clipper-svg-') === 0) { return className; }
|
||||
}
|
||||
return '';
|
||||
}
|
||||
function getImageSizes(element, forceAbsoluteUrls = false) {
|
||||
const output = {};
|
||||
const images = element.getElementsByTagName('img');
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
const img = images[i];
|
||||
if (img.classList && img.classList.contains('joplin-clipper-hidden')) { continue; }
|
||||
let src = imageSrc(img);
|
||||
src = forceAbsoluteUrls ? absoluteUrl(src) : src;
|
||||
if (!output[src]) { output[src] = []; }
|
||||
output[src].push({
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
naturalWidth: img.naturalWidth,
|
||||
naturalHeight: img.naturalHeight,
|
||||
});
|
||||
}
|
||||
const svgs = element.getElementsByTagName('svg');
|
||||
for (let i = 0; i < svgs.length; i++) {
|
||||
const svg = svgs[i];
|
||||
if (svg.classList && svg.classList.contains('joplin-clipper-hidden')) { continue; }
|
||||
const className = getJoplinClipperSvgClassName(svg); // 'joplin-clipper-svg-' + i;
|
||||
if (!className) {
|
||||
console.warn('SVG without a Joplin class:', svg);
|
||||
continue;
|
||||
}
|
||||
if (!svg.classList.contains(className)) {
|
||||
svg.classList.add(className);
|
||||
}
|
||||
const rect = svg.getBoundingClientRect();
|
||||
if (!output[className]) { output[className] = []; }
|
||||
output[className].push({
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
});
|
||||
}
|
||||
return output;
|
||||
}
|
||||
exports.getImageSizes = getImageSizes;
|
||||
// In general we should use currentSrc because that's the image that's currently displayed,
|
||||
// especially within <picture> tags or with srcset. In these cases there can be multiple
|
||||
// sources and the best one is probably the one being displayed, thus currentSrc.
|
||||
function imageSrc(image) {
|
||||
if (image.currentSrc) { return image.currentSrc; }
|
||||
return image.src;
|
||||
}
|
||||
// Given a document, return a <style> tag that contains all the styles
|
||||
// required to render the page. Not currently used but could be as an
|
||||
// option to clip pages as HTML.
|
||||
// eslint-disable-next-line
|
||||
function getStyleSheets(doc) {
|
||||
const output = [];
|
||||
for (let i = 0; i < doc.styleSheets.length; i++) {
|
||||
const sheet = doc.styleSheets[i];
|
||||
try {
|
||||
for (const cssRule of sheet.cssRules) {
|
||||
output.push({ type: 'text', value: cssRule.cssText });
|
||||
}
|
||||
} catch (error) {
|
||||
// Calling sheet.cssRules will throw a CORS error on Chrome if the stylesheet is on a different domain.
|
||||
// In that case, we skip it and add it to the list of stylesheet URLs. These URls will be downloaded
|
||||
// by the desktop application, since it doesn't have CORS restrictions.
|
||||
// eslint-disable-next-line
|
||||
console.info('Could not retrieve stylesheet now:', sheet.href);
|
||||
// eslint-disable-next-line
|
||||
console.info('It will downloaded by the main application.');
|
||||
// eslint-disable-next-line
|
||||
console.info(error);
|
||||
output.push({ type: 'url', value: sheet.href });
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
exports.getStyleSheets = getStyleSheets;
|
||||
// # sourceMappingURL=clipperUtils.js.map
|
||||
@@ -20,6 +20,20 @@
|
||||
browserSupportsPromises_ = false;
|
||||
}
|
||||
|
||||
function absoluteUrl(url) {
|
||||
if (!url) return url;
|
||||
const protocol = url.toLowerCase().split(':')[0];
|
||||
if (['http', 'https', 'file', 'data'].indexOf(protocol) >= 0) return url;
|
||||
|
||||
if (url.indexOf('//') === 0) {
|
||||
return location.protocol + url;
|
||||
} else if (url[0] === '/') {
|
||||
return `${location.protocol}//${location.host}${url}`;
|
||||
} else {
|
||||
return `${baseUrl()}/${url}`;
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return s
|
||||
.replace(/&/g, '&')
|
||||
@@ -35,6 +49,85 @@
|
||||
return document.title.trim();
|
||||
}
|
||||
|
||||
function pageLocationOrigin() {
|
||||
// location.origin normally returns the protocol + domain + port (eg. https://example.com:8080)
|
||||
// but for file:// protocol this is browser dependant and in particular Firefox returns "null"
|
||||
// in this case.
|
||||
|
||||
if (location.protocol === 'file:') {
|
||||
return 'file://';
|
||||
} else {
|
||||
return location.origin;
|
||||
}
|
||||
}
|
||||
|
||||
function baseUrl() {
|
||||
let output = pageLocationOrigin() + location.pathname;
|
||||
if (output[output.length - 1] !== '/') {
|
||||
output = output.split('/');
|
||||
output.pop();
|
||||
output = output.join('/');
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function getJoplinClipperSvgClassName(svg) {
|
||||
for (const className of svg.classList) {
|
||||
if (className.indexOf('joplin-clipper-svg-') === 0) return className;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function getImageSizes(element, forceAbsoluteUrls = false) {
|
||||
const output = {};
|
||||
|
||||
const images = element.getElementsByTagName('img');
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
const img = images[i];
|
||||
if (img.classList && img.classList.contains('joplin-clipper-hidden')) continue;
|
||||
|
||||
let src = imageSrc(img);
|
||||
src = forceAbsoluteUrls ? absoluteUrl(src) : src;
|
||||
|
||||
if (!output[src]) output[src] = [];
|
||||
|
||||
output[src].push({
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
naturalWidth: img.naturalWidth,
|
||||
naturalHeight: img.naturalHeight,
|
||||
});
|
||||
}
|
||||
|
||||
const svgs = element.getElementsByTagName('svg');
|
||||
for (let i = 0; i < svgs.length; i++) {
|
||||
const svg = svgs[i];
|
||||
if (svg.classList && svg.classList.contains('joplin-clipper-hidden')) continue;
|
||||
|
||||
const className = getJoplinClipperSvgClassName(svg);// 'joplin-clipper-svg-' + i;
|
||||
|
||||
if (!className) {
|
||||
console.warn('SVG without a Joplin class:', svg);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!svg.classList.contains(className)) {
|
||||
svg.classList.add(className);
|
||||
}
|
||||
|
||||
const rect = svg.getBoundingClientRect();
|
||||
|
||||
if (!output[className]) output[className] = [];
|
||||
|
||||
output[className].push({
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
});
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function getAnchorNames(element) {
|
||||
const output = [];
|
||||
// Anchor names are normally in A tags but can be in SPAN too
|
||||
@@ -53,6 +146,14 @@
|
||||
return output;
|
||||
}
|
||||
|
||||
// In general we should use currentSrc because that's the image that's currently displayed,
|
||||
// especially within <picture> tags or with srcset. In these cases there can be multiple
|
||||
// sources and the best one is probably the one being displayed, thus currentSrc.
|
||||
function imageSrc(image) {
|
||||
if (image.currentSrc) return image.currentSrc;
|
||||
return image.src;
|
||||
}
|
||||
|
||||
// Cleans up element by removing all its invisible children (which we don't want to render as Markdown)
|
||||
// And hard-code the image dimensions so that the information can be used by the clipper server to
|
||||
// display them at the right sizes in the notes.
|
||||
@@ -80,7 +181,6 @@
|
||||
}
|
||||
|
||||
if (nodeName === 'img') {
|
||||
// eslint-disable-next-line no-undef
|
||||
const src = absoluteUrl(imageSrc(node));
|
||||
node.setAttribute('src', src);
|
||||
if (!(src in imageIndexes)) imageIndexes[src] = 0;
|
||||
@@ -99,7 +199,6 @@
|
||||
}
|
||||
|
||||
if (nodeName === 'svg') {
|
||||
// eslint-disable-next-line no-undef
|
||||
const className = getJoplinClipperSvgClassName(node);
|
||||
if (!(className in imageIndexes)) imageIndexes[className] = 0;
|
||||
|
||||
@@ -117,13 +216,11 @@
|
||||
}
|
||||
|
||||
if (nodeName === 'embed') {
|
||||
// eslint-disable-next-line no-undef
|
||||
const src = absoluteUrl(node.src);
|
||||
node.setAttribute('src', src);
|
||||
}
|
||||
|
||||
if (nodeName === 'object') {
|
||||
// eslint-disable-next-line no-undef
|
||||
const data = absoluteUrl(node.data);
|
||||
node.setAttribute('data', data);
|
||||
}
|
||||
@@ -203,7 +300,6 @@
|
||||
let svgId = 0;
|
||||
|
||||
for (const svg of svgs) {
|
||||
// eslint-disable-next-line no-undef
|
||||
if (!getJoplinClipperSvgClassName(svg)) {
|
||||
svg.classList.add(`joplin-clipper-svg-${svgId}`);
|
||||
svgId++;
|
||||
@@ -211,6 +307,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Given a document, return a <style> tag that contains all the styles
|
||||
// required to render the page. Not currently used but could be as an
|
||||
// option to clip pages as HTML.
|
||||
function getStyleSheets(doc) {
|
||||
const output = [];
|
||||
for (let i = 0; i < doc.styleSheets.length; i++) {
|
||||
const sheet = doc.styleSheets[i];
|
||||
try {
|
||||
for (const cssRule of sheet.cssRules) {
|
||||
output.push({ type: 'text', value: cssRule.cssText });
|
||||
}
|
||||
} catch (error) {
|
||||
// Calling sheet.cssRules will throw a CORS error on Chrome if the stylesheet is on a different domain.
|
||||
// In that case, we skip it and add it to the list of stylesheet URLs. These URls will be downloaded
|
||||
// by the desktop application, since it doesn't have CORS restrictions.
|
||||
console.info('Could not retrieve stylesheet now:', sheet.href);
|
||||
console.info('It will downloaded by the main application.');
|
||||
console.info(error);
|
||||
output.push({ type: 'url', value: sheet.href });
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function documentForReadability() {
|
||||
// Readability directly change the passed document so clone it so as
|
||||
// to preserve the original web page.
|
||||
@@ -252,9 +372,7 @@
|
||||
name: shouldSendToJoplin ? 'sendContentToJoplin' : 'clippedContent',
|
||||
title: title,
|
||||
html: html,
|
||||
// eslint-disable-next-line no-undef
|
||||
base_url: baseUrl(),
|
||||
// eslint-disable-next-line no-undef
|
||||
url: pageLocationOrigin() + location.pathname + location.search,
|
||||
parent_id: command.parent_id,
|
||||
tags: command.tags || '',
|
||||
@@ -279,7 +397,6 @@
|
||||
response.warning = 'Could not retrieve simplified version of page - full page has been saved instead.';
|
||||
return response;
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
return clippedContentResponse(article.title, article.body, getImageSizes(document), getAnchorNames(document));
|
||||
|
||||
} else if (command.name === 'isProbablyReaderable') {
|
||||
@@ -291,7 +408,6 @@
|
||||
} else if (command.name === 'completePageHtml') {
|
||||
|
||||
if (isPagePdf()) {
|
||||
// eslint-disable-next-line no-undef
|
||||
return clippedContentResponse(pageTitle(), embedPageUrl(), getImageSizes(document), getAnchorNames(document));
|
||||
}
|
||||
|
||||
@@ -301,34 +417,11 @@
|
||||
// Because cleanUpElement is going to modify the DOM and remove elements we don't want to work
|
||||
// directly on the document, so we make a copy of it first.
|
||||
const cleanDocument = document.body.cloneNode(true);
|
||||
// eslint-disable-next-line no-undef
|
||||
const imageSizes = getImageSizes(document, true);
|
||||
const imageIndexes = {};
|
||||
cleanUpElement(convertToMarkup, cleanDocument, imageSizes, imageIndexes);
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const stylesheets = convertToMarkup === 'html' ? getStyleSheets(document) : null;
|
||||
|
||||
// The <BODY> tag may have a style in the CSS stylesheets. This
|
||||
// style can be overriden by setting the `style` attribute on the
|
||||
// BODY tag. Since we don't keep the body tag, it means we may be
|
||||
// missing some styling, which may break the page.
|
||||
//
|
||||
// For example, on this page:
|
||||
// https://devblogs.microsoft.com/oldnewthing/20180529-00/?p=98855
|
||||
// The BODY tag has visibility set to hidden in the stylesheet, and
|
||||
// made visible by setting the style attribute. Because of that,
|
||||
// previously that imported note would show blank content, while now
|
||||
// it will be visible.
|
||||
//
|
||||
// Fixes https://github.com/laurent22/joplin/issues/7925
|
||||
if (document.body.getAttribute('style')) {
|
||||
stylesheets.push({
|
||||
type: 'text',
|
||||
value: `body { ${document.body.getAttribute('style')} }`,
|
||||
});
|
||||
}
|
||||
|
||||
return clippedContentResponse(pageTitle(), cleanDocument.innerHTML, imageSizes, getAnchorNames(document), stylesheets);
|
||||
|
||||
} else if (command.name === 'selectedHtml') {
|
||||
@@ -348,11 +441,9 @@
|
||||
container.appendChild(range.cloneContents());
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const imageSizes = getImageSizes(document, true);
|
||||
const imageIndexes = {};
|
||||
cleanUpElement(convertToMarkup, container, imageSizes, imageIndexes);
|
||||
// eslint-disable-next-line no-undef
|
||||
return clippedContentResponse(pageTitle(), container.innerHTML, getImageSizes(document), getAnchorNames(document));
|
||||
|
||||
} else if (command.name === 'screenshot') {
|
||||
@@ -455,7 +546,6 @@
|
||||
const content = {
|
||||
title: pageTitle(),
|
||||
crop_rect: selectionArea,
|
||||
// eslint-disable-next-line no-undef
|
||||
url: pageLocationOrigin() + location.pathname + location.search,
|
||||
parent_id: command.parent_id,
|
||||
tags: command.tags,
|
||||
@@ -480,9 +570,7 @@
|
||||
|
||||
} else if (command.name === 'pageUrl') {
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const url = pageLocationOrigin() + location.pathname + location.search;
|
||||
// eslint-disable-next-line no-undef
|
||||
return clippedContentResponse(pageTitle(), url, getImageSizes(document), getAnchorNames(document));
|
||||
|
||||
} else {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Joplin Web Clipper [DEV]",
|
||||
"version": "2.13.1",
|
||||
"version": "2.13.0",
|
||||
"description": "Capture and save web pages and screenshots from your browser to Joplin.",
|
||||
"homepage_url": "https://joplinapp.org",
|
||||
"content_security_policy": "script-src 'self'; object-src 'self'",
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
const fs = require('fs-extra');
|
||||
|
||||
const sourcePath = `${__dirname}/../../lib/randomClipperPort.js`;
|
||||
const clipperUtilsPath = `${__dirname}/../../lib/clipperUtils.js`;
|
||||
|
||||
|
||||
// Mozilla insists on building the clipper from a tarball, not from the repository
|
||||
// so we add this check and only copy the file if it's present. Normally it rarely
|
||||
@@ -12,10 +10,6 @@ if (fs.pathExistsSync(sourcePath)) {
|
||||
fs.copySync(sourcePath, `${__dirname}/src/randomClipperPort.js`);
|
||||
}
|
||||
|
||||
if (fs.pathExistsSync(clipperUtilsPath)) {
|
||||
fs.copySync(clipperUtilsPath, `${__dirname}/../content_scripts/clipperUtils.js`);
|
||||
}
|
||||
|
||||
// These files give warnings when loading the extension in Chrome, in dev mode
|
||||
fs.removeSync(`${__dirname}/node_modules/public-encrypt/test/test_key.pem`);
|
||||
fs.removeSync(`${__dirname}/node_modules/public-encrypt/test/test_rsa_pubkey.pem`);
|
||||
|
||||
@@ -182,7 +182,6 @@ class AppComponent extends Component {
|
||||
await bridge().tabsExecuteScript({ file: '/content_scripts/JSDOMParser.js' });
|
||||
await bridge().tabsExecuteScript({ file: '/content_scripts/Readability.js' });
|
||||
await bridge().tabsExecuteScript({ file: '/content_scripts/Readability-readerable.js' });
|
||||
await bridge().tabsExecuteScript({ file: '/content_scripts/clipperUtils.js' });
|
||||
await bridge().tabsExecuteScript({ file: '/content_scripts/index.js' });
|
||||
}
|
||||
|
||||
|
||||
4
packages/app-desktop/.gitignore
vendored
4
packages/app-desktop/.gitignore
vendored
@@ -14,7 +14,3 @@ style.min.css
|
||||
build/lib/
|
||||
vendor/*
|
||||
!vendor/loadEmojiLib.js
|
||||
test-results/
|
||||
playwright-report/
|
||||
playwright/.cache/
|
||||
integration-tests/test-profile/
|
||||
|
||||
@@ -4,7 +4,6 @@ import shim from '@joplin/lib/shim';
|
||||
import { isCallbackUrl } from '@joplin/lib/callbackUrlUtils';
|
||||
|
||||
import { BrowserWindow, Tray, screen } from 'electron';
|
||||
import bridge from './bridge';
|
||||
const url = require('url');
|
||||
const path = require('path');
|
||||
const { dirname } = require('@joplin/lib/path-utils');
|
||||
@@ -143,15 +142,6 @@ export default class ElectronAppWrapper {
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// will-frame-navigate is fired by clicking on a link within the BrowserWindow.
|
||||
this.win_.webContents.on('will-frame-navigate', event => {
|
||||
// If the link changes the URL of the browser window,
|
||||
if (event.isMainFrame) {
|
||||
event.preventDefault();
|
||||
void bridge().openExternal(event.url);
|
||||
}
|
||||
});
|
||||
|
||||
this.win_.on('close', (event: any) => {
|
||||
// If it's on macOS, the app is completely closed only if the user chooses to close the app (willQuitApp_ will be true)
|
||||
// otherwise the window is simply hidden, and will be re-open once the app is "activated" (which happens when the
|
||||
|
||||
@@ -67,7 +67,6 @@ import eventManager from '@joplin/lib/eventManager';
|
||||
import path = require('path');
|
||||
import { checkPreInstalledDefaultPlugins, installDefaultPlugins, setSettingsForDefaultPlugins } from '@joplin/lib/services/plugins/defaultPlugins/defaultPluginsUtils';
|
||||
import userFetcher, { initializeUserFetcher } from '@joplin/lib/utils/userFetcher';
|
||||
import { parseNotesParent } from '@joplin/lib/reducer';
|
||||
|
||||
const pluginClasses = [
|
||||
require('./plugins/GotoAnything').default,
|
||||
@@ -193,7 +192,7 @@ class Application extends BaseApplication {
|
||||
// The '*' and '!important' parts are necessary to make sure Russian text is displayed properly
|
||||
// https://github.com/laurent22/joplin/issues/155
|
||||
|
||||
const css = `.CodeMirror *, .cm-editor .cm-content { font-family: ${fontFamilies.join(', ')} !important; }`;
|
||||
const css = `.CodeMirror * { font-family: ${fontFamilies.join(', ')} !important; }`;
|
||||
const styleTag = document.createElement('style');
|
||||
styleTag.type = 'text/css';
|
||||
styleTag.appendChild(document.createTextNode(css));
|
||||
@@ -448,35 +447,10 @@ class Application extends BaseApplication {
|
||||
// items: masterKeys,
|
||||
// });
|
||||
|
||||
const getNotesParent = async () => {
|
||||
let notesParent = parseNotesParent(Setting.value('notesParent'), Setting.value('activeFolderId'));
|
||||
if (notesParent.type === 'Tag' && !(await Tag.load(notesParent.selectedItemId))) {
|
||||
notesParent = {
|
||||
type: 'Folder',
|
||||
selectedItemId: Setting.value('activeFolderId'),
|
||||
};
|
||||
}
|
||||
return notesParent;
|
||||
};
|
||||
|
||||
const notesParent = await getNotesParent();
|
||||
|
||||
if (notesParent.type === 'SmartFilter') {
|
||||
this.store().dispatch({
|
||||
type: 'SMART_FILTER_SELECT',
|
||||
id: notesParent.selectedItemId,
|
||||
});
|
||||
} else if (notesParent.type === 'Tag') {
|
||||
this.store().dispatch({
|
||||
type: 'TAG_SELECT',
|
||||
id: notesParent.selectedItemId,
|
||||
});
|
||||
} else {
|
||||
this.store().dispatch({
|
||||
type: 'FOLDER_SELECT',
|
||||
id: notesParent.selectedItemId,
|
||||
});
|
||||
}
|
||||
this.store().dispatch({
|
||||
type: 'FOLDER_SELECT',
|
||||
id: Setting.value('activeFolderId'),
|
||||
});
|
||||
|
||||
this.store().dispatch({
|
||||
type: 'FOLDER_SET_COLLAPSED_ALL',
|
||||
|
||||
@@ -6,7 +6,7 @@ import KvStore from '@joplin/lib/services/KvStore';
|
||||
import * as ArrayUtils from '@joplin/lib/ArrayUtils';
|
||||
import { CheckForUpdateOptions, extractVersionInfo, GitHubRelease } from './utils/checkForUpdatesUtils';
|
||||
const packageInfo = require('./packageInfo.js');
|
||||
import { compareVersions } from 'compare-versions';
|
||||
const compareVersions = require('compare-versions');
|
||||
|
||||
const logger = Logger.create('checkForUpdates');
|
||||
|
||||
|
||||
@@ -260,7 +260,7 @@ const EncryptionConfigScreen = (props: Props) => {
|
||||
<br/>
|
||||
<MacOSMissingPasswordHelpLink
|
||||
theme={theme}
|
||||
text={_('%s: Missing password.', _('Help'))}
|
||||
text={_('%s: Missing password', _('Help'))}
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
|
||||
@@ -79,7 +79,6 @@ interface Props {
|
||||
startupPluginsLoaded: boolean;
|
||||
shareInvitations: ShareInvitation[];
|
||||
isSafeMode: boolean;
|
||||
enableBetaMarkdownEditor: boolean;
|
||||
needApiAuth: boolean;
|
||||
processingShareInvitationResponse: boolean;
|
||||
isResettingLayout: boolean;
|
||||
@@ -738,13 +737,7 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
},
|
||||
|
||||
editor: () => {
|
||||
let bodyEditor = this.props.settingEditorCodeView ? 'CodeMirror' : 'TinyMCE';
|
||||
|
||||
if (this.props.isSafeMode) {
|
||||
bodyEditor = 'PlainText';
|
||||
} else if (this.props.settingEditorCodeView && this.props.enableBetaMarkdownEditor) {
|
||||
bodyEditor = 'CodeMirror6';
|
||||
}
|
||||
const bodyEditor = this.props.settingEditorCodeView ? 'CodeMirror' : 'TinyMCE';
|
||||
return <NoteEditor key={key} bodyEditor={bodyEditor} />;
|
||||
},
|
||||
};
|
||||
@@ -916,7 +909,6 @@ const mapStateToProps = (state: AppState) => {
|
||||
shareInvitations: state.shareService.shareInvitations,
|
||||
processingShareInvitationResponse: state.shareService.processingShareInvitationResponse,
|
||||
isSafeMode: state.settings.isSafeMode,
|
||||
enableBetaMarkdownEditor: state.settings['editor.beta'],
|
||||
needApiAuth: state.needApiAuth,
|
||||
showInstallTemplatesPlugin: state.hasLegacyTemplates && !state.pluginService.plugins['joplin.plugin.templates'],
|
||||
isResettingLayout: state.isResettingLayout,
|
||||
|
||||
@@ -817,7 +817,6 @@ function useMenu(props: Props) {
|
||||
menuItemDic.setTags,
|
||||
menuItemDic.showShareNoteDialog,
|
||||
separator(),
|
||||
menuItemDic.showNoteProperties,
|
||||
menuItemDic.showNoteContentProperties,
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,45 +1,55 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect, useRef, forwardRef, useCallback, useImperativeHandle, useMemo, ForwardedRef } from 'react';
|
||||
import { useState, useEffect, useRef, forwardRef, useCallback, useImperativeHandle, useMemo } from 'react';
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { EditorCommand, NoteBodyEditorProps, NoteBodyEditorRef } from '../../../utils/types';
|
||||
import { commandAttachFileToBody, getResourcesFromPasteEvent } from '../../../utils/resourceHandling';
|
||||
import { ScrollOptions, ScrollOptionTypes } from '../../../utils/types';
|
||||
import { CommandValue } from '../../../utils/types';
|
||||
import { usePrevious, cursorPositionToTextOffset } from '../utils';
|
||||
import useScrollHandler from '../utils/useScrollHandler';
|
||||
import { EditorCommand, NoteBodyEditorProps } from '../../utils/types';
|
||||
import { commandAttachFileToBody, getResourcesFromPasteEvent } from '../../utils/resourceHandling';
|
||||
import { ScrollOptions, ScrollOptionTypes } from '../../utils/types';
|
||||
import { CommandValue } from '../../utils/types';
|
||||
import { usePrevious, cursorPositionToTextOffset } from './utils';
|
||||
import useScrollHandler from './utils/useScrollHandler';
|
||||
import useElementSize from '@joplin/lib/hooks/useElementSize';
|
||||
import Toolbar from '../Toolbar';
|
||||
import { RenderedBody, defaultRenderedBody } from '../utils/types';
|
||||
import NoteTextViewer from '../../../../NoteTextViewer';
|
||||
import Toolbar from './Toolbar';
|
||||
import styles_ from './styles';
|
||||
import { RenderedBody, defaultRenderedBody } from './utils/types';
|
||||
import NoteTextViewer from '../../../NoteTextViewer';
|
||||
import Editor from './Editor';
|
||||
import usePluginServiceRegistration from '../../../utils/usePluginServiceRegistration';
|
||||
import usePluginServiceRegistration from '../../utils/usePluginServiceRegistration';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import bridge from '../../../../../services/bridge';
|
||||
import bridge from '../../../../services/bridge';
|
||||
import markdownUtils from '@joplin/lib/markdownUtils';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { MenuItemLocation } from '@joplin/lib/services/plugins/api/types';
|
||||
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import { ThemeAppearance } from '@joplin/lib/themes/type';
|
||||
import dialogs from '../../../../dialogs';
|
||||
import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService';
|
||||
import dialogs from '../../../dialogs';
|
||||
import convertToScreenCoordinates from '../../../utils/convertToScreenCoordinates';
|
||||
import { MarkupToHtml } from '@joplin/renderer';
|
||||
const { clipboard } = require('electron');
|
||||
const debounce = require('debounce');
|
||||
|
||||
import shared from '@joplin/lib/components/shared/note-screen-shared';
|
||||
const Menu = bridge().Menu;
|
||||
const MenuItem = bridge().MenuItem;
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import ErrorBoundary from '../../../../ErrorBoundary';
|
||||
import { MarkupToHtmlOptions } from '../../../utils/useMarkupToHtml';
|
||||
import useStyles from '../utils/useStyles';
|
||||
import useContextMenu from '../utils/useContextMenu';
|
||||
import useWebviewIpcMessage from '../utils/useWebviewIpcMessage';
|
||||
import ErrorBoundary from '../../../ErrorBoundary';
|
||||
import { MarkupToHtmlOptions } from '../../utils/useMarkupToHtml';
|
||||
import eventManager from '@joplin/lib/eventManager';
|
||||
import { EditContextMenuFilterObject } from '@joplin/lib/services/plugins/api/JoplinWorkspace';
|
||||
import type { ContextMenuEvent, ContextMenuParams } from 'electron';
|
||||
|
||||
const menuUtils = new MenuUtils(CommandService.instance());
|
||||
|
||||
function markupRenderOptions(override: MarkupToHtmlOptions = null): MarkupToHtmlOptions {
|
||||
return { ...override };
|
||||
}
|
||||
|
||||
function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditorRef>) {
|
||||
const styles = useStyles(props);
|
||||
function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
const styles = styles_(props);
|
||||
|
||||
const [renderedBody, setRenderedBody] = useState<RenderedBody>(defaultRenderedBody()); // Viewer content
|
||||
const [renderedBodyContentKey, setRenderedBodyContentKey] = useState<string>(null);
|
||||
@@ -589,13 +599,29 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
setWebviewReady(true);
|
||||
}, []);
|
||||
|
||||
const webview_ipcMessage = useWebviewIpcMessage({
|
||||
editorRef,
|
||||
setEditorPercentScroll,
|
||||
getLineScrollPercent,
|
||||
content: props.content,
|
||||
onMessage: props.onMessage,
|
||||
});
|
||||
const webview_ipcMessage = useCallback((event: any) => {
|
||||
const msg = event.channel ? event.channel : '';
|
||||
const args = event.args;
|
||||
const arg0 = args && args.length >= 1 ? args[0] : null;
|
||||
|
||||
if (msg.indexOf('checkboxclick:') === 0) {
|
||||
const { line, from, to } = shared.toggleCheckboxRange(msg, props.content);
|
||||
if (editorRef.current) {
|
||||
// To cancel CodeMirror's layout drift, the scroll position
|
||||
// is recorded before updated, and then it is restored.
|
||||
// Ref. https://github.com/laurent22/joplin/issues/5890
|
||||
const percent = getLineScrollPercent();
|
||||
editorRef.current.replaceRange(line, from, to);
|
||||
setEditorPercentScroll(percent);
|
||||
}
|
||||
} else if (msg === 'percentScroll') {
|
||||
const percent = arg0;
|
||||
setEditorPercentScroll(percent);
|
||||
} else {
|
||||
props.onMessage(event);
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [props.onMessage, props.content, setEditorPercentScroll]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -724,29 +750,19 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
return output;
|
||||
}, [styles.cellViewer, props.visiblePanes]);
|
||||
|
||||
// Disable this effect to fix this:
|
||||
//
|
||||
// https://github.com/laurent22/joplin/issues/6514 It doesn't seem essential
|
||||
// to automatically focus the editor when the layout changes. The workaround
|
||||
// is to toggle the layout Cmd+L, then manually focus the editor Cmd+Shift+B.
|
||||
//
|
||||
// On the other hand, if we automatically focus the editor, and the user
|
||||
// does not want this, there's no workaround, so it's better to have this
|
||||
// disabled.
|
||||
const editorPaneVisible = props.visiblePanes.indexOf('editor') >= 0;
|
||||
|
||||
// const editorPaneVisible = props.visiblePanes.indexOf('editor') >= 0;
|
||||
useEffect(() => {
|
||||
if (!editorRef.current) return;
|
||||
|
||||
// useEffect(() => {
|
||||
// if (!editorRef.current) return;
|
||||
|
||||
// // Anytime the user toggles the visible panes AND the editor is visible as a result
|
||||
// // we should focus the editor
|
||||
// // The intuition is that a panel toggle (with editor in view) is the equivalent of
|
||||
// // an editor interaction so users should expect the editor to be focused
|
||||
// if (editorPaneVisible) {
|
||||
// editorRef.current.focus();
|
||||
// }
|
||||
// }, [editorPaneVisible]);
|
||||
// Anytime the user toggles the visible panes AND the editor is visible as a result
|
||||
// we should focus the editor
|
||||
// The intuition is that a panel toggle (with editor in view) is the equivalent of
|
||||
// an editor interaction so users should expect the editor to be focused
|
||||
if (editorPaneVisible) {
|
||||
editorRef.current.focus();
|
||||
}
|
||||
}, [editorPaneVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editorRef.current) return;
|
||||
@@ -763,12 +779,142 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
editorRef.current.refresh();
|
||||
}, [rootSize, styles.editor, props.visiblePanes]);
|
||||
|
||||
useContextMenu({
|
||||
plugins: props.plugins,
|
||||
editorCutText, editorCopyText, editorPaste,
|
||||
editorRef,
|
||||
editorClassName: 'codeMirrorEditor',
|
||||
});
|
||||
// The below code adds support for spellchecking when it is enabled
|
||||
// It might be buggy, refer to the below issue
|
||||
// https://github.com/laurent22/joplin/pull/3974#issuecomment-718936703
|
||||
useEffect(() => {
|
||||
const isAncestorOfCodeMirrorEditor = (elem: HTMLElement) => {
|
||||
for (; elem.parentElement; elem = elem.parentElement) {
|
||||
if (elem.classList.contains('codeMirrorEditor')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
let lastInCodeMirrorContextMenuTimestamp = 0;
|
||||
|
||||
// The browser's contextmenu event provides additional information about the
|
||||
// target of the event, not provided by the Electron context-menu event.
|
||||
const onBrowserContextMenu = (event: Event) => {
|
||||
if (isAncestorOfCodeMirrorEditor(event.target as HTMLElement)) {
|
||||
lastInCodeMirrorContextMenuTimestamp = Date.now();
|
||||
}
|
||||
};
|
||||
|
||||
function pointerInsideEditor(params: ContextMenuParams) {
|
||||
const x = params.x, y = params.y, isEditable = params.isEditable;
|
||||
const elements = document.getElementsByClassName('codeMirrorEditor');
|
||||
|
||||
// Note: We can't check inputFieldType here. When spellcheck is enabled,
|
||||
// params.inputFieldType is "none". When spellcheck is disabled,
|
||||
// params.inputFieldType is "plainText". Thus, such a check would be inconsistent.
|
||||
if (!elements.length || !isEditable) return false;
|
||||
|
||||
const maximumMsSinceBrowserEvent = 100;
|
||||
if (Date.now() - lastInCodeMirrorContextMenuTimestamp > maximumMsSinceBrowserEvent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const rect = convertToScreenCoordinates(Setting.value('windowContentZoomFactor'), elements[0].getBoundingClientRect());
|
||||
return rect.x < x && rect.y < y && rect.right > x && rect.bottom > y;
|
||||
}
|
||||
|
||||
async function onContextMenu(event: ContextMenuEvent, params: ContextMenuParams) {
|
||||
if (!pointerInsideEditor(params)) return;
|
||||
|
||||
// Don't show the default menu.
|
||||
event.preventDefault();
|
||||
|
||||
const menu = new Menu();
|
||||
|
||||
const hasSelectedText = editorRef.current && !!editorRef.current.getSelection() ;
|
||||
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: _('Cut'),
|
||||
enabled: hasSelectedText,
|
||||
click: async () => {
|
||||
editorCutText();
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: _('Copy'),
|
||||
enabled: hasSelectedText,
|
||||
click: async () => {
|
||||
editorCopyText();
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: _('Paste'),
|
||||
enabled: true,
|
||||
click: async () => {
|
||||
editorPaste();
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const spellCheckerMenuItems = SpellCheckerService.instance().contextMenuItems(params.misspelledWord, params.dictionarySuggestions);
|
||||
|
||||
for (const item of spellCheckerMenuItems) {
|
||||
menu.append(new MenuItem(item));
|
||||
}
|
||||
|
||||
// Typically CodeMirror handles all interactions itself (highlighting etc.)
|
||||
// But in the case of clicking a mispelled word, we need electron to handle the click
|
||||
// The result is that CodeMirror doesn't know what's been selected and doesn't
|
||||
// move the cursor into the correct location.
|
||||
// and when the user selects a new spelling it will be inserted in the wrong location
|
||||
// So in this situation, we use must manually align the internal codemirror selection
|
||||
// to the contextmenu selection
|
||||
if (editorRef.current && spellCheckerMenuItems.length > 0) {
|
||||
editorRef.current.alignSelection(params);
|
||||
}
|
||||
|
||||
let filterObject: EditContextMenuFilterObject = {
|
||||
items: [],
|
||||
};
|
||||
|
||||
filterObject = await eventManager.filterEmit('editorContextMenu', filterObject);
|
||||
|
||||
for (const item of filterObject.items) {
|
||||
menu.append(new MenuItem({
|
||||
label: item.label,
|
||||
click: async () => {
|
||||
const args = item.commandArgs || [];
|
||||
void CommandService.instance().execute(item.commandName, ...args);
|
||||
},
|
||||
type: item.type,
|
||||
}));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line github/array-foreach -- Old code before rule was applied
|
||||
menuUtils.pluginContextMenuItems(props.plugins, MenuItemLocation.EditorContextMenu).forEach((item: any) => {
|
||||
menu.append(new MenuItem(item));
|
||||
});
|
||||
|
||||
menu.popup();
|
||||
}
|
||||
|
||||
// Prepend the event listener so that it gets called before
|
||||
// the listener that shows the default menu.
|
||||
bridge().window().webContents.prependListener('context-menu', onContextMenu);
|
||||
|
||||
window.addEventListener('contextmenu', onBrowserContextMenu);
|
||||
|
||||
return () => {
|
||||
bridge().window().webContents.off('context-menu', onContextMenu);
|
||||
window.removeEventListener('contextmenu', onBrowserContextMenu);
|
||||
};
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [props.plugins]);
|
||||
|
||||
function renderEditor() {
|
||||
|
||||
@@ -12,15 +12,15 @@ import 'codemirror/addon/scroll/annotatescrollbar';
|
||||
import 'codemirror/addon/search/matchesonscrollbar';
|
||||
import 'codemirror/addon/search/searchcursor';
|
||||
|
||||
import useListIdent from '../utils/useListIdent';
|
||||
import useScrollUtils from '../utils/useScrollUtils';
|
||||
import useCursorUtils from '../utils/useCursorUtils';
|
||||
import useLineSorting from '../utils/useLineSorting';
|
||||
import useEditorSearch from '../utils/useEditorSearch';
|
||||
import useJoplinMode from '../utils/useJoplinMode';
|
||||
import useKeymap from '../utils/useKeymap';
|
||||
import useExternalPlugins from '../utils/useExternalPlugins';
|
||||
import useJoplinCommands from '../utils/useJoplinCommands';
|
||||
import useListIdent from './utils/useListIdent';
|
||||
import useScrollUtils from './utils/useScrollUtils';
|
||||
import useCursorUtils from './utils/useCursorUtils';
|
||||
import useLineSorting from './utils/useLineSorting';
|
||||
import useEditorSearch from './utils/useEditorSearch';
|
||||
import useJoplinMode from './utils/useJoplinMode';
|
||||
import useKeymap from './utils/useKeymap';
|
||||
import useExternalPlugins from './utils/useExternalPlugins';
|
||||
import useJoplinCommands from './utils/useJoplinCommands';
|
||||
|
||||
import 'codemirror/keymap/emacs';
|
||||
import 'codemirror/keymap/vim';
|
||||
@@ -0,0 +1,60 @@
|
||||
import { NoteBodyEditorProps } from '../../../utils/types';
|
||||
const { buildStyle } = require('@joplin/lib/theme');
|
||||
|
||||
export default function styles(props: NoteBodyEditorProps) {
|
||||
return buildStyle(['CodeMirror', props.fontSize], props.themeId, (theme: any) => {
|
||||
return {
|
||||
root: {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
...props.style,
|
||||
},
|
||||
rowToolbar: {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
rowEditorViewer: {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flex: 1,
|
||||
paddingTop: 10,
|
||||
},
|
||||
cellEditor: {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
},
|
||||
cellViewer: {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: theme.dividerColor,
|
||||
borderLeftStyle: 'solid',
|
||||
},
|
||||
viewer: {
|
||||
display: 'flex',
|
||||
overflow: 'hidden',
|
||||
verticalAlign: 'top',
|
||||
boxSizing: 'border-box',
|
||||
width: '100%',
|
||||
},
|
||||
editor: {
|
||||
display: 'flex',
|
||||
width: 'auto',
|
||||
height: 'auto',
|
||||
flex: 1,
|
||||
overflowY: 'hidden',
|
||||
paddingTop: 0,
|
||||
lineHeight: `${Math.round(17 * props.fontSize / 12)}px`,
|
||||
fontSize: `${props.fontSize}px`,
|
||||
color: theme.color,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
codeMirrorTheme: theme.codeMirrorTheme, // Defined in theme.js
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
|
||||
|
||||
const setupVim = (CodeMirror: CodeMirrorControl) => {
|
||||
CodeMirror.Vim.defineAction('swapLineDown', CodeMirror.commands.swapLineDown);
|
||||
CodeMirror.Vim.mapCommand('<A-j>', 'action', 'swapLineDown', {}, { context: 'normal', isEdit: true });
|
||||
CodeMirror.Vim.defineAction('swapLineUp', CodeMirror.commands.swapLineUp);
|
||||
CodeMirror.Vim.mapCommand('<A-k>', 'action', 'swapLineUp', {}, { context: 'normal', isEdit: true });
|
||||
CodeMirror.Vim.defineAction('insertListElement', CodeMirror.commands.vimInsertListElement);
|
||||
CodeMirror.Vim.mapCommand('o', 'action', 'insertListElement', { after: true }, { context: 'normal', isEdit: true, interlaceInsertRepeat: true });
|
||||
};
|
||||
|
||||
export default setupVim;
|
||||
@@ -1,168 +0,0 @@
|
||||
|
||||
import { ContextMenuEvent, ContextMenuParams } from 'electron';
|
||||
import { useEffect, RefObject } from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import { MenuItemLocation } from '@joplin/lib/services/plugins/api/types';
|
||||
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService';
|
||||
import { EditContextMenuFilterObject } from '@joplin/lib/services/plugins/api/JoplinWorkspace';
|
||||
import type CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
|
||||
import eventManager from '@joplin/lib/eventManager';
|
||||
import bridge from '../../../../../services/bridge';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
const Menu = bridge().Menu;
|
||||
const MenuItem = bridge().MenuItem;
|
||||
const menuUtils = new MenuUtils(CommandService.instance());
|
||||
|
||||
|
||||
interface ContextMenuProps {
|
||||
plugins: PluginStates;
|
||||
editorCutText: ()=> void;
|
||||
editorCopyText: ()=> void;
|
||||
editorPaste: ()=> void;
|
||||
editorRef: RefObject<CodeMirrorControl>;
|
||||
editorClassName: string;
|
||||
}
|
||||
|
||||
const useContextMenu = (props: ContextMenuProps) => {
|
||||
const editorRef = props.editorRef;
|
||||
|
||||
// The below code adds support for spellchecking when it is enabled
|
||||
// It might be buggy, refer to the below issue
|
||||
// https://github.com/laurent22/joplin/pull/3974#issuecomment-718936703
|
||||
useEffect(() => {
|
||||
const isAncestorOfCodeMirrorEditor = (elem: Element) => {
|
||||
for (; elem.parentElement; elem = elem.parentElement) {
|
||||
if (elem.classList.contains(props.editorClassName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const convertFromScreenCoordinates = (zoomPercent: number, screenXY: number) => {
|
||||
const zoomFraction = zoomPercent / 100;
|
||||
return screenXY / zoomFraction;
|
||||
};
|
||||
|
||||
function pointerInsideEditor(params: ContextMenuParams) {
|
||||
const x = params.x, y = params.y, isEditable = params.isEditable;
|
||||
const elements = document.getElementsByClassName(props.editorClassName);
|
||||
|
||||
// Note: We can't check inputFieldType here. When spellcheck is enabled,
|
||||
// params.inputFieldType is "none". When spellcheck is disabled,
|
||||
// params.inputFieldType is "plainText". Thus, such a check would be inconsistent.
|
||||
if (!elements.length || !isEditable) return false;
|
||||
|
||||
// Checks whether the element the pointer clicked on is inside the editor.
|
||||
// This logic will need to be changed if the editor is eventually wrapped
|
||||
// in an iframe, as elementFromPoint will return the iframe container (and not
|
||||
// a child of the editor).
|
||||
const zoom = Setting.value('windowContentZoomFactor');
|
||||
const xScreen = convertFromScreenCoordinates(zoom, x);
|
||||
const yScreen = convertFromScreenCoordinates(zoom, y);
|
||||
const intersectingElement = document.elementFromPoint(xScreen, yScreen);
|
||||
return intersectingElement && isAncestorOfCodeMirrorEditor(intersectingElement);
|
||||
}
|
||||
|
||||
async function onContextMenu(event: ContextMenuEvent, params: ContextMenuParams) {
|
||||
if (!pointerInsideEditor(params)) return;
|
||||
|
||||
// Don't show the default menu.
|
||||
event.preventDefault();
|
||||
|
||||
const menu = new Menu();
|
||||
|
||||
const hasSelectedText = editorRef.current && !!editorRef.current.getSelection() ;
|
||||
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: _('Cut'),
|
||||
enabled: hasSelectedText,
|
||||
click: async () => {
|
||||
props.editorCutText();
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: _('Copy'),
|
||||
enabled: hasSelectedText,
|
||||
click: async () => {
|
||||
props.editorCopyText();
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: _('Paste'),
|
||||
enabled: true,
|
||||
click: async () => {
|
||||
props.editorPaste();
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const spellCheckerMenuItems = SpellCheckerService.instance().contextMenuItems(params.misspelledWord, params.dictionarySuggestions);
|
||||
|
||||
for (const item of spellCheckerMenuItems) {
|
||||
menu.append(new MenuItem(item));
|
||||
}
|
||||
|
||||
// CodeMirror 5 only:
|
||||
// Typically CodeMirror handles all interactions itself (highlighting etc.)
|
||||
// But in the case of clicking a mispelled word, we need electron to handle the click
|
||||
// The result is that CodeMirror doesn't know what's been selected and doesn't
|
||||
// move the cursor into the correct location.
|
||||
// and when the user selects a new spelling it will be inserted in the wrong location
|
||||
// So in this situation, we use must manually align the internal codemirror selection
|
||||
// to the contextmenu selection
|
||||
if (editorRef.current && !editorRef.current.cm6 && spellCheckerMenuItems.length > 0) {
|
||||
(editorRef.current as any).alignSelection(params);
|
||||
}
|
||||
|
||||
let filterObject: EditContextMenuFilterObject = {
|
||||
items: [],
|
||||
};
|
||||
|
||||
filterObject = await eventManager.filterEmit('editorContextMenu', filterObject);
|
||||
|
||||
for (const item of filterObject.items) {
|
||||
menu.append(new MenuItem({
|
||||
label: item.label,
|
||||
click: async () => {
|
||||
const args = item.commandArgs || [];
|
||||
void CommandService.instance().execute(item.commandName, ...args);
|
||||
},
|
||||
type: item.type,
|
||||
}));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line github/array-foreach -- Old code before rule was applied
|
||||
menuUtils.pluginContextMenuItems(props.plugins, MenuItemLocation.EditorContextMenu).forEach((item: any) => {
|
||||
menu.append(new MenuItem(item));
|
||||
});
|
||||
|
||||
menu.popup();
|
||||
}
|
||||
|
||||
// Prepend the event listener so that it gets called before
|
||||
// the listener that shows the default menu.
|
||||
bridge().window().webContents.prependListener('context-menu', onContextMenu);
|
||||
|
||||
return () => {
|
||||
bridge().window().webContents.off('context-menu', onContextMenu);
|
||||
};
|
||||
}, [
|
||||
props.plugins, props.editorClassName, editorRef,
|
||||
props.editorCutText, props.editorCopyText, props.editorPaste,
|
||||
]);
|
||||
};
|
||||
|
||||
export default useContextMenu;
|
||||
@@ -4,7 +4,6 @@ import KeymapService, { KeymapItem } from '@joplin/lib/services/KeymapService';
|
||||
import { EditorCommand } from '../../../utils/types';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import setupVim from './setupVim';
|
||||
|
||||
export default function useKeymap(CodeMirror: any) {
|
||||
|
||||
@@ -18,6 +17,14 @@ export default function useKeymap(CodeMirror: any) {
|
||||
CodeMirror.keyMap.emacs['Shift-Tab'] = 'smartListUnindent';
|
||||
}
|
||||
|
||||
function setupVim() {
|
||||
CodeMirror.Vim.defineAction('swapLineDown', CodeMirror.commands.swapLineDown);
|
||||
CodeMirror.Vim.mapCommand('<A-j>', 'action', 'swapLineDown', {}, { context: 'normal', isEdit: true });
|
||||
CodeMirror.Vim.defineAction('swapLineUp', CodeMirror.commands.swapLineUp);
|
||||
CodeMirror.Vim.mapCommand('<A-k>', 'action', 'swapLineUp', {}, { context: 'normal', isEdit: true });
|
||||
CodeMirror.Vim.defineAction('insertListElement', CodeMirror.commands.vimInsertListElement);
|
||||
CodeMirror.Vim.mapCommand('o', 'action', 'insertListElement', { after: true }, { context: 'normal', isEdit: true, interlaceInsertRepeat: true });
|
||||
}
|
||||
function isEditorCommand(command: string) {
|
||||
return command.startsWith('editor.');
|
||||
}
|
||||
@@ -177,7 +184,7 @@ export default function useKeymap(CodeMirror: any) {
|
||||
keymapService.on('keymapChange', registerKeymap);
|
||||
|
||||
setupEmacs();
|
||||
setupVim(CodeMirror);
|
||||
setupVim();
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, []);
|
||||
}
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import { Theme } from '@joplin/lib/themes/type';
|
||||
import { NoteBodyEditorProps } from '../../../utils/types';
|
||||
import { buildStyle } from '@joplin/lib/theme';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
|
||||
|
||||
const useStyles = (props: NoteBodyEditorProps) => {
|
||||
return useMemo(() => {
|
||||
return buildStyle(['CodeMirror', props.fontSize], props.themeId, (theme: Theme) => {
|
||||
return {
|
||||
root: {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minHeight: 0,
|
||||
...props.style,
|
||||
},
|
||||
rowToolbar: {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
rowEditorViewer: {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flex: 1,
|
||||
paddingTop: 10,
|
||||
|
||||
// Allow the editor container to shrink (allowing the editor to scroll)
|
||||
minHeight: 0,
|
||||
},
|
||||
cellEditor: {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
},
|
||||
cellViewer: {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: theme.dividerColor,
|
||||
borderLeftStyle: 'solid',
|
||||
},
|
||||
viewer: {
|
||||
display: 'flex',
|
||||
overflow: 'hidden',
|
||||
verticalAlign: 'top',
|
||||
boxSizing: 'border-box',
|
||||
width: '100%',
|
||||
},
|
||||
editor: {
|
||||
display: 'flex',
|
||||
width: 'auto',
|
||||
height: 'auto',
|
||||
flex: 1,
|
||||
overflowY: 'hidden',
|
||||
paddingTop: 0,
|
||||
lineHeight: `${Math.round(17 * props.fontSize / 12)}px`,
|
||||
fontSize: `${props.fontSize}px`,
|
||||
color: theme.color,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
|
||||
// CM5 only
|
||||
codeMirrorTheme: theme.codeMirrorTheme, // Defined in theme.js
|
||||
},
|
||||
|
||||
// CM6 only
|
||||
globalTheme: {
|
||||
...theme,
|
||||
fontFamily: 'inherit',
|
||||
fontSize: props.fontSize,
|
||||
fontSizeUnits: 'px',
|
||||
isDesktop: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
}, [props.style, props.themeId, props.fontSize]);
|
||||
};
|
||||
export default useStyles;
|
||||
@@ -1,46 +0,0 @@
|
||||
import type CodeMirror5Emulation from '@joplin/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation';
|
||||
import shared from '@joplin/lib/components/shared/note-screen-shared';
|
||||
import { useCallback, RefObject } from 'react';
|
||||
|
||||
interface Props {
|
||||
onMessage(event: any): void;
|
||||
getLineScrollPercent(): number;
|
||||
setEditorPercentScroll(fraction: number): void;
|
||||
editorRef: RefObject<CodeMirror5Emulation>;
|
||||
content: string;
|
||||
}
|
||||
|
||||
const useWebviewIpcMessage = (props: Props) => {
|
||||
const editorRef = props.editorRef;
|
||||
|
||||
return useCallback((event: any) => {
|
||||
const msg = event.channel ? event.channel : '';
|
||||
const args = event.args;
|
||||
const arg0 = args && args.length >= 1 ? args[0] : null;
|
||||
|
||||
if (msg.indexOf('checkboxclick:') === 0) {
|
||||
const { line, from, to } = shared.toggleCheckboxRange(msg, props.content);
|
||||
if (editorRef.current) {
|
||||
// To cancel CodeMirror's layout drift, the scroll position
|
||||
// is recorded before updated, and then it is restored.
|
||||
// Ref. https://github.com/laurent22/joplin/issues/5890
|
||||
const percent = props.getLineScrollPercent();
|
||||
editorRef.current.replaceRange(line, from, to);
|
||||
props.setEditorPercentScroll(percent);
|
||||
}
|
||||
} else if (msg === 'percentScroll') {
|
||||
const percent = arg0;
|
||||
props.setEditorPercentScroll(percent);
|
||||
} else {
|
||||
props.onMessage(event);
|
||||
}
|
||||
}, [
|
||||
props.onMessage,
|
||||
props.content,
|
||||
editorRef,
|
||||
props.getLineScrollPercent,
|
||||
props.setEditorPercentScroll,
|
||||
]);
|
||||
};
|
||||
|
||||
export default useWebviewIpcMessage;
|
||||
@@ -1,439 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect, useRef, forwardRef, useCallback, useImperativeHandle, useMemo, ForwardedRef } from 'react';
|
||||
|
||||
import { EditorCommand, NoteBodyEditorProps, NoteBodyEditorRef, OnChangeEvent } from '../../../utils/types';
|
||||
import { getResourcesFromPasteEvent } from '../../../utils/resourceHandling';
|
||||
import { ScrollOptions, ScrollOptionTypes } from '../../../utils/types';
|
||||
import NoteTextViewer from '../../../../NoteTextViewer';
|
||||
import Editor from './Editor';
|
||||
import usePluginServiceRegistration from '../../../utils/usePluginServiceRegistration';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import bridge from '../../../../../services/bridge';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { MarkupToHtml } from '@joplin/renderer';
|
||||
const { clipboard } = require('electron');
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import ErrorBoundary from '../../../../ErrorBoundary';
|
||||
import { MarkupToHtmlOptions } from '../../../utils/useMarkupToHtml';
|
||||
import { EditorKeymap, EditorLanguageType, EditorSettings } from '@joplin/editor/types';
|
||||
import useStyles from '../utils/useStyles';
|
||||
import { EditorEvent, EditorEventType } from '@joplin/editor/events';
|
||||
import useScrollHandler from '../utils/useScrollHandler';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import useEditorCommands from './useEditorCommands';
|
||||
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
|
||||
import useContextMenu from '../utils/useContextMenu';
|
||||
import useWebviewIpcMessage from '../utils/useWebviewIpcMessage';
|
||||
import Toolbar from '../Toolbar';
|
||||
|
||||
const logger = Logger.create('CodeMirror6');
|
||||
const logDebug = (message: string) => logger.debug(message);
|
||||
|
||||
interface RenderedBody {
|
||||
html: string;
|
||||
pluginAssets: any[];
|
||||
}
|
||||
|
||||
function defaultRenderedBody(): RenderedBody {
|
||||
return {
|
||||
html: '',
|
||||
pluginAssets: [],
|
||||
};
|
||||
}
|
||||
|
||||
function markupRenderOptions(override: MarkupToHtmlOptions = null): MarkupToHtmlOptions {
|
||||
return { ...override };
|
||||
}
|
||||
|
||||
const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditorRef>) => {
|
||||
const styles = useStyles(props);
|
||||
|
||||
const [renderedBody, setRenderedBody] = useState<RenderedBody>(defaultRenderedBody()); // Viewer content
|
||||
const [renderedBodyContentKey, setRenderedBodyContentKey] = useState<string>(null);
|
||||
|
||||
const [webviewReady, setWebviewReady] = useState(false);
|
||||
|
||||
const editorRef = useRef<CodeMirrorControl>(null);
|
||||
const rootRef = useRef(null);
|
||||
const webviewRef = useRef(null);
|
||||
|
||||
type OnChangeCallback = (event: OnChangeEvent)=> void;
|
||||
const props_onChangeRef = useRef<OnChangeCallback>(null);
|
||||
props_onChangeRef.current = props.onChange;
|
||||
|
||||
const [selectionRange, setSelectionRange] = useState({ from: 0, to: 0 });
|
||||
|
||||
const {
|
||||
resetScroll, editor_scroll, setEditorPercentScroll, setViewerPercentScroll, getLineScrollPercent,
|
||||
} = useScrollHandler(editorRef, webviewRef, props.onScroll);
|
||||
|
||||
usePluginServiceRegistration(ref);
|
||||
|
||||
const codeMirror_change = useCallback((newBody: string) => {
|
||||
if (newBody !== props.content) {
|
||||
props_onChangeRef.current({ changeId: null, content: newBody });
|
||||
}
|
||||
}, [props.content]);
|
||||
|
||||
const onEditorPaste = useCallback(async (event: any = null) => {
|
||||
const resourceMds = await getResourcesFromPasteEvent(event);
|
||||
if (!resourceMds.length) return;
|
||||
if (editorRef.current) {
|
||||
editorRef.current.insertText(resourceMds.join('\n'));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const editorCutText = useCallback(() => {
|
||||
if (editorRef.current) {
|
||||
const selections = editorRef.current.getSelections();
|
||||
if (selections.length > 0 && selections[0]) {
|
||||
clipboard.writeText(selections[0]);
|
||||
// Easy way to wipe out just the first selection
|
||||
selections[0] = '';
|
||||
editorRef.current.replaceSelections(selections);
|
||||
} else {
|
||||
const cursor = editorRef.current.getCursor();
|
||||
const line = editorRef.current.getLine(cursor.line);
|
||||
clipboard.writeText(`${line}\n`);
|
||||
const startLine = editorRef.current.getCursor('head');
|
||||
startLine.ch = 0;
|
||||
const endLine = {
|
||||
line: startLine.line + 1,
|
||||
ch: 0,
|
||||
};
|
||||
editorRef.current.replaceRange('', startLine, endLine);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const editorCopyText = useCallback(() => {
|
||||
if (editorRef.current) {
|
||||
const selections = editorRef.current.getSelections();
|
||||
|
||||
// Handle the case when there is a selection - copy the selection to the clipboard
|
||||
// When there is no selection, the selection array contains an empty string.
|
||||
if (selections.length > 0 && selections[0]) {
|
||||
clipboard.writeText(selections[0]);
|
||||
} else {
|
||||
// This is the case when there is no selection - copy the current line to the clipboard
|
||||
const cursor = editorRef.current.getCursor();
|
||||
const line = editorRef.current.getLine(cursor.line);
|
||||
clipboard.writeText(line);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const editorPasteText = useCallback(async () => {
|
||||
if (editorRef.current) {
|
||||
const modifiedMd = await Note.replaceResourceExternalToInternalLinks(clipboard.readText(), { useAbsolutePaths: true });
|
||||
editorRef.current.insertText(modifiedMd);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const editorPaste = useCallback(() => {
|
||||
const clipboardText = clipboard.readText();
|
||||
|
||||
if (clipboardText) {
|
||||
void editorPasteText();
|
||||
} else {
|
||||
// To handle pasting images
|
||||
void onEditorPaste();
|
||||
}
|
||||
}, [editorPasteText, onEditorPaste]);
|
||||
|
||||
const commands = useEditorCommands({
|
||||
webviewRef,
|
||||
editorRef,
|
||||
selectionRange,
|
||||
|
||||
editorCopyText, editorCutText, editorPaste,
|
||||
editorContent: props.content,
|
||||
visiblePanes: props.visiblePanes,
|
||||
});
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
content: () => props.content,
|
||||
resetScroll: () => {
|
||||
resetScroll();
|
||||
},
|
||||
scrollTo: (options: ScrollOptions) => {
|
||||
if (options.type === ScrollOptionTypes.Hash) {
|
||||
if (!webviewRef.current) return;
|
||||
webviewRef.current.send('scrollToHash', options.value as string);
|
||||
} else if (options.type === ScrollOptionTypes.Percent) {
|
||||
const percent = options.value as number;
|
||||
setEditorPercentScroll(percent);
|
||||
setViewerPercentScroll(percent);
|
||||
|
||||
} else {
|
||||
throw new Error(`Unsupported scroll options: ${options.type}`);
|
||||
}
|
||||
},
|
||||
supportsCommand: (name: string) => {
|
||||
return name in commands || editorRef.current.supportsCommand(name);
|
||||
},
|
||||
execCommand: async (cmd: EditorCommand) => {
|
||||
if (!editorRef.current) return false;
|
||||
|
||||
logger.debug('execCommand', cmd);
|
||||
|
||||
let commandOutput = null;
|
||||
if (cmd.name in commands) {
|
||||
commandOutput = (commands as any)[cmd.name](cmd.value);
|
||||
} else if (editorRef.current.supportsCommand(cmd.name)) {
|
||||
commandOutput = editorRef.current.execCommand(cmd.name);
|
||||
} else if (editorRef.current.supportsJoplinCommand(cmd.name)) {
|
||||
commandOutput = editorRef.current.execJoplinCommand(cmd.name);
|
||||
} else {
|
||||
reg.logger().warn('CodeMirror: unsupported Joplin command: ', cmd);
|
||||
}
|
||||
|
||||
return commandOutput;
|
||||
},
|
||||
};
|
||||
}, [props.content, commands, resetScroll, setEditorPercentScroll, setViewerPercentScroll]);
|
||||
|
||||
const webview_domReady = useCallback(() => {
|
||||
setWebviewReady(true);
|
||||
}, []);
|
||||
|
||||
const webview_ipcMessage = useWebviewIpcMessage({
|
||||
editorRef,
|
||||
setEditorPercentScroll,
|
||||
getLineScrollPercent,
|
||||
content: props.content,
|
||||
onMessage: props.onMessage,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
// When a new note is loaded (contentKey is different), we want the note to be displayed
|
||||
// right away. However once that's done, we put a small delay so that the view is not
|
||||
// being constantly updated while the user changes the note.
|
||||
const interval = renderedBodyContentKey !== props.contentKey ? 0 : 500;
|
||||
|
||||
const timeoutId = shim.setTimeout(async () => {
|
||||
let bodyToRender = props.content;
|
||||
|
||||
if (!bodyToRender.trim() && props.visiblePanes.indexOf('viewer') >= 0 && props.visiblePanes.indexOf('editor') < 0) {
|
||||
// Fixes https://github.com/laurent22/joplin/issues/217
|
||||
bodyToRender = `<i>${_('This note has no content. Click on "%s" to toggle the editor and edit the note.', _('Layout'))}</i>`;
|
||||
}
|
||||
|
||||
const result = await props.markupToHtml(props.contentMarkupLanguage, bodyToRender, markupRenderOptions({
|
||||
resourceInfos: props.resourceInfos,
|
||||
contentMaxWidth: props.contentMaxWidth,
|
||||
mapsToLine: true,
|
||||
// Always using useCustomPdfViewer for now, we can add a new setting for it in future if we need to.
|
||||
useCustomPdfViewer: props.useCustomPdfViewer,
|
||||
noteId: props.noteId,
|
||||
vendorDir: bridge().vendorDir(),
|
||||
}));
|
||||
|
||||
if (cancelled) return;
|
||||
|
||||
setRenderedBody(result);
|
||||
|
||||
// Since we set `renderedBodyContentKey` here, it means this effect is going to
|
||||
// be triggered again, but that's hard to avoid and the second call would be cheap
|
||||
// anyway since the renderered markdown is cached by MdToHtml. We could use a ref
|
||||
// to avoid this, but a second rendering might still happens anyway to render images,
|
||||
// resources, or for other reasons. So it's best to focus on making any second call
|
||||
// to this effect as cheap as possible with caching, etc.
|
||||
setRenderedBodyContentKey(props.contentKey);
|
||||
}, interval);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
shim.clearTimeout(timeoutId);
|
||||
};
|
||||
}, [
|
||||
props.content, props.contentKey, renderedBodyContentKey, props.contentMarkupLanguage,
|
||||
props.visiblePanes, props.resourceInfos, props.markupToHtml, props.contentMaxWidth,
|
||||
props.noteId, props.useCustomPdfViewer,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!webviewReady) return;
|
||||
|
||||
let lineCount = 0;
|
||||
if (editorRef.current) {
|
||||
lineCount = editorRef.current.editor.state.doc.lines;
|
||||
}
|
||||
|
||||
const options: any = {
|
||||
pluginAssets: renderedBody.pluginAssets,
|
||||
downloadResources: Setting.value('sync.resourceDownloadMode'),
|
||||
markupLineCount: lineCount,
|
||||
};
|
||||
|
||||
// It seems when there's an error immediately when the component is
|
||||
// mounted, webviewReady might be true, but webviewRef.current will be
|
||||
// undefined. Maybe due to the error boundary that unmount components.
|
||||
// Since we can't do much about it we just print an error.
|
||||
if (webviewRef.current) {
|
||||
// To keep consistency among CodeMirror's editing and scroll percents
|
||||
// of Editor and Viewer.
|
||||
const percent = getLineScrollPercent();
|
||||
setEditorPercentScroll(percent);
|
||||
options.percent = percent;
|
||||
webviewRef.current.send('setHtml', renderedBody.html, options);
|
||||
} else {
|
||||
console.error('Trying to set HTML on an undefined webview ref');
|
||||
}
|
||||
}, [renderedBody, webviewReady, getLineScrollPercent, setEditorPercentScroll]);
|
||||
|
||||
const cellEditorStyle = useMemo(() => {
|
||||
const output = { ...styles.cellEditor };
|
||||
if (!props.visiblePanes.includes('editor')) {
|
||||
output.display = 'none'; // Seems to work fine since the refactoring
|
||||
}
|
||||
|
||||
return output;
|
||||
}, [styles.cellEditor, props.visiblePanes]);
|
||||
|
||||
const cellViewerStyle = useMemo(() => {
|
||||
const output = { ...styles.cellViewer };
|
||||
if (!props.visiblePanes.includes('viewer')) {
|
||||
// Note: setting webview.display to "none" is currently not supported due
|
||||
// to this bug: https://github.com/electron/electron/issues/8277
|
||||
// So instead setting the width 0.
|
||||
output.width = 1;
|
||||
output.maxWidth = 1;
|
||||
} else if (!props.visiblePanes.includes('editor')) {
|
||||
output.borderLeftStyle = 'none';
|
||||
}
|
||||
return output;
|
||||
}, [styles.cellViewer, props.visiblePanes]);
|
||||
|
||||
// Disable this effect to fix this:
|
||||
//
|
||||
// https://github.com/laurent22/joplin/issues/6514 It doesn't seem essential
|
||||
// to automatically focus the editor when the layout changes. The workaround
|
||||
// is to toggle the layout Cmd+L, then manually focus the editor Cmd+Shift+B.
|
||||
//
|
||||
// On the other hand, if we automatically focus the editor, and the user
|
||||
// does not want this, there's no workaround, so it's better to have this
|
||||
// disabled.
|
||||
|
||||
// const editorPaneVisible = props.visiblePanes.indexOf('editor') >= 0;
|
||||
|
||||
// useEffect(() => {
|
||||
// if (!editorRef.current) return;
|
||||
|
||||
// // Anytime the user toggles the visible panes AND the editor is visible as a result
|
||||
// // we should focus the editor
|
||||
// // The intuition is that a panel toggle (with editor in view) is the equivalent of
|
||||
// // an editor interaction so users should expect the editor to be focused
|
||||
// if (editorPaneVisible) {
|
||||
// editorRef.current.focus();
|
||||
// }
|
||||
// }, [editorPaneVisible]);
|
||||
|
||||
useContextMenu({
|
||||
plugins: props.plugins,
|
||||
editorCutText, editorCopyText, editorPaste,
|
||||
editorRef,
|
||||
editorClassName: 'cm-editor',
|
||||
});
|
||||
|
||||
const onEditorEvent = useCallback((event: EditorEvent) => {
|
||||
if (event.kind === EditorEventType.Scroll) {
|
||||
editor_scroll();
|
||||
} else if (event.kind === EditorEventType.Change) {
|
||||
codeMirror_change(event.value);
|
||||
} else if (event.kind === EditorEventType.SelectionRangeChange) {
|
||||
setSelectionRange({ from: event.from, to: event.to });
|
||||
}
|
||||
}, [editor_scroll, codeMirror_change]);
|
||||
|
||||
const editorSettings = useMemo((): EditorSettings => {
|
||||
const isHTMLNote = props.contentMarkupLanguage === MarkupToHtml.MARKUP_LANGUAGE_HTML;
|
||||
|
||||
let keyboardMode = EditorKeymap.Default;
|
||||
if (props.keyboardMode === 'vim') {
|
||||
keyboardMode = EditorKeymap.Vim;
|
||||
} else if (props.keyboardMode === 'emacs') {
|
||||
keyboardMode = EditorKeymap.Emacs;
|
||||
}
|
||||
|
||||
return {
|
||||
language: isHTMLNote ? EditorLanguageType.Html : EditorLanguageType.Markdown,
|
||||
readOnly: props.disabled || props.visiblePanes.indexOf('editor') < 0,
|
||||
katexEnabled: Setting.value('markdown.plugin.katex'),
|
||||
themeData: {
|
||||
...styles.globalTheme,
|
||||
monospaceFont: Setting.value('style.editor.monospaceFontFamily'),
|
||||
},
|
||||
automatchBraces: Setting.value('editor.autoMatchingBraces'),
|
||||
useExternalSearch: false,
|
||||
ignoreModifiers: true,
|
||||
spellcheckEnabled: Setting.value('editor.spellcheckBeta'),
|
||||
keymap: keyboardMode,
|
||||
indentWithTabs: true,
|
||||
};
|
||||
}, [
|
||||
props.contentMarkupLanguage, props.disabled, props.visiblePanes,
|
||||
props.keyboardMode, styles.globalTheme,
|
||||
]);
|
||||
|
||||
// Update the editor's value
|
||||
useEffect(() => {
|
||||
if (editorRef.current?.updateBody(props.content)) {
|
||||
editorRef.current?.clearHistory();
|
||||
}
|
||||
}, [props.content]);
|
||||
|
||||
const renderEditor = () => {
|
||||
return (
|
||||
<div style={cellEditorStyle}>
|
||||
<Editor
|
||||
style={styles.editor}
|
||||
initialText={props.content}
|
||||
ref={editorRef}
|
||||
settings={editorSettings}
|
||||
pluginStates={props.plugins}
|
||||
onEvent={onEditorEvent}
|
||||
onLogMessage={logDebug}
|
||||
onEditorPaste={onEditorPaste}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderViewer = () => {
|
||||
return (
|
||||
<div style={cellViewerStyle}>
|
||||
<NoteTextViewer
|
||||
ref={webviewRef}
|
||||
themeId={props.themeId}
|
||||
viewerStyle={styles.viewer}
|
||||
onIpcMessage={webview_ipcMessage}
|
||||
onDomReady={webview_domReady}
|
||||
contentMaxWidth={props.contentMaxWidth}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ErrorBoundary message="The text editor encountered a fatal error and could not continue. The error might be due to a plugin, so please try to disable some of them and try again.">
|
||||
<div style={styles.root} ref={rootRef}>
|
||||
<div style={styles.rowToolbar}>
|
||||
<Toolbar themeId={props.themeId}/>
|
||||
{props.noteToolbar}
|
||||
</div>
|
||||
<div style={styles.rowEditorViewer}>
|
||||
{renderEditor()}
|
||||
{renderViewer()}
|
||||
</div>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(CodeMirror);
|
||||
@@ -1,121 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { ForwardedRef } from 'react';
|
||||
import { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
import { EditorProps, LogMessageCallback, OnEventCallback, PluginData } from '@joplin/editor/types';
|
||||
import createEditor from '@joplin/editor/CodeMirror/createEditor';
|
||||
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import { ContentScriptType } from '@joplin/lib/services/plugins/api/types';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import setupVim from '../utils/setupVim';
|
||||
|
||||
interface Props extends EditorProps {
|
||||
style: React.CSSProperties;
|
||||
pluginStates: PluginStates;
|
||||
|
||||
onEditorPaste: (event: Event)=> void;
|
||||
}
|
||||
|
||||
const Editor = (props: Props, ref: ForwardedRef<CodeMirrorControl>) => {
|
||||
const editorContainerRef = useRef<HTMLDivElement>();
|
||||
const [editor, setEditor] = useState<CodeMirrorControl|null>(null);
|
||||
|
||||
// The editor will only be created once, so callbacks that could
|
||||
// change need to be stored as references.
|
||||
const onEventRef = useRef<OnEventCallback>(props.onEvent);
|
||||
const onLogMessageRef = useRef<LogMessageCallback>(props.onLogMessage);
|
||||
|
||||
useEffect(() => {
|
||||
onEventRef.current = props.onEvent;
|
||||
onLogMessageRef.current = props.onLogMessage;
|
||||
}, [props.onEvent, props.onLogMessage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const pasteEventHandler = (_editor: any, event: Event) => {
|
||||
props.onEditorPaste(event);
|
||||
};
|
||||
|
||||
editor.on('paste', pasteEventHandler);
|
||||
|
||||
return () => {
|
||||
editor.off('paste', pasteEventHandler);
|
||||
};
|
||||
}, [editor, props.onEditorPaste]);
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
return editor;
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const plugins: PluginData[] = [];
|
||||
for (const pluginId in props.pluginStates) {
|
||||
const pluginState = props.pluginStates[pluginId];
|
||||
const codeMirrorContentScripts = pluginState.contentScripts[ContentScriptType.CodeMirrorPlugin] ?? [];
|
||||
|
||||
for (const contentScript of codeMirrorContentScripts) {
|
||||
plugins.push({
|
||||
pluginId,
|
||||
contentScriptId: contentScript.id,
|
||||
contentScriptJs: () => shim.fsDriver().readFile(contentScript.path),
|
||||
postMessageHandler: (message: any) => {
|
||||
const plugin = PluginService.instance().pluginById(pluginId);
|
||||
return plugin.emitContentScriptMessage(contentScript.id, message);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void editor.setPlugins(plugins);
|
||||
}, [editor, props.pluginStates]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editorContainerRef.current) return () => {};
|
||||
|
||||
const editorProps: EditorProps = {
|
||||
...props,
|
||||
onEvent: event => onEventRef.current(event),
|
||||
onLogMessage: message => onLogMessageRef.current(message),
|
||||
};
|
||||
|
||||
const editor = createEditor(editorContainerRef.current, editorProps);
|
||||
editor.addStyles({
|
||||
'.cm-scroller': { overflow: 'auto' },
|
||||
});
|
||||
setEditor(editor);
|
||||
|
||||
return () => {
|
||||
editor.remove();
|
||||
};
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Should run just once
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
editor?.updateSettings(props.settings);
|
||||
}, [props.settings, editor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
setupVim(editor);
|
||||
}, [editor]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={props.style}
|
||||
ref={editorContainerRef}
|
||||
></div>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(Editor);
|
||||
@@ -1,136 +0,0 @@
|
||||
|
||||
import { RefObject, useMemo } from 'react';
|
||||
import { CommandValue } from '../../../utils/types';
|
||||
import { commandAttachFileToBody } from '../../../utils/resourceHandling';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import dialogs from '../../../../dialogs';
|
||||
import { EditorCommandType } from '@joplin/editor/types';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
|
||||
|
||||
const logger = Logger.create('CodeMirror 6 commands');
|
||||
|
||||
const wrapSelectionWithStrings = (editor: CodeMirrorControl, string1: string, string2 = '', defaultText = '') => {
|
||||
if (editor.somethingSelected()) {
|
||||
editor.wrapSelections(string1, string2);
|
||||
} else {
|
||||
editor.wrapSelections(string1 + defaultText, string2);
|
||||
|
||||
// Now select the default text so the user can replace it
|
||||
const selections = editor.listSelections();
|
||||
const newSelections = [];
|
||||
for (let i = 0; i < selections.length; i++) {
|
||||
const s = selections[i];
|
||||
const anchor = { line: s.anchor.line, ch: s.anchor.ch + string1.length };
|
||||
const head = { line: s.head.line, ch: s.head.ch - string2.length };
|
||||
newSelections.push({ anchor: anchor, head: head });
|
||||
}
|
||||
editor.setSelections(newSelections);
|
||||
}
|
||||
};
|
||||
|
||||
interface Props {
|
||||
webviewRef: RefObject<any>;
|
||||
editorRef: RefObject<CodeMirrorControl>;
|
||||
editorContent: string;
|
||||
|
||||
editorCutText(): void;
|
||||
editorCopyText(): void;
|
||||
editorPaste(): void;
|
||||
selectionRange: { from: number; to: number };
|
||||
|
||||
visiblePanes: string[];
|
||||
}
|
||||
|
||||
const useEditorCommands = (props: Props) => {
|
||||
const editorRef = props.editorRef;
|
||||
|
||||
return useMemo(() => {
|
||||
const selectedText = () => {
|
||||
if (!editorRef.current) return '';
|
||||
return editorRef.current.getSelection();
|
||||
};
|
||||
|
||||
return {
|
||||
dropItems: async (cmd: any) => {
|
||||
if (cmd.type === 'notes') {
|
||||
editorRef.current.insertText(cmd.markdownTags.join('\n'));
|
||||
} else if (cmd.type === 'files') {
|
||||
const pos = props.selectionRange.from;
|
||||
const newBody = await commandAttachFileToBody(props.editorContent, cmd.paths, { createFileURL: !!cmd.createFileURL, position: pos });
|
||||
editorRef.current.updateBody(newBody);
|
||||
} else {
|
||||
logger.warn('CodeMirror: unsupported drop item: ', cmd);
|
||||
}
|
||||
},
|
||||
selectedText: () => {
|
||||
return selectedText();
|
||||
},
|
||||
selectedHtml: () => {
|
||||
return selectedText();
|
||||
},
|
||||
replaceSelection: (value: string) => {
|
||||
return editorRef.current.insertText(value);
|
||||
},
|
||||
textCopy: () => {
|
||||
props.editorCopyText();
|
||||
},
|
||||
textCut: () => {
|
||||
props.editorCutText();
|
||||
},
|
||||
textPaste: () => {
|
||||
props.editorPaste();
|
||||
},
|
||||
textSelectAll: () => {
|
||||
return editorRef.current.execCommand(EditorCommandType.SelectAll);
|
||||
},
|
||||
textLink: async () => {
|
||||
const url = await dialogs.prompt(_('Insert Hyperlink'));
|
||||
editorRef.current.focus();
|
||||
if (url) wrapSelectionWithStrings(editorRef.current, '[', `](${url})`);
|
||||
},
|
||||
insertText: (value: any) => editorRef.current.insertText(value),
|
||||
attachFile: async () => {
|
||||
const newBody = await commandAttachFileToBody(
|
||||
props.editorContent, null, { position: props.selectionRange.from },
|
||||
);
|
||||
if (newBody) {
|
||||
editorRef.current.updateBody(newBody);
|
||||
}
|
||||
},
|
||||
textHorizontalRule: () => editorRef.current.insertText('* * *'),
|
||||
'editor.execCommand': (value: CommandValue) => {
|
||||
if (!('args' in value)) value.args = [];
|
||||
|
||||
if ((editorRef.current as any)[value.name]) {
|
||||
const result = (editorRef.current as any)[value.name](...value.args);
|
||||
return result;
|
||||
} else if (editorRef.current.commandExists(value.name)) {
|
||||
const result = editorRef.current.execCommand(value.name);
|
||||
return result;
|
||||
} else {
|
||||
logger.warn('CodeMirror execCommand: unsupported command: ', value.name);
|
||||
}
|
||||
},
|
||||
'editor.focus': () => {
|
||||
if (props.visiblePanes.indexOf('editor') >= 0) {
|
||||
editorRef.current.editor.focus();
|
||||
} else {
|
||||
// If we just call focus() then the iframe is focused,
|
||||
// but not its content, such that scrolling up / down
|
||||
// with arrow keys fails
|
||||
props.webviewRef.current.send('focus');
|
||||
}
|
||||
},
|
||||
search: () => {
|
||||
editorRef.current.execCommand(EditorCommandType.ShowSearch);
|
||||
},
|
||||
};
|
||||
}, [
|
||||
props.visiblePanes, props.editorContent, props.editorCopyText, props.editorCutText, props.editorPaste,
|
||||
props.selectionRange,
|
||||
|
||||
props.webviewRef, editorRef,
|
||||
]);
|
||||
};
|
||||
export default useEditorCommands;
|
||||
@@ -1,56 +0,0 @@
|
||||
|
||||
// Used in safe mode
|
||||
|
||||
import * as React from 'react';
|
||||
import { ForwardedRef } from 'react';
|
||||
import { useEffect, useCallback, useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
import { NoteBodyEditorProps, NoteBodyEditorRef } from '../../utils/types';
|
||||
|
||||
const PlainEditor = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditorRef>) => {
|
||||
const editorRef = useRef<HTMLTextAreaElement>();
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
content: () => editorRef.current?.value ?? '',
|
||||
resetScroll: () => {
|
||||
editorRef.current.scrollTop = 0;
|
||||
},
|
||||
scrollTo: () => {
|
||||
// Not supported
|
||||
},
|
||||
|
||||
supportsCommand: _name => {
|
||||
return false;
|
||||
},
|
||||
execCommand: async _command => {
|
||||
// Not supported
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editorRef.current) return;
|
||||
|
||||
if (editorRef.current.value !== props.content) {
|
||||
editorRef.current.value = props.content;
|
||||
}
|
||||
}, [props.content]);
|
||||
|
||||
const onChange = useCallback((event: any) => {
|
||||
props.onChange({ changeId: null, content: event.target.value });
|
||||
}, [props.onChange]);
|
||||
|
||||
return (
|
||||
<div style={props.style}>
|
||||
<textarea
|
||||
ref={editorRef}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
defaultValue={props.content}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(PlainEditor);
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
// Kept only for reference
|
||||
|
||||
import * as React from 'react';
|
||||
import { useEffect, useCallback, useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
|
||||
export interface OnChangeEvent {
|
||||
changeId: number,
|
||||
content: any,
|
||||
}
|
||||
|
||||
interface PlainEditorProps {
|
||||
style: any,
|
||||
onChange(event: OnChangeEvent): void,
|
||||
onWillChange(event:any): void,
|
||||
markupToHtml: Function,
|
||||
disabled: boolean,
|
||||
}
|
||||
|
||||
const PlainEditor = (props:PlainEditorProps, ref:any) => {
|
||||
const editorRef = useRef<any>();
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
content: () => '',
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editorRef.current) return;
|
||||
editorRef.current.value = props.defaultEditorState.value;
|
||||
}, [props.defaultEditorState]);
|
||||
|
||||
const onChange = useCallback((event:any) => {
|
||||
props.onChange({ changeId: null, content: event.target.value });
|
||||
}, [props.onWillChange, props.onChange]);
|
||||
|
||||
return (
|
||||
<div style={props.style}>
|
||||
<textarea
|
||||
ref={editorRef}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
defaultValue={props.defaultEditorState.value}
|
||||
onChange={onChange}
|
||||
/>;
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(PlainEditor);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||
import TinyMCE from './NoteBody/TinyMCE/TinyMCE';
|
||||
import CodeMirror from './NoteBody/CodeMirror/CodeMirror';
|
||||
import { connect } from 'react-redux';
|
||||
import MultiNoteActions from '../MultiNoteActions';
|
||||
import { htmlToMarkdown, formNoteToNote } from './utils';
|
||||
@@ -14,7 +15,7 @@ import useFormNote, { OnLoadEvent } from './utils/useFormNote';
|
||||
import useEffectiveNoteId from './utils/useEffectiveNoteId';
|
||||
import useFolder from './utils/useFolder';
|
||||
import styles_ from './styles';
|
||||
import { NoteEditorProps, FormNote, ScrollOptions, ScrollOptionTypes, OnChangeEvent, NoteBodyEditorProps, AllAssetsOptions, NoteBodyEditorRef } from './utils/types';
|
||||
import { NoteEditorProps, FormNote, ScrollOptions, ScrollOptionTypes, OnChangeEvent, NoteBodyEditorProps, AllAssetsOptions } from './utils/types';
|
||||
import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher/index';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import ToolbarButton from '../ToolbarButton/ToolbarButton';
|
||||
@@ -45,9 +46,6 @@ import { ModelType } from '@joplin/lib/BaseModel';
|
||||
import BaseItem from '@joplin/lib/models/BaseItem';
|
||||
import { ErrorCode } from '@joplin/lib/errors';
|
||||
import ItemChange from '@joplin/lib/models/ItemChange';
|
||||
import PlainEditor from './NoteBody/PlainEditor/PlainEditor';
|
||||
import CodeMirror6 from './NoteBody/CodeMirror/v6/CodeMirror';
|
||||
import CodeMirror5 from './NoteBody/CodeMirror/v5/CodeMirror';
|
||||
|
||||
const commands = [
|
||||
require('./commands/showRevisions'),
|
||||
@@ -61,7 +59,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
const [scrollWhenReady, setScrollWhenReady] = useState<ScrollOptions>(null);
|
||||
const [isReadOnly, setIsReadOnly] = useState<boolean>(false);
|
||||
|
||||
const editorRef = useRef<NoteBodyEditorRef>();
|
||||
const editorRef = useRef<any>();
|
||||
const titleInputRef = useRef<any>();
|
||||
const isMountedRef = useRef(true);
|
||||
const noteSearchBarRef = useRef(null);
|
||||
@@ -382,7 +380,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
};
|
||||
}, [setShowRevisions]);
|
||||
|
||||
const onScroll = useCallback((event: { percent: number }) => {
|
||||
const onScroll = useCallback((event: any) => {
|
||||
props.dispatch({
|
||||
type: 'EDITOR_SCROLL_PERCENT_SET',
|
||||
// In callbacks of setTimeout()/setInterval(), props/state cannot be used
|
||||
@@ -463,12 +461,8 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
|
||||
if (props.bodyEditor === 'TinyMCE') {
|
||||
editor = <TinyMCE {...editorProps}/>;
|
||||
} else if (props.bodyEditor === 'PlainText') {
|
||||
editor = <PlainEditor {...editorProps}/>;
|
||||
} else if (props.bodyEditor === 'CodeMirror') {
|
||||
editor = <CodeMirror5 {...editorProps}/>;
|
||||
} else if (props.bodyEditor === 'CodeMirror6') {
|
||||
editor = <CodeMirror6 {...editorProps}/>;
|
||||
editor = <CodeMirror {...editorProps}/>;
|
||||
} else {
|
||||
throw new Error(`Invalid editor: ${props.bodyEditor}`);
|
||||
}
|
||||
@@ -608,7 +602,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
disabled={isReadOnly}
|
||||
/>
|
||||
{renderSearchInfo()}
|
||||
<div style={{ display: 'flex', flex: 1, paddingLeft: theme.editorPaddingLeft, maxHeight: '100%', minHeight: '0' }}>
|
||||
<div style={{ display: 'flex', flex: 1, paddingLeft: theme.editorPaddingLeft, maxHeight: '100%' }}>
|
||||
{editor}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
|
||||
@@ -109,7 +109,7 @@ export default function NoteTitleBar(props: Props) {
|
||||
className="title-input"
|
||||
type="text"
|
||||
ref={props.titleInputRef}
|
||||
placeholder={props.isProvisional ? (props.noteIsTodo ? _('Creating new to-do...') : _('Creating new note...')) : ''}
|
||||
placeholder={props.isProvisional ? _('Creating new %s...', props.noteIsTodo ? _('to-do') : _('note')) : ''}
|
||||
style={styles.titleInput}
|
||||
readOnly={props.disabled}
|
||||
onChange={props.onTitleChange}
|
||||
|
||||
@@ -48,15 +48,6 @@ export interface NoteEditorProps {
|
||||
syncUserId: string;
|
||||
}
|
||||
|
||||
export interface NoteBodyEditorRef {
|
||||
content(): string|Promise<string>;
|
||||
resetScroll(): void;
|
||||
scrollTo(options: ScrollOptions): void;
|
||||
|
||||
supportsCommand(name: string): boolean;
|
||||
execCommand(command: CommandValue): Promise<void>;
|
||||
}
|
||||
|
||||
export interface NoteBodyEditorProps {
|
||||
style: any;
|
||||
ref: any;
|
||||
@@ -68,7 +59,7 @@ export interface NoteBodyEditorProps {
|
||||
onChange(event: OnChangeEvent): void;
|
||||
onWillChange(event: any): void;
|
||||
onMessage(event: any): void;
|
||||
onScroll(event: { percent: number }): void;
|
||||
onScroll(event: any): void;
|
||||
markupToHtml: (markupLanguage: MarkupLanguage, markup: string, options: MarkupToHtmlOptions)=> Promise<RenderResult>;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
htmlToMarkdown: Function;
|
||||
|
||||
@@ -3,16 +3,6 @@ import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import useFormNote, { HookDependencies } from './useFormNote';
|
||||
|
||||
const defaultFormNoteProps: HookDependencies = {
|
||||
syncStarted: false,
|
||||
decryptionStarted: false,
|
||||
noteId: '',
|
||||
isProvisional: false,
|
||||
titleInputRef: null,
|
||||
editorRef: null,
|
||||
onBeforeLoad: ()=>{},
|
||||
onAfterLoad: ()=>{},
|
||||
};
|
||||
|
||||
describe('useFormNote', () => {
|
||||
beforeEach(async () => {
|
||||
@@ -25,10 +15,14 @@ describe('useFormNote', () => {
|
||||
|
||||
const makeFormNoteProps = (syncStarted: boolean, decryptionStarted: boolean): HookDependencies => {
|
||||
return {
|
||||
...defaultFormNoteProps,
|
||||
syncStarted,
|
||||
decryptionStarted,
|
||||
noteId: testNote.id,
|
||||
isProvisional: false,
|
||||
titleInputRef: null,
|
||||
editorRef: null,
|
||||
onBeforeLoad: ()=>{},
|
||||
onAfterLoad: ()=>{},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -76,37 +70,4 @@ describe('useFormNote', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// It seems this test is crashing the worker on CI (out of memory), so disabling it for now.
|
||||
|
||||
// it('should reload the note when it is changed outside of the editor', async () => {
|
||||
// const note = await Note.save({ title: 'Test Note!' });
|
||||
|
||||
// const makeFormNoteProps = (dbNote: DbNote): HookDependencies => {
|
||||
// return {
|
||||
// ...defaultFormNoteProps,
|
||||
// noteId: note.id,
|
||||
// dbNote,
|
||||
// };
|
||||
// };
|
||||
|
||||
// const formNote = renderHook(props => useFormNote(props), {
|
||||
// initialProps: makeFormNoteProps({ id: note.id, updated_time: note.updated_time }),
|
||||
// });
|
||||
|
||||
// await formNote.waitFor(() => {
|
||||
// expect(formNote.result.current.formNote.title).toBe('Test Note!');
|
||||
// });
|
||||
|
||||
// // Simulate the note being modified outside the editor
|
||||
// const modifiedNote = await Note.save({ id: note.id, title: 'Modified' });
|
||||
|
||||
// // NoteEditor then would update `dbNote`
|
||||
// formNote.rerender(makeFormNoteProps({ id: note.id, updated_time: modifiedNote.updated_time }));
|
||||
|
||||
// await formNote.waitFor(() => {
|
||||
// expect(formNote.result.current.formNote.title).toBe('Modified');
|
||||
// });
|
||||
// });
|
||||
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RefObject, useEffect } from 'react';
|
||||
import { FormNote, NoteBodyEditorRef, ScrollOptionTypes } from './types';
|
||||
import { useEffect } from 'react';
|
||||
import { FormNote, ScrollOptionTypes } from './types';
|
||||
import editorCommandDeclarations, { enabledCondition } from '../editorCommandDeclarations';
|
||||
import CommandService, { CommandDeclaration, CommandRuntime, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import time from '@joplin/lib/time';
|
||||
@@ -12,8 +12,6 @@ const commandsWithDependencies = [
|
||||
require('../commands/pasteAsText'),
|
||||
];
|
||||
|
||||
type SetFormNoteCallback = (callback: (prev: FormNote)=> FormNote)=> void;
|
||||
|
||||
interface HookDependencies {
|
||||
formNote: FormNote;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
@@ -21,18 +19,16 @@ interface HookDependencies {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
dispatch: Function;
|
||||
noteSearchBarRef: any;
|
||||
editorRef: RefObject<NoteBodyEditorRef>;
|
||||
editorRef: any;
|
||||
titleInputRef: any;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
saveNoteAndWait: Function;
|
||||
setFormNote: SetFormNoteCallback;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
setFormNote: Function;
|
||||
}
|
||||
|
||||
function editorCommandRuntime(
|
||||
declaration: CommandDeclaration,
|
||||
editorRef: RefObject<NoteBodyEditorRef>,
|
||||
setFormNote: SetFormNoteCallback,
|
||||
): CommandRuntime {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
function editorCommandRuntime(declaration: CommandDeclaration, editorRef: any, setFormNote: Function): CommandRuntime {
|
||||
return {
|
||||
execute: async (_context: CommandContext, ...args: any[]) => {
|
||||
if (!editorRef.current) {
|
||||
|
||||
@@ -271,7 +271,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
controlComp = (
|
||||
<Datetime
|
||||
ref="editField"
|
||||
initialValue={value}
|
||||
defaultValue={value}
|
||||
dateFormat={time.dateFormat()}
|
||||
timeFormat={time.timeFormat()}
|
||||
inputProps={{
|
||||
|
||||
@@ -67,6 +67,5 @@ export default function() {
|
||||
'switchProfile2',
|
||||
'switchProfile3',
|
||||
'pasteAsText',
|
||||
'showNoteProperties',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
# Integration tests
|
||||
|
||||
The integration tests in this directory can be run with `yarn playwright test`.
|
||||
|
||||
- Tests use a `test-profile` directory that should be re-created before every test.
|
||||
- Only one Electron application should be instantiated per test file.
|
||||
- Files in the `models/` directory follow [the page object model](https://playwright.dev/docs/pom).
|
||||
|
||||
# References
|
||||
|
||||
The following sources are helpful for designing and implementing Electron integration tests
|
||||
with Playwright:
|
||||
- [A setup guide from an organisation that uses Playwright](https://dev.to/kubeshop/testing-electron-apps-with-playwright-3f89)
|
||||
and [that organisation's test suite](https://github.com/kubeshop/monokle/blob/main/tests/base.test.ts).
|
||||
- [The Playwright ElectronApp docs](https://playwright.dev/docs/api/class-electronapplication)
|
||||
- [Electron Playwright example repository](https://github.com/spaceagetv/electron-playwright-example)
|
||||
- [Playwright best practices](https://playwright.dev/docs/best-practices)
|
||||
@@ -1,124 +0,0 @@
|
||||
import { test, expect } from './util/test';
|
||||
import MainScreen from './models/MainScreen';
|
||||
import activateMainMenuItem from './util/activateMainMenuItem';
|
||||
import SettingsScreen from './models/SettingsScreen';
|
||||
|
||||
|
||||
test.describe('main', () => {
|
||||
test('app should launch', async ({ mainWindow }) => {
|
||||
// A window should open with the correct title
|
||||
expect(await mainWindow.title()).toMatch(/^Joplin/);
|
||||
|
||||
const mainPage = new MainScreen(mainWindow);
|
||||
await mainPage.waitFor();
|
||||
});
|
||||
|
||||
test('should be able to create and edit a new note', async ({ mainWindow }) => {
|
||||
const mainScreen = new MainScreen(mainWindow);
|
||||
await mainScreen.newNoteButton.click();
|
||||
|
||||
const editor = mainScreen.noteEditor;
|
||||
await editor.waitFor();
|
||||
|
||||
// Wait for the title input to have the correct placeholder
|
||||
await mainWindow.locator('input[placeholder^="Creating new note"]').waitFor();
|
||||
|
||||
// Fill the title
|
||||
await editor.noteTitleInput.click();
|
||||
await editor.noteTitleInput.fill('Test note');
|
||||
|
||||
// Note list should contain the new note
|
||||
await expect(mainScreen.noteListContainer.getByText('Test note')).toBeVisible();
|
||||
|
||||
// Focus the editor
|
||||
await editor.codeMirrorEditor.click();
|
||||
|
||||
// Type some text
|
||||
await mainWindow.keyboard.type('# Test note!');
|
||||
await mainWindow.keyboard.press('Enter');
|
||||
await mainWindow.keyboard.press('Enter');
|
||||
await mainWindow.keyboard.type('New note content!');
|
||||
|
||||
// Should render
|
||||
const viewerFrame = editor.getNoteViewerIframe();
|
||||
await expect(viewerFrame.locator('h1')).toHaveText('Test note!');
|
||||
});
|
||||
|
||||
test('should be possible to remove sort order buttons in settings', async ({ electronApp, mainWindow }) => {
|
||||
const mainScreen = new MainScreen(mainWindow);
|
||||
await mainScreen.waitFor();
|
||||
|
||||
// Sort order buttons should be visible by default
|
||||
await expect(mainScreen.noteListContainer.locator('[title^="Toggle sort order"]')).toBeVisible();
|
||||
|
||||
// Open settings (check both labels so that this works on MacOS)
|
||||
expect(
|
||||
await activateMainMenuItem(electronApp, 'Preferences...') || await activateMainMenuItem(electronApp, 'Options'),
|
||||
).toBe(true);
|
||||
|
||||
// Should be on the settings screen
|
||||
const settingsScreen = new SettingsScreen(mainWindow);
|
||||
await settingsScreen.waitFor();
|
||||
|
||||
// Open the appearance tab
|
||||
await settingsScreen.appearanceTabButton.click();
|
||||
|
||||
// Find the sort order visible checkbox
|
||||
const sortOrderVisibleCheckbox = mainWindow.getByLabel(/^Show sort order/);
|
||||
|
||||
await expect(sortOrderVisibleCheckbox).toBeChecked();
|
||||
await sortOrderVisibleCheckbox.click();
|
||||
await expect(sortOrderVisibleCheckbox).not.toBeChecked();
|
||||
|
||||
// Save settings & close
|
||||
await settingsScreen.okayButton.click();
|
||||
await mainScreen.waitFor();
|
||||
|
||||
await expect(mainScreen.noteListContainer.locator('[title^="Toggle sort order"]')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('clicking on an external link should try to launch a browser', async ({ electronApp, mainWindow }) => {
|
||||
const mainScreen = new MainScreen(mainWindow);
|
||||
await mainScreen.waitFor();
|
||||
|
||||
// Mock openExternal
|
||||
const nextExternalUrlPromise = electronApp.evaluate(({ shell }) => {
|
||||
return new Promise<string>(resolve => {
|
||||
const openExternal = async (url: string) => {
|
||||
resolve(url);
|
||||
};
|
||||
shell.openExternal = openExternal;
|
||||
});
|
||||
});
|
||||
|
||||
// Create a test link
|
||||
const testLinkTitle = 'This is a test link!';
|
||||
const linkHref = 'https://joplinapp.org/';
|
||||
|
||||
await mainWindow.evaluate(({ testLinkTitle, linkHref }) => {
|
||||
const testLink = document.createElement('a');
|
||||
testLink.textContent = testLinkTitle;
|
||||
testLink.onclick = () => {
|
||||
// We need to navigate by setting location.href -- clicking on a link
|
||||
// directly within the main window (i.e. not in a PDF viewer) doesn't
|
||||
// navigate.
|
||||
location.href = linkHref;
|
||||
};
|
||||
testLink.href = '#';
|
||||
|
||||
// Display on top of everything
|
||||
testLink.style.zIndex = '99999';
|
||||
testLink.style.position = 'fixed';
|
||||
testLink.style.top = '0';
|
||||
testLink.style.left = '0';
|
||||
|
||||
document.body.appendChild(testLink);
|
||||
}, { testLinkTitle, linkHref });
|
||||
|
||||
const testLink = mainWindow.getByText(testLinkTitle);
|
||||
await expect(testLink).toBeVisible();
|
||||
await testLink.click({ noWaitAfter: true });
|
||||
|
||||
expect(await nextExternalUrlPromise).toBe(linkHref);
|
||||
});
|
||||
});
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
import NoteEditorScreen from './NoteEditorScreen';
|
||||
|
||||
export default class MainScreen {
|
||||
public readonly newNoteButton: Locator;
|
||||
public readonly noteListContainer: Locator;
|
||||
public readonly noteEditor: NoteEditorScreen;
|
||||
|
||||
public constructor(page: Page) {
|
||||
this.newNoteButton = page.locator('.new-note-button');
|
||||
this.noteListContainer = page.locator('.rli-noteList');
|
||||
this.noteEditor = new NoteEditorScreen(page);
|
||||
}
|
||||
|
||||
public async waitFor() {
|
||||
await this.newNoteButton.waitFor();
|
||||
await this.noteEditor.waitFor();
|
||||
await this.noteListContainer.waitFor();
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
|
||||
import { Locator, Page } from '@playwright/test';
|
||||
|
||||
export default class NoteEditorPage {
|
||||
public readonly codeMirrorEditor: Locator;
|
||||
public readonly noteTitleInput: Locator;
|
||||
private readonly containerLocator: Locator;
|
||||
|
||||
public constructor(private readonly page: Page) {
|
||||
this.containerLocator = page.locator('.rli-editor');
|
||||
this.codeMirrorEditor = this.containerLocator.locator('.codeMirrorEditor');
|
||||
this.noteTitleInput = this.containerLocator.locator('.title-input');
|
||||
}
|
||||
|
||||
public getNoteViewerIframe() {
|
||||
// The note viewer can change content when the note re-renders. As such,
|
||||
// a new locator needs to be created after re-renders (and this can't be a
|
||||
// static property).
|
||||
return this.page.frame({ url: /.*note-viewer[/\\]index.html.*/ });
|
||||
}
|
||||
|
||||
public async waitFor() {
|
||||
await this.codeMirrorEditor.waitFor();
|
||||
await this.noteTitleInput.waitFor();
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
|
||||
export default class SettingsScreen {
|
||||
public readonly okayButton: Locator;
|
||||
public readonly appearanceTabButton: Locator;
|
||||
|
||||
public constructor(page: Page) {
|
||||
this.okayButton = page.locator('button', { hasText: 'OK' });
|
||||
this.appearanceTabButton = page.getByText('Appearance');
|
||||
}
|
||||
|
||||
public async waitFor() {
|
||||
await this.okayButton.waitFor();
|
||||
await this.appearanceTabButton.waitFor();
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "Running desktop integration tests..."
|
||||
|
||||
export CI=true
|
||||
|
||||
if test "$RUNNER_OS" = "Linux" ; then
|
||||
# The Ubuntu Github CI doesn't have a display server.
|
||||
# Start a virtual one with xvfb-run.
|
||||
xvfb-run -- yarn run playwright test
|
||||
else
|
||||
yarn run playwright test
|
||||
fi
|
||||
@@ -1,36 +0,0 @@
|
||||
|
||||
import type { ElectronApplication } from '@playwright/test';
|
||||
import type { MenuItem } from 'electron';
|
||||
|
||||
|
||||
// Roughly based on
|
||||
// https://github.com/spaceagetv/electron-playwright-helpers/blob/main/src/menu_helpers.ts
|
||||
|
||||
// `menuItemPath` should be a list of menu labels (e.g. [["&JoplinMainMenu", "&File"], "Synchronise"]).
|
||||
const activateMainMenuItem = (electronApp: ElectronApplication, menuItemLabel: string) => {
|
||||
return electronApp.evaluate(async ({ Menu }, menuItemLabel) => {
|
||||
const activateItemInSubmenu = (submenu: MenuItem[]) => {
|
||||
for (const item of submenu) {
|
||||
if (item.label === menuItemLabel && item.visible) {
|
||||
// Found!
|
||||
item.click();
|
||||
return true;
|
||||
} else if (item.submenu) {
|
||||
const foundItem = activateItemInSubmenu(item.submenu.items);
|
||||
|
||||
if (foundItem) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No item found
|
||||
return false;
|
||||
};
|
||||
|
||||
const appMenu = Menu.getApplicationMenu();
|
||||
return activateItemInSubmenu(appMenu.items);
|
||||
}, menuItemLabel);
|
||||
};
|
||||
|
||||
export default activateMainMenuItem;
|
||||
@@ -1,45 +0,0 @@
|
||||
import { resolve, join, dirname } from 'path';
|
||||
import { remove, mkdirp } from 'fs-extra';
|
||||
import { _electron as electron, Page, ElectronApplication, test as base } from '@playwright/test';
|
||||
import uuid from '@joplin/lib/uuid';
|
||||
|
||||
|
||||
|
||||
type JoplinFixtures = {
|
||||
electronApp: ElectronApplication;
|
||||
mainWindow: Page;
|
||||
};
|
||||
|
||||
// A custom fixture that loads an electron app. See
|
||||
// https://playwright.dev/docs/test-fixtures
|
||||
|
||||
export const test = base.extend<JoplinFixtures>({
|
||||
// Playwright fails if we don't use the object destructuring
|
||||
// pattern in the first argument.
|
||||
//
|
||||
// See https://github.com/microsoft/playwright/issues/8798
|
||||
//
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
electronApp: async ({ }, use) => {
|
||||
const profilePath = resolve(join(dirname(__dirname), 'test-profile'));
|
||||
const profileSubdir = join(profilePath, uuid.createNano());
|
||||
await mkdirp(profileSubdir);
|
||||
|
||||
const startupArgs = ['main.js', '--env', 'dev', '--profile', profileSubdir];
|
||||
const electronApp = await electron.launch({ args: startupArgs });
|
||||
|
||||
await use(electronApp);
|
||||
|
||||
await electronApp.firstWindow();
|
||||
await electronApp.close();
|
||||
await remove(profileSubdir);
|
||||
},
|
||||
|
||||
mainWindow: async ({ electronApp }, use) => {
|
||||
const window = await electronApp.firstWindow();
|
||||
await use(window);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
export { expect } from '@playwright/test';
|
||||
@@ -31,6 +31,12 @@ const React = require('react');
|
||||
const nodeSqlite = require('sqlite3');
|
||||
const initLib = require('@joplin/lib/initLib').default;
|
||||
|
||||
// Security: If we attempt to navigate away from the root HTML page, it's likely because
|
||||
// of an improperly sanitized link. Prevent this by closing the window before we can
|
||||
// navigate away.
|
||||
window.onbeforeunload = () => {
|
||||
window.close();
|
||||
};
|
||||
|
||||
if (bridge().env() === 'dev') {
|
||||
const newConsole = function(oldConsole) {
|
||||
|
||||
@@ -40,17 +40,18 @@ a {
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(100, 100, 100, 0.3);
|
||||
background: rgba(150, 149, 149, 0.4);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(223, 223, 223, 0.6);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track:hover {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(100, 100, 100, 0.7);
|
||||
}
|
||||
|
||||
.fade_out {
|
||||
-webkit-transition: 0.15s;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.13.3",
|
||||
"version": "2.13.1",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"private": true,
|
||||
@@ -14,8 +14,7 @@
|
||||
"watch": "tsc --watch --preserveWatchOutput --project tsconfig.json",
|
||||
"start": "gulp build && electron . --env dev --log-level debug --open-dev-tools",
|
||||
"test": "jest",
|
||||
"test-ui": "playwright test",
|
||||
"test-ci": "yarn test && sh ./integration-tests/run-ci.sh",
|
||||
"test-ci": "yarn test",
|
||||
"renameReleaseAssets": "node tools/renameReleaseAssets.js"
|
||||
},
|
||||
"repository": {
|
||||
@@ -117,16 +116,15 @@
|
||||
"devDependencies": {
|
||||
"@electron/rebuild": "3.3.0",
|
||||
"@joplin/tools": "~2.13",
|
||||
"@playwright/test": "1.38.1",
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@types/jest": "29.5.4",
|
||||
"@types/node": "18.17.19",
|
||||
"@types/react": "18.2.24",
|
||||
"@types/react-redux": "7.1.27",
|
||||
"@types/styled-components": "5.1.28",
|
||||
"electron": "25.9.0",
|
||||
"@types/node": "18.17.12",
|
||||
"@types/react": "18.2.21",
|
||||
"@types/react-redux": "7.1.26",
|
||||
"@types/styled-components": "5.1.26",
|
||||
"electron": "25.3.1",
|
||||
"electron-builder": "24.4.0",
|
||||
"glob": "10.3.10",
|
||||
"glob": "10.3.3",
|
||||
"gulp": "4.0.2",
|
||||
"jest": "29.6.4",
|
||||
"jest-environment-jsdom": "29.6.4",
|
||||
@@ -142,18 +140,17 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron/notarize": "2.1.0",
|
||||
"@electron/remote": "2.0.11",
|
||||
"@electron/remote": "2.0.10",
|
||||
"@fortawesome/fontawesome-free": "5.15.4",
|
||||
"@joeattardi/emoji-button": "4.6.4",
|
||||
"@joplin/editor": "~2.13",
|
||||
"@joplin/lib": "~2.13",
|
||||
"@joplin/renderer": "~2.13",
|
||||
"@joplin/utils": "~2.13",
|
||||
"@types/mustache": "4.2.3",
|
||||
"@types/mustache": "4.2.2",
|
||||
"async-mutex": "0.4.0",
|
||||
"codemirror": "5.65.9",
|
||||
"color": "3.2.1",
|
||||
"compare-versions": "6.1.0",
|
||||
"compare-versions": "3.6.0",
|
||||
"countable": "3.0.1",
|
||||
"debounce": "1.2.1",
|
||||
"electron-window-state": "5.0.3",
|
||||
@@ -174,8 +171,8 @@
|
||||
"react": "18.2.0",
|
||||
"react-datetime": "3.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-redux": "8.1.3",
|
||||
"react-select": "5.7.7",
|
||||
"react-redux": "8.1.2",
|
||||
"react-select": "5.7.4",
|
||||
"react-toggle-button": "2.2.0",
|
||||
"react-tooltip": "4.5.1",
|
||||
"redux": "4.2.1",
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { defineConfig } from '@playwright/test';
|
||||
|
||||
// See https://playwright.dev/docs/test-configuration.
|
||||
export default defineConfig({
|
||||
testDir: './integration-tests',
|
||||
|
||||
// Only match .ts files (no compiled .js files)
|
||||
testMatch: '*.spec.ts',
|
||||
|
||||
// Allow running tests in parallel (note: each Joplin instance
|
||||
// is given its own profile directory).
|
||||
fullyParallel: true,
|
||||
|
||||
// Fail the build on CI if you accidentally left test.only in the source code.
|
||||
forbidOnly: !!process.env.CI,
|
||||
|
||||
// Retry on CI only
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
|
||||
// Opt out of parallel tests on CI
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
|
||||
// Reporter to use. See https://playwright.dev/docs/test-reporters
|
||||
reporter: process.env.CI ? 'line' : 'html',
|
||||
|
||||
// Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions.
|
||||
use: {
|
||||
// Base URL to use in actions like `await page.goto('/')`.
|
||||
// baseURL: 'http://127.0.0.1:3000',
|
||||
|
||||
// Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
});
|
||||
@@ -12,9 +12,8 @@ export default function stateToWhenClauseContext(state: AppState, options: WhenC
|
||||
...libStateToWhenClauseContext(state, options),
|
||||
|
||||
// UI elements
|
||||
markdownEditorVisible: !!state.settings['editor.codeView'] && !state.settings['isSafeMode'],
|
||||
richTextEditorVisible: !state.settings['editor.codeView'] && !state.settings['isSafeMode'],
|
||||
|
||||
markdownEditorVisible: !!state.settings['editor.codeView'],
|
||||
richTextEditorVisible: !state.settings['editor.codeView'],
|
||||
markdownEditorPaneVisible: state.settings['editor.codeView'] && state.noteVisiblePanes.includes('editor'),
|
||||
markdownViewerPaneVisible: state.settings['editor.codeView'] && state.noteVisiblePanes.includes('viewer'),
|
||||
modalDialogVisible: !!Object.keys(state.visibleDialogs).length,
|
||||
|
||||
@@ -5,22 +5,17 @@
|
||||
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
TEMP_PATH=~/src/plugin-tests
|
||||
NEED_COMPILING=0
|
||||
PLUGIN_PATH=~/src/joplin/packages/app-cli/tests/support/plugins/simple
|
||||
PLUGIN_PATH=~/src/joplin/packages/app-cli/tests/support/plugins/note_list_renderer
|
||||
|
||||
if [[ $NEED_COMPILING == 1 ]]; then
|
||||
mkdir -p "$TEMP_PATH"
|
||||
PLUGIN_NAME=$(echo "$PLUGIN_PATH" | awk -F/ '{print $NF}')
|
||||
TEMP_PLUGIN_PATH="$TEMP_PATH/$PLUGIN_NAME"
|
||||
mkdir -p "$TEMP_PATH"
|
||||
PLUGIN_NAME=$(echo "$PLUGIN_PATH" | awk -F/ '{print $NF}')
|
||||
TEMP_PLUGIN_PATH="$TEMP_PATH/$PLUGIN_NAME"
|
||||
|
||||
echo "Copying from: $PLUGIN_PATH"
|
||||
echo "To: $TEMP_PLUGIN_PATH"
|
||||
echo "Copying from: $PLUGIN_PATH"
|
||||
echo "To: $TEMP_PLUGIN_PATH"
|
||||
|
||||
rsync -a --delete "$PLUGIN_PATH/" "$TEMP_PLUGIN_PATH/"
|
||||
rsync -a --delete "$PLUGIN_PATH/" "$TEMP_PLUGIN_PATH/"
|
||||
|
||||
npm install --prefix="$TEMP_PLUGIN_PATH" && yarn start --dev-plugins "$TEMP_PLUGIN_PATH"
|
||||
else
|
||||
yarn start --dev-plugins "$PLUGIN_PATH"
|
||||
fi
|
||||
npm install --prefix="$TEMP_PLUGIN_PATH" && yarn start --dev-plugins "$TEMP_PLUGIN_PATH"
|
||||
|
||||
# Add eg "--profile $HOME/.config/joplindev-desktop-1" to test with a different profile
|
||||
5
packages/app-mobile/.gitignore
vendored
5
packages/app-mobile/.gitignore
vendored
@@ -66,10 +66,9 @@ yarn-error.log
|
||||
lib/csstojs/
|
||||
lib/rnInjectedJs/
|
||||
dist/
|
||||
components/NoteEditor/**/*.bundle.js
|
||||
components/NoteEditor/CodeMirror/CodeMirror.bundle.js
|
||||
components/NoteEditor/CodeMirror/CodeMirror.bundle.min.js
|
||||
components/NoteEditor/**/*.bundle.js.md5
|
||||
components/NoteEditor/**/*.bundle.min.js
|
||||
components/NoteEditor/**/*.bundle.js.LICENSE.txt
|
||||
|
||||
utils/fs-driver-android.js
|
||||
android/app/build-*
|
||||
|
||||
@@ -110,8 +110,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097722
|
||||
versionName "2.13.2"
|
||||
versionCode 2097720
|
||||
versionName "2.13.0"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ const ActionButton = (props: ActionButtonProps) => {
|
||||
};
|
||||
}), [props.buttons]);
|
||||
|
||||
const closedIcon = useIcon(props.mainButton?.icon ?? 'add');
|
||||
const closedIcon = useIcon(props.mainButton?.icon ?? 'md-add');
|
||||
const openIcon = useIcon('close');
|
||||
|
||||
return (
|
||||
|
||||
@@ -158,11 +158,11 @@ class CameraView extends Component {
|
||||
}
|
||||
|
||||
public render() {
|
||||
const photoIcon = this.state.snapping ? 'checkmark' : 'camera';
|
||||
const photoIcon = this.state.snapping ? 'md-checkmark' : 'md-camera';
|
||||
|
||||
const displayRatios = this.supportsRatios() && this.state.ratios.length > 1;
|
||||
|
||||
const reverseCameraButton = this.renderButton(this.reverse_onPress, 'camera-reverse', { flex: 1, flexDirection: 'row', justifyContent: 'flex-start', marginLeft: 20 });
|
||||
const reverseCameraButton = this.renderButton(this.reverse_onPress, 'md-camera-reverse', { flex: 1, flexDirection: 'row', justifyContent: 'flex-start', marginLeft: 20 });
|
||||
const ratioButton = !displayRatios ? <View style={{ flex: 1 }}/> : this.renderButton(this.ratio_onPress, <Text style={{ fontWeight: 'bold', fontSize: 20 }}>{Setting.value('camera.ratio')}</Text>, { flex: 1, flexDirection: 'row', justifyContent: 'flex-end', marginRight: 20 });
|
||||
|
||||
let cameraRatio = '4:3';
|
||||
@@ -202,7 +202,7 @@ class CameraView extends Component {
|
||||
<TouchableOpacity onPress={this.back_onPress}>
|
||||
<View style={{ marginLeft: 5, marginTop: 5, borderColor: '#00000040', borderWidth: 1, borderStyle: 'solid', borderRadius: 90, width: 50, height: 50, display: 'flex', backgroundColor: '#ffffff77', justifyContent: 'center', alignItems: 'center' }}>
|
||||
<Icon
|
||||
name={'arrow-back'}
|
||||
name={'md-arrow-back'}
|
||||
style={{
|
||||
fontSize: 40,
|
||||
color: 'black',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useRef, useCallback } from 'react';
|
||||
|
||||
import useSource from './hooks/useSource';
|
||||
import useOnMessage, { HandleMessageCallback, OnMarkForDownloadCallback } from './hooks/useOnMessage';
|
||||
import useOnMessage from './hooks/useOnMessage';
|
||||
import useOnResourceLongPress from './hooks/useOnResourceLongPress';
|
||||
|
||||
const React = require('react');
|
||||
@@ -19,11 +19,14 @@ interface Props {
|
||||
noteResources: any;
|
||||
paddingBottom: number;
|
||||
noteHash: string;
|
||||
onJoplinLinkClick: HandleMessageCallback;
|
||||
onCheckboxChange?: HandleMessageCallback;
|
||||
onRequestEditResource?: HandleMessageCallback;
|
||||
onMarkForDownload?: OnMarkForDownloadCallback;
|
||||
onLoadEnd?: ()=> void;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
onJoplinLinkClick: Function;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
onCheckboxChange?: Function;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
onMarkForDownload?: Function;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
onLoadEnd?: Function;
|
||||
}
|
||||
|
||||
const webViewStyle = {
|
||||
@@ -44,22 +47,16 @@ export default function NoteBodyViewer(props: Props) {
|
||||
);
|
||||
|
||||
const onResourceLongPress = useOnResourceLongPress(
|
||||
{
|
||||
onJoplinLinkClick: props.onJoplinLinkClick,
|
||||
onRequestEditResource: props.onRequestEditResource,
|
||||
},
|
||||
props.onJoplinLinkClick,
|
||||
dialogBoxRef,
|
||||
);
|
||||
|
||||
const onMessage = useOnMessage(
|
||||
props.onCheckboxChange,
|
||||
props.noteBody,
|
||||
{
|
||||
onCheckboxChange: props.onCheckboxChange,
|
||||
onMarkForDownload: props.onMarkForDownload,
|
||||
onJoplinLinkClick: props.onJoplinLinkClick,
|
||||
onRequestEditResource: props.onRequestEditResource,
|
||||
onResourceLongPress,
|
||||
},
|
||||
props.onMarkForDownload,
|
||||
props.onJoplinLinkClick,
|
||||
onResourceLongPress,
|
||||
);
|
||||
|
||||
const onLoadEnd = useCallback(() => {
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import { writeFileSync } from 'fs-extra';
|
||||
import { join } from 'path';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
// Mock react-native-vector-icons -- it uses ESM imports, which, by default, are not
|
||||
// supported by jest.
|
||||
jest.doMock('react-native-vector-icons/Ionicons', () => {
|
||||
return {
|
||||
default: {
|
||||
getImageSourceSync: () => {
|
||||
// Create an empty file that can be read/used as an image resource.
|
||||
const iconPath = join(Setting.value('cacheDir'), 'test-icon.png');
|
||||
writeFileSync(iconPath, '', 'utf-8');
|
||||
|
||||
return { uri: iconPath };
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
import lightTheme from '@joplin/lib/themes/light';
|
||||
import { editPopupClass, getEditPopupSource } from './useEditPopup';
|
||||
import { describe, it, expect, beforeAll, jest } from '@jest/globals';
|
||||
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
|
||||
const createEditPopup = (target: HTMLElement) => {
|
||||
const { createEditPopupSyntax } = getEditPopupSource(lightTheme);
|
||||
eval(`(${createEditPopupSyntax})`)(target, 'someresourceid', '() => {}');
|
||||
};
|
||||
|
||||
const destroyEditPopup = () => {
|
||||
const { destroyEditPopupSyntax } = getEditPopupSource(lightTheme);
|
||||
eval(`(${destroyEditPopupSyntax})`)();
|
||||
};
|
||||
|
||||
describe('useEditPopup', () => {
|
||||
beforeAll(async () => {
|
||||
// useEditPopup relies on the resourceDir setting, which is set by
|
||||
// switchClient.
|
||||
await setupDatabaseAndSynchronizer(0);
|
||||
await switchClient(0);
|
||||
});
|
||||
|
||||
it('should attach an edit popup to an image', () => {
|
||||
const container = document.createElement('div');
|
||||
const targetImage = document.createElement('img');
|
||||
container.appendChild(targetImage);
|
||||
|
||||
createEditPopup(targetImage);
|
||||
|
||||
// Popup should be present in the document
|
||||
expect(container.querySelector(`.${editPopupClass}`)).not.toBeNull();
|
||||
|
||||
// Destroy the edit popup
|
||||
jest.useFakeTimers();
|
||||
destroyEditPopup();
|
||||
|
||||
// Give time for the popup's fade out animation to run.
|
||||
jest.advanceTimersByTime(1000 * 10);
|
||||
|
||||
// Popup should be destroyed.
|
||||
expect(container.querySelector(`.${editPopupClass}`)).toBeNull();
|
||||
|
||||
targetImage.remove();
|
||||
});
|
||||
|
||||
it('should auto-remove the edit popup after a delay', () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
const container = document.createElement('div');
|
||||
const targetImage = document.createElement('img');
|
||||
container.appendChild(targetImage);
|
||||
|
||||
jest.useFakeTimers();
|
||||
createEditPopup(targetImage);
|
||||
|
||||
|
||||
expect(container.querySelector(`.${editPopupClass}`)).not.toBeNull();
|
||||
jest.advanceTimersByTime(1000 * 20); // ms
|
||||
expect(container.querySelector(`.${editPopupClass}`)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,145 +0,0 @@
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import { Theme } from '@joplin/lib/themes/type';
|
||||
import { useMemo } from 'react';
|
||||
import { extname } from 'path';
|
||||
import shim from '@joplin/lib/shim';
|
||||
const Icon = require('react-native-vector-icons/Ionicons').default;
|
||||
|
||||
export const editPopupClass = 'joplin-editPopup';
|
||||
|
||||
const getEditIconSrc = (theme: Theme) => {
|
||||
const iconUri = Icon.getImageSourceSync('pencil', 20, theme.color2).uri;
|
||||
|
||||
// Copy to a location that can be read within a WebView
|
||||
// (necessary on iOS)
|
||||
const destPath = `${Setting.value('resourceDir')}/edit-icon${extname(iconUri)}`;
|
||||
|
||||
// Copy in the background -- the edit icon popover script doesn't need the
|
||||
// icon immediately.
|
||||
void (async () => {
|
||||
await shim.fsDriver().copy(iconUri, destPath);
|
||||
})();
|
||||
|
||||
return destPath;
|
||||
};
|
||||
|
||||
// Creates JavaScript/CSS that can be used to create an "Edit" button.
|
||||
// Exported to facilitate testing.
|
||||
export const getEditPopupSource = (theme: Theme) => {
|
||||
const fadeOutDelay = 400;
|
||||
const editPopupDestroyDelay = 5000;
|
||||
|
||||
const editPopupCss = `
|
||||
@keyframes fade-in {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
0% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
|
||||
.${editPopupClass} {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
|
||||
/* Don't take up any space in the line, overlay the button */
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: visible;
|
||||
|
||||
--edit-popup-width: 40px;
|
||||
--edit-popup-padding: 10px;
|
||||
|
||||
/* Shift the popup such that it overlaps with the previous element. */
|
||||
left: calc(0px - var(--edit-popup-width));
|
||||
|
||||
/* Match the top of the image */
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.${editPopupClass} > button {
|
||||
padding: var(--edit-popup-padding);
|
||||
width: var(--edit-popup-width);
|
||||
|
||||
animation: fade-in 0.4s ease;
|
||||
|
||||
background-color: ${theme.backgroundColor2};
|
||||
color: ${theme.color2};
|
||||
|
||||
border: none;
|
||||
}
|
||||
|
||||
.${editPopupClass} img {
|
||||
/* Make the image take up as much space as possible (minus padding) */
|
||||
width: calc(var(--edit-popup-width) - var(--edit-popup-padding));
|
||||
}
|
||||
|
||||
.${editPopupClass}.fadeOut {
|
||||
animation: fade-out ${fadeOutDelay}ms ease;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
const destroyEditPopupSyntax = `() => {
|
||||
if (!window.editPopup) {
|
||||
return;
|
||||
}
|
||||
|
||||
const popup = editPopup;
|
||||
popup.classList.add('fadeOut');
|
||||
window.editPopup = null;
|
||||
|
||||
setTimeout(() => {
|
||||
popup.remove();
|
||||
}, ${fadeOutDelay});
|
||||
}`;
|
||||
|
||||
const createEditPopupSyntax = `(parent, resourceId, onclick) => {
|
||||
if (window.editPopupTimeout) {
|
||||
clearTimeout(window.editPopupTimeout);
|
||||
window.editPopupTimeout = undefined;
|
||||
}
|
||||
|
||||
window.editPopupTimeout = setTimeout(${destroyEditPopupSyntax}, ${editPopupDestroyDelay});
|
||||
|
||||
if (window.lastEditPopupTarget !== parent) {
|
||||
(${destroyEditPopupSyntax})();
|
||||
} else if (window.editPopup) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.editPopup = document.createElement('div');
|
||||
const popupButton = document.createElement('button');
|
||||
|
||||
const popupIcon = new Image();
|
||||
popupIcon.alt = ${JSON.stringify(_('Edit'))};
|
||||
popupIcon.title = popupIcon.alt;
|
||||
popupIcon.src = ${JSON.stringify(getEditIconSrc(theme))};
|
||||
popupButton.appendChild(popupIcon);
|
||||
|
||||
popupButton.onclick = onclick;
|
||||
editPopup.appendChild(popupButton);
|
||||
|
||||
editPopup.classList.add(${JSON.stringify(editPopupClass)});
|
||||
parent.insertAdjacentElement('afterEnd', editPopup);
|
||||
|
||||
// Ensure that the edit popup is focused immediately by screen
|
||||
// readers.
|
||||
editPopup.focus();
|
||||
window.lastEditPopupTarget = parent;
|
||||
}`;
|
||||
|
||||
return { createEditPopupSyntax, destroyEditPopupSyntax, editPopupCss };
|
||||
};
|
||||
|
||||
const useEditPopup = (themeId: number) => {
|
||||
return useMemo(() => {
|
||||
return getEditPopupSource(themeStyle(themeId));
|
||||
}, [themeId]);
|
||||
};
|
||||
|
||||
export default useEditPopup;
|
||||
@@ -1,31 +1,8 @@
|
||||
import { useCallback } from 'react';
|
||||
import shared from '@joplin/lib/components/shared/note-screen-shared';
|
||||
|
||||
export type HandleMessageCallback = (message: string)=> void;
|
||||
export type OnMarkForDownloadCallback = (resource: { resourceId: string })=> void;
|
||||
|
||||
interface MessageCallbacks {
|
||||
onMarkForDownload?: OnMarkForDownloadCallback;
|
||||
onJoplinLinkClick: HandleMessageCallback;
|
||||
onResourceLongPress: HandleMessageCallback;
|
||||
onRequestEditResource?: HandleMessageCallback;
|
||||
onCheckboxChange: HandleMessageCallback;
|
||||
}
|
||||
|
||||
export default function useOnMessage(
|
||||
noteBody: string,
|
||||
callbacks: MessageCallbacks,
|
||||
) {
|
||||
// Dectructure callbacks. Because we have that ({ a: 1 }) !== ({ a: 1 }),
|
||||
// we can expect the `callbacks` variable from the last time useOnMessage was called to
|
||||
// not equal the current` callbacks` variable, even if the callbacks themselves are the
|
||||
// same.
|
||||
//
|
||||
// Thus, useCallback should depend on each callback individually.
|
||||
const {
|
||||
onMarkForDownload, onResourceLongPress, onCheckboxChange, onRequestEditResource, onJoplinLinkClick,
|
||||
} = callbacks;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
export default function useOnMessage(onCheckboxChange: Function, noteBody: string, onMarkForDownload: Function, onJoplinLinkClick: Function, onResourceLongPress: Function) {
|
||||
return useCallback((event: any) => {
|
||||
// 2021-05-19: Historically this was unescaped twice as it was
|
||||
// apparently needed after an upgrade to RN 58 (or 59). However this is
|
||||
@@ -40,15 +17,13 @@ export default function useOnMessage(
|
||||
|
||||
if (msg.indexOf('checkboxclick:') === 0) {
|
||||
const newBody = shared.toggleCheckbox(msg, noteBody);
|
||||
onCheckboxChange?.(newBody);
|
||||
if (onCheckboxChange) onCheckboxChange(newBody);
|
||||
} else if (msg.indexOf('markForDownload:') === 0) {
|
||||
const splittedMsg = msg.split(':');
|
||||
const resourceId = splittedMsg[1];
|
||||
onMarkForDownload?.({ resourceId: resourceId });
|
||||
if (onMarkForDownload) onMarkForDownload({ resourceId: resourceId });
|
||||
} else if (msg.startsWith('longclick:')) {
|
||||
onResourceLongPress(msg);
|
||||
} else if (msg.startsWith('edit:')) {
|
||||
onRequestEditResource?.(msg);
|
||||
} else if (msg.startsWith('joplin:')) {
|
||||
onJoplinLinkClick(msg);
|
||||
} else if (msg.startsWith('error:')) {
|
||||
@@ -56,12 +31,5 @@ export default function useOnMessage(
|
||||
} else {
|
||||
onJoplinLinkClick(msg);
|
||||
}
|
||||
}, [
|
||||
noteBody,
|
||||
onCheckboxChange,
|
||||
onMarkForDownload,
|
||||
onJoplinLinkClick,
|
||||
onResourceLongPress,
|
||||
onRequestEditResource,
|
||||
]);
|
||||
}, [onCheckboxChange, noteBody, onMarkForDownload, onJoplinLinkClick, onResourceLongPress]);
|
||||
}
|
||||
|
||||
@@ -6,40 +6,20 @@ import { reg } from '@joplin/lib/registry';
|
||||
const { dialogs } = require('../../../utils/dialogs.js');
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
import { copyToCache } from '../../../utils/ShareUtils';
|
||||
import isEditableResource from '../../NoteEditor/ImageEditor/isEditableResource';
|
||||
const Share = require('react-native-share').default;
|
||||
|
||||
interface Callbacks {
|
||||
onJoplinLinkClick: (link: string)=> void;
|
||||
onRequestEditResource: (message: string)=> void;
|
||||
}
|
||||
|
||||
export default function useOnResourceLongPress(callbacks: Callbacks, dialogBoxRef: any) {
|
||||
const { onJoplinLinkClick, onRequestEditResource } = callbacks;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
export default function useOnResourceLongPress(onJoplinLinkClick: Function, dialogBoxRef: any) {
|
||||
return useCallback(async (msg: string) => {
|
||||
try {
|
||||
const resourceId = msg.split(':')[1];
|
||||
const resource = await Resource.load(resourceId);
|
||||
|
||||
// Handle the case where it's a long press on a link with no resource
|
||||
if (!resource) {
|
||||
reg.logger().warn(`Long-press: Resource with ID ${resourceId} does not exist (may be a note).`);
|
||||
return;
|
||||
}
|
||||
|
||||
const name = resource.title ? resource.title : resource.file_name;
|
||||
const mime: string|undefined = resource.mime;
|
||||
|
||||
const actions = [];
|
||||
|
||||
actions.push({ text: _('Open'), id: 'open' });
|
||||
if (mime && isEditableResource(mime)) {
|
||||
actions.push({ text: _('Edit'), id: 'edit' });
|
||||
}
|
||||
actions.push({ text: _('Share'), id: 'share' });
|
||||
|
||||
const action = await dialogs.pop({ dialogbox: dialogBoxRef.current }, name, actions);
|
||||
const action = await dialogs.pop({ dialogbox: dialogBoxRef.current }, name, [
|
||||
{ text: _('Open'), id: 'open' },
|
||||
{ text: _('Share'), id: 'share' },
|
||||
]);
|
||||
|
||||
if (action === 'open') {
|
||||
onJoplinLinkClick(`joplin://${resourceId}`);
|
||||
@@ -52,12 +32,11 @@ export default function useOnResourceLongPress(callbacks: Callbacks, dialogBoxRe
|
||||
url: `file://${fileToShare}`,
|
||||
failOnCancel: false,
|
||||
});
|
||||
} else if (action === 'edit') {
|
||||
onRequestEditResource(`edit:${resourceId}`);
|
||||
}
|
||||
} catch (e) {
|
||||
reg.logger().error('Could not handle link long press', e);
|
||||
ToastAndroid.show('An error occurred, check log for details', ToastAndroid.SHORT);
|
||||
}
|
||||
}, [onJoplinLinkClick, onRequestEditResource, dialogBoxRef]);
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [onJoplinLinkClick]);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import shim from '@joplin/lib/shim';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
const { themeStyle } = require('../../global-style.js');
|
||||
import markupLanguageUtils from '@joplin/lib/markupLanguageUtils';
|
||||
import useEditPopup from './useEditPopup';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
const { assetsToHeaders } = require('@joplin/renderer');
|
||||
|
||||
@@ -91,8 +90,6 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
|
||||
const onlyNoteBodyHasChanged = Object.keys(changedDeps).length === 1 && changedDeps[0];
|
||||
const onlyCheckboxesHaveChanged = previousDeps[0] && changedDeps[0] && onlyCheckboxHasChangedHack(previousDeps[0], noteBody);
|
||||
|
||||
const { createEditPopupSyntax, destroyEditPopupSyntax, editPopupCss } = useEditPopup(themeId);
|
||||
|
||||
useEffect(() => {
|
||||
if (onlyNoteBodyHasChanged && onlyCheckboxesHaveChanged) {
|
||||
logger.info('Only a checkbox has changed - not updating HTML');
|
||||
@@ -115,11 +112,6 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
|
||||
codeTheme: theme.codeThemeCss,
|
||||
postMessageSyntax: 'window.joplinPostMessage_',
|
||||
enableLongPress: true,
|
||||
|
||||
// Show an 'edit' popup over SVG images
|
||||
editPopupFiletypes: ['image/svg+xml'],
|
||||
createEditPopupSyntax,
|
||||
destroyEditPopupSyntax,
|
||||
};
|
||||
|
||||
// Whenever a resource state changes, for example when it goes from "not downloaded" to "downloaded", the "noteResources"
|
||||
@@ -217,9 +209,7 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
${defaultCss}
|
||||
${shim.mobilePlatform() === 'ios' ? iOSSpecificCss : ''}
|
||||
${editPopupCss}
|
||||
${shim.mobilePlatform() === 'ios' ? `${iOSSpecificCss}\n${defaultCss}` : defaultCss}
|
||||
</style>
|
||||
${assetsToHeaders(result.pluginAssets, { asHtml: true })}
|
||||
</head>
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import { EditorSettings } from '../types';
|
||||
import { initCodeMirror } from './CodeMirror';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { forceParsing } from '@codemirror/language';
|
||||
import loadLangauges from './testUtil/loadLanguages';
|
||||
|
||||
import { expect, describe, it } from '@jest/globals';
|
||||
|
||||
|
||||
const createEditorSettings = (themeId: number) => {
|
||||
const themeData = themeStyle(themeId);
|
||||
const editorSettings: EditorSettings = {
|
||||
katexEnabled: true,
|
||||
spellcheckEnabled: true,
|
||||
readOnly: false,
|
||||
themeId,
|
||||
themeData,
|
||||
};
|
||||
|
||||
return editorSettings;
|
||||
};
|
||||
|
||||
describe('CodeMirror', () => {
|
||||
// This checks for a regression -- occasionally, when updating packages,
|
||||
// syntax highlighting in the CodeMirror editor stops working. This is usually
|
||||
// fixed by
|
||||
// 1. removing all `@codemirror/` and `@lezer/` dependencies from yarn.lock,
|
||||
// 2. upgrading all CodeMirror packages to the latest versions in package.json, and
|
||||
// 3. re-running `yarn install`.
|
||||
//
|
||||
// See https://github.com/laurent22/joplin/issues/7253
|
||||
it('should give headings a different style', async () => {
|
||||
const headerLineText = '# Testing...';
|
||||
const initialText = `${headerLineText}\nThis is a test.`;
|
||||
const editorSettings = createEditorSettings(Setting.THEME_LIGHT);
|
||||
|
||||
await loadLangauges();
|
||||
const editor = initCodeMirror(document.body, initialText, editorSettings);
|
||||
|
||||
// Force the generation of the syntax tree now.
|
||||
forceParsing(editor.editor);
|
||||
|
||||
// CodeMirror nests the tag that styles the header within .cm-headerLine:
|
||||
// <div class='cm-headerLine'><span class='someclass'>Testing...</span></div>
|
||||
const headerLineContent = document.body.querySelector('.cm-headerLine > span')!;
|
||||
|
||||
|
||||
expect(headerLineContent.textContent).toBe(headerLineText);
|
||||
|
||||
const style = getComputedStyle(headerLineContent);
|
||||
expect(style.borderBottom).not.toBe('');
|
||||
expect(style.fontSize).toBe('1.6em');
|
||||
});
|
||||
});
|
||||
@@ -9,24 +9,448 @@
|
||||
// wrapper to access CodeMirror functionalities. Anything else should be done
|
||||
// from NoteEditor.tsx.
|
||||
|
||||
import { EditorSettings } from '@joplin/editor/types';
|
||||
import createEditor from '@joplin/editor/CodeMirror/createEditor';
|
||||
import { MarkdownMathExtension } from './markdownMathParser';
|
||||
import createTheme from './theme';
|
||||
import decoratorExtension from './decoratorExtension';
|
||||
|
||||
import syntaxHighlightingLanguages from './syntaxHighlightingLanguages';
|
||||
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { markdown } from '@codemirror/lang-markdown';
|
||||
import { GFM as GitHubFlavoredMarkdownExtension } from '@lezer/markdown';
|
||||
import { indentOnInput, indentUnit, syntaxTree } from '@codemirror/language';
|
||||
import {
|
||||
openSearchPanel, closeSearchPanel, SearchQuery, setSearchQuery, getSearchQuery,
|
||||
/* highlightSelectionMatches, */ search, findNext, findPrevious, replaceAll, replaceNext,
|
||||
} from '@codemirror/search';
|
||||
|
||||
import {
|
||||
EditorView, drawSelection, highlightSpecialChars, ViewUpdate, Command,
|
||||
} from '@codemirror/view';
|
||||
import { undo, redo, history, undoDepth, redoDepth, indentWithTab } from '@codemirror/commands';
|
||||
|
||||
import { keymap, KeyBinding } from '@codemirror/view';
|
||||
import { searchKeymap } from '@codemirror/search';
|
||||
import { historyKeymap, defaultKeymap } from '@codemirror/commands';
|
||||
|
||||
import { CodeMirrorControl } from './types';
|
||||
import { EditorSettings, ListType, SearchState } from '../types';
|
||||
import { ChangeEvent, SelectionChangeEvent, Selection } from '../types';
|
||||
import SelectionFormatting from '../SelectionFormatting';
|
||||
import { logMessage, postMessage } from './webviewLogger';
|
||||
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
|
||||
import {
|
||||
decreaseIndent, increaseIndent,
|
||||
toggleBolded, toggleCode,
|
||||
toggleHeaderLevel, toggleItalicized,
|
||||
toggleList, toggleMath, updateLink,
|
||||
} from './markdownCommands';
|
||||
|
||||
export function initCodeMirror(
|
||||
parentElement: HTMLElement, initialText: string, settings: EditorSettings,
|
||||
): CodeMirrorControl {
|
||||
return createEditor(parentElement, {
|
||||
initialText,
|
||||
settings,
|
||||
|
||||
onLogMessage: message => {
|
||||
logMessage(message);
|
||||
},
|
||||
onEvent: (event): void => {
|
||||
postMessage('onEditorEvent', event);
|
||||
},
|
||||
});
|
||||
interface CodeMirrorResult extends CodeMirrorControl {
|
||||
editor: EditorView;
|
||||
}
|
||||
|
||||
export function initCodeMirror(
|
||||
parentElement: any, initialText: string, settings: EditorSettings,
|
||||
): CodeMirrorResult {
|
||||
logMessage('Initializing CodeMirror...');
|
||||
const theme = settings.themeData;
|
||||
|
||||
let searchVisible = false;
|
||||
|
||||
let schedulePostUndoRedoDepthChangeId_: any = 0;
|
||||
const schedulePostUndoRedoDepthChange = (editor: EditorView, doItNow = false) => {
|
||||
if (schedulePostUndoRedoDepthChangeId_) {
|
||||
if (doItNow) {
|
||||
clearTimeout(schedulePostUndoRedoDepthChangeId_);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
schedulePostUndoRedoDepthChangeId_ = setTimeout(() => {
|
||||
schedulePostUndoRedoDepthChangeId_ = null;
|
||||
postMessage('onUndoRedoDepthChange', {
|
||||
undoDepth: undoDepth(editor.state),
|
||||
redoDepth: redoDepth(editor.state),
|
||||
});
|
||||
}, doItNow ? 0 : 1000);
|
||||
};
|
||||
|
||||
const notifyDocChanged = (viewUpdate: ViewUpdate) => {
|
||||
if (viewUpdate.docChanged) {
|
||||
const event: ChangeEvent = {
|
||||
value: editor.state.doc.toString(),
|
||||
};
|
||||
|
||||
postMessage('onChange', event);
|
||||
schedulePostUndoRedoDepthChange(editor);
|
||||
}
|
||||
};
|
||||
|
||||
const notifyLinkEditRequest = () => {
|
||||
postMessage('onRequestLinkEdit', null);
|
||||
};
|
||||
|
||||
const showSearchDialog = () => {
|
||||
const query = getSearchQuery(editor.state);
|
||||
const searchState: SearchState = {
|
||||
searchText: query.search,
|
||||
replaceText: query.replace,
|
||||
useRegex: query.regexp,
|
||||
caseSensitive: query.caseSensitive,
|
||||
dialogVisible: true,
|
||||
};
|
||||
|
||||
postMessage('onRequestShowSearch', searchState);
|
||||
searchVisible = true;
|
||||
};
|
||||
|
||||
const hideSearchDialog = () => {
|
||||
postMessage('onRequestHideSearch', null);
|
||||
searchVisible = false;
|
||||
};
|
||||
|
||||
const notifySelectionChange = (viewUpdate: ViewUpdate) => {
|
||||
if (!viewUpdate.state.selection.eq(viewUpdate.startState.selection)) {
|
||||
const mainRange = viewUpdate.state.selection.main;
|
||||
const selection: Selection = {
|
||||
start: mainRange.from,
|
||||
end: mainRange.to,
|
||||
};
|
||||
const event: SelectionChangeEvent = {
|
||||
selection,
|
||||
};
|
||||
postMessage('onSelectionChange', event);
|
||||
}
|
||||
};
|
||||
|
||||
const notifySelectionFormattingChange = (viewUpdate?: ViewUpdate) => {
|
||||
// If we can't determine the previous formatting, post the update regardless
|
||||
if (!viewUpdate) {
|
||||
const formatting = computeSelectionFormatting(editor.state);
|
||||
postMessage('onSelectionFormattingChange', formatting.toJSON());
|
||||
} else if (viewUpdate.docChanged || !viewUpdate.state.selection.eq(viewUpdate.startState.selection)) {
|
||||
// Only post the update if something changed
|
||||
const oldFormatting = computeSelectionFormatting(viewUpdate.startState);
|
||||
const newFormatting = computeSelectionFormatting(viewUpdate.state);
|
||||
|
||||
if (!oldFormatting.eq(newFormatting)) {
|
||||
postMessage('onSelectionFormattingChange', newFormatting.toJSON());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const computeSelectionFormatting = (state: EditorState): SelectionFormatting => {
|
||||
const range = state.selection.main;
|
||||
const formatting: SelectionFormatting = new SelectionFormatting();
|
||||
formatting.selectedText = state.doc.sliceString(range.from, range.to);
|
||||
formatting.spellChecking = editor.contentDOM.spellcheck;
|
||||
|
||||
const parseLinkData = (nodeText: string) => {
|
||||
const linkMatch = nodeText.match(/\[([^\]]*)\]\(([^)]*)\)/);
|
||||
|
||||
if (linkMatch) {
|
||||
return {
|
||||
linkText: linkMatch[1],
|
||||
linkURL: linkMatch[2],
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// Find nodes that overlap/are within the selected region
|
||||
syntaxTree(state).iterate({
|
||||
from: range.from, to: range.to,
|
||||
enter: node => {
|
||||
// Checklists don't have a specific containing node. As such,
|
||||
// we're in a checklist if we've selected a 'Task' node.
|
||||
if (node.name === 'Task') {
|
||||
formatting.inChecklist = true;
|
||||
}
|
||||
|
||||
// Only handle notes that contain the entire range.
|
||||
if (node.from > range.from || node.to < range.to) {
|
||||
return;
|
||||
}
|
||||
// Lazily compute the node's text
|
||||
const nodeText = () => state.doc.sliceString(node.from, node.to);
|
||||
|
||||
switch (node.name) {
|
||||
case 'StrongEmphasis':
|
||||
formatting.bolded = true;
|
||||
break;
|
||||
case 'Emphasis':
|
||||
formatting.italicized = true;
|
||||
break;
|
||||
case 'ListItem':
|
||||
formatting.listLevel += 1;
|
||||
break;
|
||||
case 'BulletList':
|
||||
formatting.inUnorderedList = true;
|
||||
break;
|
||||
case 'OrderedList':
|
||||
formatting.inOrderedList = true;
|
||||
break;
|
||||
case 'TaskList':
|
||||
formatting.inChecklist = true;
|
||||
break;
|
||||
case 'InlineCode':
|
||||
case 'FencedCode':
|
||||
formatting.inCode = true;
|
||||
formatting.unspellCheckableRegion = true;
|
||||
break;
|
||||
case 'InlineMath':
|
||||
case 'BlockMath':
|
||||
formatting.inMath = true;
|
||||
formatting.unspellCheckableRegion = true;
|
||||
break;
|
||||
case 'ATXHeading1':
|
||||
formatting.headerLevel = 1;
|
||||
break;
|
||||
case 'ATXHeading2':
|
||||
formatting.headerLevel = 2;
|
||||
break;
|
||||
case 'ATXHeading3':
|
||||
formatting.headerLevel = 3;
|
||||
break;
|
||||
case 'ATXHeading4':
|
||||
formatting.headerLevel = 4;
|
||||
break;
|
||||
case 'ATXHeading5':
|
||||
formatting.headerLevel = 5;
|
||||
break;
|
||||
case 'URL':
|
||||
formatting.inLink = true;
|
||||
formatting.linkData.linkURL = nodeText();
|
||||
formatting.unspellCheckableRegion = true;
|
||||
break;
|
||||
case 'Link':
|
||||
formatting.inLink = true;
|
||||
formatting.linkData = parseLinkData(nodeText());
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// The markdown parser marks checklists as unordered lists. Ensure
|
||||
// that they aren't marked as such.
|
||||
if (formatting.inChecklist) {
|
||||
if (!formatting.inUnorderedList) {
|
||||
// Even if the selection contains a Task, because an unordered list node
|
||||
// must contain a valid Task node, we're only in a checklist if we're also in
|
||||
// an unordered list.
|
||||
formatting.inChecklist = false;
|
||||
} else {
|
||||
formatting.inUnorderedList = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (formatting.unspellCheckableRegion) {
|
||||
formatting.spellChecking = false;
|
||||
}
|
||||
|
||||
return formatting;
|
||||
};
|
||||
|
||||
// Returns a keyboard command that returns true (so accepts the keybind)
|
||||
const keyCommand = (key: string, run: Command): KeyBinding => {
|
||||
return {
|
||||
key,
|
||||
run,
|
||||
preventDefault: true,
|
||||
};
|
||||
};
|
||||
|
||||
const editor = new EditorView({
|
||||
state: EditorState.create({
|
||||
// See https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts
|
||||
// for a sample configuration.
|
||||
extensions: [
|
||||
markdown({
|
||||
extensions: [
|
||||
GitHubFlavoredMarkdownExtension,
|
||||
|
||||
// Don't highlight KaTeX if the user disabled it
|
||||
settings.katexEnabled ? MarkdownMathExtension : [],
|
||||
],
|
||||
codeLanguages: syntaxHighlightingLanguages,
|
||||
}),
|
||||
...createTheme(theme),
|
||||
history(),
|
||||
search({
|
||||
createPanel(_: EditorView) {
|
||||
return {
|
||||
// The actual search dialog is implemented with react native,
|
||||
// use a dummy element.
|
||||
dom: document.createElement('div'),
|
||||
mount() {
|
||||
showSearchDialog();
|
||||
},
|
||||
destroy() {
|
||||
hideSearchDialog();
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
drawSelection(),
|
||||
highlightSpecialChars(),
|
||||
// highlightSelectionMatches(),
|
||||
indentOnInput(),
|
||||
|
||||
// By default, indent with four spaces
|
||||
indentUnit.of(' '),
|
||||
EditorState.tabSize.of(4),
|
||||
|
||||
// Apply styles to entire lines (block-display decorations)
|
||||
decoratorExtension,
|
||||
|
||||
EditorView.lineWrapping,
|
||||
EditorView.contentAttributes.of({
|
||||
autocapitalize: 'sentence',
|
||||
autocorrect: settings.spellcheckEnabled ? 'true' : 'false',
|
||||
spellcheck: settings.spellcheckEnabled ? 'true' : 'false',
|
||||
}),
|
||||
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
|
||||
notifyDocChanged(viewUpdate);
|
||||
notifySelectionChange(viewUpdate);
|
||||
notifySelectionFormattingChange(viewUpdate);
|
||||
}),
|
||||
keymap.of([
|
||||
// Custom mod-f binding: Toggle the external dialog implementation
|
||||
// (don't show/hide the Panel dialog).
|
||||
keyCommand('Mod-f', (_: EditorView) => {
|
||||
if (searchVisible) {
|
||||
hideSearchDialog();
|
||||
} else {
|
||||
showSearchDialog();
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
// Markdown formatting keyboard shortcuts
|
||||
keyCommand('Mod-b', toggleBolded),
|
||||
keyCommand('Mod-i', toggleItalicized),
|
||||
keyCommand('Mod-$', toggleMath),
|
||||
keyCommand('Mod-`', toggleCode),
|
||||
keyCommand('Mod-[', decreaseIndent),
|
||||
keyCommand('Mod-]', increaseIndent),
|
||||
keyCommand('Mod-k', (_: EditorView) => {
|
||||
notifyLinkEditRequest();
|
||||
return true;
|
||||
}),
|
||||
|
||||
...defaultKeymap, ...historyKeymap, indentWithTab, ...searchKeymap,
|
||||
]),
|
||||
|
||||
EditorState.readOnly.of(settings.readOnly),
|
||||
],
|
||||
doc: initialText,
|
||||
}),
|
||||
parent: parentElement,
|
||||
});
|
||||
|
||||
// HACK: 09/02/22: Work around https://github.com/laurent22/joplin/issues/6802 by creating a copy mousedown
|
||||
// event to prevent the Editor's .preventDefault from making the context menu not appear.
|
||||
// TODO: Track the upstream issue at https://github.com/codemirror/dev/issues/935 and remove this workaround
|
||||
// when the upstream bug is fixed.
|
||||
document.body.addEventListener('mousedown', (evt) => {
|
||||
if (!evt.isTrusted) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Walk up the tree -- is evt.target or any of its parent nodes the editor's input region?
|
||||
for (let current: Record<string, any> = evt.target; current; current = current.parentElement) {
|
||||
if (current === editor.contentDOM) {
|
||||
evt.stopPropagation();
|
||||
|
||||
const copyEvent = new Event('mousedown', evt);
|
||||
editor.contentDOM.dispatchEvent(copyEvent);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, true);
|
||||
|
||||
const updateSearchQuery = (newState: SearchState) => {
|
||||
const query = new SearchQuery({
|
||||
search: newState.searchText,
|
||||
caseSensitive: newState.caseSensitive,
|
||||
regexp: newState.useRegex,
|
||||
replace: newState.replaceText,
|
||||
});
|
||||
editor.dispatch({
|
||||
effects: setSearchQuery.of(query),
|
||||
});
|
||||
};
|
||||
|
||||
const editorControls = {
|
||||
editor,
|
||||
undo: () => {
|
||||
undo(editor);
|
||||
schedulePostUndoRedoDepthChange(editor, true);
|
||||
},
|
||||
redo: () => {
|
||||
redo(editor);
|
||||
schedulePostUndoRedoDepthChange(editor, true);
|
||||
},
|
||||
select: (anchor: number, head: number) => {
|
||||
editor.dispatch(editor.state.update({
|
||||
selection: { anchor, head },
|
||||
scrollIntoView: true,
|
||||
}));
|
||||
},
|
||||
scrollSelectionIntoView: () => {
|
||||
editor.dispatch(editor.state.update({
|
||||
scrollIntoView: true,
|
||||
}));
|
||||
},
|
||||
insertText: (text: string) => {
|
||||
editor.dispatch(editor.state.replaceSelection(text));
|
||||
},
|
||||
toggleFindDialog: () => {
|
||||
const opened = openSearchPanel(editor);
|
||||
if (!opened) {
|
||||
closeSearchPanel(editor);
|
||||
}
|
||||
},
|
||||
|
||||
// Formatting
|
||||
toggleBolded: () => { toggleBolded(editor); },
|
||||
toggleItalicized: () => { toggleItalicized(editor); },
|
||||
toggleCode: () => { toggleCode(editor); },
|
||||
toggleMath: () => { toggleMath(editor); },
|
||||
increaseIndent: () => { increaseIndent(editor); },
|
||||
decreaseIndent: () => { decreaseIndent(editor); },
|
||||
toggleList: (kind: ListType) => { toggleList(kind)(editor); },
|
||||
toggleHeaderLevel: (level: number) => { toggleHeaderLevel(level)(editor); },
|
||||
updateLink: (label: string, url: string) => { updateLink(label, url)(editor); },
|
||||
|
||||
// Search
|
||||
searchControl: {
|
||||
findNext: () => {
|
||||
findNext(editor);
|
||||
},
|
||||
findPrevious: () => {
|
||||
findPrevious(editor);
|
||||
},
|
||||
replaceCurrent: () => {
|
||||
replaceNext(editor);
|
||||
},
|
||||
replaceAll: () => {
|
||||
replaceAll(editor);
|
||||
},
|
||||
setSearchState: (state: SearchState) => {
|
||||
updateSearchQuery(state);
|
||||
},
|
||||
showSearch: () => {
|
||||
showSearchDialog();
|
||||
},
|
||||
hideSearch: () => {
|
||||
hideSearchDialog();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return editorControls;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,10 +40,6 @@ const urlDecoration = Decoration.mark({
|
||||
attributes: { class: 'cm-url', ...noSpellCheckAttrs },
|
||||
});
|
||||
|
||||
const htmlTagNameDecoration = Decoration.mark({
|
||||
attributes: { class: 'cm-htmlTag', ...noSpellCheckAttrs },
|
||||
});
|
||||
|
||||
const blockQuoteDecoration = Decoration.line({
|
||||
attributes: { class: 'cm-blockQuote' },
|
||||
});
|
||||
@@ -52,26 +48,6 @@ const headerLineDecoration = Decoration.line({
|
||||
attributes: { class: 'cm-headerLine' },
|
||||
});
|
||||
|
||||
const tableHeaderDecoration = Decoration.line({
|
||||
attributes: { class: 'cm-tableHeader' },
|
||||
});
|
||||
|
||||
const tableBodyDecoration = Decoration.line({
|
||||
attributes: { class: 'cm-tableRow' },
|
||||
});
|
||||
|
||||
const tableDelimiterDecoration = Decoration.line({
|
||||
attributes: { class: 'cm-tableDelimiter' },
|
||||
});
|
||||
|
||||
const horizontalRuleDecoration = Decoration.mark({
|
||||
attributes: { class: 'cm-hr' },
|
||||
});
|
||||
|
||||
const taskMarkerDecoration = Decoration.mark({
|
||||
attributes: { class: 'cm-taskMarker' },
|
||||
});
|
||||
|
||||
type DecorationDescription = { pos: number; length?: number; decoration: Decoration };
|
||||
|
||||
// Returns a set of [Decoration]s, associated with block syntax groups that require
|
||||
@@ -149,25 +125,6 @@ const computeDecorations = (view: EditorView) => {
|
||||
case 'ATXHeading6':
|
||||
addDecorationToLines(viewFrom, viewTo, headerLineDecoration);
|
||||
break;
|
||||
case 'HTMLTag':
|
||||
case 'TagName':
|
||||
addDecorationToRange(viewFrom, viewTo, htmlTagNameDecoration);
|
||||
break;
|
||||
case 'TableHeader':
|
||||
addDecorationToLines(viewFrom, viewTo, tableHeaderDecoration);
|
||||
break;
|
||||
case 'TableDelimiter':
|
||||
addDecorationToLines(viewFrom, viewTo, tableDelimiterDecoration);
|
||||
break;
|
||||
case 'TableRow':
|
||||
addDecorationToLines(viewFrom, viewTo, tableBodyDecoration);
|
||||
break;
|
||||
case 'HorizontalRule':
|
||||
addDecorationToRange(viewFrom, viewTo, horizontalRuleDecoration);
|
||||
break;
|
||||
case 'TaskMarker':
|
||||
addDecorationToRange(viewFrom, viewTo, taskMarkerDecoration);
|
||||
break;
|
||||
}
|
||||
|
||||
// Only block decorations will have differing first and last lines
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EditorSelection } from '@codemirror/state';
|
||||
import { ListType } from '../../types';
|
||||
import createTestEditor from '../testUtil/createTestEditor';
|
||||
import { ListType } from '../types';
|
||||
import createEditor from './testUtil/createEditor';
|
||||
import { toggleList } from './markdownCommands';
|
||||
|
||||
describe('markdownCommands.bulletedVsChecklist', () => {
|
||||
@@ -13,7 +13,7 @@ describe('markdownCommands.bulletedVsChecklist', () => {
|
||||
const expectedTags = ['BulletList', 'Task'];
|
||||
|
||||
it('should remove a checklist following a bulleted list without modifying the bulleted list', async () => {
|
||||
const editor = await createTestEditor(
|
||||
const editor = await createEditor(
|
||||
initialDocText, EditorSelection.cursor(bulletedListPart.length + 5), expectedTags,
|
||||
);
|
||||
|
||||
@@ -24,7 +24,7 @@ describe('markdownCommands.bulletedVsChecklist', () => {
|
||||
});
|
||||
|
||||
it('should remove an unordered list following a checklist without modifying the checklist', async () => {
|
||||
const editor = await createTestEditor(
|
||||
const editor = await createEditor(
|
||||
initialDocText, EditorSelection.cursor(bulletedListPart.length - 5), expectedTags,
|
||||
);
|
||||
|
||||
@@ -35,7 +35,7 @@ describe('markdownCommands.bulletedVsChecklist', () => {
|
||||
});
|
||||
|
||||
it('should replace a selection of unordered and task lists with a correctly-numbered list', async () => {
|
||||
const editor = await createTestEditor(
|
||||
const editor = await createEditor(
|
||||
initialDocText, EditorSelection.range(0, initialDocText.length), expectedTags,
|
||||
);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { EditorSelection } from '@codemirror/state';
|
||||
import {
|
||||
insertOrIncreaseIndent,
|
||||
toggleBolded, toggleCode, toggleHeaderLevel, toggleItalicized, toggleMath, updateLink,
|
||||
} from './markdownCommands';
|
||||
import createTestEditor from '../testUtil/createTestEditor';
|
||||
import createEditor from './testUtil/createEditor';
|
||||
import { blockMathTagName } from './markdownMathParser';
|
||||
|
||||
describe('markdownCommands', () => {
|
||||
@@ -12,7 +11,7 @@ describe('markdownCommands', () => {
|
||||
|
||||
it('should bold/italicize everything selected', async () => {
|
||||
const initialDocText = 'Testing...';
|
||||
const editor = await createTestEditor(
|
||||
const editor = await createEditor(
|
||||
initialDocText, EditorSelection.range(0, initialDocText.length), [],
|
||||
);
|
||||
|
||||
@@ -39,7 +38,7 @@ describe('markdownCommands', () => {
|
||||
|
||||
it('for a cursor, bolding, then italicizing, should produce a bold-italic region', async () => {
|
||||
const initialDocText = '';
|
||||
const editor = await createTestEditor(
|
||||
const editor = await createEditor(
|
||||
initialDocText, EditorSelection.cursor(0), [],
|
||||
);
|
||||
|
||||
@@ -57,7 +56,7 @@ describe('markdownCommands', () => {
|
||||
|
||||
it('toggling math should both create and navigate out of math regions', async () => {
|
||||
const initialDocText = 'Testing... ';
|
||||
const editor = await createTestEditor(initialDocText, EditorSelection.cursor(initialDocText.length), []);
|
||||
const editor = await createEditor(initialDocText, EditorSelection.cursor(initialDocText.length), []);
|
||||
|
||||
toggleMath(editor);
|
||||
expect(editor.state.doc.toString()).toBe('Testing... $$');
|
||||
@@ -73,7 +72,7 @@ describe('markdownCommands', () => {
|
||||
|
||||
it('toggling inline code should both create and navigate out of an inline code region', async () => {
|
||||
const initialDocText = 'Testing...\n\n';
|
||||
const editor = await createTestEditor(initialDocText, EditorSelection.cursor(initialDocText.length), []);
|
||||
const editor = await createEditor(initialDocText, EditorSelection.cursor(initialDocText.length), []);
|
||||
|
||||
toggleCode(editor);
|
||||
editor.dispatch(editor.state.replaceSelection('f(x) = ...'));
|
||||
@@ -85,7 +84,7 @@ describe('markdownCommands', () => {
|
||||
|
||||
it('should set headers to the proper levels (when toggling)', async () => {
|
||||
const initialDocText = 'Testing...\nThis is a test.';
|
||||
const editor = await createTestEditor(initialDocText, EditorSelection.cursor(3), []);
|
||||
const editor = await createEditor(initialDocText, EditorSelection.cursor(3), []);
|
||||
|
||||
toggleHeaderLevel(1)(editor);
|
||||
|
||||
@@ -111,7 +110,7 @@ describe('markdownCommands', () => {
|
||||
|
||||
it('headers should toggle properly within block quotes', async () => {
|
||||
const initialDocText = 'Testing...\n\n> This is a test.\n> ...a test';
|
||||
const editor = await createTestEditor(
|
||||
const editor = await createEditor(
|
||||
initialDocText,
|
||||
EditorSelection.cursor('Testing...\n\n> This'.length),
|
||||
['Blockquote'],
|
||||
@@ -135,7 +134,7 @@ describe('markdownCommands', () => {
|
||||
|
||||
it('block math should be created correctly within block quotes', async () => {
|
||||
const initialDocText = 'Testing...\n\n> This is a test.\n> y = mx + b\n> ...a test';
|
||||
const editor = await createTestEditor(
|
||||
const editor = await createEditor(
|
||||
initialDocText,
|
||||
EditorSelection.range(
|
||||
'Testing...\n\n> This'.length,
|
||||
@@ -158,7 +157,7 @@ describe('markdownCommands', () => {
|
||||
it('block math should be correctly removed within block quotes', async () => {
|
||||
const initialDocText = 'Testing...\n\n> $$\n> This is a test.\n> y = mx + b\n> $$\n> ...a test';
|
||||
|
||||
const editor = await createTestEditor(
|
||||
const editor = await createEditor(
|
||||
initialDocText,
|
||||
EditorSelection.cursor('Testing...\n\n> $$\n> This is'.length),
|
||||
['Blockquote', blockMathTagName],
|
||||
@@ -174,7 +173,7 @@ describe('markdownCommands', () => {
|
||||
|
||||
it('updateLink should replace link titles and isolate URLs if no title is given', async () => {
|
||||
const initialDocText = '[foo](http://example.com/)';
|
||||
const editor = await createTestEditor(initialDocText, EditorSelection.cursor('[f'.length), ['Link']);
|
||||
const editor = await createEditor(initialDocText, EditorSelection.cursor('[f'.length), ['Link']);
|
||||
|
||||
updateLink('bar', 'https://example.com/')(editor);
|
||||
expect(editor.state.doc.toString()).toBe(
|
||||
@@ -189,7 +188,7 @@ describe('markdownCommands', () => {
|
||||
|
||||
it('toggling math twice, starting on a line with content, should a math block', async () => {
|
||||
const initialDocText = 'Testing... ';
|
||||
const editor = await createTestEditor(initialDocText, EditorSelection.cursor(initialDocText.length), []);
|
||||
const editor = await createEditor(initialDocText, EditorSelection.cursor(initialDocText.length), []);
|
||||
|
||||
toggleMath(editor);
|
||||
toggleMath(editor);
|
||||
@@ -199,7 +198,7 @@ describe('markdownCommands', () => {
|
||||
|
||||
it('toggling math twice on an empty line should create an empty math block', async () => {
|
||||
const initialDocText = 'Testing...\n\n';
|
||||
const editor = await createTestEditor(initialDocText, EditorSelection.cursor(initialDocText.length), []);
|
||||
const editor = await createEditor(initialDocText, EditorSelection.cursor(initialDocText.length), []);
|
||||
|
||||
toggleMath(editor);
|
||||
toggleMath(editor);
|
||||
@@ -209,7 +208,7 @@ describe('markdownCommands', () => {
|
||||
|
||||
it('toggling code twice on an empty line should create an empty code block', async () => {
|
||||
const initialDocText = 'Testing...\n\n';
|
||||
const editor = await createTestEditor(initialDocText, EditorSelection.cursor(initialDocText.length), []);
|
||||
const editor = await createEditor(initialDocText, EditorSelection.cursor(initialDocText.length), []);
|
||||
|
||||
// Toggling code twice should create a block code region
|
||||
toggleCode(editor);
|
||||
@@ -223,7 +222,7 @@ describe('markdownCommands', () => {
|
||||
|
||||
it('toggling math twice inside a block quote should produce an empty math block', async () => {
|
||||
const initialDocText = '> Testing...> \n> ';
|
||||
const editor = await createTestEditor(initialDocText, EditorSelection.cursor(initialDocText.length), ['Blockquote']);
|
||||
const editor = await createEditor(initialDocText, EditorSelection.cursor(initialDocText.length), ['Blockquote']);
|
||||
|
||||
toggleMath(editor);
|
||||
toggleMath(editor);
|
||||
@@ -239,41 +238,5 @@ describe('markdownCommands', () => {
|
||||
expect(sel.from).toBe('> Testing...> \n> \n'.length);
|
||||
expect(sel.to).toBe(editor.state.doc.length);
|
||||
});
|
||||
|
||||
it('insertOrIncreaseIndent should indent when text is selected', async () => {
|
||||
const initialText = '> Testing...\n> Test.';
|
||||
const editor = await createTestEditor(
|
||||
initialText,
|
||||
EditorSelection.range(0, initialText.length),
|
||||
['Blockquote'],
|
||||
);
|
||||
|
||||
insertOrIncreaseIndent(editor);
|
||||
|
||||
expect(editor.state.doc.toString()).toBe('> \tTesting...\n> \tTest.');
|
||||
});
|
||||
|
||||
it('insertOrIncreaseIndent should insert tabs when selection is empty, in a paragraph', async () => {
|
||||
const initialText = 'This is a test\nof indentation.';
|
||||
const editor = await createTestEditor(
|
||||
initialText,
|
||||
EditorSelection.cursor(initialText.length),
|
||||
[],
|
||||
);
|
||||
|
||||
insertOrIncreaseIndent(editor);
|
||||
|
||||
const finalText = editor.state.doc.toString();
|
||||
|
||||
// Should add tab character at the cursor
|
||||
expect(finalText).toBe('This is a test\nof indentation.\t');
|
||||
|
||||
// Should move the selection after the tab
|
||||
expect(editor.state.selection.ranges).toHaveLength(1);
|
||||
expect(editor.state.selection.main).toMatchObject({
|
||||
from: finalText.length,
|
||||
to: finalText.length,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,8 +2,8 @@ import { EditorSelection, EditorState } from '@codemirror/state';
|
||||
import {
|
||||
increaseIndent, toggleList,
|
||||
} from './markdownCommands';
|
||||
import { ListType } from '../../types';
|
||||
import createTestEditor from '../testUtil/createTestEditor';
|
||||
import { ListType } from '../types';
|
||||
import createEditor from './testUtil/createEditor';
|
||||
|
||||
describe('markdownCommands.toggleList', () => {
|
||||
|
||||
@@ -12,7 +12,7 @@ describe('markdownCommands.toggleList', () => {
|
||||
it('should remove the same type of list', async () => {
|
||||
const initialDocText = '- testing\n- this is a `test`\n';
|
||||
|
||||
const editor = await createTestEditor(
|
||||
const editor = await createEditor(
|
||||
initialDocText,
|
||||
EditorSelection.cursor(5),
|
||||
['BulletList', 'InlineCode'],
|
||||
@@ -26,7 +26,7 @@ describe('markdownCommands.toggleList', () => {
|
||||
|
||||
it('should insert a numbered list with correct numbering', async () => {
|
||||
const initialDocText = 'Testing...\nThis is a test\nof list toggling...';
|
||||
const editor = await createTestEditor(
|
||||
const editor = await createEditor(
|
||||
initialDocText,
|
||||
EditorSelection.cursor('Testing...\nThis is a'.length),
|
||||
[],
|
||||
@@ -51,7 +51,7 @@ describe('markdownCommands.toggleList', () => {
|
||||
const unorderedListText = '- 1\n- 2\n- 3\n- 4\n- 5\n- 6\n- 7';
|
||||
|
||||
it('should correctly replace an unordered list with a numbered list', async () => {
|
||||
const editor = await createTestEditor(
|
||||
const editor = await createEditor(
|
||||
unorderedListText,
|
||||
EditorSelection.cursor(unorderedListText.length),
|
||||
['BulletList'],
|
||||
@@ -63,35 +63,6 @@ describe('markdownCommands.toggleList', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should not toggle a the full list when the cursor is on a blank line', async () => {
|
||||
const checklistStartText = [
|
||||
'# Test',
|
||||
'',
|
||||
'- [ ] This',
|
||||
'- [ ] is',
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
const checklistEndText = [
|
||||
'- [ ] a',
|
||||
'- [ ] test',
|
||||
].join('\n');
|
||||
|
||||
const editor = await createTestEditor(
|
||||
`${checklistStartText}\n${checklistEndText}`,
|
||||
|
||||
// Place the cursor on the blank line between the checklist
|
||||
// regions
|
||||
EditorSelection.cursor(unorderedListText.length + 1),
|
||||
['BulletList', 'ATXHeading1'],
|
||||
);
|
||||
|
||||
// Should create a checkbox on the blank line
|
||||
toggleList(ListType.CheckList)(editor);
|
||||
expect(editor.state.doc.toString()).toBe(
|
||||
`${checklistStartText}- [ ] \n${checklistEndText}`,
|
||||
);
|
||||
});
|
||||
|
||||
// it('should correctly replace an unordered list with a checklist', async () => {
|
||||
// const editor = await createEditor(
|
||||
@@ -183,7 +154,7 @@ describe('markdownCommands.toggleList', () => {
|
||||
it('should toggle a numbered list without changing its sublists', async () => {
|
||||
const initialDocText = '1. Foo\n2. Bar\n3. Baz\n\t- Test\n\t- of\n\t- sublists\n4. Foo';
|
||||
|
||||
const editor = await createTestEditor(
|
||||
const editor = await createEditor(
|
||||
initialDocText,
|
||||
EditorSelection.cursor(0),
|
||||
['OrderedList', 'BulletList'],
|
||||
@@ -198,7 +169,7 @@ describe('markdownCommands.toggleList', () => {
|
||||
it('should toggle a sublist without changing the parent list', async () => {
|
||||
const initialDocText = '1. This\n2. is\n3. ';
|
||||
|
||||
const editor = await createTestEditor(
|
||||
const editor = await createEditor(
|
||||
initialDocText,
|
||||
EditorSelection.cursor(initialDocText.length),
|
||||
['OrderedList'],
|
||||
@@ -221,7 +192,7 @@ describe('markdownCommands.toggleList', () => {
|
||||
it('should toggle lists properly within block quotes', async () => {
|
||||
const preSubListText = '> # List test\n> * This\n> * is\n';
|
||||
const initialDocText = `${preSubListText}> \t* a\n> \t* test\n> * of list toggling`;
|
||||
const editor = await createTestEditor(
|
||||
const editor = await createEditor(
|
||||
initialDocText, EditorSelection.cursor(preSubListText.length + 3),
|
||||
['BlockQuote', 'BulletList'],
|
||||
);
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { EditorView, Command } from '@codemirror/view';
|
||||
|
||||
import { ListType } from '../../types';
|
||||
import { ListType } from '../types';
|
||||
import {
|
||||
SelectionRange, EditorSelection, ChangeSpec, Line, TransactionSpec,
|
||||
} from '@codemirror/state';
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
toggleInlineFormatGlobally, toggleRegionFormatGlobally, toggleSelectedLinesStartWith,
|
||||
isIndentationEquivalent, stripBlockquote, tabsToSpaces,
|
||||
} from './markdownReformatter';
|
||||
import intersectsSyntaxNode from '../util/isInSyntaxNode';
|
||||
|
||||
const startingSpaceRegex = /^(\s*)/;
|
||||
|
||||
@@ -184,11 +183,8 @@ export const toggleList = (listType: ListType): Command => {
|
||||
const origFirstLineIndentation = firstLineIndentation;
|
||||
const origContainerType = containerType;
|
||||
|
||||
// Grow `sel` to the smallest containing list, unless the
|
||||
// cursor is on an empty line, in which case, the user
|
||||
// probably wants to add a list item (and not select the entire
|
||||
// list).
|
||||
if (sel.empty && fromLine.text.trim() !== '') {
|
||||
// Grow [sel] to the smallest containing list
|
||||
if (sel.empty) {
|
||||
sel = growSelectionToNode(state, sel, [orderedListTag, unorderedListTag]);
|
||||
computeSelectionProps();
|
||||
}
|
||||
@@ -424,35 +420,6 @@ export const increaseIndent: Command = (view: EditorView): boolean => {
|
||||
return true;
|
||||
};
|
||||
|
||||
// Like `increaseIndent`, but may insert tabs, rather than
|
||||
// indenting, in some instances.
|
||||
export const insertOrIncreaseIndent: Command = (view: EditorView): boolean => {
|
||||
const selection = view.state.selection;
|
||||
const mainSelection = selection.main;
|
||||
if (selection.ranges.length !== 1 || !mainSelection.empty) {
|
||||
return increaseIndent(view);
|
||||
}
|
||||
|
||||
|
||||
if (intersectsSyntaxNode(view.state, mainSelection, 'ListItem')) {
|
||||
return increaseIndent(view);
|
||||
}
|
||||
|
||||
const indentUnit = indentString(view.state, getIndentUnit(view.state));
|
||||
view.dispatch(view.state.changeByRange(selection => {
|
||||
return {
|
||||
// Move the selection to after the inserted text
|
||||
range: EditorSelection.cursor(selection.from + indentUnit.length),
|
||||
changes: {
|
||||
from: selection.from,
|
||||
insert: indentUnit,
|
||||
},
|
||||
};
|
||||
}));
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const decreaseIndent: Command = (view: EditorView): boolean => {
|
||||
const matchEmpty = true;
|
||||
const changes = toggleSelectedLinesStartWith(
|
||||
@@ -3,11 +3,11 @@ import { SyntaxNode } from '@lezer/common';
|
||||
import { EditorSelection, EditorState } from '@codemirror/state';
|
||||
import { blockMathTagName, inlineMathContentTagName, inlineMathTagName } from './markdownMathParser';
|
||||
|
||||
import createTestEditor from '../testUtil/createTestEditor';
|
||||
import createEditor from './testUtil/createEditor';
|
||||
|
||||
// Creates an EditorState with math and markdown extensions
|
||||
const createEditorState = async (initialText: string, expectedTags: string[]): Promise<EditorState> => {
|
||||
return (await createTestEditor(initialText, EditorSelection.cursor(0), expectedTags)).state;
|
||||
return (await createEditor(initialText, EditorSelection.cursor(0), expectedTags)).state;
|
||||
};
|
||||
|
||||
// Returns a list of all nodes with the given name in the given editor's syntax tree.
|
||||
@@ -3,13 +3,13 @@ import { GFM as GithubFlavoredMarkdownExt } from '@lezer/markdown';
|
||||
import { indentUnit, syntaxTree } from '@codemirror/language';
|
||||
import { SelectionRange, EditorSelection, EditorState } from '@codemirror/state';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { MarkdownMathExtension } from '../markdown/markdownMathParser';
|
||||
import { MarkdownMathExtension } from '../markdownMathParser';
|
||||
import forceFullParse from './forceFullParse';
|
||||
import loadLangauges from './loadLanguages';
|
||||
|
||||
// Creates and returns a minimal editor with markdown extensions. Waits to return the editor
|
||||
// until all syntax tree tags in `expectedSyntaxTreeTags` exist.
|
||||
const createTestEditor = async (
|
||||
const createEditor = async (
|
||||
initialText: string, initialSelection: SelectionRange, expectedSyntaxTreeTags: string[],
|
||||
): Promise<EditorView> => {
|
||||
await loadLangauges();
|
||||
@@ -62,4 +62,4 @@ const createTestEditor = async (
|
||||
return editor;
|
||||
};
|
||||
|
||||
export default createTestEditor;
|
||||
export default createEditor;
|
||||
@@ -1,4 +1,4 @@
|
||||
import syntaxHighlightingLanguages from '../markdown/syntaxHighlightingLanguages';
|
||||
import syntaxHighlightingLanguages from '../syntaxHighlightingLanguages';
|
||||
|
||||
// Ensure languages we use are loaded. Without this, tests may randomly fail (LanguageDescriptions
|
||||
// are loaded asyncronously, in the background).
|
||||
@@ -8,7 +8,7 @@ import { tags } from '@lezer/highlight';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { Extension } from '@codemirror/state';
|
||||
|
||||
import { inlineMathTag, mathTag } from './markdown/markdownMathParser';
|
||||
import { inlineMathTag, mathTag } from './markdownMathParser';
|
||||
|
||||
// For an example on how to customize the theme, see:
|
||||
//
|
||||
@@ -25,12 +25,6 @@ import { inlineMathTag, mathTag } from './markdown/markdownMathParser';
|
||||
//
|
||||
// [theme] should be a joplin theme (see @joplin/lib/theme)
|
||||
const createTheme = (theme: any): Extension[] => {
|
||||
// If the theme hasn't loaded yet, return nothing.
|
||||
// (createTheme should be called again after the theme has loaded).
|
||||
if (!theme) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const isDarkTheme = theme.appearance === 'dark';
|
||||
|
||||
const baseGlobalStyle: Record<string, string> = {
|
||||
@@ -40,19 +34,15 @@ const createTheme = (theme: any): Extension[] => {
|
||||
// On iOS, apply system font scaling (e.g. font scaling
|
||||
// set in accessibility settings).
|
||||
font: '-apple-system-body',
|
||||
|
||||
// Fill container horizontally
|
||||
width: '100%',
|
||||
boxSizing: 'border-box',
|
||||
};
|
||||
const baseCursorStyle: Record<string, string> = { };
|
||||
const baseContentStyle: Record<string, string> = {
|
||||
fontFamily: theme.fontFamily,
|
||||
fontSize: `${theme.fontSize}${theme.fontSizeUnits ?? 'px'}`,
|
||||
|
||||
// Avoid using units here -- 1.55em, for example, can cause lines to overlap
|
||||
// if some lines contain text with a large enough font size.
|
||||
lineHeight: theme.isDesktop ? '1.55' : undefined,
|
||||
// To allow accessibility font scaling, we also need to set the
|
||||
// fontSize to a value in `em`s (relative scaling relative to
|
||||
// parent font size).
|
||||
fontSize: `${theme.fontSize}em`,
|
||||
};
|
||||
const baseSelectionStyle: Record<string, string> = { };
|
||||
const blurredSelectionStyle: Record<string, string> = { };
|
||||
@@ -70,42 +60,17 @@ const createTheme = (theme: any): Extension[] => {
|
||||
blurredSelectionStyle.backgroundColor = '#444';
|
||||
}
|
||||
|
||||
const monospaceStyle = {
|
||||
fontFamily: theme.monospaceFont || 'monospace',
|
||||
};
|
||||
|
||||
// This is equivalent to the default selection style -- our styling must
|
||||
// be at least this specific.
|
||||
const selectionBackgroundSelector = '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground';
|
||||
|
||||
const codeMirrorTheme = EditorView.theme({
|
||||
const baseTheme = EditorView.baseTheme({
|
||||
'&': baseGlobalStyle,
|
||||
|
||||
// These must be !important or more specific than CodeMirror's built-ins
|
||||
'.cm-content': {
|
||||
fontFamily: theme.fontFamily,
|
||||
...baseContentStyle,
|
||||
paddingBottom: theme.isDesktop ? '400px' : undefined,
|
||||
},
|
||||
'&.cm-focused .cm-cursor': baseCursorStyle,
|
||||
|
||||
// The desktop app sets the font for these elements to a specific font.
|
||||
// Override this.
|
||||
'& div, & span, & a': {
|
||||
fontFamily: 'inherit',
|
||||
},
|
||||
|
||||
// Override the default border around CodeMirror panels
|
||||
'& > .cm-panels': {
|
||||
border: 'none',
|
||||
},
|
||||
|
||||
// &.cm-focused is used to give these styles higher specificity
|
||||
// than the defaults.
|
||||
[selectionBackgroundSelector]: baseSelectionStyle,
|
||||
'&.cm-focused ::selection': baseSelectionStyle,
|
||||
'& ::selection': blurredSelectionStyle,
|
||||
'& .cm-selectionLayer .cm-selectionBackground': blurredSelectionStyle,
|
||||
'&.cm-focused .cm-selectionBackground, ::selection': baseSelectionStyle,
|
||||
'.cm-selectionBackground': blurredSelectionStyle,
|
||||
|
||||
'&.cm-editor.cm-focused': {
|
||||
outline: 'none !important',
|
||||
@@ -136,8 +101,6 @@ const createTheme = (theme: any): Extension[] => {
|
||||
borderStyle: 'solid',
|
||||
borderColor: theme.colorFaded,
|
||||
backgroundColor: 'rgba(155, 155, 155, 0.1)',
|
||||
|
||||
...(theme.isDesktop ? monospaceStyle : {}),
|
||||
},
|
||||
|
||||
// CodeMirror wraps the existing inline span in an additional element.
|
||||
@@ -150,21 +113,12 @@ const createTheme = (theme: any): Extension[] => {
|
||||
borderStyle: 'solid',
|
||||
borderColor: isDarkTheme ? 'rgba(200, 200, 200, 0.5)' : 'rgba(100, 100, 100, 0.5)',
|
||||
borderRadius: '4px',
|
||||
|
||||
...(theme.isDesktop ? monospaceStyle : {}),
|
||||
},
|
||||
|
||||
'& .cm-mathBlock, & .cm-inlineMath': {
|
||||
color: isDarkTheme ? '#9fa' : '#276',
|
||||
},
|
||||
|
||||
'& .cm-tableHeader, & .cm-tableRow, & .cm-tableDelimiter': monospaceStyle,
|
||||
'& .cm-taskMarker': monospaceStyle,
|
||||
|
||||
// Override the default URL style when the URL is within a link
|
||||
'& .tok-url.tok-link, & .tok-link.tok-meta, & .tok-link.tok-string': {
|
||||
opacity: theme.isDesktop ? 0.6 : 1,
|
||||
},
|
||||
|
||||
// Style the search widget. Use ':root' to increase the selector's precedence
|
||||
// (override the existing preset styles).
|
||||
@@ -174,7 +128,9 @@ const createTheme = (theme: any): Extension[] => {
|
||||
color: isDarkTheme ? 'white' : 'black',
|
||||
},
|
||||
},
|
||||
}, { dark: isDarkTheme });
|
||||
});
|
||||
|
||||
const appearanceTheme = EditorView.theme({}, { dark: isDarkTheme });
|
||||
|
||||
const baseHeadingStyle = {
|
||||
fontWeight: 'bold',
|
||||
@@ -194,6 +150,7 @@ const createTheme = (theme: any): Extension[] => {
|
||||
...baseHeadingStyle,
|
||||
tag: tags.heading1,
|
||||
fontSize: '1.6em',
|
||||
borderBottom: `1px solid ${theme.dividerColor}`,
|
||||
},
|
||||
{
|
||||
...baseHeadingStyle,
|
||||
@@ -232,7 +189,7 @@ const createTheme = (theme: any): Extension[] => {
|
||||
{
|
||||
tag: tags.link,
|
||||
color: theme.urlColor,
|
||||
textDecoration: theme.isDesktop ? undefined : 'underline',
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
{
|
||||
tag: [mathTag, inlineMathTag],
|
||||
@@ -263,7 +220,8 @@ const createTheme = (theme: any): Extension[] => {
|
||||
]);
|
||||
|
||||
return [
|
||||
codeMirrorTheme,
|
||||
baseTheme,
|
||||
appearanceTheme,
|
||||
syntaxHighlighting(highlightingStyle),
|
||||
|
||||
// If we haven't defined highlighting for tags, fall back
|
||||
@@ -0,0 +1,27 @@
|
||||
import { ListType, SearchControl } from '../types';
|
||||
|
||||
// Controls for the CodeMirror portion of the editor
|
||||
export interface CodeMirrorControl {
|
||||
undo(): void;
|
||||
redo(): void;
|
||||
select(anchor: number, head: number): void;
|
||||
insertText(text: string): void;
|
||||
|
||||
// Toggle whether we're in a type of region.
|
||||
toggleBolded(): void;
|
||||
toggleItalicized(): void;
|
||||
toggleList(kind: ListType): void;
|
||||
toggleCode(): void;
|
||||
toggleMath(): void;
|
||||
toggleHeaderLevel(level: number): void;
|
||||
|
||||
// Create a new link or update the currently selected link with
|
||||
// the given [label] and [url].
|
||||
updateLink(label: string, url: string): void;
|
||||
|
||||
increaseIndent(): void;
|
||||
decreaseIndent(): void;
|
||||
scrollSelectionIntoView(): void;
|
||||
|
||||
searchControl: SearchControl;
|
||||
}
|
||||
@@ -9,8 +9,8 @@ import Modal from '../Modal';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { EditorControl } from './types';
|
||||
import SelectionFormatting from './SelectionFormatting';
|
||||
import { useCallback } from 'react';
|
||||
import SelectionFormatting from '@joplin/editor/SelectionFormatting';
|
||||
|
||||
interface LinkDialogProps {
|
||||
editorControl: EditorControl;
|
||||
@@ -21,7 +21,7 @@ interface LinkDialogProps {
|
||||
|
||||
const EditLinkDialog = (props: LinkDialogProps) => {
|
||||
// The content of the link selected in the editor (if any)
|
||||
const editorLinkData = props.selectionState.linkData;
|
||||
const editorLinkData = props.selectionState.linkData ?? {};
|
||||
const [linkLabel, setLinkLabel] = useState('');
|
||||
const [linkURL, setLinkURL] = useState('');
|
||||
|
||||
|
||||
@@ -1,302 +0,0 @@
|
||||
const React = require('react');
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import { Theme } from '@joplin/lib/themes/type';
|
||||
import { MutableRefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Alert, BackHandler } from 'react-native';
|
||||
import { WebViewMessageEvent } from 'react-native-webview';
|
||||
import ExtendedWebView, { WebViewControl } from '../../ExtendedWebView';
|
||||
import { clearAutosave, writeAutosave } from './autosave';
|
||||
import { LocalizedStrings } from './js-draw/types';
|
||||
|
||||
const logger = Logger.create('ImageEditor');
|
||||
|
||||
type OnSaveCallback = (svgData: string)=> void;
|
||||
type OnCancelCallback = ()=> void;
|
||||
type LoadInitialSVGCallback = ()=> Promise<string>;
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
loadInitialSVGData: LoadInitialSVGCallback|null;
|
||||
onSave: OnSaveCallback;
|
||||
onExit: OnCancelCallback;
|
||||
}
|
||||
|
||||
const useCss = (editorTheme: Theme) => {
|
||||
return useMemo(() => {
|
||||
// Ensure we have contrast between the background and selection. Some themes
|
||||
// have the same backgroundColor and selectionColor2. (E.g. Aritim Dark)
|
||||
let selectionBackgroundColor = editorTheme.selectedColor2;
|
||||
if (selectionBackgroundColor === editorTheme.backgroundColor) {
|
||||
selectionBackgroundColor = editorTheme.selectedColor;
|
||||
}
|
||||
|
||||
return `
|
||||
:root .imageEditorContainer {
|
||||
--background-color-1: ${editorTheme.backgroundColor};
|
||||
--foreground-color-1: ${editorTheme.color};
|
||||
--background-color-2: ${editorTheme.backgroundColor3};
|
||||
--foreground-color-2: ${editorTheme.color3};
|
||||
--background-color-3: ${editorTheme.raisedBackgroundColor};
|
||||
--foreground-color-3: ${editorTheme.raisedColor};
|
||||
|
||||
--selection-background-color: ${editorTheme.backgroundColorHover3};
|
||||
--selection-foreground-color: ${editorTheme.color3};
|
||||
--primary-action-foreground-color: ${editorTheme.color4};
|
||||
|
||||
--primary-shadow-color: ${editorTheme.colorFaded};
|
||||
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body, html {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Hide the scrollbar. See scrollbar accessibility concerns
|
||||
(https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-width#accessibility_concerns)
|
||||
for why this isn't done in js-draw itself. */
|
||||
.toolbar-tool-row::-webkit-scrollbar {
|
||||
display: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* Hide the save/close icons on small screens. This isn't done in the upstream
|
||||
js-draw repository partially beause it isn't as well localized as Joplin
|
||||
(icons can be used to suggest the meaning of a button when a translation is
|
||||
unavailable). */
|
||||
.toolbar-edge-toolbar:not(.one-row) .toolwidget-tag--save .toolbar-icon,
|
||||
.toolbar-edge-toolbar:not(.one-row) .toolwidget-tag--exit .toolbar-icon {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
}, [editorTheme]);
|
||||
};
|
||||
|
||||
const ImageEditor = (props: Props) => {
|
||||
const editorTheme: Theme = themeStyle(props.themeId);
|
||||
const webviewRef: MutableRefObject<WebViewControl>|null = useRef(null);
|
||||
const [imageChanged, setImageChanged] = useState(false);
|
||||
|
||||
const onRequestCloseEditor = useCallback((promptIfUnsaved: boolean) => {
|
||||
const discardChangesAndClose = async () => {
|
||||
await clearAutosave();
|
||||
props.onExit();
|
||||
};
|
||||
|
||||
if (!imageChanged || !promptIfUnsaved) {
|
||||
void discardChangesAndClose();
|
||||
return true;
|
||||
}
|
||||
|
||||
Alert.alert(
|
||||
_('Save changes?'), _('This drawing may have unsaved changes.'), [
|
||||
{
|
||||
text: _('Discard changes'),
|
||||
onPress: discardChangesAndClose,
|
||||
style: 'destructive',
|
||||
},
|
||||
{
|
||||
text: _('Save changes'),
|
||||
onPress: () => {
|
||||
// saveDrawing calls props.onSave(...) which may close the
|
||||
// editor.
|
||||
webviewRef.current.injectJS('window.editorControl.saveThenExit()');
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
return true;
|
||||
}, [webviewRef, props.onExit, imageChanged]);
|
||||
|
||||
useEffect(() => {
|
||||
const hardwareBackPressListener = () => {
|
||||
onRequestCloseEditor(true);
|
||||
return true;
|
||||
};
|
||||
BackHandler.addEventListener('hardwareBackPress', hardwareBackPressListener);
|
||||
|
||||
return () => {
|
||||
BackHandler.removeEventListener('hardwareBackPress', hardwareBackPressListener);
|
||||
};
|
||||
}, [onRequestCloseEditor]);
|
||||
|
||||
const css = useCss(editorTheme);
|
||||
const html = useMemo(() => `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"/>
|
||||
|
||||
<style>
|
||||
${css}
|
||||
</style>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
`, [css]);
|
||||
|
||||
// A set of localization overrides (Joplin is better localized than js-draw).
|
||||
// All localizable strings (some unused?) can be found at
|
||||
// https://github.com/personalizedrefrigerator/js-draw/blob/main/.github/ISSUE_TEMPLATE/translation-js-draw-new.yml
|
||||
const localizedStrings: LocalizedStrings = useMemo(() => ({
|
||||
save: _('Save'),
|
||||
close: _('Close'),
|
||||
undo: _('Undo'),
|
||||
redo: _('Redo'),
|
||||
}), []);
|
||||
|
||||
const injectedJavaScript = useMemo(() => `
|
||||
window.onerror = (message, source, lineno) => {
|
||||
window.ReactNativeWebView.postMessage(
|
||||
"error: " + message + " in file://" + source + ", line " + lineno
|
||||
);
|
||||
};
|
||||
|
||||
const setImageHasChanges = (hasChanges) => {
|
||||
window.ReactNativeWebView.postMessage(
|
||||
JSON.stringify({
|
||||
action: 'set-image-has-changes',
|
||||
data: hasChanges,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
window.updateEditorTemplate = (templateData) => {
|
||||
window.ReactNativeWebView.postMessage(
|
||||
JSON.stringify({
|
||||
action: 'set-image-template-data',
|
||||
data: templateData,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const notifyReadyToLoadSVG = () => {
|
||||
window.ReactNativeWebView.postMessage(
|
||||
JSON.stringify({
|
||||
action: 'ready-to-load-data',
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const saveDrawing = async (drawing, isAutosave) => {
|
||||
window.ReactNativeWebView.postMessage(
|
||||
JSON.stringify({
|
||||
action: isAutosave ? 'autosave' : 'save',
|
||||
data: drawing.outerHTML,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const closeEditor = (promptIfUnsaved) => {
|
||||
window.ReactNativeWebView.postMessage(JSON.stringify({
|
||||
action: 'close',
|
||||
promptIfUnsaved,
|
||||
}));
|
||||
};
|
||||
|
||||
try {
|
||||
if (window.editorControl === undefined) {
|
||||
${shim.injectedJs('svgEditorBundle')}
|
||||
|
||||
window.editorControl = svgEditorBundle.createJsDrawEditor(
|
||||
{
|
||||
saveDrawing,
|
||||
closeEditor,
|
||||
updateEditorTemplate,
|
||||
setImageHasChanges,
|
||||
},
|
||||
${JSON.stringify(Setting.value('imageeditor.jsdrawToolbar'))},
|
||||
${JSON.stringify(Setting.value('locale'))},
|
||||
${JSON.stringify(localizedStrings)},
|
||||
);
|
||||
|
||||
// Start loading the SVG file (if present) after loading the editor.
|
||||
// This shows the user that progress is being made (loading large SVGs
|
||||
// from disk into memory can take several seconds).
|
||||
notifyReadyToLoadSVG();
|
||||
}
|
||||
} catch(e) {
|
||||
window.ReactNativeWebView.postMessage(
|
||||
'error: ' + e.message + ': ' + JSON.stringify(e)
|
||||
);
|
||||
}
|
||||
true;
|
||||
`, [localizedStrings]);
|
||||
|
||||
useEffect(() => {
|
||||
webviewRef.current?.injectJS(`
|
||||
if (window.editorControl) {
|
||||
window.editorControl.onThemeUpdate();
|
||||
}
|
||||
`);
|
||||
}, [editorTheme]);
|
||||
|
||||
const onReadyToLoadData = useCallback(async () => {
|
||||
const initialSVGData = await props.loadInitialSVGData?.() ?? '';
|
||||
|
||||
// It can take some time for initialSVGData to be transferred to the WebView.
|
||||
// Thus, do so after the main content has been loaded.
|
||||
webviewRef.current.injectJS(`(async () => {
|
||||
if (window.editorControl) {
|
||||
const initialSVGData = ${JSON.stringify(initialSVGData)};
|
||||
const initialTemplateData = ${JSON.stringify(Setting.value('imageeditor.imageTemplate'))};
|
||||
|
||||
editorControl.loadImageOrTemplate(initialSVGData, initialTemplateData);
|
||||
}
|
||||
})();`);
|
||||
}, [webviewRef, props.loadInitialSVGData]);
|
||||
|
||||
const onMessage = useCallback(async (event: WebViewMessageEvent) => {
|
||||
const data = event.nativeEvent.data;
|
||||
if (data.startsWith('error:')) {
|
||||
logger.error('ImageEditor:', data);
|
||||
return;
|
||||
}
|
||||
|
||||
const json = JSON.parse(data);
|
||||
if (json.action === 'save') {
|
||||
await clearAutosave();
|
||||
props.onSave(json.data);
|
||||
} else if (json.action === 'autosave') {
|
||||
await writeAutosave(json.data);
|
||||
} else if (json.action === 'save-toolbar') {
|
||||
Setting.setValue('imageeditor.jsdrawToolbar', json.data);
|
||||
} else if (json.action === 'close') {
|
||||
onRequestCloseEditor(json.promptIfUnsaved);
|
||||
} else if (json.action === 'ready-to-load-data') {
|
||||
void onReadyToLoadData();
|
||||
} else if (json.action === 'set-image-has-changes') {
|
||||
setImageChanged(json.data);
|
||||
} else if (json.action === 'set-image-template-data') {
|
||||
Setting.setValue('imageeditor.imageTemplate', json.data);
|
||||
} else {
|
||||
logger.error('Unknown action,', json.action);
|
||||
}
|
||||
}, [props.onSave, onRequestCloseEditor, onReadyToLoadData]);
|
||||
|
||||
const onError = useCallback((event: any) => {
|
||||
logger.error('ImageEditor: WebView error: ', event);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ExtendedWebView
|
||||
themeId={props.themeId}
|
||||
html={html}
|
||||
injectedJavaScript={injectedJavaScript}
|
||||
onMessage={onMessage}
|
||||
onError={onError}
|
||||
ref={webviewRef}
|
||||
webviewInstanceId={'image-editor-js-draw'}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageEditor;
|
||||
@@ -1,26 +0,0 @@
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import shim from '@joplin/lib/shim';
|
||||
|
||||
export const autosaveFilename = 'autosaved-drawing.joplin.svg';
|
||||
|
||||
const logger = Logger.create('ImageEditor/autosave');
|
||||
|
||||
export const getAutosaveFilepath = () => {
|
||||
return `${Setting.value('resourceDir')}/${autosaveFilename}`;
|
||||
};
|
||||
|
||||
export const writeAutosave = async (data: string) => {
|
||||
const filePath = getAutosaveFilepath();
|
||||
logger.info(`Auto-saving drawing to ${JSON.stringify(filePath)}`);
|
||||
|
||||
await shim.fsDriver().writeFile(filePath, data, 'utf8');
|
||||
};
|
||||
|
||||
export const readAutosave = async (): Promise<string|null> => {
|
||||
return await shim.fsDriver().readFile(getAutosaveFilepath());
|
||||
};
|
||||
|
||||
export const clearAutosave = async () => {
|
||||
await shim.fsDriver().remove(getAutosaveFilepath());
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user