You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2026-01-14 00:29:38 +02:00
Compare commits
71 Commits
table_edit
...
note_link_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29426814e4 | ||
|
|
a5e18200e8 | ||
|
|
ab5313e37f | ||
|
|
54cc7063ad | ||
|
|
12a510c464 | ||
|
|
21d5800923 | ||
|
|
1d5e8e65d9 | ||
|
|
d2a6d24846 | ||
|
|
fb372723a4 | ||
|
|
b32a341700 | ||
|
|
caef5449dc | ||
|
|
864a3a7efe | ||
|
|
ce02d4c94f | ||
|
|
052d9f03d6 | ||
|
|
8a8def39f0 | ||
|
|
f0831f1d60 | ||
|
|
0e532fbaf0 | ||
|
|
11a1e1cb6b | ||
|
|
37b89b5644 | ||
|
|
2c464e89e6 | ||
|
|
68764bd82e | ||
|
|
520d9746c5 | ||
|
|
c3df191a95 | ||
|
|
06d5feaa63 | ||
|
|
0b3c4edb92 | ||
|
|
58045f87d8 | ||
|
|
28e66e2619 | ||
|
|
c3179a39a4 | ||
|
|
eb71260674 | ||
|
|
ed4a013cfc | ||
|
|
5ffe90c4b0 | ||
|
|
8a836ea4f9 | ||
|
|
1f2930f037 | ||
|
|
ef3afb2a01 | ||
|
|
5d873a3264 | ||
|
|
effba83a0e | ||
|
|
55d98346ee | ||
|
|
d848865b0d | ||
|
|
879702dadf | ||
|
|
8bb5b4a557 | ||
|
|
2c4cf9fbdb | ||
|
|
3b35ab6581 | ||
|
|
6744dc3a8a | ||
|
|
97c6684154 | ||
|
|
e797ebb864 | ||
|
|
f99b8dfde8 | ||
|
|
c21b28e6e6 | ||
|
|
c58e9fe346 | ||
|
|
c58ce8e2da | ||
|
|
f64d046c62 | ||
|
|
c7e3245008 | ||
|
|
8f3fd0bf8b | ||
|
|
d293474402 | ||
|
|
aaa610d5f4 | ||
|
|
20a7cd2323 | ||
|
|
d7af060564 | ||
|
|
d7663212cf | ||
|
|
429a49b07e | ||
|
|
124ce342d8 | ||
|
|
19f4139470 | ||
|
|
21b6564301 | ||
|
|
c8b6122a65 | ||
|
|
c0bc4c38c3 | ||
|
|
0c50a5ab9b | ||
|
|
ce6797d842 | ||
|
|
29a1cc022c | ||
|
|
af665f247c | ||
|
|
8ea32201e7 | ||
|
|
4c88376449 | ||
|
|
0618e9ec90 | ||
|
|
176c9e0bcf |
@@ -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
|
||||
|
||||
@@ -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
45
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
26
Assets/TinyMCE/JoplinLists/src/main/ts/core/Mouse.ts
Normal file
26
Assets/TinyMCE/JoplinLists/src/main/ts/core/Mouse.ts
Normal 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 };
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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 -->
|
||||
<!--
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -325,6 +325,7 @@
|
||||
"homenote",
|
||||
"hotfolder",
|
||||
"Howver",
|
||||
"hpagent",
|
||||
"Hrvatska",
|
||||
"htmlentities",
|
||||
"htmlfile",
|
||||
@@ -950,4 +951,4 @@
|
||||
"မြန်မာ",
|
||||
"កម្ពុជា"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 : '';
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -122,7 +122,7 @@ class Command extends BaseCommand {
|
||||
}
|
||||
|
||||
|
||||
if (args.name == 'locale') {
|
||||
if (args.name === 'locale') {
|
||||
setLocale(Setting.value('locale'));
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -32,6 +32,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return s
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -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'",
|
||||
|
||||
@@ -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
|
||||
|
||||
19
packages/app-clipper/popup/package-lock.json
generated
19
packages/app-clipper/popup/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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$/);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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}/>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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_ = {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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')));
|
||||
|
||||
6
packages/app-mobile/.gitignore
vendored
6
packages/app-mobile/.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
];
|
||||
@@ -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;
|
||||
234
packages/app-mobile/components/NoteEditor/CodeMirror/theme.ts
vendored
Normal file
234
packages/app-mobile/components/NoteEditor/CodeMirror/theme.ts
vendored
Normal 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;
|
||||
@@ -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}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'),
|
||||
25
packages/app-mobile/injectedJS.config.js
Normal file
25
packages/app-mobile/injectedJS.config.js
Normal 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(),
|
||||
],
|
||||
};
|
||||
@@ -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)";
|
||||
|
||||
17
packages/app-mobile/jest.config.js
Normal file
17
packages/app-mobile/jest.config.js
Normal 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;
|
||||
@@ -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'),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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;
|
||||
209
packages/app-mobile/tools/buildInjectedJs.ts
Normal file
209
packages/app-mobile/tools/buildInjectedJs.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
|
||||
@@ -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 %>",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
2
packages/lib/.gitignore
vendored
2
packages/lib/.gitignore
vendored
@@ -1,2 +1,2 @@
|
||||
plugin_types/
|
||||
markdownUtils.test.js
|
||||
markdownUtils.test.js
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user