1
0
mirror of https://github.com/laurent22/joplin.git synced 2026-01-14 00:29:38 +02:00

Compare commits

..

71 Commits

Author SHA1 Message Date
Laurent Cozic
29426814e4 item type 2022-08-04 10:49:19 +02:00
Laurent Cozic
a5e18200e8 Merge branch 'dev' into note_link_indexer 2022-07-30 14:46:44 +02:00
SFulpius
ab5313e37f Desktop: Fixes #6434: Play flac files (#6666) 2022-07-30 13:11:21 +01:00
Henry Heino
54cc7063ad Chore: Migrate from rollup to webpack to build mobile assets (#6705) 2022-07-30 13:07:38 +01:00
Henry Heino
12a510c464 Chore: Fix CI: Use strict equality (#6702) 2022-07-29 09:38:44 +01:00
Henry Heino
21d5800923 iOS: Fixes #6636: Fix occasional overscroll when opening the keyboard (#6700) 2022-07-28 17:05:41 +01:00
Peter Baumgartner
1d5e8e65d9 Doc: Update known problems (#6691) 2022-07-28 17:04:19 +01:00
Henry Heino
d2a6d24846 iOS: Resolves #6685: Respect system accessibility font size in rendered markdown (#6686) 2022-07-28 17:02:46 +01:00
Henry Heino
fb372723a4 Mobile: Improve syntax highlighting on mobile beta editor (#6684) 2022-07-28 17:01:34 +01:00
Henry Heino
b32a341700 Chore: Migrate EventDispatcher to TypeScript, add tests (#6673) 2022-07-28 16:46:52 +01:00
Henry Heino
caef5449dc Doc: Document coding style (#6657) 2022-07-28 16:36:39 +01:00
Joplin Bot
864a3a7efe Doc: Auto-update documentation
Auto-updated using release-website.sh
2022-07-23 12:22:35 +00:00
Laurent Cozic
ce02d4c94f Chore: Finished applying eqeqeq rule 2022-07-23 11:33:12 +02:00
Laurent Cozic
052d9f03d6 Chore: Add eslint rule to enforce strict equality (eqeqeq) 2022-07-23 09:31:32 +02:00
Laurent Cozic
8a8def39f0 Doc: Mime type comment 2022-07-23 08:48:40 +02:00
Henry Heino
f0831f1d60 Chore: Fixes #6663: Fix watchInjectedJs and support multiple output bundles (#6664) 2022-07-22 18:51:12 +01:00
Henry Heino
0e532fbaf0 Chore: Set up repository for testing/preparation for mobile markdown toolbar PR (#6650) 2022-07-22 10:44:19 +01:00
Joplin Bot
11a1e1cb6b Doc: Auto-update documentation
Auto-updated using release-website.sh
2022-07-20 18:17:06 +00:00
Joplin Bot
37b89b5644 Doc: Auto-update documentation
Auto-updated using release-website.sh
2022-07-18 06:19:10 +00:00
Laurent Cozic
2c464e89e6 update db 2022-07-12 15:25:33 +01:00
Laurent Cozic
68764bd82e update db 2022-07-12 15:18:43 +01:00
Laurent Cozic
520d9746c5 Mobile: Fixes #6515: Note links with HTML notation did not work 2022-07-12 11:52:20 +01:00
SFulpius
c3df191a95 Desktop: Fixes #6570: Fixed broken image links (#6590) 2022-07-12 11:34:56 +01:00
Laurent Cozic
06d5feaa63 All: Fixes #6645: Do not encrypt non-owned note if it was not shared encrypted 2022-07-12 11:28:48 +01:00
Laurent Cozic
0b3c4edb92 Chore: Clean up react-native-saf-x 2022-07-11 17:41:44 +01:00
Henry Heino
58045f87d8 Doc: Update outdated path reference in CONTRIBUTING (#6658) 2022-07-11 17:28:14 +01:00
Joplin Bot
28e66e2619 Doc: Auto-update documentation
Auto-updated using release-website.sh
2022-07-11 12:31:33 +00:00
Laurent Cozic
c3179a39a4 Desktop release v2.9.1 2022-07-11 10:17:37 +01:00
Laurent Cozic
eb71260674 Chore: Setup new release 2.9 2022-07-11 10:07:21 +01:00
Laurent Cozic
ed4a013cfc Tools: Fixed /setupNewRelease script 2022-07-11 10:06:15 +01:00
Laurent Cozic
5ffe90c4b0 Chore: Add debug message to try to debug scroll to top issue 2022-07-11 10:00:17 +01:00
Laurent Cozic
8a836ea4f9 Tools: Check licenses for one package 2022-07-11 10:00:16 +01:00
Joplin Bot
1f2930f037 Doc: Auto-update documentation
Auto-updated using release-website.sh
2022-07-11 06:20:21 +00:00
X3NO
ef3afb2a01 updated pl_PL.po (#6613)
updated polish translation
2022-07-10 18:05:25 +01:00
Kane
5d873a3264 Update Chinese Simplified (zh_CN) translation (#6634) 2022-07-10 18:05:07 +01:00
jd1378
effba83a0e Android: Fixes #5779: Fixed android filesystem sync (#6395) 2022-07-10 15:26:24 +01:00
Kenichi Kobayashi
55d98346ee Desktop: Fixes #6639: Re-ordering note list items causes unwanted height change (#6640) 2022-07-10 15:10:08 +01:00
Henry Heino
d848865b0d Chore: Fix injectedJavaScript not evaluating to true on mobile (#6609) 2022-07-10 15:00:21 +01:00
Tom
879702dadf Mobile: Removes whitespace above navigation component (#6597) 2022-07-10 14:59:33 +01:00
Jason Williams
8bb5b4a557 Desktop: Resolves #164: Add support for proxy (#6537) 2022-07-10 14:54:31 +01:00
Laurent Cozic
2c4cf9fbdb Server: Answer recurrent question 2022-07-05 15:16:33 +01:00
Laurent Cozic
3b35ab6581 Plugins: Added joplin.versionInfo method 2022-07-03 14:32:29 +01:00
Joplin Bot
6744dc3a8a Doc: Auto-update documentation
Auto-updated using release-website.sh
2022-07-02 00:44:48 +00:00
Joplin Bot
97c6684154 Doc: Auto-update documentation
Auto-updated using release-website.sh
2022-07-01 18:17:01 +00:00
Laurent Cozic
e797ebb864 Desktop: Security: Fixes XSS in GotoAnything dialog 2022-06-30 18:25:38 +01:00
Laurent Cozic
f99b8dfde8 Server: Process user deletions once an hour 2022-06-28 11:05:09 +01:00
Joplin Bot
c21b28e6e6 Doc: Auto-update documentation
Auto-updated using release-website.sh
2022-06-27 00:48:12 +00:00
Joplin Bot
c58e9fe346 Doc: Auto-update documentation
Auto-updated using release-website.sh
2022-06-26 18:15:45 +00:00
Henry Heino
c58ce8e2da Mobile: Add alt text/roles to some buttons to improve accessibility (#6616) 2022-06-26 18:23:41 +01:00
Daeraxa
f64d046c62 Docs: Add section to FAQ about appimages not starting (#6614) 2022-06-26 18:22:18 +01:00
Henry Heino
c7e3245008 Mobile: Fixes #5949: Scroll selection into view in beta editor when window resizes (#6610) 2022-06-26 18:21:38 +01:00
Eduardo Esparza
8f3fd0bf8b Cli: Resolves #6478: Added note count indicator per notebook (#6526) 2022-06-26 17:55:49 +01:00
Laurent Cozic
d293474402 Doc: Disable a-b test 2022-06-24 13:43:04 +01:00
Henry Heino
aaa610d5f4 Mobile: Ctrl+F search support in beta editor (#6587) 2022-06-24 10:56:59 +01:00
Joplin Bot
20a7cd2323 Doc: Auto-update documentation
Auto-updated using release-website.sh
2022-06-24 00:42:18 +00:00
Laurent Cozic
d7af060564 Revert "Mobile: Fixes #3564: "Move Note" dropdown menu can be very narrow (#6306)"
This reverts commit cffea3ea1e.

https://github.com/laurent22/joplin/pull/6306#issuecomment-1161575676
2022-06-21 11:50:10 +01:00
Laurent Cozic
d7663212cf Revert "Chore: Fixed mobile dropbown regression"
This reverts commit 671077e1bb.

https://github.com/laurent22/joplin/pull/6306#issuecomment-1161575676
2022-06-21 11:49:38 +01:00
Laurent Cozic
429a49b07e Chore: Fixed database type generation script 2022-06-21 11:48:55 +01:00
Laurent Cozic
124ce342d8 Chore: Fixed database type generation script 2022-06-21 11:48:53 +01:00
Jonatan
19f4139470 Update Swedish translation (#6589) 2022-06-20 14:33:28 +01:00
Henry Heino
21b6564301 Mobile: Fixes #6576: Fix checklist continuation in beta editor (#6577) 2022-06-20 14:31:30 +01:00
SFulpius
c8b6122a65 Desktop: Resolves #6172: Checkbox don't function while checkbox format button hidden from toolbar (#6567) 2022-06-20 14:29:32 +01:00
asrient
c0bc4c38c3 Clipper: Resolves #6247: Clipper unable to pull and store PDFs (#6384) 2022-06-20 13:56:54 +01:00
Joplin Bot
0c50a5ab9b Doc: Auto-update documentation
Auto-updated using release-website.sh
2022-06-18 06:18:17 +00:00
Laurent Cozic
ce6797d842 Server: Fixed recursively sharing note 2022-06-14 18:48:15 +01:00
Laurent Cozic
29a1cc022c Server: Add support for recursively publishing a note 2022-06-14 18:47:43 +01:00
Laurent Cozic
af665f247c Server: Fixes #6370: Published note must be scrollable when it contains a large table 2022-06-14 15:39:04 +01:00
Laurent Cozic
8ea32201e7 Server: Fixes #6491: Could not manually start task 2022-06-14 15:30:13 +01:00
Laurent Cozic
4c88376449 Desktop: Fixes #6514: Search field focus is stolen on layout change 2022-06-14 15:25:23 +01:00
Laurent Cozic
0618e9ec90 Server: Fixes #6531: Fixed Unsupported File Type error when sharing certain notes 2022-06-14 14:58:52 +01:00
Laurent Cozic
176c9e0bcf Desktop: Fixes #6557: Search field would not clear as expected 2022-06-14 14:24:51 +01:00
231 changed files with 9098 additions and 3142 deletions

View File

@@ -46,7 +46,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/CodeMirror.bundle.js
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.bundle.js
packages/app-mobile/ios
packages/app-mobile/lib/rnInjectedJs/
packages/app-mobile/locales
@@ -421,9 +421,6 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/styles/index.js.map
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.d.ts
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js.map
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/tables.d.ts
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/tables.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/tables.js.map
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.d.ts
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js.map
@@ -691,9 +688,6 @@ packages/app-desktop/gui/StyleSheets/StyleSheetContainer.js.map
packages/app-desktop/gui/SyncWizard/Dialog.d.ts
packages/app-desktop/gui/SyncWizard/Dialog.js
packages/app-desktop/gui/SyncWizard/Dialog.js.map
packages/app-desktop/gui/TableEditorDialog/Dialog.d.ts
packages/app-desktop/gui/TableEditorDialog/Dialog.js
packages/app-desktop/gui/TableEditorDialog/Dialog.js.map
packages/app-desktop/gui/TagList.d.ts
packages/app-desktop/gui/TagList.js
packages/app-desktop/gui/TagList.js.map
@@ -859,9 +853,24 @@ packages/app-mobile/components/NoteBodyViewer/hooks/useOnResourceLongPress.js.ma
packages/app-mobile/components/NoteBodyViewer/hooks/useSource.d.ts
packages/app-mobile/components/NoteBodyViewer/hooks/useSource.js
packages/app-mobile/components/NoteBodyViewer/hooks/useSource.js.map
packages/app-mobile/components/NoteEditor/CodeMirror.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror.js
packages/app-mobile/components/NoteEditor/CodeMirror.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.js
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/decoratorExtension.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/decoratorExtension.js
packages/app-mobile/components/NoteEditor/CodeMirror/decoratorExtension.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.test.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.test.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/syntaxHighlightingLanguages.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/syntaxHighlightingLanguages.js
packages/app-mobile/components/NoteEditor/CodeMirror/syntaxHighlightingLanguages.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/theme.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/theme.js
packages/app-mobile/components/NoteEditor/CodeMirror/theme.js.map
packages/app-mobile/components/NoteEditor/NoteEditor.d.ts
packages/app-mobile/components/NoteEditor/NoteEditor.js
packages/app-mobile/components/NoteEditor/NoteEditor.js.map
@@ -880,6 +889,9 @@ packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js.map
packages/app-mobile/components/screens/encryption-config.d.ts
packages/app-mobile/components/screens/encryption-config.js
packages/app-mobile/components/screens/encryption-config.js.map
packages/app-mobile/gulpfile.d.ts
packages/app-mobile/gulpfile.js
packages/app-mobile/gulpfile.js.map
packages/app-mobile/root.d.ts
packages/app-mobile/root.js
packages/app-mobile/root.js.map
@@ -895,6 +907,9 @@ packages/app-mobile/services/e2ee/RSA.react-native.js.map
packages/app-mobile/setupQuickActions.d.ts
packages/app-mobile/setupQuickActions.js
packages/app-mobile/setupQuickActions.js.map
packages/app-mobile/tools/buildInjectedJs.d.ts
packages/app-mobile/tools/buildInjectedJs.js
packages/app-mobile/tools/buildInjectedJs.js.map
packages/app-mobile/utils/ShareExtension.d.ts
packages/app-mobile/utils/ShareExtension.js
packages/app-mobile/utils/ShareExtension.js.map
@@ -994,6 +1009,12 @@ packages/lib/ClipperServer.js.map
packages/lib/CssUtils.d.ts
packages/lib/CssUtils.js
packages/lib/CssUtils.js.map
packages/lib/EventDispatcher.d.ts
packages/lib/EventDispatcher.js
packages/lib/EventDispatcher.js.map
packages/lib/EventDispatcher.test.d.ts
packages/lib/EventDispatcher.test.js
packages/lib/EventDispatcher.test.js.map
packages/lib/HtmlToMd.d.ts
packages/lib/HtmlToMd.js
packages/lib/HtmlToMd.js.map
@@ -1495,6 +1516,9 @@ packages/lib/services/keychain/KeychainServiceDriver.node.js.map
packages/lib/services/keychain/KeychainServiceDriverBase.d.ts
packages/lib/services/keychain/KeychainServiceDriverBase.js
packages/lib/services/keychain/KeychainServiceDriverBase.js.map
packages/lib/services/plugins/BasePlatformImplementation.d.ts
packages/lib/services/plugins/BasePlatformImplementation.js
packages/lib/services/plugins/BasePlatformImplementation.js.map
packages/lib/services/plugins/BasePluginRunner.d.ts
packages/lib/services/plugins/BasePluginRunner.js
packages/lib/services/plugins/BasePluginRunner.js.map
@@ -1936,6 +1960,9 @@ packages/plugins/ToggleSidebars/api/types.js.map
packages/plugins/ToggleSidebars/src/index.d.ts
packages/plugins/ToggleSidebars/src/index.js
packages/plugins/ToggleSidebars/src/index.js.map
packages/react-native-saf-x/src/index.d.ts
packages/react-native-saf-x/src/index.js
packages/react-native-saf-x/src/index.js.map
packages/renderer/HtmlToHtml.d.ts
packages/renderer/HtmlToHtml.js
packages/renderer/HtmlToHtml.js.map

View File

@@ -76,6 +76,7 @@ module.exports = {
'no-array-constructor': ['error'],
'radix': ['error'],
'eqeqeq': ['error', 'always'],
// Warn only for now because fixing everything would take too much
// refactoring, but new code should try to stick to it.

45
.gitignore vendored
View File

@@ -411,9 +411,6 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/styles/index.js.map
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.d.ts
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js.map
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/tables.d.ts
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/tables.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/tables.js.map
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.d.ts
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js.map
@@ -681,9 +678,6 @@ packages/app-desktop/gui/StyleSheets/StyleSheetContainer.js.map
packages/app-desktop/gui/SyncWizard/Dialog.d.ts
packages/app-desktop/gui/SyncWizard/Dialog.js
packages/app-desktop/gui/SyncWizard/Dialog.js.map
packages/app-desktop/gui/TableEditorDialog/Dialog.d.ts
packages/app-desktop/gui/TableEditorDialog/Dialog.js
packages/app-desktop/gui/TableEditorDialog/Dialog.js.map
packages/app-desktop/gui/TagList.d.ts
packages/app-desktop/gui/TagList.js
packages/app-desktop/gui/TagList.js.map
@@ -849,9 +843,24 @@ packages/app-mobile/components/NoteBodyViewer/hooks/useOnResourceLongPress.js.ma
packages/app-mobile/components/NoteBodyViewer/hooks/useSource.d.ts
packages/app-mobile/components/NoteBodyViewer/hooks/useSource.js
packages/app-mobile/components/NoteBodyViewer/hooks/useSource.js.map
packages/app-mobile/components/NoteEditor/CodeMirror.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror.js
packages/app-mobile/components/NoteEditor/CodeMirror.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.js
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/decoratorExtension.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/decoratorExtension.js
packages/app-mobile/components/NoteEditor/CodeMirror/decoratorExtension.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.test.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.test.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/syntaxHighlightingLanguages.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/syntaxHighlightingLanguages.js
packages/app-mobile/components/NoteEditor/CodeMirror/syntaxHighlightingLanguages.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/theme.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/theme.js
packages/app-mobile/components/NoteEditor/CodeMirror/theme.js.map
packages/app-mobile/components/NoteEditor/NoteEditor.d.ts
packages/app-mobile/components/NoteEditor/NoteEditor.js
packages/app-mobile/components/NoteEditor/NoteEditor.js.map
@@ -870,6 +879,9 @@ packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js.map
packages/app-mobile/components/screens/encryption-config.d.ts
packages/app-mobile/components/screens/encryption-config.js
packages/app-mobile/components/screens/encryption-config.js.map
packages/app-mobile/gulpfile.d.ts
packages/app-mobile/gulpfile.js
packages/app-mobile/gulpfile.js.map
packages/app-mobile/root.d.ts
packages/app-mobile/root.js
packages/app-mobile/root.js.map
@@ -885,6 +897,9 @@ packages/app-mobile/services/e2ee/RSA.react-native.js.map
packages/app-mobile/setupQuickActions.d.ts
packages/app-mobile/setupQuickActions.js
packages/app-mobile/setupQuickActions.js.map
packages/app-mobile/tools/buildInjectedJs.d.ts
packages/app-mobile/tools/buildInjectedJs.js
packages/app-mobile/tools/buildInjectedJs.js.map
packages/app-mobile/utils/ShareExtension.d.ts
packages/app-mobile/utils/ShareExtension.js
packages/app-mobile/utils/ShareExtension.js.map
@@ -984,6 +999,12 @@ packages/lib/ClipperServer.js.map
packages/lib/CssUtils.d.ts
packages/lib/CssUtils.js
packages/lib/CssUtils.js.map
packages/lib/EventDispatcher.d.ts
packages/lib/EventDispatcher.js
packages/lib/EventDispatcher.js.map
packages/lib/EventDispatcher.test.d.ts
packages/lib/EventDispatcher.test.js
packages/lib/EventDispatcher.test.js.map
packages/lib/HtmlToMd.d.ts
packages/lib/HtmlToMd.js
packages/lib/HtmlToMd.js.map
@@ -1485,6 +1506,9 @@ packages/lib/services/keychain/KeychainServiceDriver.node.js.map
packages/lib/services/keychain/KeychainServiceDriverBase.d.ts
packages/lib/services/keychain/KeychainServiceDriverBase.js
packages/lib/services/keychain/KeychainServiceDriverBase.js.map
packages/lib/services/plugins/BasePlatformImplementation.d.ts
packages/lib/services/plugins/BasePlatformImplementation.js
packages/lib/services/plugins/BasePlatformImplementation.js.map
packages/lib/services/plugins/BasePluginRunner.d.ts
packages/lib/services/plugins/BasePluginRunner.js
packages/lib/services/plugins/BasePluginRunner.js.map
@@ -1926,6 +1950,9 @@ packages/plugins/ToggleSidebars/api/types.js.map
packages/plugins/ToggleSidebars/src/index.d.ts
packages/plugins/ToggleSidebars/src/index.js
packages/plugins/ToggleSidebars/src/index.js.map
packages/react-native-saf-x/src/index.d.ts
packages/react-native-saf-x/src/index.js
packages/react-native-saf-x/src/index.js.map
packages/renderer/HtmlToHtml.d.ts
packages/renderer/HtmlToHtml.js
packages/renderer/HtmlToHtml.js.map

View File

@@ -9,11 +9,13 @@ import PluginManager from 'tinymce/core/api/PluginManager';
import * as Api from './api/Api';
import * as Commands from './api/Commands';
import * as Keyboard from './core/Keyboard';
import * as Mouse from './core/Mouse'
import * as Buttons from './ui/Buttons';
export default function () {
PluginManager.add('joplinLists', function (editor) {
Keyboard.setup(editor);
Mouse.setup(editor);
Buttons.register(editor);
Commands.register(editor);

View File

@@ -0,0 +1,26 @@
import { isJoplinChecklistItem } from '../listModel/JoplinListUtil';
const setup = function (editor) {
const editorClickHandler = (event) => {
if (!isJoplinChecklistItem(event.target)) return;
// We only process the click if it's within the checkbox itself (and not the label).
// That checkbox, based on
// the current styling is in the negative margin, so offsetX is negative when clicking
// on the checkbox itself, and positive when clicking on the label. This is strongly
// dependent on how the checkbox is styled, so if the style is changed, this might need
// to be updated too.
// For the styling, see:
// packages/renderer/MdToHtml/rules/checkbox.ts
//
// The previous solution was to use "pointer-event: none", which mostly work, however
// it means that links are no longer clickable when they are within the checkbox label.
if (event.offsetX >= 0) return;
editor.execCommand('ToggleJoplinChecklistItem', false, { element: event.target });
}
editor.on('click', editorClickHandler);
};
export { setup };

View File

@@ -10,7 +10,7 @@ import * as Settings from '../api/Settings';
import * as NodeType from '../core/NodeType';
import Editor from 'tinymce/core/api/Editor';
import { isCustomList } from '../core/Util';
import { findContainerListTypeFromEvent, isJoplinChecklistItem } from '../listModel/JoplinListUtil';
import { findContainerListTypeFromEvent } from '../listModel/JoplinListUtil';
const findIndex = function (list, predicate) {
for (let index = 0; index < list.length; index++) {
@@ -38,37 +38,11 @@ const listState = function (editor: Editor, listName, options:any = {}) {
buttonApi.setActive(listType === options.listType && lists.length > 0 && lists[0].nodeName === listName && !isCustomList(lists[0]));
};
const editorClickHandler = (event) => {
if (!isJoplinChecklistItem(event.target)) return;
// We only process the click if it's within the checkbox itself (and not the label).
// That checkbox, based on
// the current styling is in the negative margin, so offsetX is negative when clicking
// on the checkbox itself, and positive when clicking on the label. This is strongly
// dependent on how the checkbox is styled, so if the style is changed, this might need
// to be updated too.
// For the styling, see:
// packages/renderer/MdToHtml/rules/checkbox.ts
//
// The previous solution was to use "pointer-event: none", which mostly work, however
// it means that links are no longer clickable when they are within the checkbox label.
if (event.offsetX >= 0) return;
editor.execCommand('ToggleJoplinChecklistItem', false, { element: event.target });
}
if (options.listType === 'joplinChecklist') {
editor.on('click', editorClickHandler);
}
editor.on('NodeChange', nodeChangeHandler);
return () => {
if (options.listType === 'joplinChecklist') {
editor.off('click', editorClickHandler);
}
editor.off('NodeChange', nodeChangeHandler);
}
}
};
};

View File

@@ -1,5 +1,7 @@
<!-- Monthly/Yearly plan A/B testing -->
<!--
<script src="https://www.googleoptimize.com/optimize.js?id=OPT-PW3ZPK3"></script>
-->
<!-- Donate button A/B testing -->
<!--

View File

@@ -45,11 +45,11 @@ Building the apps is relatively easy - please [see the build instructions](https
## Coding style
Coding style is enforced by a pre-commit hook that runs eslint. This hook is installed whenever running `yarn install` on any of the application directory. If for some reason the pre-commit hook didn't get installed, you can manually install it by running `yarn install` at the root of the repository.
Please see [readme/coding_style.md](readme/coding_style.md).
For new React components, please use [React Hooks](https://reactjs.org/docs/hooks-intro.html). For new code in general, please use TypeScript. Even if you are modifying a file that was originally in JavaScript you should ideally convert it first to TypeScript before modifying it. Doing so is relatively easy and it helps maintain code quality.
## GUI style
For changes made to the Desktop client that affect the user interface, refer to `packages/app-desktop/theme.ts` for all styling information. The goal is to create a consistent user interface to allow for easy navigation of Joplin's various features and improve the overall user experience.
For changes made to the Desktop and mobile clients that affect the user interface, refer to `packages/lib/theme.ts` for all styling information. The goal is to create a consistent user interface to allow for easy navigation of Joplin's various features and improve the overall user experience.
## Automated tests

View File

@@ -325,6 +325,7 @@
"homenote",
"hotfolder",
"Howver",
"hpagent",
"Hrvatska",
"htmlentities",
"htmlfile",
@@ -950,4 +951,4 @@
"မြန်မာ",
"កម្ពុជា"
]
}
}

View File

@@ -412,7 +412,7 @@ class AppGui {
const widget = this.widget('mainWindow').focusedWidget;
if (!widget) return null;
if (widget.name == 'noteList' || widget.name == 'folderList') {
if (widget.name === 'noteList' || widget.name === 'folderList') {
return widget.currentItem;
}
@@ -521,11 +521,11 @@ class AppGui {
const args = splitCommandString(cmd);
for (let i = 0; i < args.length; i++) {
if (args[i] == '$n') {
if (args[i] === '$n') {
args[i] = note ? note.id : '';
} else if (args[i] == '$b') {
} else if (args[i] === '$b') {
args[i] = folder ? folder.id : '';
} else if (args[i] == '$c') {
} else if (args[i] === '$c') {
const item = this.activeListItem();
args[i] = item ? item.id : '';
}

View File

@@ -81,21 +81,21 @@ class Application extends BaseApplication {
pattern = pattern ? pattern.toString() : '';
if (type == BaseModel.TYPE_FOLDER && (pattern == Folder.conflictFolderTitle() || pattern == Folder.conflictFolderId())) return [Folder.conflictFolder()];
if (type === BaseModel.TYPE_FOLDER && (pattern === Folder.conflictFolderTitle() || pattern === Folder.conflictFolderId())) return [Folder.conflictFolder()];
if (!options) options = {};
const parent = options.parent ? options.parent : app().currentFolder();
const ItemClass = BaseItem.itemClass(type);
if (type == BaseModel.TYPE_NOTE && pattern.indexOf('*') >= 0) {
if (type === BaseModel.TYPE_NOTE && pattern.indexOf('*') >= 0) {
// Handle it as pattern
if (!parent) throw new Error(_('No notebook selected.'));
return await Note.previews(parent.id, { titlePattern: pattern });
} else {
// Single item
let item = null;
if (type == BaseModel.TYPE_NOTE) {
if (type === BaseModel.TYPE_NOTE) {
if (!parent) throw new Error(_('No notebook has been specified.'));
item = await ItemClass.loadFolderNoteByField(parent.id, 'title', pattern);
} else {
@@ -137,7 +137,7 @@ class Application extends BaseApplication {
if (!options.booleanAnswerDefault) options.booleanAnswerDefault = 'y';
if (!options.answers) options.answers = options.booleanAnswerDefault === 'y' ? [_('Y'), _('n')] : [_('N'), _('y')];
if (options.type == 'boolean') {
if (options.type === 'boolean') {
message += ` (${options.answers.join('/')})`;
}
@@ -146,7 +146,7 @@ class Application extends BaseApplication {
if (options.type === 'boolean') {
if (answer === null) return false; // Pressed ESCAPE
if (!answer) answer = options.answers[0];
const positiveIndex = options.booleanAnswerDefault == 'y' ? 0 : 1;
const positiveIndex = options.booleanAnswerDefault === 'y' ? 0 : 1;
return answer.toLowerCase() === options.answers[positiveIndex].toLowerCase();
} else {
return answer;
@@ -181,7 +181,7 @@ class Application extends BaseApplication {
fs.readdirSync(__dirname).forEach(path => {
if (path.indexOf('command-') !== 0) return;
const ext = fileExtension(path);
if (ext != 'js') return;
if (ext !== 'js') return;
const CommandClass = require(`./${path}`);
let cmd = new CommandClass();

View File

@@ -12,7 +12,7 @@ async function handleAutocompletionPromise(line) {
const words = getArguments(line);
// If there is only one word and it is not already a command name then you
// should look for commands it could be
if (words.length == 1) {
if (words.length === 1) {
if (names.indexOf(words[0]) === -1) {
const x = names.filter(n => n.indexOf(words[0]) === 0);
if (x.length === 1) {
@@ -78,38 +78,38 @@ async function handleAutocompletionPromise(line) {
const currentFolder = app().currentFolder();
if (argName == 'note' || argName == 'note-pattern') {
if (argName === 'note' || argName === 'note-pattern') {
const notes = currentFolder ? await Note.previews(currentFolder.id, { titlePattern: `${next}*` }) : [];
l.push(...notes.map(n => n.title));
}
if (argName == 'notebook') {
if (argName === 'notebook') {
const folders = await Folder.search({ titlePattern: `${next}*` });
l.push(...folders.map(n => n.title));
}
if (argName == 'item') {
if (argName === 'item') {
const notes = currentFolder ? await Note.previews(currentFolder.id, { titlePattern: `${next}*` }) : [];
const folders = await Folder.search({ titlePattern: `${next}*` });
l.push(...notes.map(n => n.title), folders.map(n => n.title));
}
if (argName == 'tag') {
if (argName === 'tag') {
const tags = await Tag.search({ titlePattern: `${next}*` });
l.push(...tags.map(n => n.title));
}
if (argName == 'file') {
if (argName === 'file') {
const files = await fs.readdir('.');
l.push(...files);
}
if (argName == 'tag-command') {
if (argName === 'tag-command') {
const c = filterList(['add', 'remove', 'list', 'notetags'], next);
l.push(...c);
}
if (argName == 'todo-command') {
if (argName === 'todo-command') {
const c = filterList(['toggle', 'clear'], next);
l.push(...c);
}

View File

@@ -52,7 +52,7 @@ function getCommands() {
fs.readdirSync(__dirname).forEach(path => {
if (path.indexOf('command-') !== 0) return;
const ext = fileExtension(path);
if (ext != 'js') return;
if (ext !== 'js') return;
const CommandClass = require(`./${path}`);
const cmd = new CommandClass();

View File

@@ -222,7 +222,7 @@ async function main() {
for (const n in testUnits) {
if (!testUnits.hasOwnProperty(n)) continue;
if (onlyThisTest && n != onlyThisTest) continue;
if (onlyThisTest && n !== onlyThisTest) continue;
await clearDatabase();
const testName = n.substr(4).toLowerCase();

View File

@@ -21,7 +21,7 @@ cliUtils.printArray = function(logFunction, rows) {
for (let j = 0; j < row.length; j++) {
const item = row[j];
const width = item ? item.toString().length : 0;
const align = typeof item == 'number' ? ALIGN_RIGHT : ALIGN_LEFT;
const align = typeof item === 'number' ? ALIGN_RIGHT : ALIGN_LEFT;
if (!colWidths[j] || colWidths[j] < width) colWidths[j] = width;
if (colAligns.length <= j) colAligns[j] = align;
}
@@ -32,7 +32,7 @@ cliUtils.printArray = function(logFunction, rows) {
for (let col = 0; col < colWidths.length; col++) {
const item = rows[row][col];
const width = colWidths[col];
const dir = colAligns[col] == ALIGN_LEFT ? stringPadding.RIGHT : stringPadding.LEFT;
const dir = colAligns[col] === ALIGN_LEFT ? stringPadding.RIGHT : stringPadding.LEFT;
line.push(stringPadding(item, width, ' ', dir));
}
logFunction(line.join(' '));
@@ -45,13 +45,13 @@ cliUtils.parseFlags = function(flags) {
for (let i = 0; i < flags.length; i++) {
let f = flags[i].trim();
if (f.substr(0, 2) == '--') {
if (f.substr(0, 2) === '--') {
f = f.split(' ');
output.long = f[0].substr(2).trim();
if (f.length == 2) {
if (f.length === 2) {
output.arg = cliUtils.parseCommandArg(f[1].trim());
}
} else if (f.substr(0, 1) == '-') {
} else if (f.substr(0, 1) === '-') {
output.short = f.substr(1);
}
}
@@ -65,9 +65,9 @@ cliUtils.parseCommandArg = function(arg) {
const c2 = arg[arg.length - 1];
const name = arg.substr(1, arg.length - 2);
if (c1 == '<' && c2 == '>') {
if (c1 === '<' && c2 === '>') {
return { required: true, name: name };
} else if (c1 == '[' && c2 == ']') {
} else if (c1 === '[' && c2 === ']') {
return { required: false, name: name };
} else {
throw new Error(`Invalid command arg: ${arg}`);
@@ -83,7 +83,7 @@ cliUtils.makeCommandArgs = function(cmd, argv) {
const booleanFlags = [];
const aliases = {};
for (let i = 0; i < options.length; i++) {
if (options[i].length != 2) throw new Error(`Invalid options: ${options[i]}`);
if (options[i].length !== 2) throw new Error(`Invalid options: ${options[i]}`);
let flags = options[i][0];
flags = cliUtils.parseFlags(flags);
@@ -117,7 +117,7 @@ cliUtils.makeCommandArgs = function(cmd, argv) {
const argOptions = {};
for (const key in args) {
if (!args.hasOwnProperty(key)) continue;
if (key == '_') continue;
if (key === '_') continue;
argOptions[key] = args[key];
}
@@ -170,7 +170,7 @@ cliUtils.promptConfirm = function(message, answers = null) {
return new Promise((resolve) => {
rl.question(`${message} `, answer => {
const ok = !answer || answer.toLowerCase() == answers[0].toLowerCase();
const ok = !answer || answer.toLowerCase() === answers[0].toLowerCase();
rl.close();
resolve(ok);
});

View File

@@ -122,7 +122,7 @@ class Command extends BaseCommand {
}
if (args.name == 'locale') {
if (args.name === 'locale') {
setLocale(Setting.value('locale'));
}

View File

@@ -44,7 +44,7 @@ class Command extends BaseCommand {
queryOptions.orderBy = options.sort;
queryOptions.orderByDir = 'ASC';
}
if (options.reverse === true) queryOptions.orderByDir = queryOptions.orderByDir == 'ASC' ? 'DESC' : 'ASC';
if (options.reverse === true) queryOptions.orderByDir = queryOptions.orderByDir === 'ASC' ? 'DESC' : 'ASC';
queryOptions.caseInsensitive = true;
if (options.type) {
queryOptions.itemTypes = [];
@@ -55,7 +55,7 @@ class Command extends BaseCommand {
queryOptions.uncompletedTodosOnTop = Setting.value('uncompletedTodosOnTop');
let modelType = null;
if (pattern == '/' || !app().currentFolder()) {
if (pattern === '/' || !app().currentFolder()) {
queryOptions.includeConflictFolder = true;
items = await Folder.all(queryOptions);
modelType = Folder.modelType();
@@ -65,7 +65,7 @@ class Command extends BaseCommand {
modelType = Note.modelType();
}
if (options.format && options.format == 'json') {
if (options.format && options.format === 'json') {
this.stdout(JSON.stringify(items));
} else {
let hasTodos = false;
@@ -88,7 +88,7 @@ class Command extends BaseCommand {
row.push(BaseModel.shortId(item.id));
shortIdShown = true;
if (modelType == Folder.modelType()) {
if (modelType === Folder.modelType()) {
row.push(await Folder.noteCount(item.id));
}

View File

@@ -133,7 +133,7 @@ class Command extends BaseCommand {
this.releaseLockFn_ = await Command.lockFile(lockFilePath);
} catch (error) {
if (error.code == 'ELOCKED') {
if (error.code === 'ELOCKED') {
const msg = _('Lock file is already being hold. If you know that no synchronisation is taking place, you may delete the lock file at "%s" and resume the operation.', error.file);
this.stdout(msg);
return;
@@ -222,7 +222,7 @@ class Command extends BaseCommand {
const newContext = await sync.start(options);
Setting.setValue(contextKey, JSON.stringify(newContext));
} catch (error) {
if (error.code == 'alreadyStarted') {
if (error.code === 'alreadyStarted') {
this.stdout(error.message);
} else {
throw error;

View File

@@ -30,21 +30,21 @@ class Command extends BaseCommand {
const command = args['tag-command'];
if (command == 'remove' && !tag) throw new Error(_('Cannot find "%s".', args.tag));
if (command === 'remove' && !tag) throw new Error(_('Cannot find "%s".', args.tag));
if (command == 'add') {
if (command === 'add') {
if (!notes.length) throw new Error(_('Cannot find "%s".', args.note));
if (!tag) tag = await Tag.save({ title: args.tag }, { userSideValidation: true });
for (let i = 0; i < notes.length; i++) {
await Tag.addNote(tag.id, notes[i].id);
}
} else if (command == 'remove') {
} else if (command === 'remove') {
if (!tag) throw new Error(_('Cannot find "%s".', args.tag));
if (!notes.length) throw new Error(_('Cannot find "%s".', args.note));
for (let i = 0; i < notes.length; i++) {
await Tag.removeNote(tag.id, notes[i].id);
}
} else if (command == 'list') {
} else if (command === 'list') {
if (tag) {
const notes = await Tag.notes(tag.id);
notes.map(note => {
@@ -75,7 +75,7 @@ class Command extends BaseCommand {
this.stdout(tag.title);
});
}
} else if (command == 'notetags') {
} else if (command === 'notetags') {
if (args.tag) {
const note = await app().loadItem(BaseModel.TYPE_NOTE, args.tag);
if (!note) throw new Error(_('Cannot find "%s".', args.tag));

View File

@@ -29,13 +29,13 @@ class Command extends BaseCommand {
id: note.id,
};
if (action == 'toggle') {
if (action === 'toggle') {
if (!note.is_todo) {
toSave = Note.toggleIsTodo(note);
} else {
toSave.todo_completed = note.todo_completed ? 0 : time.unixMs();
}
} else if (action == 'clear') {
} else if (action === 'clear') {
toSave.is_todo = 0;
}

View File

@@ -2071,7 +2071,7 @@ function execCommand(client, command, options = {}) {
return new Promise((resolve, reject) => {
const childProcess = exec(cmd, (error, stdout, stderr) => {
if (error) {
if (error.signal == 'SIGTERM') {
if (error.signal === 'SIGTERM') {
resolve('Process was killed');
} else {
logger.error(stderr);
@@ -2103,7 +2103,7 @@ async function clientItems(client) {
function randomTag(items) {
const tags = [];
for (let i = 0; i < items.length; i++) {
if (items[i].type_ != 5) continue;
if (items[i].type_ !== 5) continue;
tags.push(items[i]);
}
@@ -2113,7 +2113,7 @@ function randomTag(items) {
function randomNote(items) {
const notes = [];
for (let i = 0; i < items.length; i++) {
if (items[i].type_ != 1) continue;
if (items[i].type_ !== 1) continue;
notes.push(items[i]);
}
@@ -2131,11 +2131,11 @@ async function execRandomCommand(client) {
const item = randomElement(items);
if (!item) return;
if (item.type_ == 1) {
if (item.type_ === 1) {
return execCommand(client, `rm -f ${item.id}`);
} else if (item.type_ == 2) {
} else if (item.type_ === 2) {
return execCommand(client, `rm -r -f ${item.id}`);
} else if (item.type_ == 5) {
} else if (item.type_ === 5) {
// tag
} else {
throw new Error(`Unknown type: ${item.type_}`);
@@ -2213,7 +2213,7 @@ function randomNextCheckTime() {
function findItem(items, itemId) {
for (let i = 0; i < items.length; i++) {
if (items[i].id == itemId) return items[i];
if (items[i].id === itemId) return items[i];
}
return null;
}
@@ -2225,7 +2225,7 @@ function compareItems(item1, item2) {
const p1 = item1[n];
const p2 = item2[n];
if (n == 'notes_') {
if (n === 'notes_') {
p1.sort();
p2.sort();
if (JSON.stringify(p1) !== JSON.stringify(p2)) {
@@ -2246,7 +2246,7 @@ function findMissingItems_(items1, items2) {
let found = false;
for (let j = 0; j < items2.length; j++) {
const item2 = items2[j];
if (item1.id == item2.id) {
if (item1.id === item2.id) {
found = true;
break;
}
@@ -2340,9 +2340,9 @@ async function main() {
let state = 'commands';
setInterval(async () => {
if (state == 'waitForSyncCheck') return;
if (state === 'waitForSyncCheck') return;
if (state == 'syncCheck') {
if (state === 'syncCheck') {
state = 'waitForSyncCheck';
const clientItems = [];
// Up to 3 sync operations must be performed by each clients in order for them
@@ -2371,7 +2371,7 @@ async function main() {
return;
}
if (state == 'waitForClients') {
if (state === 'waitForClients') {
for (let i = 0; i < clients.length; i++) {
if (clients[i].activeCommandCount > 0) return;
}
@@ -2380,7 +2380,7 @@ async function main() {
return;
}
if (state == 'commands') {
if (state === 'commands') {
if (nextSyncCheckTime <= time.unixMs()) {
state = 'waitForClients';
return;

View File

@@ -2,6 +2,7 @@ const Folder = require('@joplin/lib/models/Folder').default;
const Tag = require('@joplin/lib/models/Tag').default;
const BaseModel = require('@joplin/lib/BaseModel').default;
const ListWidget = require('tkwidgets/ListWidget.js');
const Setting = require('@joplin/lib/models/Setting').default;
const _ = require('@joplin/lib/locale')._;
class FolderListWidget extends ListWidget {
@@ -25,6 +26,18 @@ class FolderListWidget extends ListWidget {
output.push('-'.repeat(this.innerWidth));
} else if (item.type_ === Folder.modelType()) {
output.push(' '.repeat(this.folderDepth(this.folders, item.id)) + Folder.displayTitle(item));
if (Setting.value('showNoteCounts')) {
let noteCount = item.note_count;
// Subtract children note_count from parent folder.
if (this.folderHasChildren_(this.folders,item.id)) {
for (let i = 0; i < this.folders.length; i++) {
if (this.folders[i].parent_id === item.id) {
noteCount -= this.folders[i].note_count;
}
}
}
output.push(noteCount);
}
} else if (item.type_ === Tag.modelType()) {
output.push(`[${Folder.displayTitle(item)}]`);
} else if (item.type_ === BaseModel.TYPE_SEARCH) {

View File

@@ -26,7 +26,7 @@ const sharp = require('sharp');
const { shimInit } = require('@joplin/lib/shim-init-node.js');
const shim = require('@joplin/lib/shim').default;
const { _ } = require('@joplin/lib/locale');
const { FileApiDriverLocal } = require('@joplin/lib/file-api-driver-local.js');
const { FileApiDriverLocal } = require('@joplin/lib/file-api-driver-local');
const EncryptionService = require('@joplin/lib/services/e2ee/EncryptionService').default;
const envFromArgs = require('@joplin/lib/envFromArgs');
const nodeSqlite = require('sqlite3');
@@ -82,13 +82,13 @@ if (process.platform === 'win32') {
process.stdout.on('error', function(err) {
// https://stackoverflow.com/questions/12329816/error-write-epipe-when-piping-node-output-to-head#15884508
if (err.code == 'EPIPE') {
if (err.code === 'EPIPE') {
process.exit(0);
}
});
application.start(process.argv).catch(error => {
if (error.code == 'flagError') {
if (error.code === 'flagError') {
console.error(error.message);
console.error(_('Type `joplin help` for usage information.'));
} else {

View File

@@ -33,14 +33,14 @@
],
"owner": "Laurent Cozic"
},
"version": "2.8.1",
"version": "2.9.0",
"bin": "./main.js",
"engines": {
"node": ">=10.0.0"
},
"dependencies": {
"@joplin/lib": "~2.8",
"@joplin/renderer": "~2.8",
"@joplin/lib": "~2.9",
"@joplin/renderer": "~2.9",
"aws-sdk": "^2.588.0",
"chalk": "^4.1.0",
"compare-version": "^0.1.2",
@@ -67,7 +67,7 @@
"yargs-parser": "^7.0.0"
},
"devDependencies": {
"@joplin/tools": "~2.8",
"@joplin/tools": "~2.9",
"@types/fs-extra": "^9.0.6",
"@types/jest": "^26.0.15",
"@types/node": "^14.14.6",

View File

@@ -30,7 +30,7 @@ describe('feature_NoteHistory', function() {
});
afterEach(async (done) => {
if (testApp !== null) await testApp.destroy();
if (testApp) await testApp.destroy();
testApp = null;
done();
});

View File

@@ -32,6 +32,15 @@
}
}
function escapeHtml(s) {
return s
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function pageTitle() {
const titleElements = document.getElementsByTagName('title');
if (titleElements.length) return titleElements[0].text.trim();
@@ -204,6 +213,16 @@
}
}
if (nodeName === 'embed') {
const src = absoluteUrl(node.src);
node.setAttribute('src', src);
}
if (nodeName === 'object') {
const data = absoluteUrl(node.data);
node.setAttribute('data', data);
}
cleanUpElement(convertToMarkup, node, imageSizes, imageIndexes);
}
}
@@ -317,6 +336,9 @@
}
function readabilityProcess() {
if (isPagePdf()) throw new Error('Could not parse PDF document with Readability');
// eslint-disable-next-line no-undef
const readability = new Readability(documentForReadability());
const article = readability.parse();
@@ -329,6 +351,14 @@
};
}
function isPagePdf() {
return document.contentType === 'application/pdf';
}
function embedPageUrl() {
return `<embed src="${escapeHtml(window.location.href)}" type="${escapeHtml(document.contentType)}" />`;
}
async function prepareCommandResponse(command) {
console.info(`Got command: ${command.name}`);
const shouldSendToJoplin = !!command.shouldSendToJoplin;
@@ -375,6 +405,10 @@
} else if (command.name === 'completePageHtml') {
if (isPagePdf()) {
return clippedContentResponse(pageTitle(), embedPageUrl(), getImageSizes(document), getAnchorNames(document));
}
hardcodePreStyles(document);
addSvgClass(document);
preProcessDocument(document);

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "Joplin Web Clipper [DEV]",
"version": "2.8.1",
"version": "2.9.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'",

View File

@@ -16,6 +16,8 @@ function getAdditionalModulePaths(options = {}) {
// We need to explicitly check for null and undefined (and not a falsy value) because
// TypeScript treats an empty string as `.`.
//
// eslint-disable-next-line eqeqeq
if (baseUrl == null) {
// If there's no baseUrl set we respect NODE_PATH
// Note that NODE_PATH is deprecated and will be removed

View File

@@ -20253,6 +20253,19 @@
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
"node_modules/typescript": {
"version": "3.9.10",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz",
"integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/unicode-canonical-property-names-ecmascript": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",
@@ -37995,6 +38008,12 @@
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
"typescript": {
"version": "3.9.10",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz",
"integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==",
"peer": true
},
"unicode-canonical-property-names-ecmascript": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",

View File

@@ -67,7 +67,7 @@ checkBrowsers(paths.appPath, isInteractive)
return choosePort(HOST, DEFAULT_PORT);
})
.then(port => {
if (port == null) {
if (!port) {
// We have not found a port.
return;
}

View File

@@ -104,22 +104,22 @@ class Application extends BaseApplication {
}
protected async generalMiddleware(store: any, next: any, action: any) {
if (action.type == 'SETTING_UPDATE_ONE' && action.key == 'locale' || action.type == 'SETTING_UPDATE_ALL') {
if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'locale' || action.type === 'SETTING_UPDATE_ALL') {
setLocale(Setting.value('locale'));
// The bridge runs within the main process, with its own instance of locale.js
// so it needs to be set too here.
bridge().setLocale(Setting.value('locale'));
}
if (action.type == 'SETTING_UPDATE_ONE' && action.key == 'showTrayIcon' || action.type == 'SETTING_UPDATE_ALL') {
if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'showTrayIcon' || action.type === 'SETTING_UPDATE_ALL') {
this.updateTray();
}
if (action.type == 'SETTING_UPDATE_ONE' && action.key == 'style.editor.fontFamily' || action.type == 'SETTING_UPDATE_ALL') {
if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'style.editor.fontFamily' || action.type === 'SETTING_UPDATE_ALL') {
this.updateEditorFont();
}
if (action.type == 'SETTING_UPDATE_ONE' && action.key == 'windowContentZoomFactor' || action.type == 'SETTING_UPDATE_ALL') {
if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'windowContentZoomFactor' || action.type === 'SETTING_UPDATE_ALL') {
webFrame.setZoomFactor(Setting.value('windowContentZoomFactor') / 100);
}
@@ -142,7 +142,7 @@ class Application extends BaseApplication {
await Folder.expandTree(newState.folders, action.folderId);
}
if (this.hasGui() && ((action.type == 'SETTING_UPDATE_ONE' && ['themeAutoDetect', 'theme', 'preferredLightTheme', 'preferredDarkTheme'].includes(action.key)) || action.type == 'SETTING_UPDATE_ALL')) {
if (this.hasGui() && ((action.type === 'SETTING_UPDATE_ONE' && ['themeAutoDetect', 'theme', 'preferredLightTheme', 'preferredDarkTheme'].includes(action.key)) || action.type === 'SETTING_UPDATE_ALL')) {
this.handleThemeAutoDetect();
}
@@ -387,7 +387,7 @@ class Application extends BaseApplication {
PerFolderSortOrderService.initialize();
CommandService.instance().initialize(this.store(), Setting.value('env') == 'dev', stateToWhenClauseContext);
CommandService.instance().initialize(this.store(), Setting.value('env') === 'dev', stateToWhenClauseContext);
for (const command of commands) {
CommandService.instance().registerDeclaration(command.declaration);

View File

@@ -86,7 +86,7 @@ async function fetchLatestRelease(options: CheckForUpdateOptions) {
const ext = fileExtension(asset.name);
if (platform === 'win32' && ext === 'exe') {
if (shim.isPortable()) {
found = asset.name == 'JoplinPortable.exe';
found = asset.name === 'JoplinPortable.exe';
} else {
found = !!asset.name.match(/^Joplin-Setup-[\d.]+\.exe$/);
}

View File

@@ -4,7 +4,7 @@ const os = require('os');
const sha512 = require('js-sha512');
const generateChecksumFile = () => {
if (os.platform() != 'linux') {
if (os.platform() !== 'linux') {
return []; // SHA-512 is only for AppImage
}
const distDirName = 'dist';
@@ -18,7 +18,7 @@ const generateChecksumFile = () => {
break;
}
}
if (appImageName == '') {
if (appImageName === '') {
throw 'AppImage not found!';
}
const appImagePath = path.join(distPath, appImageName);

View File

@@ -174,7 +174,7 @@ function useMenuStates(menu: any, props: Props) {
menuItemSetChecked(`sort:${type}:${field}`, (props as any)[`${type}.sortOrder.field`] === field);
}
const id = type == 'notes' ? 'toggleNotesSortOrderReverse' : `sort:${type}:reverse`;
const id = type === 'notes' ? 'toggleNotesSortOrderReverse' : `sort:${type}:reverse`;
menuItemSetChecked(id, (props as any)[`${type}.sortOrder.reverse`]);
}
@@ -332,7 +332,7 @@ function useMenu(props: Props) {
sortItems.push({ type: 'separator' });
if (type == 'notes') {
if (type === 'notes') {
sortItems.push(
{ ...menuItemDic.toggleNotesSortOrderReverse, type: 'checkbox' },
{ ...menuItemDic.toggleNotesSortOrderField, visible: false }

View File

@@ -38,7 +38,6 @@ 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 { checkTableIsUnderCursor, readTableAroundCursor } from './utils/tables';
const menuUtils = new MenuUtils(CommandService.instance());
@@ -707,6 +706,8 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
return output;
}, [styles.cellViewer, props.visiblePanes]);
const editorPaneVisible = props.visiblePanes.indexOf('editor') >= 0;
useEffect(() => {
if (!editorRef.current) return;
@@ -714,10 +715,10 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
// 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 (props.visiblePanes.indexOf('editor') >= 0) {
if (editorPaneVisible) {
editorRef.current.focus();
}
}, [props.visiblePanes]);
}, [editorPaneVisible]);
useEffect(() => {
if (!editorRef.current) return;
@@ -754,14 +755,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
const menu = new Menu();
const cm = editorRef.current;
const hasSelectedText = cm && !!cm.getSelection() ;
const tableIsUnderCursor = checkTableIsUnderCursor(cm);
let tableUnderCursor: string = null;
if (tableIsUnderCursor) tableUnderCursor = readTableAroundCursor(cm);
const hasSelectedText = editorRef.current && !!editorRef.current.getSelection() ;
menu.append(
new MenuItem({
@@ -793,27 +787,6 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
})
);
if (tableUnderCursor) {
menu.append(
new MenuItem({ type: 'separator' })
);
menu.append(
new MenuItem({
label: _('Edit table...'),
click: async () => {
props.dispatch({
type: 'DIALOG_OPEN',
name: 'tableEditor',
props: {
markdownTable: tableUnderCursor,
},
});
},
})
);
}
const spellCheckerMenuItems = SpellCheckerService.instance().contextMenuItems(params.misspelledWord, params.dictionarySuggestions);
for (const item of spellCheckerMenuItems) {

View File

@@ -1,48 +0,0 @@
function findElementWithClass(element: any, className: string): any {
if (element.classList && element.classList.contains(className)) return element;
for (const child of element.childNodes) {
const hasClass = findElementWithClass(child, className);
if (hasClass) return hasClass;
}
return null;
}
export const checkTableIsUnderCursor = (cm: any) => {
if (!cm) return false;
const coords = cm.cursorCoords(cm.getCursor());
const element = document.elementFromPoint(coords.left, coords.top);
if (!element) return false;
return !!findElementWithClass(element, 'cm-jn-table-item');
};
export const readTableAroundCursor = (cm: any) => {
const idxAtCursor = cm.doc.getCursor().line;
const lineCount = cm.lineCount();
const lines: string[] = [];
for (let i = idxAtCursor - 1; i >= 0; i--) {
const line: string = cm.doc.getLine(i);
if (line.startsWith('|')) {
lines.splice(0, 0, line);
} else {
break;
}
}
lines.push(cm.doc.getLine(idxAtCursor));
for (let i = idxAtCursor + 1; i < lineCount; i++) {
const line: string = cm.doc.getLine(i);
if (line.startsWith('|')) {
lines.push(line);
} else {
break;
}
}
return lines.join('\n');
};

View File

@@ -37,7 +37,7 @@ export default function useEditorSearch(CodeMirror: any) {
return { token: function(stream: any) {
query.lastIndex = stream.pos;
const match = query.exec(stream.string);
if (match && match.index == stream.pos) {
if (match && match.index === stream.pos) {
stream.pos += match[0].length || 1;
return 'search-marker';
} else if (match) {
@@ -126,7 +126,7 @@ export default function useEditorSearch(CodeMirror: any) {
// SEARCHOVERLAY
// We only want to highlight all matches when there is only 1 search term
if (keywords.length !== 1 || keywords[0].value == '') {
if (keywords.length !== 1 || keywords[0].value === '') {
clearOverlay(this);
const prev = keywords.length > 1 ? keywords[0].value : '';
setPreviousKeywordValue(prev);

View File

@@ -165,7 +165,6 @@ export default function useJoplinMode(CodeMirror: any) {
}
if (isMonospace) { token = `${token} jn-monospace`; }
if (state.inTable) { token = `${token} jn-table-item`; }
// //////// End Monospace //////////
return token;

View File

@@ -2078,6 +2078,17 @@
setup(editor);
};
var setup$2 = function (editor) {
var editorClickHandler = function (event) {
if (!isJoplinChecklistItem(event.target))
return;
if (event.offsetX >= 0)
return;
editor.execCommand('ToggleJoplinChecklistItem', false, { element: event.target });
};
editor.on('click', editorClickHandler);
};
var findIndex = function (list, predicate) {
for (var index = 0; index < list.length; index++) {
var element = list[index];
@@ -2100,21 +2111,8 @@
var listType = findContainerListTypeFromEvent(e);
buttonApi.setActive(listType === options.listType && lists.length > 0 && lists[0].nodeName === listName && !isCustomList(lists[0]));
};
var editorClickHandler = function (event) {
if (!isJoplinChecklistItem(event.target))
return;
if (event.offsetX >= 0)
return;
editor.execCommand('ToggleJoplinChecklistItem', false, { element: event.target });
};
if (options.listType === 'joplinChecklist') {
editor.on('click', editorClickHandler);
}
editor.on('NodeChange', nodeChangeHandler);
return function () {
if (options.listType === 'joplinChecklist') {
editor.off('click', editorClickHandler);
}
editor.off('NodeChange', nodeChangeHandler);
};
};
@@ -2158,6 +2156,7 @@
function Plugin () {
PluginManager.add('joplinLists', function (editor) {
setup$1(editor);
setup$2(editor);
register$1(editor);
register(editor);
return get(editor);

View File

@@ -100,7 +100,7 @@ export function menuItems(dispatch: Function): ContextMenuItems {
label: _('Save as %s', 'PNG'),
onAction: async (options: ContextMenuOptions) => {
// First convert it to png then save
if (options.mime != 'image/svg+xml') {
if (options.mime !== 'image/svg+xml') {
throw new Error(`Unsupported image type: ${options.mime}`);
}
if (!options.filename) {
@@ -151,14 +151,14 @@ export function menuItems(dispatch: Function): ContextMenuItems {
handleCopyToClipboard(options);
options.insertContent('');
},
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => itemType != ContextMenuItemType.Image && (!options.isReadOnly && (!!options.textToCopy || !!options.htmlToCopy)),
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => itemType !== ContextMenuItemType.Image && (!options.isReadOnly && (!!options.textToCopy || !!options.htmlToCopy)),
},
copy: {
label: _('Copy'),
onAction: async (options: ContextMenuOptions) => {
handleCopyToClipboard(options);
},
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => itemType != ContextMenuItemType.Image && (!!options.textToCopy || !!options.htmlToCopy),
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => itemType !== ContextMenuItemType.Image && (!!options.textToCopy || !!options.htmlToCopy),
},
paste: {
label: _('Paste'),

View File

@@ -114,7 +114,7 @@ export default function useFormNote(dependencies: HookDependencies) {
if (syncStarted) return () => {};
if (formNote.hasChanged) return () => {};
reg.logger().debug('Sync has finished and note has never been changed - reloading it');
reg.logger().info('Sync has finished and note has never been changed - reloading it');
let cancelled = false;

View File

@@ -342,7 +342,7 @@ const NoteListComponent = (props: Props) => {
const keyCode = event.keyCode;
const noteIds = props.selectedNoteIds;
if (noteIds.length > 0 && (keyCode === 40 || keyCode === 38 || keyCode === 33 || keyCode === 34 || keyCode === 35 || keyCode == 36)) {
if (noteIds.length > 0 && (keyCode === 40 || keyCode === 38 || keyCode === 33 || keyCode === 34 || keyCode === 35 || keyCode === 36)) {
// DOWN / UP / PAGEDOWN / PAGEUP / END / HOME
const noteId = noteIds[0];
let noteIndex = BaseModel.modelIndexById(props.notes, noteId);
@@ -456,6 +456,11 @@ const NoteListComponent = (props: Props) => {
useEffect(() => {
// When a note list item is styled by userchrome.css, its height is reflected.
// Ref. https://github.com/laurent22/joplin/pull/6542
if (dragOverTargetNoteIndex !== null) {
// When dragged, its height should not be considered.
// Ref. https://github.com/laurent22/joplin/issues/6639
return;
}
const noteItem = Object.values<any>(itemAnchorRefs_.current)[0]?.current;
const actualItemHeight = noteItem?.getHeight() ?? 0;
if (actualItemHeight >= 8) { // To avoid generating too many narrow items

View File

@@ -41,7 +41,7 @@ class NotePropertiesDialog extends React.Component {
}
componentDidUpdate() {
if (this.state.editedKey == null) {
if (this.state.editedKey === null) {
this.okButton.current.focus();
}
}
@@ -59,7 +59,7 @@ class NotePropertiesDialog extends React.Component {
latLongFromLocation(location) {
const o = {};
const l = location.split(',');
if (l.length == 2) {
if (l.length === 2) {
o.latitude = l[0].trim();
o.longitude = l[1].trim();
} else {

View File

@@ -22,7 +22,6 @@ import Dialog from './Dialog';
import SyncWizardDialog from './SyncWizard/Dialog';
import MasterPasswordDialog from './MasterPasswordDialog/Dialog';
import EditFolderDialog from './EditFolderDialog/Dialog';
import TableEditorDialog from './TableEditorDialog/Dialog';
import StyleSheetContainer from './StyleSheets/StyleSheetContainer';
const { ImportScreen } = require('./ImportScreen.min.js');
const { ResourceScreen } = require('./ResourceScreen.js');
@@ -39,7 +38,6 @@ interface Props {
zoomFactor: number;
needApiAuth: boolean;
dialogs: AppStateDialog[];
dialogContentMaxSize: Size;
}
interface ModalDialogProps {
@@ -53,7 +51,6 @@ interface RegisteredDialogProps {
themeId: number;
key: string;
dispatch: Function;
dialogContentMaxSize: Size;
}
interface RegisteredDialog {
@@ -63,25 +60,19 @@ interface RegisteredDialog {
const registeredDialogs: Record<string, RegisteredDialog> = {
syncWizard: {
render: (props: RegisteredDialogProps, customProps: any) => {
return <SyncWizardDialog key={props.key} dispatch={props.dispatch} dialogContentMaxSize={props.dialogContentMaxSize} themeId={props.themeId} {...customProps}/>;
return <SyncWizardDialog key={props.key} dispatch={props.dispatch} themeId={props.themeId} {...customProps}/>;
},
},
masterPassword: {
render: (props: RegisteredDialogProps, customProps: any) => {
return <MasterPasswordDialog key={props.key} dispatch={props.dispatch} dialogContentMaxSize={props.dialogContentMaxSize} themeId={props.themeId} {...customProps}/>;
return <MasterPasswordDialog key={props.key} dispatch={props.dispatch} themeId={props.themeId} {...customProps}/>;
},
},
editFolder: {
render: (props: RegisteredDialogProps, customProps: any) => {
return <EditFolderDialog key={props.key} dispatch={props.dispatch} dialogContentMaxSize={props.dialogContentMaxSize} themeId={props.themeId} {...customProps}/>;
},
},
tableEditor: {
render: (props: RegisteredDialogProps, customProps: any) => {
return <TableEditorDialog key={props.key} dispatch={props.dispatch} dialogContentMaxSize={props.dialogContentMaxSize} themeId={props.themeId} {...customProps}/>;
return <EditFolderDialog key={props.key} dispatch={props.dispatch} themeId={props.themeId} {...customProps}/>;
},
},
};
@@ -130,7 +121,7 @@ async function initialize() {
class RootComponent extends React.Component<Props, any> {
public async componentDidMount() {
if (this.props.appState == 'starting') {
if (this.props.appState === 'starting') {
this.props.dispatch({
type: 'APP_STATE_SET',
state: 'initializing',
@@ -204,12 +195,10 @@ class RootComponent extends React.Component<Props, any> {
for (const dialog of props.dialogs) {
const md = registeredDialogs[dialog.name];
if (!md) throw new Error(`Unknown dialog: ${dialog.name}`);
output.push(md.render({
key: dialog.name,
themeId: props.themeId,
dispatch: props.dispatch,
dialogContentMaxSize: props.dialogContentMaxSize,
}, dialog.props));
}
return output;
@@ -256,11 +245,6 @@ const mapStateToProps = (state: AppState) => {
themeId: state.settings.theme,
needApiAuth: state.needApiAuth,
dialogs: state.dialogs,
dialogContentMaxSize: {
// Minus padding, margins and dialog header and button bar.
width: state.windowContentSize.width - 36 * 2,
height: state.windowContentSize.height - 36 * 2 - 28 - 30 - 20,
},
profileConfigCurrentProfileId: state.profileConfig.currentProfileId,
};
};

View File

@@ -119,9 +119,7 @@ function SearchBar(props: Props) {
}, [onExitSearch]);
const onSearchButtonClick = useCallback(() => {
console.info('isFocused', props.isFocused);
if (props.isFocused) {
if (props.isFocused || searchStarted) {
void onExitSearch();
} else {
setSearchStarted(true);
@@ -131,7 +129,7 @@ function SearchBar(props: Props) {
field: 'globalSearch',
});
}
}, [onExitSearch, props.isFocused]);
}, [onExitSearch, props.isFocused, searchStarted]);
useEffect(() => {
if (props.notesParentType !== 'Search') {

View File

@@ -1,101 +0,0 @@
import * as React from 'react';
import { useCallback, useEffect } from 'react';
import { _ } from '@joplin/lib/locale';
import DialogButtonRow, { ClickEvent } from '../DialogButtonRow';
import Dialog from '../Dialog';
import DialogTitle from '../DialogTitle';
import { parseMarkdownTable } from '../../../lib/markdownUtils';
import { Size } from '../ResizableLayout/utils/types';
interface Props {
themeId: number;
dispatch: Function;
markdownTable: string;
dialogContentMaxSize: Size;
}
const markdownTableToObject = (markdownTable: string): any => {
const table = parseMarkdownTable(markdownTable);
return {
columns: table.headers.map(h => {
return {
title: h.label,
field: h.name,
hozAlign: h.justify,
editor: 'input',
};
}),
data: table.rows.map(row => {
return {
...row,
};
}),
};
};
export default function(props: Props) {
const elementId = `tabulator_${Math.floor(Math.random() * 1000000)}`;
const onClose = useCallback(() => {
props.dispatch({
type: 'DIALOG_CLOSE',
name: 'tableEditor',
});
}, [props.dispatch]);
const onButtonRowClick = useCallback(async (event: ClickEvent) => {
if (event.buttonName === 'cancel') {
onClose();
return;
}
if (event.buttonName === 'ok') {
return;
}
}, [onClose]);
useEffect(() => {
const table = markdownTableToObject(props.markdownTable);
const Tabulator = (window as any).Tabulator;
// TODO: probably doesn't need to be called every time
// TODO: Load CSS/JS dynamically?
// TODO: Clean up on exit
Tabulator.extendModule('edit', 'editors', {});
new Tabulator(`#${elementId}`, {
...table,
height: props.dialogContentMaxSize.height,
});
}, []);
function renderContent() {
return (
<div className="dialog-content">
<div id={elementId}></div>
</div>
);
}
function renderDialogWrapper() {
return (
<div className="dialog-root">
<DialogTitle title={_('Edit table')}/>
{renderContent()}
<DialogButtonRow
themeId={props.themeId}
onClick={onButtonRowClick}
okButtonLabel={_('Save')}
/>
</div>
);
}
return (
<Dialog onClose={onClose} renderContent={renderDialogWrapper}/>
);
}

View File

@@ -40,7 +40,7 @@ scrollmap.get_ = () => {
// embedded into elements by the renderer.
// See also renderer/MdToHtml/rules/source_map.ts.
const elems = document.getElementsByClassName('maps-to-line');
if (elems.length == 0) return null;
if (elems.length === 0) return null;
const map = { line: [0], percent: [0], viewHeight: height, lineCount: 0 };
// Each map entry is total-ordered.
let last = 0;

View File

@@ -7,10 +7,6 @@
uses 'eval'.
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval'">
-->
<!--
To add files below, then need to be in the "vendor" directory. To make this happen, use copyApplicationAssets.js
-->
<title>Joplin</title>
<link rel="stylesheet" href="style.min.css">
<link rel="stylesheet" href="style/icons/style.css">
@@ -19,9 +15,6 @@
<link rel="stylesheet" href="vendor/lib/smalltalk/css/smalltalk.css">
<link rel="stylesheet" href="vendor/lib/roboto-fontface/css/roboto/roboto-fontface.css">
<link rel="stylesheet" href="vendor/lib/codemirror/lib/codemirror.css">
<link rel="stylesheet" href="vendor/lib/tabulator-tables/dist/css/tabulator.min.css">
<script type="text/javascript" src="vendor/lib/tabulator-tables/dist/js/tabulator.min.js"></script>
<style>
.smalltalk {

View File

@@ -26,7 +26,7 @@ const shim = require('@joplin/lib/shim').default;
const { shimInit } = require('@joplin/lib/shim-init-node.js');
const bridge = require('@electron/remote').require('./bridge').default;
const EncryptionService = require('@joplin/lib/services/e2ee/EncryptionService').default;
const { FileApiDriverLocal } = require('@joplin/lib/file-api-driver-local.js');
const { FileApiDriverLocal } = require('@joplin/lib/file-api-driver-local');
const React = require('react');
const nodeSqlite = require('sqlite3');
@@ -131,7 +131,7 @@ app().start(bridge().processArgv()).then((result) => {
}).catch((error) => {
const env = bridge().env();
if (error.code == 'flagError') {
if (error.code === 'flagError') {
bridge().showErrorMessageBox(error.message);
} else {
// If something goes wrong at this stage we don't have a console or a log file

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "2.8.8",
"version": "2.9.1",
"description": "Joplin for Desktop",
"main": "main.js",
"private": true,
@@ -105,7 +105,7 @@
},
"homepage": "https://github.com/laurent22/joplin#readme",
"devDependencies": {
"@joplin/tools": "~2.8",
"@joplin/tools": "~2.9",
"@testing-library/react-hooks": "^3.4.2",
"@types/jest": "^26.0.15",
"@types/node": "^14.14.6",
@@ -137,8 +137,8 @@
"@electron/remote": "^2.0.1",
"@fortawesome/fontawesome-free": "^5.13.0",
"@joeattardi/emoji-button": "^4.6.0",
"@joplin/lib": "~2.8",
"@joplin/renderer": "~2.8",
"@joplin/lib": "~2.9",
"@joplin/renderer": "~2.9",
"async-mutex": "^0.1.3",
"codemirror": "^5.56.0",
"color": "^3.1.2",
@@ -174,7 +174,6 @@
"styled-components": "5.1.1",
"styled-system": "5.1.5",
"taboverride": "^4.0.3",
"tabulator-tables": "^5.1.4",
"tinymce": "^5.2.0"
}
}

View File

@@ -214,7 +214,7 @@ class Dialog extends React.PureComponent<Props, State> {
}
modalLayer_onClick(event: any) {
if (event.currentTarget == event.target) {
if (event.currentTarget === event.target) {
this.props.dispatch({
pluginName: PLUGIN_NAME,
type: 'PLUGINLEGACY_DIALOG_SET',

View File

@@ -1,19 +1,12 @@
import bridge from '../bridge';
import { Implementation as WindowImplementation } from '@joplin/lib/services/plugins/api/JoplinWindow';
import { injectCustomStyles } from '@joplin/lib/CssUtils';
import { VersionInfo } from '@joplin/lib/services/plugins/api/types';
import Setting from '@joplin/lib/models/Setting';
import { reg } from '@joplin/lib/registry';
import BasePlatformImplementation, { Joplin } from '@joplin/lib/services/plugins/BasePlatformImplementation';
const { clipboard, nativeImage } = require('electron');
interface JoplinViewsDialogs {
showMessageBox(message: string): Promise<number>;
}
interface JoplinViews {
dialogs: JoplinViewsDialogs;
}
interface Joplin {
views: JoplinViews;
}
const packageInfo = require('../../packageInfo');
interface Components {
[key: string]: any;
@@ -22,7 +15,7 @@ interface Components {
// PlatformImplementation provides access to platform specific dependencies,
// such as the clipboard, message dialog, etc. It allows having the same plugin
// API for all platforms, but with different implementations.
export default class PlatformImplementation {
export default class PlatformImplementation extends BasePlatformImplementation {
private static instance_: PlatformImplementation;
private joplin_: Joplin;
@@ -33,6 +26,14 @@ export default class PlatformImplementation {
return this.instance_;
}
public get versionInfo(): VersionInfo {
return {
version: packageInfo.version,
syncVersion: Setting.value('syncVersion'),
profileVersion: reg.db().version(),
};
}
public get clipboard() {
return clipboard;
}
@@ -48,6 +49,8 @@ export default class PlatformImplementation {
}
public constructor() {
super();
this.components_ = {};
this.joplin_ = {

View File

@@ -19,7 +19,7 @@ export default function(frameWindow: any, onSubmit: Function, onDismiss: Functio
// Disable enter key from submitting when a text area is in focus!
// https://github.com/laurent22/joplin/issues/4766
//
if (frameWindow.document.activeElement.tagName != 'TEXTAREA') {
if (frameWindow.document.activeElement.tagName !== 'TEXTAREA') {
if (onSubmit) onSubmit();
}
}

View File

@@ -5,7 +5,7 @@ let perFieldReverse: { [field: string]: boolean } = null;
export const notesSortOrderFieldArray = (): string[] => {
// The order of the fields is strictly determinate.
if (fields == null) {
if (fields === null) {
fields = Setting.enumOptionValues('notes.sortOrder.field').sort().reverse();
}
return fields;

View File

@@ -83,8 +83,6 @@ async function main() {
'codemirror/addon/dialog/dialog.css',
'@joeattardi/emoji-button/dist/index.js',
'mark.js/dist/mark.min.js',
'tabulator-tables/dist/css/tabulator.min.css',
'tabulator-tables/dist/js/tabulator.min.js',
{
src: resolve(__dirname, '../../lib/services/plugins/sandboxProxy.js'),
dest: `${buildLibDir}/@joplin/lib/services/plugins/sandboxProxy.js`,

View File

@@ -6,7 +6,7 @@ const execCommand = function(command) {
return new Promise((resolve, reject) => {
exec(command, (error, stdout) => {
if (error) {
if (error.signal == 'SIGTERM') {
if (error.signal === 'SIGTERM') {
resolve('Process was killed');
} else {
reject(error);

View File

@@ -8,7 +8,7 @@ function execCommand(command) {
return new Promise((resolve, reject) => {
exec(command, (error, stdout, stderr) => {
if (error) {
if (error.signal == 'SIGTERM') {
if (error.signal === 'SIGTERM') {
resolve('Process was killed');
} else {
reject(new Error([stdout.trim(), stderr.trim()].join('\n')));

View File

@@ -63,5 +63,7 @@ buck-out/
lib/csstojs/
lib/rnInjectedJs/
dist/
components/NoteEditor/CodeMirror.bundle.js
components/NoteEditor/CodeMirror.bundle.min.js
components/NoteEditor/CodeMirror/CodeMirror.bundle.js
components/NoteEditor/CodeMirror/CodeMirror.bundle.min.js
utils/fs-driver-android.js

View File

@@ -147,7 +147,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2097668
versionName "2.8.1"
versionName "2.9.0"
ndk {
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}

View File

@@ -38,9 +38,10 @@ class Dropdown extends React.Component {
const listTop = Math.min(maxListTop, this.state.headerSize.y + this.state.headerSize.height);
const wrapperStyle = {
width: this.state.headerSize.width,
height: listHeight + 2, // +2 for the border (otherwise it makes the scrollbar appear)
marginTop: listTop,
alignSelf: 'center',
marginLeft: this.state.headerSize.x,
};
const itemListStyle = Object.assign({}, this.props.itemListStyle ? this.props.itemListStyle : {}, {
@@ -86,7 +87,6 @@ class Dropdown extends React.Component {
if (this.props.labelTransform && this.props.labelTransform === 'trim') headerLabel = headerLabel.trim();
const closeList = () => {
if (this.props.onClose) this.props.onClose();
this.setState({ listVisible: false });
};
@@ -116,7 +116,6 @@ class Dropdown extends React.Component {
onPress={() => {
this.updateHeaderCoordinates();
this.setState({ listVisible: true });
if (this.props.onOpen) this.props.onOpen();
}}
>
<Text ellipsizeMode="tail" numberOfLines={1} style={headerStyle}>

View File

@@ -139,6 +139,17 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
js.push('}');
js.push('true;');
// iOS doesn't automatically adjust the WebView's font size to match users'
// accessibility settings. To do this, we need to tell it to match the system font.
// See https://github.com/ionic-team/capacitor/issues/2748#issuecomment-612923135
const iOSSpecificCss = `
@media screen {
:root body {
font: -apple-system-body;
}
}
`;
html =
`
<!DOCTYPE html>
@@ -146,6 +157,9 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
${shim.mobilePlatform() === 'ios' ? iOSSpecificCss : ''}
</style>
${assetsToHeaders(result.pluginAssets, { asHtml: true })}
</head>
<body>

View File

@@ -9,18 +9,30 @@
// wrapper to access CodeMirror functionalities. Anything else should be done
// from NoteEditor.tsx.
import { EditorState, Extension } from '@codemirror/state';
import { EditorView, drawSelection, highlightSpecialChars, ViewUpdate } from '@codemirror/view';
import createTheme from './theme';
import decoratorExtension from './decoratorExtension';
import { EditorState } from '@codemirror/state';
import { markdown } from '@codemirror/lang-markdown';
import { defaultHighlightStyle, HighlightStyle, tags } from '@codemirror/highlight';
import { undo, redo, history, undoDepth, redoDepth } from '@codemirror/history';
import { highlightSelectionMatches, search } from '@codemirror/search';
import { EditorView, drawSelection, highlightSpecialChars, ViewUpdate } from '@codemirror/view';
import { undo, redo, history, undoDepth, redoDepth } from '@codemirror/commands';
import { keymap } from '@codemirror/view';
import { indentOnInput } from '@codemirror/language';
import { searchKeymap } from '@codemirror/search';
import { historyKeymap, defaultKeymap } from '@codemirror/commands';
import { MarkdownMathExtension } from './markdownMathParser';
import { GFM as GitHubFlavoredMarkdownExtension } from '@lezer/markdown';
import syntaxHighlightingLanguages from './syntaxHighlightingLanguages';
interface CodeMirrorResult {
editor: EditorView;
undo: Function;
redo: Function;
select: (anchor: number, head: number)=> void;
insertText: (text: string)=> void;
select(anchor: number, head: number): void;
scrollSelectionIntoView(): void;
insertText(text: string): void;
}
function postMessage(name: string, data: any) {
@@ -34,116 +46,6 @@ function logMessage(...msg: any[]) {
postMessage('onLog', { value: msg });
}
// For an example on how to customize the theme, see:
//
// https://github.com/codemirror/theme-one-dark/blob/main/src/one-dark.ts
//
// For a tutorial, see:
//
// https://codemirror.net/6/examples/styling/#themes
//
// Use Safari developer tools to view the content of the CodeMirror iframe while
// the app is running. It seems that what appears as ".ͼ1" in the CSS is the
// equivalent of "&" in the theme object. So to target ".ͼ1.cm-focused", you'd
// use '&.cm-focused' in the theme.
const createTheme = (theme: any): Extension => {
const isDarkTheme = theme.appearance === 'dark';
const baseGlobalStyle: Record<string, string> = {
color: theme.color,
backgroundColor: theme.backgroundColor,
fontFamily: theme.fontFamily,
fontSize: `${theme.fontSize}px`,
};
const baseCursorStyle: Record<string, string> = { };
const baseContentStyle: Record<string, string> = { };
const baseSelectionStyle: Record<string, string> = { };
// If we're in dark mode, the caret and selection are difficult to see.
// Adjust them appropriately
if (isDarkTheme) {
// Styling the caret requires styling both the caret itself
// and the CodeMirror caret.
// See https://codemirror.net/6/examples/styling/#themes
baseContentStyle.caretColor = 'white';
baseCursorStyle.borderLeftColor = 'white';
baseSelectionStyle.backgroundColor = '#6b6b6b';
}
const baseTheme = EditorView.baseTheme({
'&': baseGlobalStyle,
// These must be !important or more specific than CodeMirror's built-ins
'.cm-content': baseContentStyle,
'&.cm-focused .cm-cursor': baseCursorStyle,
'&.cm-focused .cm-selectionBackground, ::selection': baseSelectionStyle,
'&.cm-focused': {
outline: 'none',
},
});
const appearanceTheme = EditorView.theme({}, { dark: isDarkTheme });
const baseHeadingStyle = {
fontWeight: 'bold',
fontFamily: theme.fontFamily,
};
const syntaxHighlighting = HighlightStyle.define([
{
tag: tags.strong,
fontWeight: 'bold',
},
{
tag: tags.emphasis,
fontStyle: 'italic',
},
{
...baseHeadingStyle,
tag: tags.heading1,
fontSize: '1.6em',
borderBottom: `1px solid ${theme.dividerColor}`,
},
{
...baseHeadingStyle,
tag: tags.heading2,
fontSize: '1.4em',
},
{
...baseHeadingStyle,
tag: tags.heading3,
fontSize: '1.3em',
},
{
...baseHeadingStyle,
tag: tags.heading4,
fontSize: '1.2em',
},
{
...baseHeadingStyle,
tag: tags.heading5,
fontSize: '1.1em',
},
{
...baseHeadingStyle,
tag: tags.heading6,
fontSize: '1.0em',
},
{
tag: tags.list,
fontFamily: theme.fontFamily,
},
]);
return [
baseTheme,
appearanceTheme,
syntaxHighlighting,
];
};
export function initCodeMirror(parentElement: any, initialText: string, theme: any): CodeMirrorResult {
logMessage('Initializing CodeMirror...');
@@ -168,15 +70,27 @@ export function initCodeMirror(parentElement: any, initialText: string, theme: a
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(),
createTheme(theme),
markdown({
extensions: [
MarkdownMathExtension,
GitHubFlavoredMarkdownExtension,
],
codeLanguages: syntaxHighlightingLanguages,
}),
...createTheme(theme),
history(),
search(),
drawSelection(),
highlightSpecialChars(),
highlightSelectionMatches(),
indentOnInput(),
decoratorExtension,
EditorView.lineWrapping,
EditorView.contentAttributes.of({ autocapitalize: 'sentence' }),
defaultHighlightStyle.fallback,
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
if (viewUpdate.docChanged) {
postMessage('onChange', { value: editor.state.doc.toString() });
@@ -190,6 +104,9 @@ export function initCodeMirror(parentElement: any, initialText: string, theme: a
postMessage('onSelectionChange', { selection: { start: selStart, end: selEnd } });
}
}),
keymap.of([
...defaultKeymap, ...historyKeymap, ...searchKeymap,
]),
],
doc: initialText,
}),
@@ -212,6 +129,11 @@ export function initCodeMirror(parentElement: any, initialText: string, theme: a
scrollIntoView: true,
}));
},
scrollSelectionIntoView: () => {
editor.dispatch(editor.state.update({
scrollIntoView: true,
}));
},
insertText: (text: string) => {
editor.dispatch(editor.state.replaceSelection(text));
},

View File

@@ -0,0 +1,170 @@
//
// Exports an editor plugin that creates multi-line decorations based on the
// editor's syntax tree (assumes markdown).
//
// For more about creating decorations, see https://codemirror.net/examples/zebra/
//
import { Decoration, EditorView } from '@codemirror/view';
import { ViewPlugin, DecorationSet, ViewUpdate } from '@codemirror/view';
import { ensureSyntaxTree } from '@codemirror/language';
import { RangeSetBuilder } from '@codemirror/state';
const regionStartDecoration = Decoration.line({
attributes: { class: 'cm-regionFirstLine' },
});
const regionStopDecoration = Decoration.line({
attributes: { class: 'cm-regionLastLine' },
});
const codeBlockDecoration = Decoration.line({
attributes: { class: 'cm-codeBlock', spellcheck: 'false' },
});
const inlineCodeDecoration = Decoration.mark({
attributes: { class: 'cm-inlineCode', spellcheck: 'false' },
});
const mathBlockDecoration = Decoration.line({
attributes: { class: 'cm-mathBlock', spellcheck: 'false' },
});
const inlineMathDecoration = Decoration.mark({
attributes: { class: 'cm-inlineMath', spellcheck: 'false' },
});
const urlDecoration = Decoration.mark({
attributes: { class: 'cm-url', spellcheck: 'false' },
});
const blockQuoteDecoration = Decoration.line({
attributes: { class: 'cm-blockQuote' },
});
const headerLineDecoration = Decoration.line({
attributes: { class: 'cm-headerLine' },
});
type DecorationDescription = { pos: number; length?: number; decoration: Decoration };
// Returns a set of [Decoration]s, associated with block syntax groups that require
// full-line styling.
const computeDecorations = (view: EditorView) => {
const decorations: DecorationDescription[] = [];
// Add a decoration to all lines between the document position [from] up to
// and includeing the position [to].
const addDecorationToLines = (from: number, to: number, decoration: Decoration) => {
let pos = from;
while (pos <= to) {
const line = view.state.doc.lineAt(pos);
decorations.push({
pos: line.from,
decoration,
});
// Move to the next line
pos = line.to + 1;
}
};
const addDecorationToRange = (from: number, to: number, decoration: Decoration) => {
decorations.push({
pos: from,
length: to - from,
decoration,
});
};
for (const { from, to } of view.visibleRanges) {
ensureSyntaxTree(
view.state,
to
)?.iterate({
from, to,
enter: node => {
let blockDecorated = false;
// Compute the visible region of the node.
const viewFrom = Math.max(from, node.from);
const viewTo = Math.min(to, node.to);
switch (node.name) {
case 'FencedCode':
case 'CodeBlock':
addDecorationToLines(viewFrom, viewTo, codeBlockDecoration);
blockDecorated = true;
break;
case 'BlockMath':
addDecorationToLines(viewFrom, viewTo, mathBlockDecoration);
blockDecorated = true;
break;
case 'Blockquote':
addDecorationToLines(viewFrom, viewTo, blockQuoteDecoration);
blockDecorated = true;
break;
case 'InlineMath':
addDecorationToRange(viewFrom, viewTo, inlineMathDecoration);
break;
case 'InlineCode':
addDecorationToRange(viewFrom, viewTo, inlineCodeDecoration);
break;
case 'URL':
addDecorationToRange(viewFrom, viewTo, urlDecoration);
break;
case 'SetextHeading1':
case 'SetextHeading2':
case 'ATXHeading1':
case 'ATXHeading2':
case 'ATXHeading3':
case 'ATXHeading4':
case 'ATXHeading5':
case 'ATXHeading6':
addDecorationToLines(viewFrom, viewTo, headerLineDecoration);
break;
}
// Only block decorations will have differing first and last lines
if (blockDecorated) {
// Allow different styles for the first, last lines in a block.
if (viewFrom === node.from) {
addDecorationToLines(viewFrom, viewFrom, regionStartDecoration);
}
if (viewTo === node.to) {
addDecorationToLines(viewTo, viewTo, regionStopDecoration);
}
}
},
});
}
decorations.sort((a, b) => a.pos - b.pos);
// Items need to be added to a RangeSetBuilder in ascending order
const decorationBuilder = new RangeSetBuilder<Decoration>();
for (const { pos, length, decoration } of decorations) {
// Null length => entire line
decorationBuilder.add(pos, pos + (length ?? 0), decoration);
}
return decorationBuilder.finish();
};
const decoratorExtension = ViewPlugin.fromClass(class {
public decorations: DecorationSet;
public constructor(view: EditorView) {
this.decorations = computeDecorations(view);
}
public update(viewUpdate: ViewUpdate) {
if (viewUpdate.docChanged || viewUpdate.viewportChanged) {
this.decorations = computeDecorations(viewUpdate.view);
}
}
}, {
decorations: pluginVal => pluginVal.decorations,
});
export default decoratorExtension;

View File

@@ -0,0 +1,152 @@
import { markdown } from '@codemirror/lang-markdown';
import { ensureSyntaxTree } from '@codemirror/language';
import { SyntaxNode } from '@lezer/common';
import { EditorState } from '@codemirror/state';
import { blockMathTagName, inlineMathContentTagName, inlineMathTagName, MarkdownMathExtension } from './markdownMathParser';
import { GFM as GithubFlavoredMarkdownExt } from '@lezer/markdown';
const syntaxTreeCreateTimeout = 100; // ms
/** Create an EditorState with markdown extensions */
const createEditorState = (initialText: string): EditorState => {
return EditorState.create({
doc: initialText,
extensions: [
markdown({
extensions: [MarkdownMathExtension, GithubFlavoredMarkdownExt],
}),
],
});
};
/**
* Returns a list of all nodes with the given name in the given editor's syntax tree.
* Attempts to create the syntax tree if it doesn't exist.
*/
const findNodesWithName = (editor: EditorState, nodeName: string) => {
const result: SyntaxNode[] = [];
ensureSyntaxTree(editor, syntaxTreeCreateTimeout)?.iterate({
enter: (node) => {
if (node.name === nodeName) {
result.push(node.node);
}
},
});
return result;
};
describe('Inline parsing', () => {
it('Document with just a math region', () => {
const documentText = '$3 + 3$';
const editor = createEditorState(documentText);
const inlineMathNodes = findNodesWithName(editor, inlineMathTagName);
const inlineMathContentNodes = findNodesWithName(editor, inlineMathContentTagName);
// There should only be one inline node
expect(inlineMathNodes.length).toBe(1);
expect(inlineMathNodes[0].from).toBe(0);
expect(inlineMathNodes[0].to).toBe(documentText.length);
// The content tag should be replaced by the internal sTeX parser
expect(inlineMathContentNodes.length).toBe(0);
});
it('Inline math mixed with text', () => {
const beforeMath = '# Testing!\n\nThis is a test of ';
const mathRegion = '$\\TeX % TeX Comment!$';
const afterMath = ' formatting.';
const documentText = `${beforeMath}${mathRegion}${afterMath}`;
const editor = createEditorState(documentText);
const inlineMathNodes = findNodesWithName(editor, inlineMathTagName);
const blockMathNodes = findNodesWithName(editor, blockMathTagName);
const commentNodes = findNodesWithName(editor, 'comment');
expect(inlineMathNodes.length).toBe(1);
expect(blockMathNodes.length).toBe(0);
expect(commentNodes.length).toBe(1);
expect(inlineMathNodes[0].from).toBe(beforeMath.length);
expect(inlineMathNodes[0].to).toBe(beforeMath.length + mathRegion.length);
});
it('Inline math with no ending $ in a block', () => {
const documentText = 'This is a $test\n\nof inline math$...';
const editor = createEditorState(documentText);
const inlineMathNodes = findNodesWithName(editor, inlineMathTagName);
// Math should end if there is no matching '$'.
expect(inlineMathNodes.length).toBe(0);
});
it('Shouldn\'t start if block would have spaces just inside', () => {
const documentText = 'This is a $ test of inline math$...\n\n$Testing... $...';
const editor = createEditorState(documentText);
expect(findNodesWithName(editor, inlineMathTagName).length).toBe(0);
});
it('Shouldn\'t start if $ is escaped', () => {
const documentText = 'This is a \\$test of inline math$...';
const editor = createEditorState(documentText);
expect(findNodesWithName(editor, inlineMathTagName).length).toBe(0);
});
});
describe('Block math tests', () => {
it('Document with just block math', () => {
const documentText = '$$\n\t\\{ 1, 1, 2, 3, 5, ... \\}\n$$';
const editor = createEditorState(documentText);
const inlineMathNodes = findNodesWithName(editor, inlineMathTagName);
const blockMathNodes = findNodesWithName(editor, blockMathTagName);
expect(inlineMathNodes.length).toBe(0);
expect(blockMathNodes.length).toBe(1);
expect(blockMathNodes[0].from).toBe(0);
expect(blockMathNodes[0].to).toBe(documentText.length);
});
it('Block math with comment', () => {
const startingText = '$$ % Testing...\n\t\\text{Test.}\n$$';
const afterMath = '\nTest.';
const editor = createEditorState(startingText + afterMath);
const inlineMathNodes = findNodesWithName(editor, inlineMathTagName);
const blockMathNodes = findNodesWithName(editor, blockMathTagName);
const texParserComments = findNodesWithName(editor, 'comment');
expect(inlineMathNodes.length).toBe(0);
expect(blockMathNodes.length).toBe(1);
expect(texParserComments.length).toBe(1);
expect(blockMathNodes[0].from).toBe(0);
expect(blockMathNodes[0].to).toBe(startingText.length);
expect(texParserComments[0]).toMatchObject({
from: '$$ '.length,
to: '$$ % Testing...'.length,
});
});
it('Block math without an ending tag', () => {
const beforeMath = '# Testing...\n\n';
const documentText = `${beforeMath}$$\n\t\\text{Testing...}\n\n\t3 + 3 = 6`;
const editor = createEditorState(documentText);
const blockMathNodes = findNodesWithName(editor, blockMathTagName);
expect(blockMathNodes.length).toBe(1);
expect(blockMathNodes[0].from).toBe(beforeMath.length);
expect(blockMathNodes[0].to).toBe(documentText.length);
});
it('Single-line declaration of block math', () => {
const documentText = '$$ Test. $$';
const editor = createEditorState(documentText);
const blockMathNodes = findNodesWithName(editor, blockMathTagName);
expect(blockMathNodes.length).toBe(1);
expect(blockMathNodes[0].from).toBe(0);
expect(blockMathNodes[0].to).toBe(documentText.length);
});
});

View File

@@ -0,0 +1,216 @@
/**
* Search for $s and $$s in markdown and mark the regions between them as math.
*
* Text between single $s is marked as InlineMath and text between $$s is marked
* as BlockMath.
*/
import { tags, Tag } from '@lezer/highlight';
import { parseMixed, SyntaxNodeRef, Input, NestedParse, ParseWrapper } from '@lezer/common';
// Extend the existing markdown parser
import {
MarkdownConfig, InlineContext,
BlockContext, Line, LeafBlock,
} from '@lezer/markdown';
// The existing stexMath parser is used to parse the text between the $s
import { stexMath } from '@codemirror/legacy-modes/mode/stex';
import { StreamLanguage } from '@codemirror/language';
const dollarSignCharcode = 36;
const backslashCharcode = 92;
// (?:[>]\s*)?: Optionally allow block math lines to start with '> '
const mathBlockStartRegex = /^(?:\s*[>]\s*)?\$\$/;
const mathBlockEndRegex = /\$\$\s*$/;
const texLanguage = StreamLanguage.define(stexMath);
export const blockMathTagName = 'BlockMath';
export const blockMathContentTagName = 'BlockMathContent';
export const inlineMathTagName = 'InlineMath';
export const inlineMathContentTagName = 'InlineMathContent';
export const mathTag = Tag.define(tags.monospace);
export const inlineMathTag = Tag.define(mathTag);
/**
* Wraps a TeX math-mode parser. This removes [nodeTag] from the syntax tree
* and replaces it with a region handled by the sTeXMath parser.
*
* @param nodeTag Name of the nodes to replace with regions parsed by the sTeX parser.
* @returns a wrapped sTeX parser.
*/
const wrappedTeXParser = (nodeTag: string): ParseWrapper => {
return parseMixed((node: SyntaxNodeRef, _input: Input): NestedParse => {
if (node.name !== nodeTag) {
return null;
}
return {
parser: texLanguage.parser,
};
});
};
// Markdown extension for recognizing inline code
const InlineMathConfig: MarkdownConfig = {
defineNodes: [
{
name: inlineMathTagName,
style: inlineMathTag,
},
{
name: inlineMathContentTagName,
},
],
parseInline: [{
name: inlineMathTagName,
after: 'InlineCode',
parse(cx: InlineContext, current: number, pos: number): number {
const prevCharCode = pos - 1 >= 0 ? cx.char(pos - 1) : -1;
const nextCharCode = cx.char(pos + 1);
if (current !== dollarSignCharcode
|| prevCharCode === dollarSignCharcode
|| nextCharCode === dollarSignCharcode) {
return -1;
}
// Don't match if there's a space directly after the '$'
if (/\s/.exec(String.fromCharCode(nextCharCode))) {
return -1;
}
const start = pos;
const end = cx.end;
let escaped = false;
pos ++;
// Scan ahead for the next '$' symbol
for (; pos < end && (escaped || cx.char(pos) !== dollarSignCharcode); pos++) {
if (!escaped && cx.char(pos) === backslashCharcode) {
escaped = true;
} else {
escaped = false;
}
}
// Don't match if the ending '$' is preceded by a space.
const prevChar = String.fromCharCode(cx.char(pos - 1));
if (/\s/.exec(prevChar)) {
return -1;
}
// It isn't a math region if there is no ending '$'
if (pos === end) {
return -1;
}
// Advance to just after the ending '$'
pos ++;
// Add a wraping inlineMathTagName node that contains an inlineMathContentTagName.
// The inlineMathContentTagName node can thus be safely removed and the region
// will still be marked as a math region.
const contentElem = cx.elt(inlineMathContentTagName, start + 1, pos - 1);
cx.addElement(cx.elt(inlineMathTagName, start, pos, [contentElem]));
return pos + 1;
},
}],
wrap: wrappedTeXParser(inlineMathContentTagName),
};
// Extension for recognising block code
const BlockMathConfig: MarkdownConfig = {
defineNodes: [
{
name: blockMathTagName,
style: mathTag,
},
{
name: blockMathContentTagName,
},
],
parseBlock: [{
name: blockMathTagName,
before: 'Blockquote',
parse(cx: BlockContext, line: Line): boolean {
const delimLen = 2;
// $$ delimiter? Start math!
const mathStartMatch = mathBlockStartRegex.exec(line.text);
if (mathStartMatch) {
const start = cx.lineStart + mathStartMatch[0].length;
let stop;
let endMatch = mathBlockEndRegex.exec(
line.text.substring(mathStartMatch[0].length)
);
// If the math region ends immediately (on the same line),
if (endMatch) {
const lineLength = line.text.length;
stop = cx.lineStart + lineLength - endMatch[0].length;
} else {
let hadNextLine = false;
// Otherwise, it's a multi-line block display.
// Consume lines until we reach the end.
do {
hadNextLine = cx.nextLine();
endMatch = hadNextLine ? mathBlockEndRegex.exec(line.text) : null;
}
while (hadNextLine && endMatch === null);
if (hadNextLine && endMatch) {
const lineLength = line.text.length;
// Remove the ending delimiter
stop = cx.lineStart + lineLength - endMatch[0].length;
} else {
stop = cx.lineStart;
}
}
const lineEnd = cx.lineStart + line.text.length;
// Label the region. Add two labels so that one can be removed.
const contentElem = cx.elt(blockMathContentTagName, start, stop);
const containerElement = cx.elt(
blockMathTagName,
start - delimLen,
// Math blocks don't need ending delimiters, so ensure we don't
// include text that doesn't exist.
Math.min(lineEnd, stop + delimLen),
// The child of the container element should be the content element
[contentElem]
);
cx.addElement(containerElement);
// Don't re-process the ending delimiter (it may look the same
// as the starting delimiter).
cx.nextLine();
return true;
}
return false;
},
// End paragraph-like blocks
endLeaf(_cx: BlockContext, line: Line, _leaf: LeafBlock): boolean {
// Leaf blocks (e.g. block quotes) end early if math starts.
return mathBlockStartRegex.exec(line.text) !== null;
},
}],
wrap: wrappedTeXParser(blockMathContentTagName),
};
/** Markdown configuration for block and inline math support. */
export const MarkdownMathExtension: MarkdownConfig[] = [
InlineMathConfig,
BlockMathConfig,
];

View File

@@ -0,0 +1,236 @@
//
// Exports a list of languages that can be used in fenced code blocks.
//
import { LanguageDescription, LanguageSupport, StreamParser } from '@codemirror/language';
import { StreamLanguage } from '@codemirror/language';
import { python } from '@codemirror/legacy-modes/mode/python';
import { c, dart } from '@codemirror/legacy-modes/mode/clike';
import { lua } from '@codemirror/legacy-modes/mode/lua';
import { r } from '@codemirror/legacy-modes/mode/r';
import { ruby } from '@codemirror/legacy-modes/mode/ruby';
import { swift } from '@codemirror/legacy-modes/mode/swift';
import { go } from '@codemirror/legacy-modes/mode/go';
import { vb } from '@codemirror/legacy-modes/mode/vb';
import { vbScript } from '@codemirror/legacy-modes/mode/vbscript';
import { css } from '@codemirror/legacy-modes/mode/css';
import { stex } from '@codemirror/legacy-modes/mode/stex';
import { groovy } from '@codemirror/legacy-modes/mode/groovy';
import { perl } from '@codemirror/legacy-modes/mode/perl';
import { cobol } from '@codemirror/legacy-modes/mode/cobol';
import { julia } from '@codemirror/legacy-modes/mode/julia';
import { haskell } from '@codemirror/legacy-modes/mode/haskell';
import { pascal } from '@codemirror/legacy-modes/mode/pascal';
import { yaml } from '@codemirror/legacy-modes/mode/yaml';
import { xml } from '@codemirror/legacy-modes/mode/xml';
import { shell } from '@codemirror/legacy-modes/mode/shell';
import { dockerFile } from '@codemirror/legacy-modes/mode/dockerfile';
import { diff } from '@codemirror/legacy-modes/mode/diff';
import { erlang } from '@codemirror/legacy-modes/mode/erlang';
import { sqlite, standardSQL, mySQL } from '@codemirror/legacy-modes/mode/sql';
import { javascript } from '@codemirror/lang-javascript';
import { markdown } from '@codemirror/lang-markdown';
import { html } from '@codemirror/lang-html';
import { cpp } from '@codemirror/lang-cpp';
import { php } from '@codemirror/lang-php';
import { java } from '@codemirror/lang-java';
import { rust } from '@codemirror/lang-rust';
const supportedLanguages: {
name: string;
aliases?: string[];
// Either support or parser must be given
parser?: StreamParser<any>;
support?: LanguageSupport;
}[] = [
// Based on @joplin/desktop/CodeMirror/Editor.tsx
{
name: 'LaTeX',
aliases: ['tex', 'latex', 'luatex'],
parser: stex,
},
{
name: 'python',
aliases: ['py'],
parser: python,
},
{
name: 'clike',
aliases: ['c', 'h'],
parser: c,
},
{
name: 'C++',
aliases: ['cpp', 'hpp', 'cxx', 'hxx', 'c++'],
support: cpp(),
},
{
name: 'java',
support: java(),
},
{
name: 'javascript',
aliases: ['js', 'mjs'],
support: javascript(),
},
{
name: 'typescript',
aliases: ['ts'],
support: javascript({ jsx: false, typescript: true }),
},
{
name: 'react javascript',
aliases: ['jsx'],
support: javascript({ jsx: true, typescript: false }),
},
{
name: 'react typescript',
aliases: ['tsx'],
support: javascript({ jsx: true, typescript: true }),
},
{
name: 'lua',
parser: lua,
},
{
name: 'php',
support: php(),
},
{
name: 'r',
parser: r,
},
{
name: 'swift',
parser: swift,
},
{
name: 'go',
parser: go,
},
{
name: 'visualbasic',
aliases: ['vb'],
parser: vb,
},
{
name: 'visualbasicscript',
aliases: ['vbscript', 'vbs'],
parser: vbScript,
},
{
name: 'ruby',
aliases: ['rb'],
parser: ruby,
},
{
name: 'rust',
aliases: ['rs'],
support: rust(),
},
{
name: 'dart',
parser: dart,
},
{
name: 'groovy',
parser: groovy,
},
{
name: 'perl',
aliases: ['pl'],
parser: perl,
},
{
name: 'cobol',
aliases: ['cbl', 'cob'],
parser: cobol,
},
{
name: 'julia',
aliases: ['jl'],
parser: julia,
},
{
name: 'haskell',
aliases: ['hs'],
parser: haskell,
},
{
name: 'pascal',
parser: pascal,
},
{
name: 'css',
parser: css,
},
{
name: 'xml',
aliases: ['xhtml'],
parser: xml,
},
{
name: 'html',
aliases: ['html', 'htm'],
support: html(),
},
{
name: 'markdown',
support: markdown(),
},
{
name: 'yaml',
parser: yaml,
},
{
name: 'shell',
aliases: ['bash', 'sh', 'zsh', 'dash'],
parser: shell,
},
{
name: 'dockerfile',
parser: dockerFile,
},
{
name: 'diff',
parser: diff,
},
{
name: 'erlang',
parser: erlang,
},
{
name: 'sql',
parser: standardSQL,
},
{
name: 'sqlite',
parser: sqlite,
},
{
name: 'mysql',
parser: mySQL,
},
];
// Convert supportedLanguages to a CodeMirror-readable list
// of LanguageDescriptions
const syntaxHighlightingLanguages: LanguageDescription[] = [];
for (const language of supportedLanguages) {
// Convert from parsers to LanguageSupport objects as necessary
const support = language.support ?? new LanguageSupport(StreamLanguage.define(language.parser));
syntaxHighlightingLanguages.push(
LanguageDescription.of({
name: language.name,
alias: language.aliases,
support,
})
);
}
export default syntaxHighlightingLanguages;

View File

@@ -0,0 +1,234 @@
//
// Create a set of Extensions that provide syntax highlighting.
//
import { defaultHighlightStyle, syntaxHighlighting, HighlightStyle } from '@codemirror/language';
import { tags } from '@lezer/highlight';
import { EditorView } from '@codemirror/view';
import { Extension } from '@codemirror/state';
import { inlineMathTag, mathTag } from './markdownMathParser';
// For an example on how to customize the theme, see:
//
// https://github.com/codemirror/theme-one-dark/blob/main/src/one-dark.ts
//
// For a tutorial, see:
//
// https://codemirror.net/6/examples/styling/#themes
//
// Use Safari developer tools to view the content of the CodeMirror iframe while
// the app is running. It seems that what appears as ".ͼ1" in the CSS is the
// equivalent of "&" in the theme object. So to target ".ͼ1.cm-focused", you'd
// use '&.cm-focused' in the theme.
//
// [theme] should be a joplin theme (see @joplin/lib/theme)
const createTheme = (theme: any): Extension[] => {
const isDarkTheme = theme.appearance === 'dark';
const baseGlobalStyle: Record<string, string> = {
color: theme.color,
backgroundColor: theme.backgroundColor,
// On iOS, apply system font scaling (e.g. font scaling
// set in accessibility settings).
font: '-apple-system-body',
};
const baseCursorStyle: Record<string, string> = { };
const baseContentStyle: Record<string, string> = {
fontFamily: theme.fontFamily,
// 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> = { };
// If we're in dark mode, the caret and selection are difficult to see.
// Adjust them appropriately
if (isDarkTheme) {
// Styling the caret requires styling both the caret itself
// and the CodeMirror caret.
// See https://codemirror.net/6/examples/styling/#themes
baseContentStyle.caretColor = 'white';
baseCursorStyle.borderLeftColor = 'white';
baseSelectionStyle.backgroundColor = '#6b6b6b';
blurredSelectionStyle.backgroundColor = '#444';
}
const baseTheme = EditorView.baseTheme({
'&': baseGlobalStyle,
// These must be !important or more specific than CodeMirror's built-ins
'.cm-content': {
fontFamily: theme.fontFamily,
...baseContentStyle,
},
'&.cm-focused .cm-cursor': baseCursorStyle,
'&.cm-focused .cm-selectionBackground, ::selection': baseSelectionStyle,
'.cm-selectionBackground': blurredSelectionStyle,
'&.cm-focused': {
outline: 'none',
},
'& .cm-blockQuote': {
borderLeft: `4px solid ${theme.colorFaded}`,
opacity: theme.blockQuoteOpacity,
paddingLeft: '4px',
},
'& .cm-codeBlock': {
'&.cm-regionFirstLine, &.cm-regionLastLine': {
borderRadius: '3px',
},
'&:not(.cm-regionFirstLine)': {
borderTop: 'none',
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
},
'&:not(.cm-regionLastLine)': {
borderBottom: 'none',
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
},
borderWidth: '1px',
borderStyle: 'solid',
borderColor: theme.colorFaded,
backgroundColor: 'rgba(155, 155, 155, 0.1)',
},
// CodeMirror wraps the existing inline span in an additional element.
// Due to a Chrome rendering bug, because the .cm-inlineCode wraps a
// span with a larger font-size, the .cm-inlineCode's bounding box won't
// be big enough for its content.
// As such, we need to style whichever element directly wraps its content.
'& .cm-headerLine > .cm-inlineCode > *, & :not(.cm-headerLine) > .cm-inlineCode': {
borderWidth: '1px',
borderStyle: 'solid',
borderColor: isDarkTheme ? 'rgba(200, 200, 200, 0.5)' : 'rgba(100, 100, 100, 0.5)',
borderRadius: '4px',
},
'& .cm-mathBlock, & .cm-inlineMath': {
color: isDarkTheme ? '#9fa' : '#276',
},
// Style the search widget. Use ':root' to increase the selector's precedence
// (override the existing preset styles).
':root & .cm-panel.cm-search': {
'& label, & button, & input': {
fontSize: '1em',
color: isDarkTheme ? 'white' : 'black',
},
},
});
const appearanceTheme = EditorView.theme({}, { dark: isDarkTheme });
const baseHeadingStyle = {
fontWeight: 'bold',
fontFamily: theme.fontFamily,
};
const highlightingStyle = HighlightStyle.define([
{
tag: tags.strong,
fontWeight: 'bold',
},
{
tag: tags.emphasis,
fontStyle: 'italic',
},
{
...baseHeadingStyle,
tag: tags.heading1,
fontSize: '1.6em',
borderBottom: `1px solid ${theme.dividerColor}`,
},
{
...baseHeadingStyle,
tag: tags.heading2,
fontSize: '1.4em',
},
{
...baseHeadingStyle,
tag: tags.heading3,
fontSize: '1.3em',
},
{
...baseHeadingStyle,
tag: tags.heading4,
fontSize: '1.2em',
},
{
...baseHeadingStyle,
tag: tags.heading5,
fontSize: '1.1em',
},
{
...baseHeadingStyle,
tag: tags.heading6,
fontSize: '1.0em',
},
{
tag: tags.list,
fontFamily: theme.fontFamily,
},
{
tag: tags.comment,
opacity: 0.9,
fontStyle: 'italic',
},
{
tag: tags.link,
color: theme.urlColor,
textDecoration: 'underline',
},
{
tag: [mathTag, inlineMathTag],
fontStyle: 'italic',
},
// Content of code blocks
{
tag: tags.keyword,
color: isDarkTheme ? '#ff7' : '#740',
},
{
tag: tags.operator,
color: isDarkTheme ? '#f7f' : '#805',
},
{
tag: tags.literal,
color: isDarkTheme ? '#aaf' : '#037',
},
{
tag: tags.operator,
color: isDarkTheme ? '#fa9' : '#490',
},
{
tag: tags.typeName,
color: isDarkTheme ? '#7ff' : '#a00',
},
]);
return [
baseTheme,
appearanceTheme,
syntaxHighlighting(highlightingStyle),
// If we haven't defined highlighting for tags, fall back
// to the default.
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
];
};
export default createTheme;

View File

@@ -44,145 +44,6 @@ function fontFamilyFromSettings() {
return [f, 'sans-serif'].join(', ');
}
// Obsolete with CodeMirror 6. See ./CodeMirror.ts for styling.
// function useCss(themeId:number):string {
// const [css, setCss] = useState('');
// // useEffect(() => {
// // const theme = themeStyle(themeId);
// // // Selection in dark mode is hard to see so make it brighter.
// // // https://discourse.joplinapp.org/t/dragging-in-dark-theme/12433/4?u=laurent
// // const selectionColorCss = theme.appearance === ThemeAppearance.Dark ?
// // `.CodeMirror-selected {
// // background: #6b6b6b !important;
// // }` : '';
// // const monospaceFonts = [];
// // // if (Setting.value('style.editor.monospaceFontFamily')) monospaceFonts.push(`"${Setting.value('style.editor.monospaceFontFamily')}"`);
// // monospaceFonts.push('monospace');
// // const fontSize = 15;
// // const fontFamily = fontFamilyFromSettings();
// // // BUG: caret-color seems to be ignored for some reason
// // const caretColor = theme.appearance === ThemeAppearance.Dark ? "white" : 'black';
// // setCss(`
// // /* These must be important to prevent the codemirror defaults from taking over*/
// // .CodeMirror {
// // font-family: ${fontFamily};
// // font-size: ${fontSize}px;
// // height: 100% !important;
// // width: 100% !important;
// // color: ${theme.color};
// // background-color: ${theme.backgroundColor};
// // position: absolute !important;
// // -webkit-box-shadow: none !important; // Some themes add a box shadow for some reason
// // }
// // .CodeMirror-lines {
// // /* This is used to enable the scroll-past end behaviour. The same height should */
// // /* be applied to the viewer. */
// // padding-bottom: 400px !important;
// // }
// // /* Left padding is applied at the editor component level, so we should remove it from the lines */
// // .CodeMirror pre.CodeMirror-line,
// // .CodeMirror pre.CodeMirror-line-like {
// // padding-left: 0;
// // }
// // .CodeMirror-sizer {
// // /* Add a fixed right padding to account for the appearance (and disappearance) */
// // /* of the sidebar */
// // padding-right: 10px !important;
// // }
// // /* This enforces monospace for certain elements (code, tables, etc.) */
// // .cm-jn-monospace {
// // font-family: ${monospaceFonts.join(', ')} !important;
// // }
// // .cm-header-1 {
// // font-size: 1.5em;
// // }
// // .cm-header-2 {
// // font-size: 1.3em;
// // }
// // .cm-header-3 {
// // font-size: 1.1em;
// // }
// // .cm-header-4, .cm-header-5, .cm-header-6 {
// // font-size: 1em;
// // }
// // .cm-header-1, .cm-header-2, .cm-header-3, .cm-header-4, .cm-header-5, .cm-header-6 {
// // line-height: 1.5em;
// // }
// // .cm-search-marker {
// // background: ${theme.searchMarkerBackgroundColor};
// // color: ${theme.searchMarkerColor} !important;
// // }
// // .cm-search-marker-selected {
// // background: ${theme.selectedColor2};
// // color: ${theme.color2} !important;
// // }
// // .cm-search-marker-scrollbar {
// // background: ${theme.searchMarkerBackgroundColor};
// // -moz-box-sizing: border-box;
// // box-sizing: border-box;
// // opacity: .5;
// // }
// // /* We need to use important to override theme specific values */
// // .cm-error {
// // color: inherit !important;
// // background-color: inherit !important;
// // border-bottom: 1px dotted #dc322f;
// // }
// // /* The default dark theme colors don't have enough contrast with the background */
// // .cm-s-nord span.cm-comment {
// // color: #9aa4b6 !important;
// // }
// // .cm-s-dracula span.cm-comment {
// // color: #a1abc9 !important;
// // }
// // .cm-s-monokai span.cm-comment {
// // color: #908b74 !important;
// // }
// // .cm-s-material-darker span.cm-comment {
// // color: #878787 !important;
// // }
// // .cm-s-solarized.cm-s-dark span.cm-comment {
// // color: #8ba1a7 !important;
// // }
// // /* MOBILE SPECIFIC */
// // .CodeMirror .cm-scroller,
// // .CodeMirror .cm-line {
// // font-family: ${fontFamily};
// // caret-color: ${caretColor};
// // }
// // ${selectionColorCss}
// // `);
// // }, [themeId]);
// return css;
// }
function useCss(themeId: number): string {
return useMemo(() => {
const theme = themeStyle(themeId);
@@ -190,6 +51,10 @@ function useCss(themeId: number): string {
:root {
background-color: ${theme.backgroundColor};
}
body {
font-size: 13pt;
}
`;
}, [themeId]);
}
@@ -227,7 +92,7 @@ function useHtml(css: string): string {
function editorTheme(themeId: number) {
return {
...themeStyle(themeId),
fontSize: 15,
fontSize: 0.85, // em
fontFamily: fontFamilyFromSettings(),
};
}
@@ -266,11 +131,15 @@ function NoteEditor(props: Props, ref: any) {
cm = codeMirrorBundle.initCodeMirror(parentElement, initialText, theme);
${setInitialSelectionJS}
// Fixes https://github.com/laurent22/joplin/issues/5949
window.onresize = () => {
cm.scrollSelectionIntoView();
};
} catch (e) {
window.ReactNativeWebView.postMessage("error:" + e.message + ": " + JSON.stringify(e))
} finally {
true;
}
true;
`;
const css = useCss(props.themeId);
@@ -357,9 +226,12 @@ function NoteEditor(props: Props, ref: any) {
// - `setSupportMultipleWindows` must be `true` for security reasons:
// https://github.com/react-native-webview/react-native-webview/releases/tag/v11.0.0
// - `scrollEnabled` prevents iOS from scrolling the document (has no effect on Android)
// when the editor is focused.
return <WebView
style={props.style}
ref={webviewRef}
scrollEnabled={false}
useWebKit={true}
source={source}
setSupportMultipleWindows={true}

View File

@@ -68,7 +68,7 @@ export default class SelectDateTimeDialog extends React.PureComponent<any, any>
}
UNSAFE_componentWillReceiveProps(newProps: any) {
if (newProps.date != this.state.date) {
if (newProps.date !== this.state.date) {
this.setState({ date: newProps.date });
}
}

View File

@@ -60,12 +60,31 @@ class ActionButtonComponent extends React.Component {
renderIconMultiStates() {
const button = this.props.buttons[this.state.buttonIndex];
return <Icon name={button.icon} style={styles.actionButtonIcon} />;
return <Icon
name={button.icon}
style={styles.actionButtonIcon}
accessibilityLabel={button.title}
/>;
}
renderIcon() {
const mainButton = this.props.mainButton ? this.props.mainButton : {};
return mainButton.icon ? <Icon name={mainButton.icon} style={styles.actionButtonIcon} /> : <Icon name="md-add" style={styles.actionButtonIcon} />;
const iconName = mainButton.icon ?? 'md-add';
// Icons don't have alt text by default. We need to add it:
const iconTitle = mainButton.title ?? _('Add new');
// TODO: If the button toggles a sub-menu, state whether the submenu is open
// or closed.
return (
<Icon
name={iconName}
style={styles.actionButtonIcon}
accessibilityLabel={iconTitle}
/>
);
}
render() {
@@ -99,8 +118,14 @@ class ActionButtonComponent extends React.Component {
const buttonTitle = button.title ? button.title : '';
const key = `${buttonTitle.replace(/\s/g, '_')}_${button.icon}`;
buttonComps.push(
// TODO: By default, ReactNativeActionButton also adds a title, which is focusable
// by the screen reader. As such, each item currently is double-focusable
<ReactNativeActionButton.Item key={key} buttonColor={button.color} title={buttonTitle} onPress={button.onPress}>
<Icon name={button.icon} style={styles.actionButtonIcon} />
<Icon
name={button.icon}
style={styles.actionButtonIcon}
accessibilityLabel={buttonTitle}
/>
</ReactNativeActionButton.Item>
);
}

View File

@@ -48,9 +48,9 @@ class AppNavComponent extends Component {
let notesScreenVisible = false;
let searchScreenVisible = false;
if (route.routeName == 'Notes') {
if (route.routeName === 'Notes') {
notesScreenVisible = true;
} else if (route.routeName == 'Search') {
} else if (route.routeName === 'Search') {
searchScreenVisible = true;
} else {
Screen = this.props.screens[route.routeName].screen;
@@ -59,7 +59,7 @@ class AppNavComponent extends Component {
// Keep the search screen loaded if the user is viewing a note from that search screen
// so that if the back button is pressed, the screen is still loaded. However, unload
// it if navigating away.
const searchScreenLoaded = searchScreenVisible || (this.previousRouteName_ == 'Search' && route.routeName == 'Note');
const searchScreenLoaded = searchScreenVisible || (this.previousRouteName_ === 'Search' && route.routeName === 'Note');
this.previousRouteName_ = route.routeName;

View File

@@ -61,7 +61,14 @@ class Checkbox extends Component {
// if (style.display) thStyle.display = style.display;
return (
<TouchableHighlight onPress={() => this.onPress()} style={thStyle}>
<TouchableHighlight
onPress={() => this.onPress()}
style={thStyle}
accessibilityRole="checkbox"
accessibilityState={{
checked: this.state.checked,
}}
accessibilityLabel={this.props.accessibilityLabel ?? ''}>
<Icon name={iconName} style={checkboxIconStyle} />
</TouchableHighlight>
);

View File

@@ -6,6 +6,7 @@ const { Checkbox } = require('./checkbox.js');
const Note = require('@joplin/lib/models/Note').default;
const time = require('@joplin/lib/time').default;
const { themeStyle } = require('./global-style.js');
const { _ } = require('@joplin/lib/locale');
class NoteItemComponent extends Component {
constructor() {
@@ -128,13 +129,20 @@ class NoteItemComponent extends Component {
const selectionWrapperStyle = isSelected ? this.styles().selectionWrapperSelected : this.styles().selectionWrapper;
const noteTitle = Note.displayTitle(note);
return (
<TouchableOpacity onPress={() => this.onPress()} onLongPress={() => this.onLongPress()} activeOpacity={0.5}>
<View style={selectionWrapperStyle}>
<View style={opacityStyle}>
<View style={listItemStyle}>
<Checkbox style={checkboxStyle} checked={checkboxChecked} onChange={checked => this.todoCheckbox_change(checked)} />
<Text style={listItemTextStyle}>{Note.displayTitle(note)}</Text>
<Checkbox
style={checkboxStyle}
checked={checkboxChecked}
onChange={checked => this.todoCheckbox_change(checked)}
accessibilityLabel={_('to-do: %s', noteTitle)}
/>
<Text style={listItemTextStyle}>{noteTitle}</Text>
</View>
</View>
</View>

View File

@@ -57,7 +57,7 @@ class NoteListComponent extends Component {
filterNotes(notes) {
const todoFilter = 'all'; // Setting.value('todoFilter');
if (todoFilter == 'all') return notes;
if (todoFilter === 'all') return notes;
const now = time.unixMs();
const maxInterval = 1000 * 60 * 60 * 24;
@@ -67,8 +67,8 @@ class NoteListComponent extends Component {
for (let i = 0; i < notes.length; i++) {
const note = notes[i];
if (note.is_todo) {
if (todoFilter == 'recent' && note.user_updated_time < notRecentTime && !!note.todo_completed) continue;
if (todoFilter == 'nonCompleted' && !!note.todo_completed) continue;
if (todoFilter === 'recent' && note.user_updated_time < notRecentTime && !!note.todo_completed) continue;
if (todoFilter === 'nonCompleted' && !!note.todo_completed) continue;
}
output.push(note);
}
@@ -77,7 +77,7 @@ class NoteListComponent extends Component {
UNSAFE_componentWillReceiveProps(newProps) {
// Make sure scroll position is reset when switching from one folder to another or to a tag list.
if (this.rootRef_ && newProps.notesSource != this.props.notesSource) {
if (this.rootRef_ && newProps.notesSource !== this.props.notesSource) {
this.rootRef_.scrollToOffset({ offset: 0, animated: false });
}
}

View File

@@ -29,10 +29,8 @@ class ScreenHeaderComponent extends React.PureComponent {
constructor() {
super();
this.styles_ = {};
this.state = { showUndoRedoButtons: true };
}
styles() {
const themeId = Setting.value('theme');
if (this.styles_[themeId]) return this.styles_[themeId];
@@ -199,7 +197,7 @@ class ScreenHeaderComponent extends React.PureComponent {
}
menu_select(value) {
if (typeof value == 'function') {
if (typeof value === 'function') {
value();
}
}
@@ -227,7 +225,12 @@ class ScreenHeaderComponent extends React.PureComponent {
render() {
function sideMenuButton(styles, onPress) {
return (
<TouchableOpacity onPress={onPress}>
<TouchableOpacity
onPress={onPress}
accessibilityLabel={_('Sidebar')}
accessibilityHint={_('Show/hide the sidebar')}
accessibilityRole="button">
<View style={styles.sideMenuButton}>
<Icon name="md-menu" style={styles.topIcon} />
</View>
@@ -237,9 +240,18 @@ class ScreenHeaderComponent extends React.PureComponent {
function backButton(styles, onPress, disabled) {
return (
<TouchableOpacity onPress={onPress} disabled={disabled}>
<TouchableOpacity
onPress={onPress}
disabled={disabled}
accessibilityLabel={_('Back')}
accessibilityHint={_('Navigate to the previous view')}
accessibilityRole="button">
<View style={disabled ? styles.backButtonDisabled : styles.backButton}>
<Icon name="md-arrow-back" style={styles.topIcon} />
<Icon
name="md-arrow-back"
style={styles.topIcon}
/>
</View>
</TouchableOpacity>
);
@@ -251,20 +263,31 @@ class ScreenHeaderComponent extends React.PureComponent {
const icon = disabled ? <Icon name="md-checkmark" style={styles.savedButtonIcon} /> : <Image style={styles.saveButtonIcon} source={require('./SaveIcon.png')} />;
return (
<TouchableOpacity onPress={onPress} disabled={disabled} style={{ padding: 0 }}>
<TouchableOpacity
onPress={onPress}
disabled={disabled}
style={{ padding: 0 }}
accessibilityLabel={_('Save changes')}
accessibilityHint={disabled ? _('Any changes have been saved') : null}
accessibilityRole="button">
<View style={disabled ? styles.saveButtonDisabled : styles.saveButton}>{icon}</View>
</TouchableOpacity>
);
}
const renderTopButton = (options) => {
if (!options.visible || !this.state.showUndoRedoButtons) return null;
if (!options.visible) return null;
const icon = <Icon name={options.iconName} style={this.styles().topIcon} />;
const viewStyle = options.disabled ? this.styles().iconButtonDisabled : this.styles().iconButton;
return (
<TouchableOpacity onPress={options.onPress} style={{ padding: 0 }} disabled={!!options.disabled}>
<TouchableOpacity
onPress={options.onPress}
style={{ padding: 0 }}
disabled={!!options.disabled}
accessibilityRole="button">
<View style={viewStyle}>{icon}</View>
</TouchableOpacity>
);
@@ -289,7 +312,11 @@ class ScreenHeaderComponent extends React.PureComponent {
function selectAllButton(styles, onPress) {
return (
<TouchableOpacity onPress={onPress}>
<TouchableOpacity
onPress={onPress}
accessibilityLabel={_('Select all')}
accessibilityRole="button">
<View style={styles.iconButton}>
<Icon name="md-checkmark-circle-outline" style={styles.topIcon} />
</View>
@@ -299,7 +326,11 @@ class ScreenHeaderComponent extends React.PureComponent {
function searchButton(styles, onPress) {
return (
<TouchableOpacity onPress={onPress}>
<TouchableOpacity
onPress={onPress}
accessibilityLabel={_('Search')}
accessibilityRole="button">
<View style={styles.iconButton}>
<Icon name="md-search" style={styles.topIcon} />
</View>
@@ -309,7 +340,15 @@ class ScreenHeaderComponent extends React.PureComponent {
function deleteButton(styles, onPress, disabled) {
return (
<TouchableOpacity onPress={onPress} disabled={disabled}>
<TouchableOpacity
onPress={onPress}
disabled={disabled}
accessibilityLabel={_('Delete')}
accessibilityHint={
disabled ? null : _('Delete selected notes')
}
accessibilityRole="button">
<View style={disabled ? styles.iconButtonDisabled : styles.iconButton}>
<Icon name="md-trash" style={styles.topIcon} />
</View>
@@ -319,7 +358,15 @@ class ScreenHeaderComponent extends React.PureComponent {
function duplicateButton(styles, onPress, disabled) {
return (
<TouchableOpacity onPress={onPress} disabled={disabled}>
<TouchableOpacity
onPress={onPress}
disabled={disabled}
accessibilityLabel={_('Duplicate')}
accessibilityHint={
disabled ? null : _('Duplicate selected notes')
}
accessibilityRole="button">
<View style={disabled ? styles.iconButtonDisabled : styles.iconButton}>
<Icon name="md-copy" style={styles.topIcon} />
</View>
@@ -329,7 +376,11 @@ class ScreenHeaderComponent extends React.PureComponent {
function sortButton(styles, onPress) {
return (
<TouchableOpacity onPress={onPress}>
<TouchableOpacity
onPress={onPress}
accessibilityLabel={_('Sort notes by')}
accessibilityRole="button">
<View style={styles.iconButton}>
<Icon name="filter-outline" style={styles.topIcon} />
</View>
@@ -424,16 +475,6 @@ class ScreenHeaderComponent extends React.PureComponent {
color: theme.color,
fontSize: theme.fontSize,
}}
onOpen={() => {
this.setState({
showUndoRedoButtons: false,
});
}}
onClose={() => {
this.setState({
showUndoRedoButtons: true,
});
}}
onValueChange={async (folderId, itemIndex) => {
// If onValueChange is specified, use this as a callback, otherwise do the default
// which is to take the selectedNoteIds from the state and move them to the

View File

@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/explicit-member-accessibility */
import Slider from '@react-native-community/slider';
const React = require('react');
const { Platform, Linking, View, Switch, StyleSheet, ScrollView, Text, Button, TouchableOpacity, TextInput, Alert, PermissionsAndroid } = require('react-native');
const { Platform, Linking, View, Switch, StyleSheet, ScrollView, Text, Button, TouchableOpacity, TextInput, Alert, PermissionsAndroid, TouchableNativeFeedback } = require('react-native');
import Setting, { AppType } from '@joplin/lib/models/Setting';
import NavService from '@joplin/lib/services/NavService';
import ReportService from '@joplin/lib/services/ReportService';
@@ -20,6 +21,7 @@ const { Dropdown } = require('../Dropdown.js');
const { themeStyle } = require('../global-style.js');
const shared = require('@joplin/lib/components/shared/config-shared.js');
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
import { openDocumentTree } from '@joplin/react-native-saf-x';
const RNFS = require('react-native-fs');
class ConfigScreenComponent extends BaseScreenComponent {
@@ -37,12 +39,27 @@ class ConfigScreenComponent extends BaseScreenComponent {
creatingReport: false,
profileExportStatus: 'idle',
profileExportPath: '',
fileSystemSyncPath: Setting.value('sync.2.path'),
};
this.scrollViewRef_ = React.createRef();
shared.init(this, reg);
this.selectDirectoryButtonPress = async () => {
try {
const doc = await openDocumentTree(true);
if (doc?.uri) {
this.setState({ fileSystemSyncPath: doc.uri });
shared.updateSettingValue(this, 'sync.2.path', doc.uri);
} else {
throw new Error('User cancelled operation');
}
} catch (e) {
reg.logger().info('Didn\'t pick sync dir: ', e);
}
};
this.checkSyncConfig_ = async () => {
// to ignore TLS erros we need to chage the global state of the app, if the check fails we need to restore the original state
// this call sets the new value and returns the previous one which we can use later to revert the change
@@ -58,8 +75,15 @@ class ConfigScreenComponent extends BaseScreenComponent {
};
this.saveButton_press = async () => {
if (this.state.changedSettingKeys.includes('sync.target') && this.state.settings['sync.target'] === SyncTargetRegistry.nameToId('filesystem') && !(await this.checkFilesystemPermission())) {
Alert.alert(_('Warning'), _('In order to use file system synchronisation your permission to write to external storage is required.'));
if (this.state.changedSettingKeys.includes('sync.target') && this.state.settings['sync.target'] === SyncTargetRegistry.nameToId('filesystem')) {
if (Platform.OS === 'android') {
if (Platform.Version < 29) {
if (!(await this.checkFilesystemPermission())) {
Alert.alert(_('Warning'), _('In order to use file system synchronisation your permission to write to external storage is required.'));
}
}
}
// Save settings anyway, even if permission has not been granted
}
@@ -445,7 +469,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
{descriptionComp}
</View>
);
} else if (md.type == Setting.TYPE_BOOL) {
} else if (md.type === Setting.TYPE_BOOL) {
return this.renderToggle(key, md.label(), value, updateSettingValue, descriptionComp);
// return (
// <View key={key}>
@@ -458,7 +482,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
// {descriptionComp}
// </View>
// );
} else if (md.type == Setting.TYPE_INT) {
} else if (md.type === Setting.TYPE_INT) {
const unitLabel = md.unitLabel ? md.unitLabel(value) : value;
// Note: Do NOT add the minimumTrackTintColor and maximumTrackTintColor props
// on the Slider as they are buggy and can crash the app on certain devices.
@@ -475,7 +499,21 @@ class ConfigScreenComponent extends BaseScreenComponent {
</View>
</View>
);
} else if (md.type == Setting.TYPE_STRING) {
} else if (md.type === Setting.TYPE_STRING) {
if (md.key === 'sync.2.path' && Platform.OS === 'android' && Platform.Version > 28) {
return (
<TouchableNativeFeedback key={key} onPress={this.selectDirectoryButtonPress} style={this.styles().settingContainer}>
<View style={this.styles().settingContainer}>
<Text key="label" style={this.styles().settingText}>
{md.label()}
</Text>
<Text style={this.styles().settingControl}>
{this.state.fileSystemSyncPath}
</Text>
</View>
</TouchableNativeFeedback>
);
}
return (
<View key={key} style={this.styles().settingContainer}>
<Text key="label" style={this.styles().settingText}>

View File

@@ -103,8 +103,8 @@ class NoteScreenComponent extends BaseScreenComponent {
if (this.isModified()) {
const buttonId = await dialogs.pop(this, _('This note has been modified:'), [{ text: _('Save changes'), id: 'save' }, { text: _('Discard changes'), id: 'discard' }, { text: _('Cancel'), id: 'cancel' }]);
if (buttonId == 'cancel') return true;
if (buttonId == 'save') await this.saveNoteButton_press();
if (buttonId === 'cancel') return true;
if (buttonId === 'save') await this.saveNoteButton_press();
}
return false;
@@ -126,7 +126,7 @@ class NoteScreenComponent extends BaseScreenComponent {
return false;
}
if (this.state.mode == 'edit') {
if (this.state.mode === 'edit') {
Keyboard.dismiss();
this.setState({
@@ -161,8 +161,8 @@ class NoteScreenComponent extends BaseScreenComponent {
this.onJoplinLinkClick_ = async (msg: string) => {
try {
if (msg.indexOf('joplin://') === 0) {
const resourceUrlInfo = urlUtils.parseResourceUrl(msg);
const resourceUrlInfo = urlUtils.parseResourceUrl(msg);
if (resourceUrlInfo) {
const itemId = resourceUrlInfo.itemId;
const item = await BaseItem.loadItemById(itemId);
if (!item) throw new Error(_('No item with ID %s', itemId));
@@ -603,7 +603,7 @@ class NoteScreenComponent extends BaseScreenComponent {
reg.logger().info('New dimensions ', dimensions);
const format = mimeType == 'image/png' ? 'PNG' : 'JPEG';
const format = mimeType === 'image/png' ? 'PNG' : 'JPEG';
reg.logger().info(`Resizing image ${localFilePath}`);
const resizedImage = await ImageResizer.createResizedImage(localFilePath, dimensions.width, dimensions.height, format, 85); // , 0, targetPath);
@@ -676,7 +676,7 @@ class NoteScreenComponent extends BaseScreenComponent {
const targetPath = Resource.fullPath(resource);
try {
if (mimeType == 'image/jpeg' || mimeType == 'image/jpg' || mimeType == 'image/png') {
if (mimeType === 'image/jpeg' || mimeType === 'image/jpg' || mimeType === 'image/png') {
const done = await this.resizeImage(localFilePath, targetPath, mimeType);
if (!done) return;
} else {
@@ -711,7 +711,7 @@ class NoteScreenComponent extends BaseScreenComponent {
const newNote = Object.assign({}, this.state.note);
if (this.state.mode == 'edit' && !!this.selection) {
if (this.state.mode === 'edit' && !!this.selection) {
const newText = `\n${resourceTag}\n`;
const prefix = newNote.body.substring(0, this.selection.start);
@@ -1068,7 +1068,7 @@ class NoteScreenComponent extends BaseScreenComponent {
const keywords = this.props.searchQuery && !!this.props.ftsEnabled ? this.props.highlightedWords : emptyArray;
let bodyComponent = null;
if (this.state.mode == 'view') {
if (this.state.mode === 'view') {
// Note: as of 2018-12-29 it's important not to display the viewer if the note body is empty,
// to avoid the HACK_webviewLoadingState related bug.
bodyComponent =
@@ -1154,7 +1154,7 @@ class NoteScreenComponent extends BaseScreenComponent {
},
});
if (this.state.mode == 'edit') return null;
if (this.state.mode === 'edit') return null;
return <ActionButton multiStates={true} buttons={buttons} buttonIndex={0} />;
};
@@ -1162,7 +1162,7 @@ class NoteScreenComponent extends BaseScreenComponent {
const actionButtonComp = renderActionButton();
// Save button is not really needed anymore with the improved save logic
const showSaveButton = false; // this.state.mode == 'edit' || this.isModified() || this.saveButtonHasBeenShown_;
const showSaveButton = false; // this.state.mode === 'edit' || this.isModified() || this.saveButtonHasBeenShown_;
const saveButtonDisabled = true;// !this.isModified();
if (showSaveButton) this.saveButtonHasBeenShown_ = true;

View File

@@ -83,8 +83,8 @@ class LogScreenComponent extends BaseScreenComponent {
render() {
const renderRow = ({ item }) => {
let textStyle = this.styles().rowText;
if (item.level == Logger.LEVEL_WARN) textStyle = this.styles().rowTextWarn;
if (item.level == Logger.LEVEL_ERROR) textStyle = this.styles().rowTextError;
if (item.level === Logger.LEVEL_WARN) textStyle = this.styles().rowTextWarn;
if (item.level === Logger.LEVEL_ERROR) textStyle = this.styles().rowTextError;
return (
<View style={this.styles().row}>

View File

@@ -109,7 +109,7 @@ class NotesScreenComponent extends BaseScreenComponent {
}
async componentDidUpdate(prevProps) {
if (prevProps.notesOrder !== this.props.notesOrder || prevProps.selectedFolderId != this.props.selectedFolderId || prevProps.selectedTagId != this.props.selectedTagId || prevProps.selectedSmartFilterId != this.props.selectedSmartFilterId || prevProps.notesParentType != this.props.notesParentType) {
if (prevProps.notesOrder !== this.props.notesOrder || prevProps.selectedFolderId !== this.props.selectedFolderId || prevProps.selectedTagId !== this.props.selectedTagId || prevProps.selectedSmartFilterId !== this.props.selectedSmartFilterId || prevProps.notesParentType !== this.props.notesParentType) {
await this.refreshNotes(this.props);
}
}
@@ -132,7 +132,7 @@ class NotesScreenComponent extends BaseScreenComponent {
parentId: parent.id,
});
if (source == props.notesSource) return;
if (source === props.notesSource) return;
let notes = [];
if (props.notesParentType === 'Folder') {
@@ -180,11 +180,11 @@ class NotesScreenComponent extends BaseScreenComponent {
if (!props) props = this.props;
let output = null;
if (props.notesParentType == 'Folder') {
if (props.notesParentType === 'Folder') {
output = Folder.byId(props.folders, props.selectedFolderId);
} else if (props.notesParentType == 'Tag') {
} else if (props.notesParentType === 'Tag') {
output = Tag.byId(props.tags, props.selectedTagId);
} else if (props.notesParentType == 'SmartFilter') {
} else if (props.notesParentType === 'SmartFilter') {
output = { id: this.props.selectedSmartFilterId, title: _('All notes') };
} else {
return null;
@@ -230,7 +230,7 @@ class NotesScreenComponent extends BaseScreenComponent {
const icon = Folder.unserializeIcon(parent.icon);
const iconString = icon ? `${icon.emoji} ` : '';
let buttonFolderId = this.props.selectedFolderId != Folder.conflictFolderId() ? this.props.selectedFolderId : null;
let buttonFolderId = this.props.selectedFolderId !== Folder.conflictFolderId() ? this.props.selectedFolderId : null;
if (!buttonFolderId) buttonFolderId = this.props.activeFolderId;
const addFolderNoteButtons = !!buttonFolderId;

View File

@@ -250,7 +250,8 @@ class SideMenuContentComponent extends Component {
let iconWrapper = null;
const iconName = this.props.collapsedFolderIds.indexOf(folder.id) >= 0 ? 'chevron-down' : 'chevron-up';
const collapsed = this.props.collapsedFolderIds.indexOf(folder.id) >= 0;
const iconName = collapsed ? 'chevron-down' : 'chevron-up';
const iconComp = <Icon name={iconName} style={this.styles().folderIcon} />;
iconWrapper = !hasChildren ? null : (
@@ -260,6 +261,9 @@ class SideMenuContentComponent extends Component {
onPress={() => {
if (hasChildren) this.folder_togglePress(folder);
}}
accessibilityLabel={collapsed ? _('Expand folder') : _('Collapse folder')}
accessibilityRole="togglebutton"
>
{iconComp}
</TouchableOpacity>

View File

@@ -1,12 +1,16 @@
const gulp = require('gulp');
const utils = require('@joplin/tools/gulp/utils');
import { buildInjectedJS, watchInjectedJS } from './tools/buildInjectedJs';
const tasks = {
encodeAssets: {
fn: require('./tools/encodeAssets'),
},
buildInjectedJs: {
fn: require('./tools/buildInjectedJs'),
fn: buildInjectedJS,
},
watchInjectedJs: {
fn: watchInjectedJS,
},
podInstall: {
fn: require('./tools/podInstall'),

View File

@@ -0,0 +1,25 @@
// Configuration file for rollup
const { dirname } = require('path');
import typescript from '@rollup/plugin-typescript';
import { nodeResolve } from '@rollup/plugin-node-resolve';
const rootDir = dirname(dirname(dirname(__dirname)));
const mobileDir = `${rootDir}/packages/app-mobile`;
const codeMirrorDir = `${mobileDir}/components/NoteEditor/CodeMirror`;
const outputFile = `${codeMirrorDir}/CodeMirror.bundle.js`;
export default {
output: outputFile,
plugins: [
typescript({
// Exclude all .js files. Rollup will attempt to import a .js
// file if both a .ts and .js file are present, conflicting
// with our build setup. See
// https://discourse.joplinapp.org/t/importing-a-ts-file-from-a-rollup-bundled-ts-file/
exclude: `${codeMirrorDir}/*.js`,
}),
nodeResolve(),
],
};

View File

@@ -498,7 +498,7 @@
INFOPLIST_FILE = Joplin/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 12.8.1;
MARKETING_VERSION = 12.9.0;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -526,7 +526,7 @@
INFOPLIST_FILE = Joplin/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 12.8.1;
MARKETING_VERSION = 12.9.0;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -674,7 +674,7 @@
INFOPLIST_FILE = ShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 12.8.1;
MARKETING_VERSION = 12.9.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
@@ -705,7 +705,7 @@
INFOPLIST_FILE = ShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 12.8.1;
MARKETING_VERSION = 12.9.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -0,0 +1,17 @@
// Test configuration
// See https://jestjs.io/docs/configuration#testenvironment-string
const config = {
preset: 'ts-jest',
// File extensions for imports, in order of precedence:
// prefer importing from .ts or .tsx to importing from .js
// files.
moduleFileExtensions: [
'ts',
'tsx',
'js',
],
};
module.exports = config;

View File

@@ -44,6 +44,7 @@ module.exports = {
'@joplin/tools': path.resolve(__dirname, '../tools/'),
'@joplin/fork-htmlparser2': path.resolve(__dirname, '../fork-htmlparser2/'),
'@joplin/fork-uslug': path.resolve(__dirname, '../fork-uslug/'),
'@joplin/react-native-saf-x': path.resolve(__dirname, '../react-native-saf-x/'),
},
{
get: (target, name) => {
@@ -62,5 +63,6 @@ module.exports = {
path.resolve(__dirname, '../tools'),
path.resolve(__dirname, '../fork-htmlparser2'),
path.resolve(__dirname, '../fork-uslug'),
path.resolve(__dirname, '../react-native-saf-x'),
],
};

View File

@@ -2,21 +2,25 @@
"name": "@joplin/app-mobile",
"description": "Joplin for Mobile",
"license": "MIT",
"version": "2.8.0",
"version": "2.9.0",
"private": true,
"scripts": {
"start": "react-native start --reset-cache",
"android": "react-native run-android",
"build": "gulp build",
"tsc": "tsc --project tsconfig.json",
"test": "jest",
"test-ci": "yarn test",
"watch": "tsc --watch --preserveWatchOutput --project tsconfig.json",
"clean": "node tools/clean.js",
"buildInjectedJs": "gulp buildInjectedJs",
"watchInjectedJs": "nodemon --verbose --watch components/NoteEditor/CodeMirror.ts --exec \"yarn run buildInjectedJs\"",
"watchInjectedJs": "gulp watchInjectedJs",
"postinstall": "jetify && yarn run build"
},
"dependencies": {
"@joplin/lib": "~2.8",
"@joplin/renderer": "~2.8",
"@joplin/lib": "~2.9",
"@joplin/react-native-saf-x": "~2.9",
"@joplin/renderer": "~2.9",
"@react-native-community/clipboard": "^1.5.0",
"@react-native-community/datetimepicker": "^3.0.3",
"@react-native-community/geolocation": "^2.0.2",
@@ -70,26 +74,36 @@
"devDependencies": {
"@babel/core": "^7.12.9",
"@babel/runtime": "^7.12.5",
"@codemirror/highlight": "^0.18.4",
"@codemirror/history": "^0.18.1",
"@codemirror/lang-markdown": "^0.18.4",
"@codemirror/state": "^0.18.7",
"@codemirror/view": "^0.18.19",
"@joplin/tools": "~2.8",
"@rollup/plugin-node-resolve": "^13.0.0",
"@rollup/plugin-typescript": "^8.2.1",
"@types/node": "^14.14.6",
"@types/react": "^16.9.55",
"@codemirror/commands": "^6.0.0",
"@codemirror/lang-cpp": "^6.0.0",
"@codemirror/lang-html": "^6.0.0",
"@codemirror/lang-java": "^6.0.0",
"@codemirror/lang-javascript": "^6.0.0",
"@codemirror/lang-markdown": "^6.0.0",
"@codemirror/lang-php": "^6.0.0",
"@codemirror/lang-rust": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/legacy-modes": "^6.1.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@joplin/tools": "~2.9",
"@lezer/highlight": "^1.0.0",
"@types/jest": "^28.1.3",
"@types/react-native": "^0.64.4",
"babel-plugin-module-resolver": "^4.1.0",
"execa": "^4.0.0",
"fs-extra": "^8.1.0",
"gulp": "^4.0.2",
"jest": "^28.1.1",
"jest-environment-jsdom": "^28.1.1",
"jetifier": "^1.6.5",
"metro-react-native-babel-preset": "^0.66.2",
"nodemon": "^2.0.12",
"rollup": "^2.53.1",
"ts-jest": "^28.0.5",
"ts-loader": "^9.3.1",
"typescript": "^4.0.5",
"uglify-js": "^3.13.10"
"uglify-js": "^3.13.10",
"webpack": "^5.74.0"
}
}

View File

@@ -70,7 +70,7 @@ const { SideMenuContentNote } = require('./components/side-menu-content-note.js'
const { DatabaseDriverReactNative } = require('./utils/database-driver-react-native');
import { reg } from '@joplin/lib/registry';
const { defaultState } = require('@joplin/lib/reducer');
const { FileApiDriverLocal } = require('@joplin/lib/file-api-driver-local.js');
const { FileApiDriverLocal } = require('@joplin/lib/file-api-driver-local');
import ResourceFetcher from '@joplin/lib/services/ResourceFetcher';
import SearchEngine from '@joplin/lib/services/searchengine/SearchEngine';
const WelcomeUtils = require('@joplin/lib/WelcomeUtils');
@@ -126,7 +126,7 @@ const generalMiddleware = (store: any) => (next: any) => async (action: any) =>
await reduxSharedMiddleware(store, next, action);
if (action.type == 'NAV_GO') Keyboard.dismiss();
if (action.type === 'NAV_GO') Keyboard.dismiss();
if (['NOTE_UPDATE_ONE', 'NOTE_DELETE', 'FOLDER_UPDATE_ONE', 'FOLDER_DELETE'].indexOf(action.type) >= 0) {
if (!await reg.syncTarget().syncStarted()) void reg.scheduleSync(5 * 1000, { syncSteps: ['update_remote', 'delete_remote'] }, true);
@@ -137,20 +137,20 @@ const generalMiddleware = (store: any) => (next: any) => async (action: any) =>
await AlarmService.updateNoteNotification(action.id, action.type === 'NOTE_DELETE');
}
if (action.type == 'SETTING_UPDATE_ONE' && action.key == 'sync.interval' || action.type == 'SETTING_UPDATE_ALL') {
if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'sync.interval' || action.type === 'SETTING_UPDATE_ALL') {
reg.setupRecurrentSync();
}
if ((action.type == 'SETTING_UPDATE_ONE' && (action.key == 'dateFormat' || action.key == 'timeFormat')) || (action.type == 'SETTING_UPDATE_ALL')) {
if ((action.type === 'SETTING_UPDATE_ONE' && (action.key === 'dateFormat' || action.key === 'timeFormat')) || (action.type === 'SETTING_UPDATE_ALL')) {
time.setDateFormat(Setting.value('dateFormat'));
time.setTimeFormat(Setting.value('timeFormat'));
}
if (action.type == 'SETTING_UPDATE_ONE' && action.key == 'locale' || action.type == 'SETTING_UPDATE_ALL') {
if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'locale' || action.type === 'SETTING_UPDATE_ALL') {
setLocale(Setting.value('locale'));
}
if ((action.type == 'SETTING_UPDATE_ONE' && (action.key.indexOf('encryption.') === 0)) || (action.type == 'SETTING_UPDATE_ALL')) {
if ((action.type === 'SETTING_UPDATE_ONE' && (action.key.indexOf('encryption.') === 0)) || (action.type === 'SETTING_UPDATE_ALL')) {
await loadMasterKeysFromSettings(EncryptionService.instance());
void DecryptionWorker.instance().scheduleStart();
const loadedMasterKeyIds = EncryptionService.instance().loadedMasterKeyIds();
@@ -165,7 +165,7 @@ const generalMiddleware = (store: any) => (next: any) => async (action: any) =>
void reg.scheduleSync(null, null, true);
}
if (action.type == 'NAV_GO' && action.routeName == 'Notes') {
if (action.type === 'NAV_GO' && action.routeName === 'Notes') {
Setting.setValue('activeFolderId', newState.selectedFolderId);
}
@@ -224,7 +224,7 @@ const appReducer = (state = appDefaultState, action: any) => {
let newAction = null;
while (navHistory.length) {
newAction = navHistory.pop();
if (newAction.routeName != state.route.routeName) break;
if (newAction.routeName !== state.route.routeName) break;
}
action = newAction ? newAction : navHistory.pop();
@@ -243,7 +243,7 @@ const appReducer = (state = appDefaultState, action: any) => {
// If the route *name* is the same (even if the other parameters are different), we
// overwrite the last route in the history with the current one. If the route name
// is different, we push a new history entry.
if (currentRoute.routeName == action.routeName) {
if (currentRoute.routeName === action.routeName) {
// nothing
} else {
navHistory.push(currentRoute);
@@ -258,7 +258,7 @@ const appReducer = (state = appDefaultState, action: any) => {
// is probably not a common workflow.
for (let i = 0; i < navHistory.length; i++) {
const n = navHistory[i];
if (n.routeName == action.routeName) {
if (n.routeName === action.routeName) {
navHistory[i] = Object.assign({}, action);
}
}
@@ -416,7 +416,7 @@ async function initialize(dispatch: Function) {
mainLogger.addTarget(TargetType.Database, { database: logDatabase, source: 'm' });
mainLogger.setLevel(Logger.LEVEL_INFO);
if (Setting.value('env') == 'dev') {
if (Setting.value('env') === 'dev') {
mainLogger.addTarget(TargetType.Console);
mainLogger.setLevel(Logger.LEVEL_DEBUG);
}
@@ -434,7 +434,7 @@ async function initialize(dispatch: Function) {
const dbLogger = new Logger();
dbLogger.addTarget(TargetType.Database, { database: logDatabase, source: 'm' });
if (Setting.value('env') == 'dev') {
if (Setting.value('env') === 'dev') {
dbLogger.addTarget(TargetType.Console);
dbLogger.setLevel(Logger.LEVEL_INFO); // Set to LEVEL_DEBUG for full SQL queries
} else {
@@ -473,7 +473,7 @@ async function initialize(dispatch: Function) {
setRSA(RSA);
try {
if (Setting.value('env') == 'prod') {
if (Setting.value('env') === 'prod') {
await db.open({ name: 'joplin.sqlite' });
} else {
await db.open({ name: 'joplin-1.sqlite' });
@@ -672,7 +672,7 @@ async function initialize(dispatch: Function) {
// call will throw an error, alerting us of the issue. Otherwise it will
// just print some messages in the console.
// ----------------------------------------------------------------------------
if (Setting.value('env') == 'dev') await runIntegrationTests();
if (Setting.value('env') === 'dev') await runIntegrationTests();
reg.logger().info('Application initialized');
}
@@ -697,7 +697,7 @@ class AppComponent extends React.Component {
};
this.handleOpenURL_ = (event: any) => {
if (event.url == ShareExtension.shareURL) {
if (event.url === ShareExtension.shareURL) {
void this.handleShareData();
}
};
@@ -723,7 +723,7 @@ class AppComponent extends React.Component {
// https://discourse.joplinapp.org/t/webdav-config-encryption-config-randomly-lost-on-android/11364
// https://discourse.joplinapp.org/t/android-keeps-on-resetting-my-sync-and-theme/11443
public async componentDidMount() {
if (this.props.appState == 'starting') {
if (this.props.appState === 'starting') {
this.props.dispatch({
type: 'APP_STATE_SET',
state: 'initializing',
@@ -829,7 +829,7 @@ class AppComponent extends React.Component {
}
public UNSAFE_componentWillReceiveProps(newProps: any) {
if (newProps.syncStarted != this.lastSyncStarted_) {
if (newProps.syncStarted !== this.lastSyncStarted_) {
if (!newProps.syncStarted) FoldersScreenUtils.refreshFolders();
this.lastSyncStarted_ = newProps.syncStarted;
}
@@ -844,7 +844,7 @@ class AppComponent extends React.Component {
}
public render() {
if (this.props.appState != 'ready') return null;
if (this.props.appState !== 'ready') return null;
const theme = themeStyle(this.props.themeId);
let sideMenuContent = null;
@@ -872,7 +872,8 @@ class AppComponent extends React.Component {
Config: { screen: ConfigScreen },
};
const statusBarStyle = theme.appearance === 'light' ? 'dark-content' : 'light-content';
// const statusBarStyle = theme.appearance === 'light-content';
const statusBarStyle = 'light-content';
return (
<View style={{ flex: 1, backgroundColor: theme.backgroundColor }}>
@@ -889,7 +890,8 @@ class AppComponent extends React.Component {
}}
>
<StatusBar barStyle={statusBarStyle} />
<MenuContext style={{ flex: 1, backgroundColor: theme.backgroundColor }}>
<MenuContext style={{ flex: 1 }}>
<SafeAreaView style={{ flex: 0, backgroundColor: theme.backgroundColor2 }}/>
<SafeAreaView style={{ flex: 1 }}>
<View style={{ flex: 1, backgroundColor: theme.backgroundColor }}>
<AppNav screens={appNavInit} />

View File

@@ -1,55 +0,0 @@
// React Native WebView cannot load external JS files, however it can load
// arbitrary JS via the injectedJavaScript property. So we use this to load external
// files: First here we convert the JS file to a plain string, and that string
// is then loaded by eg. the Mermaid plugin, and finally injected in the WebView.
const fs = require('fs-extra');
const path = require('path');
const execa = require('execa');
const rootDir = path.dirname(path.dirname(path.dirname(__dirname)));
const mobileDir = `${rootDir}/packages/app-mobile`;
const outputDir = `${mobileDir}/lib/rnInjectedJs`;
const codeMirrorBundleFile = `${mobileDir}/components/NoteEditor/CodeMirror.bundle.min.js`;
async function copyJs(name, filePath) {
const outputPath = `${outputDir}/${name}.js`;
console.info(`Creating: ${outputPath}`);
const js = await fs.readFile(filePath, 'utf-8');
const json = `module.exports = ${JSON.stringify(js)};`;
await fs.writeFile(outputPath, json);
}
async function buildCodeMirrorBundle() {
console.info('Building CodeMirror bundle...');
const sourceFile = `${mobileDir}/components/NoteEditor/CodeMirror.ts`;
const fullBundleFile = `${mobileDir}/components/NoteEditor/CodeMirror.bundle.js`;
await execa('yarn', [
'run', 'rollup',
sourceFile,
'--name', 'codeMirrorBundle',
'-f', 'iife',
'-o', fullBundleFile,
'-p', '@rollup/plugin-node-resolve',
'-p', '@rollup/plugin-typescript',
]);
// await execa('./node_modules/uglify-js/bin/uglifyjs', [
await execa('yarn', [
'run', 'uglifyjs',
'--compress',
'-o', codeMirrorBundleFile,
fullBundleFile,
]);
}
async function main() {
await fs.mkdirp(outputDir);
await buildCodeMirrorBundle();
await copyJs('webviewLib', `${mobileDir}/../lib/renderers/webviewLib.js`);
await copyJs('CodeMirror.bundle', `${mobileDir}/components/NoteEditor/CodeMirror.bundle.min.js`);
}
module.exports = main;

View File

@@ -0,0 +1,209 @@
// React Native WebView cannot load external JS files, however it can load
// arbitrary JS via the injectedJavaScript property. So we use this to load external
// files: First here we convert the JS file to a plain string, and that string
// is then loaded by eg. the Mermaid plugin, and finally injected in the WebView.
import { mkdirp, readFile, writeFile } from 'fs-extra';
import { dirname, extname, basename } from 'path';
const execa = require('execa');
import webpack from 'webpack';
const rootDir = dirname(dirname(dirname(__dirname)));
const mobileDir = `${rootDir}/packages/app-mobile`;
const outputDir = `${mobileDir}/lib/rnInjectedJs`;
// Stores the contents of the file at [filePath] as an importable string.
// [name] should be the name (excluding the .js extension) of the output file that will contain
// the JSON-ified file content.
async function copyJs(name: string, filePath: string) {
const outputPath = `${outputDir}/${name}.js`;
console.info(`Creating: ${outputPath}`);
const js = await readFile(filePath, 'utf-8');
const json = `module.exports = ${JSON.stringify(js)};`;
await writeFile(outputPath, json);
}
class BundledFile {
private readonly bundleOutputPath: string;
private readonly bundleMinifiedPath: string;
private readonly bundleBaseName: string;
private readonly rootFileDirectory: string;
public constructor(
public readonly bundleName: string,
private readonly sourceFilePath: string
) {
this.rootFileDirectory = dirname(sourceFilePath);
this.bundleBaseName = basename(sourceFilePath, extname(sourceFilePath));
this.bundleOutputPath = `${this.rootFileDirectory}/${this.bundleBaseName}.bundle.js`;
this.bundleMinifiedPath = `${this.rootFileDirectory}/${this.bundleBaseName}.bundle.min.js`;
}
private getWebpackOptions(mode: 'production' | 'development'): webpack.Configuration {
const config: webpack.Configuration = {
mode,
entry: this.sourceFilePath,
output: {
path: this.rootFileDirectory,
filename: `${this.bundleBaseName}.bundle.js`,
library: {
type: 'window',
name: this.bundleName,
},
},
// See https://webpack.js.org/guides/typescript/
module: {
rules: [
{
// Include .tsx to include react components
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
// Increase the minimum size required
// to trigger warnings.
// See https://stackoverflow.com/a/53517149/17055750
performance: {
maxAssetSize: 2_000_000, // 2-ish MiB
maxEntrypointSize: 2_000_000,
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
};
return config;
}
private async uglify() {
console.info(`Minifying bundle: ${this.bundleName}...`);
await execa('yarn', [
'run', 'uglifyjs',
'--compress',
'-o', this.bundleMinifiedPath,
this.bundleOutputPath,
]);
}
private handleErrors(err: Error | undefined | null, stats: webpack.Stats | undefined): boolean {
let failed = false;
if (err) {
console.error(`Error: ${err.name}`, err.message, err.stack);
failed = true;
} else if (stats?.hasErrors() || stats?.hasWarnings()) {
const data = stats.toJson();
if (data.warnings && data.warningsCount) {
console.warn('Warnings: ', data.warningsCount);
for (const warning of data.warnings) {
// Stack contains the message
if (warning.stack) {
console.warn(warning.stack);
} else {
console.warn(warning.message);
}
}
}
if (data.errors && data.errorsCount) {
console.error('Errors: ', data.errorsCount);
for (const error of data.errors) {
if (error.stack) {
console.error(error.stack);
} else {
console.error(error.message);
}
console.error();
}
failed = true;
}
}
return failed;
}
// Create a minified JS file in the same directory as `this.sourceFilePath` with
// the same name.
public build() {
const compiler = webpack(this.getWebpackOptions('production'));
return new Promise<void>((resolve, reject) => {
console.info(`Building bundle: ${this.bundleName}...`);
compiler.run((err, stats) => {
let failed = this.handleErrors(err, stats);
// Clean up.
compiler.close(async (error) => {
if (error) {
console.error('Error cleaning up:', error);
failed = true;
}
if (!failed) {
await this.uglify();
resolve();
} else {
reject();
}
});
});
});
}
public startWatching() {
const compiler = webpack(this.getWebpackOptions('development'));
const watchOptions = {
ignored: '**/node_modules',
};
console.info('Watching bundle: ', this.bundleName);
compiler.watch(watchOptions, async (err, stats) => {
const failed = this.handleErrors(err, stats);
if (!failed) {
await this.uglify();
await this.copyToImportableFile();
}
});
}
// Creates a file that can be imported by React native. This file contains the
// bundled JS as a string.
public async copyToImportableFile() {
await copyJs(`${this.bundleBaseName}.bundle`, this.bundleMinifiedPath);
}
}
const bundledFiles: BundledFile[] = [
new BundledFile(
'codeMirrorBundle',
`${mobileDir}/components/NoteEditor/CodeMirror/CodeMirror.ts`
),
];
export async function buildInjectedJS() {
await mkdirp(outputDir);
// Build all in parallel
await Promise.all(bundledFiles.map(async file => {
await file.build();
await file.copyToImportableFile();
}));
await copyJs('webviewLib', `${mobileDir}/../lib/renderers/webviewLib.js`);
}
export async function watchInjectedJS() {
// Watch for changes
for (const file of bundledFiles) {
file.startWatching();
}
}

View File

@@ -1,10 +1,16 @@
{
"extends": "../../tsconfig.json",
"include": [
"**/*.ts",
"**/*.tsx",
"extends": "../../tsconfig.json",
"include": [
"**/*.ts",
"**/*.tsx",
],
"exclude": [
"**/node_modules",
// Files that don't need transpilation
"gulpfile.ts",
"tools/*.ts",
"**/*.test.ts",
"**/*.test.tsx",
],
}
}

View File

@@ -1,6 +1,13 @@
import FsDriverBase from '@joplin/lib/fs-driver-base';
import FsDriverBase, { ReadDirStatsOptions } from '@joplin/lib/fs-driver-base';
const RNFetchBlob = require('rn-fetch-blob').default;
const RNFS = require('react-native-fs');
import RNSAF, { Encoding, DocumentFileDetail } from '@joplin/react-native-saf-x';
const ANDROID_URI_PREFIX = 'content://';
function isScopedUri(path: string) {
return path.includes(ANDROID_URI_PREFIX);
}
export default class FsDriverRN extends FsDriverBase {
public appendFileSync() {
@@ -9,11 +16,17 @@ export default class FsDriverRN extends FsDriverBase {
// Encoding can be either "utf8" or "base64"
public appendFile(path: string, content: any, encoding = 'base64') {
if (isScopedUri(path)) {
return RNSAF.writeFile(path, content, { encoding: encoding as Encoding, append: true });
}
return RNFS.appendFile(path, content, encoding);
}
// Encoding can be either "utf8" or "base64"
public writeFile(path: string, content: any, encoding = 'base64') {
if (isScopedUri(path)) {
return RNSAF.writeFile(path, content, { encoding: encoding as Encoding });
}
// We need to use rn-fetch-blob here due to this bug:
// https://github.com/itinance/react-native-fs/issues/700
return RNFetchBlob.fs.writeFile(path, content, encoding);
@@ -26,52 +39,105 @@ export default class FsDriverRN extends FsDriverBase {
// Returns a format compatible with Node.js format
private rnfsStatToStd_(stat: any, path: string) {
let birthtime;
const mtime = stat.lastModified ? new Date(stat.lastModified) : stat.mtime;
if (stat.lastModified) {
birthtime = new Date(stat.lastModified);
} else if (stat.ctime) {
// Confusingly, "ctime" normally means "change time" but here it's used as "creation time". Also sometimes it is null
birthtime = stat.ctime;
} else {
birthtime = stat.mtime;
}
return {
birthtime: stat.ctime ? stat.ctime : stat.mtime, // Confusingly, "ctime" normally means "change time" but here it's used as "creation time". Also sometimes it is null
mtime: stat.mtime,
isDirectory: () => stat.isDirectory(),
birthtime,
mtime,
isDirectory: () => stat.type ? stat.type === 'directory' : stat.isDirectory(),
path: path,
size: stat.size,
};
}
public async isDirectory(path: string): Promise<boolean> {
return (await this.stat(path)).isDirectory();
}
public async readDirStats(path: string, options: any = null) {
if (!options) options = {};
if (!('recursive' in options)) options.recursive = false;
let items = [];
const isScoped = isScopedUri(path);
let stats = [];
try {
items = await RNFS.readDir(path);
if (isScoped) {
stats = await RNSAF.listFiles(path);
} else {
stats = await RNFS.readDir(path);
}
} catch (error) {
throw new Error(`Could not read directory: ${path}: ${error.message}`);
}
let output: any[] = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
const relativePath = item.path.substr(path.length + 1);
output.push(this.rnfsStatToStd_(item, relativePath));
for (let i = 0; i < stats.length; i++) {
const stat = stats[i];
const relativePath = (isScoped ? stat.uri : stat.path).substr(path.length + 1);
output.push(this.rnfsStatToStd_(stat, relativePath));
output = await this.readDirStatsHandleRecursion_(path, item, output, options);
if (isScoped) {
output = await this.readUriDirStatsHandleRecursion_(stat, output, options);
} else {
output = await this.readDirStatsHandleRecursion_(path, stat, output, options);
}
}
return output;
}
protected async readUriDirStatsHandleRecursion_(stat: DocumentFileDetail, output: DocumentFileDetail[], options: ReadDirStatsOptions) {
if (options.recursive && stat.type === 'directory') {
const subStats = await this.readDirStats(stat.uri, options);
for (let j = 0; j < subStats.length; j++) {
const subStat = subStats[j];
output.push(subStat);
}
}
return output;
}
public async move(source: string, dest: string) {
if (isScopedUri(source) && isScopedUri(dest)) {
await RNSAF.moveFile(source, dest, { replaceIfDestinationExists: true });
} else if (isScopedUri(source) || isScopedUri(dest)) {
throw new Error('Move between different storage types not supported');
}
return RNFS.moveFile(source, dest);
}
public async exists(path: string) {
if (isScopedUri(path)) {
return RNSAF.exists(path);
}
return RNFS.exists(path);
}
public async mkdir(path: string) {
if (isScopedUri(path)) {
await RNSAF.mkdir(path);
return;
}
return RNFS.mkdir(path);
}
public async stat(path: string) {
try {
const r = await RNFS.stat(path);
let r;
if (isScopedUri(path)) {
r = await RNSAF.stat(path);
} else {
r = await RNFS.stat(path);
}
return this.rnfsStatToStd_(r, path);
} catch (error) {
if (error && ((error.message && error.message.indexOf('exist') >= 0) || error.code === 'ENOENT')) {
@@ -93,6 +159,9 @@ export default class FsDriverRN extends FsDriverBase {
}
public async open(path: string, mode: number) {
if (isScopedUri(path)) {
throw new Error('open() not implemented in FsDriverAndroid');
}
// Note: RNFS.read() doesn't provide any way to know if the end of file has been reached.
// So instead we stat the file here and use stat.size to manually check for end of file.
// Bug: https://github.com/itinance/react-native-fs/issues/342
@@ -112,6 +181,9 @@ export default class FsDriverRN extends FsDriverBase {
public readFile(path: string, encoding = 'utf8') {
if (encoding === 'Buffer') throw new Error('Raw buffer output not supported for FsDriverRN.readFile');
if (isScopedUri(path)) {
return RNSAF.readFile(path, { encoding: encoding as Encoding });
}
return RNFS.readFile(path, encoding);
}
@@ -119,6 +191,12 @@ export default class FsDriverRN extends FsDriverBase {
public async copy(source: string, dest: string) {
let retry = false;
try {
if (isScopedUri(source) && isScopedUri(dest)) {
await RNSAF.copyFile(source, dest, { replaceIfDestinationExists: true });
return;
} else if (isScopedUri(source) || isScopedUri(dest)) {
throw new Error('Move between different storage types not supported');
}
await RNFS.copyFile(source, dest);
} catch (error) {
// On iOS it will throw an error if the file already exist
@@ -131,6 +209,10 @@ export default class FsDriverRN extends FsDriverBase {
public async unlink(path: string) {
try {
if (isScopedUri(path)) {
await RNSAF.unlink(path);
return;
}
await RNFS.unlink(path);
} catch (error) {
if (error && ((error.message && error.message.indexOf('exist') >= 0) || error.code === 'ENOENT')) {

View File

@@ -19,7 +19,7 @@ class GeolocationReact {
}
static currentPosition(options = null) {
if (Setting.value('env') == 'dev') return this.currentPosition_testResponse();
if (Setting.value('env') === 'dev') return this.currentPosition_testResponse();
if (!options) options = {};
if (!('enableHighAccuracy' in options)) options.enableHighAccuracy = true;

View File

@@ -22,7 +22,9 @@ function shimInit() {
shim.sjclModule = require('@joplin/lib/vendor/sjcl-rn.js');
shim.fsDriver = () => {
if (!shim.fsDriver_) shim.fsDriver_ = new FsDriverRN();
if (!shim.fsDriver_) {
shim.fsDriver_ = new FsDriverRN();
}
return shim.fsDriver_;
};

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 1,
"id": "<%= pluginId %>",
"app_min_version": "2.8",
"app_min_version": "2.9",
"version": "1.0.0",
"name": "<%= pluginName %>",
"description": "<%= pluginDescription %>",

View File

@@ -1,6 +1,6 @@
{
"name": "generator-joplin",
"version": "2.8.1",
"version": "2.9.0",
"description": "Scaffolds out a new Joplin plugin",
"homepage": "https://github.com/laurent22/joplin/tree/dev/packages/generator-joplin",
"author": {

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/htmlpack",
"version": "2.8.1",
"version": "2.9.0",
"description": "Pack an HTML file and all its linked resources into a single HTML file",
"main": "dist/index.js",
"types": "src/index.ts",

View File

@@ -1,2 +1,2 @@
plugin_types/
markdownUtils.test.js
markdownUtils.test.js

View File

@@ -18,7 +18,7 @@ export const binarySearch = function(items: any[], value: any) {
stopIndex = items.length - 1,
middle = Math.floor((stopIndex + startIndex) / 2);
while (items[middle] != value && startIndex < stopIndex) {
while (items[middle] !== value && startIndex < stopIndex) {
// adjust search area
if (value < items[middle]) {
stopIndex = middle - 1;
@@ -31,7 +31,7 @@ export const binarySearch = function(items: any[], value: any) {
}
// make sure it's the right value
return items[middle] != value ? -1 : middle;
return items[middle] !== value ? -1 : middle;
};
export const findByKey = function(array: any[], key: any, value: any) {

View File

@@ -1,6 +1,7 @@
import Setting, { Env } from './models/Setting';
import Logger, { TargetType, LoggerWrapper } from './Logger';
import shim from './shim';
const { setupProxySettings } = require('./shim-init-node');
import BaseService from './services/BaseService';
import reducer, { setStore } from './reducer';
import KeychainServiceDriver from './services/keychain/KeychainServiceDriver.node';
@@ -165,57 +166,57 @@ export default class BaseApplication {
const arg = argv[0];
const nextArg = argv.length >= 2 ? argv[1] : null;
if (arg == '--profile') {
if (arg === '--profile') {
if (!nextArg) throw new JoplinError(_('Usage: %s', '--profile <dir-path>'), 'flagError');
matched.profileDir = nextArg;
argv.splice(0, 2);
continue;
}
if (arg == '--no-welcome') {
if (arg === '--no-welcome') {
matched.welcomeDisabled = true;
argv.splice(0, 1);
continue;
}
if (arg == '--env') {
if (arg === '--env') {
if (!nextArg) throw new JoplinError(_('Usage: %s', '--env <dev|prod>'), 'flagError');
matched.env = nextArg;
argv.splice(0, 2);
continue;
}
if (arg == '--is-demo') {
if (arg === '--is-demo') {
Setting.setConstant('isDemo', true);
argv.splice(0, 1);
continue;
}
if (arg == '--open-dev-tools') {
if (arg === '--open-dev-tools') {
Setting.setConstant('flagOpenDevTools', true);
argv.splice(0, 1);
continue;
}
if (arg == '--debug') {
if (arg === '--debug') {
// Currently only handled by ElectronAppWrapper (isDebugMode property)
argv.splice(0, 1);
continue;
}
if (arg == '--update-geolocation-disabled') {
if (arg === '--update-geolocation-disabled') {
Note.updateGeolocationEnabled_ = false;
argv.splice(0, 1);
continue;
}
if (arg == '--stack-trace-enabled') {
if (arg === '--stack-trace-enabled') {
this.showStackTraces_ = true;
argv.splice(0, 1);
continue;
}
if (arg == '--log-level') {
if (arg === '--log-level') {
if (!nextArg) throw new JoplinError(_('Usage: %s', '--log-level <none|error|warn|info|debug>'), 'flagError');
matched.logLevel = Logger.levelStringToId(nextArg);
argv.splice(0, 2);
@@ -276,7 +277,7 @@ export default class BaseApplication {
continue;
}
if (arg.length && arg[0] == '-') {
if (arg.length && arg[0] === '-') {
throw new JoplinError(_('Unknown flag: %s', arg), 'flagError');
} else {
break;
@@ -456,6 +457,14 @@ export default class BaseApplication {
syswidecas.addCAs(f);
}
},
'net.proxyEnabled': async () => {
setupProxySettings({
maxConcurrentConnections: Setting.value('sync.maxConcurrentConnections'),
proxyTimeout: Setting.value('net.proxyTimeout'),
proxyEnabled: Setting.value('net.proxyEnabled'),
proxyUrl: Setting.value('net.proxyUrl'),
});
},
// Note: this used to run when "encryption.enabled" was changed, but
// now we run it anytime any property of the sync target info is
@@ -491,6 +500,9 @@ export default class BaseApplication {
sideEffects['locale'] = sideEffects['dateFormat'];
sideEffects['encryption.passwordCache'] = sideEffects['syncInfoCache'];
sideEffects['encryption.masterPassword'] = sideEffects['syncInfoCache'];
sideEffects['sync.maxConcurrentConnections'] = sideEffects['net.proxyEnabled'];
sideEffects['sync.proxyTimeout'] = sideEffects['net.proxyEnabled'];
sideEffects['sync.proxyUrl'] = sideEffects['net.proxyEnabled'];
if (action) {
const effect = sideEffects[action.key];
@@ -526,12 +538,12 @@ export default class BaseApplication {
refreshFolders = true;
}
if (action.type == 'HISTORY_BACKWARD' || action.type == 'HISTORY_FORWARD') {
if (action.type === 'HISTORY_BACKWARD' || action.type === 'HISTORY_FORWARD') {
refreshNotes = true;
refreshNotesUseSelectedNoteId = true;
}
if (action.type == 'HISTORY_BACKWARD' || action.type == 'HISTORY_FORWARD' || action.type == 'FOLDER_SELECT' || action.type === 'FOLDER_DELETE' || action.type === 'FOLDER_AND_NOTE_SELECT' || (action.type === 'SEARCH_UPDATE' && newState.notesParentType === 'Folder')) {
if (action.type === 'HISTORY_BACKWARD' || action.type === 'HISTORY_FORWARD' || action.type === 'FOLDER_SELECT' || action.type === 'FOLDER_DELETE' || action.type === 'FOLDER_AND_NOTE_SELECT' || (action.type === 'SEARCH_UPDATE' && newState.notesParentType === 'Folder')) {
Setting.setValue('activeFolderId', newState.selectedFolderId);
this.currentFolder_ = newState.selectedFolderId ? await Folder.load(newState.selectedFolderId) : null;
refreshNotes = true;
@@ -542,23 +554,23 @@ export default class BaseApplication {
}
}
if (this.hasGui() && (action.type == 'NOTE_IS_INSERTING_NOTES' && !action.value)) {
if (this.hasGui() && (action.type === 'NOTE_IS_INSERTING_NOTES' && !action.value)) {
refreshNotes = true;
}
if (this.hasGui() && ((action.type == 'SETTING_UPDATE_ONE' && action.key == 'uncompletedTodosOnTop') || action.type == 'SETTING_UPDATE_ALL')) {
if (this.hasGui() && ((action.type === 'SETTING_UPDATE_ONE' && action.key === 'uncompletedTodosOnTop') || action.type === 'SETTING_UPDATE_ALL')) {
refreshNotes = true;
}
if (this.hasGui() && ((action.type == 'SETTING_UPDATE_ONE' && action.key == 'showCompletedTodos') || action.type == 'SETTING_UPDATE_ALL')) {
if (this.hasGui() && ((action.type === 'SETTING_UPDATE_ONE' && action.key === 'showCompletedTodos') || action.type === 'SETTING_UPDATE_ALL')) {
refreshNotes = true;
}
if (this.hasGui() && ((action.type == 'SETTING_UPDATE_ONE' && action.key.indexOf('notes.sortOrder') === 0) || action.type == 'SETTING_UPDATE_ALL')) {
if (this.hasGui() && ((action.type === 'SETTING_UPDATE_ONE' && action.key.indexOf('notes.sortOrder') === 0) || action.type === 'SETTING_UPDATE_ALL')) {
refreshNotes = true;
}
if (action.type == 'SMART_FILTER_SELECT') {
if (action.type === 'SMART_FILTER_SELECT') {
refreshNotes = true;
refreshNotesUseSelectedNoteId = true;
}
@@ -571,11 +583,11 @@ export default class BaseApplication {
refreshNotes = true;
}
if (action.type == 'SEARCH_SELECT' || action.type === 'SEARCH_DELETE') {
if (action.type === 'SEARCH_SELECT' || action.type === 'SEARCH_DELETE') {
refreshNotes = true;
}
if (action.type == 'NOTE_TAG_REMOVE') {
if (action.type === 'NOTE_TAG_REMOVE') {
if (newState.notesParentType === 'Tag' && newState.selectedTagId === action.item.id) {
if (newState.notes.length === newState.selectedNoteIds.length) {
await this.refreshCurrentFolder();
@@ -603,14 +615,14 @@ export default class BaseApplication {
refreshFolders = true;
}
if (this.hasGui() && action.type == 'SETTING_UPDATE_ALL') {
if (this.hasGui() && action.type === 'SETTING_UPDATE_ALL') {
refreshFolders = 'now';
}
if (this.hasGui() && action.type == 'SETTING_UPDATE_ONE' && (
if (this.hasGui() && action.type === 'SETTING_UPDATE_ONE' && (
action.key.indexOf('folders.sortOrder') === 0 ||
action.key == 'showNoteCounts' ||
action.key == 'showCompletedTodos')) {
action.key === 'showNoteCounts' ||
action.key === 'showCompletedTodos')) {
refreshFolders = 'now';
}
@@ -622,9 +634,9 @@ export default class BaseApplication {
void ResourceFetcher.instance().autoAddResources();
}
if (action.type == 'SETTING_UPDATE_ONE') {
if (action.type === 'SETTING_UPDATE_ONE') {
await this.applySettingsSideEffects(action);
} else if (action.type == 'SETTING_UPDATE_ALL') {
} else if (action.type === 'SETTING_UPDATE_ALL') {
await this.applySettingsSideEffects();
}
@@ -716,7 +728,7 @@ export default class BaseApplication {
let initArgs = startFlags.matched;
if (argv.length) this.showPromptString_ = false;
let appName = initArgs.env == 'dev' ? 'joplindev' : 'joplin';
let appName = initArgs.env === 'dev' ? 'joplindev' : 'joplin';
if (Setting.value('appId').indexOf('-desktop') >= 0) appName += '-desktop';
Setting.setConstant('appName', appName);

View File

@@ -123,7 +123,7 @@ class BaseModel {
static byId(items: any[], id: string) {
for (let i = 0; i < items.length; i++) {
if (items[i].id == id) return items[i];
if (items[i].id === id) return items[i];
}
return null;
}
@@ -138,7 +138,7 @@ class BaseModel {
static modelIndexById(items: any[], id: string) {
for (let i = 0; i < items.length; i++) {
if (items[i].id == id) return i;
if (items[i].id === id) return i;
}
return -1;
}
@@ -200,7 +200,7 @@ class BaseModel {
static fieldType(name: string, defaultValue: any = null) {
const fields = this.fields();
for (let i = 0; i < fields.length; i++) {
if (fields[i].name == name) return fields[i].type;
if (fields[i].name === name) return fields[i].type;
}
if (defaultValue !== null) return defaultValue;
throw new Error(`Unknown field: ${name}`);
@@ -391,7 +391,7 @@ class BaseModel {
const output = [];
for (const n in newModel) {
if (!newModel.hasOwnProperty(n)) continue;
if (n == 'type_') continue;
if (n === 'type_') continue;
if (!(n in oldModel) || newModel[n] !== oldModel[n]) {
output.push(n);
}

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