mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-21 09:38:01 +02:00
Mobile: Added new beta editor based on CodeMirror 6
This commit is contained in:
parent
e01a17528a
commit
8395d5daa9
@ -53,6 +53,7 @@ packages/app-mobile/locales
|
||||
packages/app-mobile/node_modules
|
||||
packages/app-mobile/pluginAssets/
|
||||
packages/app-mobile/lib/rnInjectedJs/
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror.bundle.js
|
||||
packages/lib/assets/
|
||||
packages/lib/rnInjectedJs/
|
||||
packages/lib/vendor/
|
||||
@ -701,6 +702,12 @@ 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/NoteEditor.d.ts
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.js.map
|
||||
packages/app-mobile/components/SelectDateTimeDialog.d.ts
|
||||
packages/app-mobile/components/SelectDateTimeDialog.js
|
||||
packages/app-mobile/components/SelectDateTimeDialog.js.map
|
||||
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -687,6 +687,12 @@ 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/NoteEditor.d.ts
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.js.map
|
||||
packages/app-mobile/components/SelectDateTimeDialog.d.ts
|
||||
packages/app-mobile/components/SelectDateTimeDialog.js
|
||||
packages/app-mobile/components/SelectDateTimeDialog.js.map
|
||||
|
4
packages/app-mobile/.gitignore
vendored
4
packages/app-mobile/.gitignore
vendored
@ -61,4 +61,6 @@ buck-out/
|
||||
# Custom
|
||||
lib/csstojs/
|
||||
lib/rnInjectedJs/
|
||||
dist/
|
||||
dist/
|
||||
components/NoteEditor/CodeMirror.bundle.js
|
||||
components/NoteEditor/CodeMirror.bundle.min.js
|
||||
|
@ -141,8 +141,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097639
|
||||
versionName "2.2.0"
|
||||
versionCode 2097640
|
||||
versionName "2.2.1"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
|
160
packages/app-mobile/components/NoteEditor/CodeMirror.ts
Normal file
160
packages/app-mobile/components/NoteEditor/CodeMirror.ts
Normal file
@ -0,0 +1,160 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
|
||||
// This contains the CodeMirror instance, which needs to be built into a bundle
|
||||
// using `npm run buildInjectedJs`. This bundle is then loaded from
|
||||
// NoteEditor.tsx into the webview.
|
||||
//
|
||||
// In general, since this file is harder to debug due to the intermediate built
|
||||
// step, it's better to keep it as light as possible - it shoud just be a light
|
||||
// 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 { markdown } from '@codemirror/lang-markdown';
|
||||
import { defaultHighlightStyle, HighlightStyle, tags } from '@codemirror/highlight';
|
||||
import { undo, redo, history, undoDepth, redoDepth } from '@codemirror/history';
|
||||
|
||||
interface CodeMirrorResult {
|
||||
editor: EditorView;
|
||||
undo: Function;
|
||||
redo: Function;
|
||||
}
|
||||
|
||||
function postMessage(name: string, data: any) {
|
||||
(window as any).ReactNativeWebView.postMessage(JSON.stringify({
|
||||
data,
|
||||
name,
|
||||
}));
|
||||
}
|
||||
|
||||
function logMessage(...msg: any[]) {
|
||||
postMessage('onLog', { value: msg });
|
||||
}
|
||||
|
||||
const createTheme = (theme: any): Extension => {
|
||||
const baseTheme = EditorView.baseTheme({
|
||||
'&': {
|
||||
color: theme.color,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
fontFamily: theme.fontFamily,
|
||||
fontSize: `${theme.fontSize}px`,
|
||||
},
|
||||
});
|
||||
|
||||
const appearanceTheme = EditorView.theme({}, { dark: theme.appearance === 'dark' });
|
||||
|
||||
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...');
|
||||
|
||||
let schedulePostUndoRedoDepthChangeId_: any = 0;
|
||||
function schedulePostUndoRedoDepthChange(editor: EditorView, doItNow: boolean = false) {
|
||||
if (schedulePostUndoRedoDepthChangeId_) {
|
||||
if (doItNow) {
|
||||
clearTimeout(schedulePostUndoRedoDepthChangeId_);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
schedulePostUndoRedoDepthChangeId_ = setTimeout(() => {
|
||||
schedulePostUndoRedoDepthChangeId_ = null;
|
||||
postMessage('onUndoRedoDepthChange', {
|
||||
undoDepth: undoDepth(editor.state),
|
||||
redoDepth: redoDepth(editor.state),
|
||||
});
|
||||
}, doItNow ? 0 : 1000);
|
||||
}
|
||||
|
||||
const editor = new EditorView({
|
||||
state: EditorState.create({
|
||||
extensions: [
|
||||
markdown(),
|
||||
createTheme(theme),
|
||||
history(),
|
||||
drawSelection(),
|
||||
highlightSpecialChars(),
|
||||
EditorView.lineWrapping,
|
||||
defaultHighlightStyle.fallback,
|
||||
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
|
||||
if (viewUpdate.docChanged) {
|
||||
postMessage('onChange', { value: editor.state.doc.toString() });
|
||||
schedulePostUndoRedoDepthChange(editor);
|
||||
}
|
||||
}),
|
||||
],
|
||||
doc: initialText,
|
||||
}),
|
||||
parent: parentElement,
|
||||
});
|
||||
|
||||
return {
|
||||
editor,
|
||||
undo: () => {
|
||||
undo(editor);
|
||||
schedulePostUndoRedoDepthChange(editor, true);
|
||||
},
|
||||
redo: () => {
|
||||
redo(editor);
|
||||
schedulePostUndoRedoDepthChange(editor, true);
|
||||
},
|
||||
};
|
||||
}
|
333
packages/app-mobile/components/NoteEditor/NoteEditor.tsx
Normal file
333
packages/app-mobile/components/NoteEditor/NoteEditor.tsx
Normal file
@ -0,0 +1,333 @@
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
const React = require('react');
|
||||
const { forwardRef, useImperativeHandle, useEffect, useState, useCallback, useRef } = require('react');
|
||||
const { WebView } = require('react-native-webview');
|
||||
const { editorFont } = require('../global-style');
|
||||
|
||||
export interface ChangeEvent {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface UndoRedoDepthChangeEvent {
|
||||
undoDepth: number;
|
||||
redoDepth: number;
|
||||
}
|
||||
|
||||
type ChangeEventHandler = (event: ChangeEvent)=> void;
|
||||
type UndoRedoDepthChangeHandler = (event: UndoRedoDepthChangeEvent)=> void;
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
initialText: string;
|
||||
style: any;
|
||||
onChange: ChangeEventHandler;
|
||||
onUndoRedoDepthChange: UndoRedoDepthChangeHandler;
|
||||
}
|
||||
|
||||
function fontFamilyFromSettings() {
|
||||
const f = editorFont(Setting.value('style.editor.fontFamily'));
|
||||
return [f, 'sans-serif'].join(', ');
|
||||
}
|
||||
|
||||
// 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 useHtml(css: string): string {
|
||||
const [html, setHtml] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setHtml(
|
||||
`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<style>
|
||||
.cm-editor {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
${css}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin:0; height:100vh; width:100vh; width:100vw; min-width:100vw;">
|
||||
<div class="CodeMirror" style="height:100%;"></div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
);
|
||||
}, [css]);
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function editorTheme(themeId: number) {
|
||||
return {
|
||||
...themeStyle(themeId),
|
||||
fontSize: 15,
|
||||
fontFamily: fontFamilyFromSettings(),
|
||||
};
|
||||
}
|
||||
|
||||
function NoteEditor(props: Props, ref: any) {
|
||||
const [source, setSource] = useState(undefined);
|
||||
const webviewRef = useRef(null);
|
||||
|
||||
const injectedJavaScript = `
|
||||
function postMessage(name, data) {
|
||||
window.ReactNativeWebView.postMessage(JSON.stringify({
|
||||
data,
|
||||
name,
|
||||
}));
|
||||
}
|
||||
|
||||
function logMessage(...msg) {
|
||||
postMessage('onLog', { value: msg });
|
||||
}
|
||||
|
||||
// This variable is not used within this script
|
||||
// but is called using "injectJavaScript" from
|
||||
// the wrapper component.
|
||||
let cm = null;
|
||||
|
||||
try {
|
||||
${shim.injectedJs('codeMirrorBundle')};
|
||||
|
||||
const parentElement = document.getElementsByClassName('CodeMirror')[0];
|
||||
const theme = ${JSON.stringify(editorTheme(props.themeId))};
|
||||
const initialText = ${JSON.stringify(props.initialText)};
|
||||
|
||||
cm = codeMirrorBundle.initCodeMirror(parentElement, initialText, theme);
|
||||
} catch (e) {
|
||||
window.ReactNativeWebView.postMessage("error:" + e.message + ": " + JSON.stringify(e))
|
||||
} finally {
|
||||
true;
|
||||
}
|
||||
`;
|
||||
|
||||
// const css = useCss(props.themeId);
|
||||
const html = useHtml('');
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
undo: function() {
|
||||
webviewRef.current.injectJavaScript('cm.undo(); true;');
|
||||
},
|
||||
redo: function() {
|
||||
webviewRef.current.injectJavaScript('cm.redo(); true;');
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
async function createHtmlFile() {
|
||||
const tempFile = `${Setting.value('resourceDir')}/NoteEditor.html`;
|
||||
await shim.fsDriver().writeFile(tempFile, html, 'utf8');
|
||||
if (cancelled) return;
|
||||
|
||||
setSource({
|
||||
uri: `file://${tempFile}?r=${Math.round(Math.random() * 100000000)}`,
|
||||
baseUrl: `file://${Setting.value('resourceDir')}/`,
|
||||
});
|
||||
}
|
||||
|
||||
void createHtmlFile();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [html]);
|
||||
|
||||
const onMessage = useCallback((event: any) => {
|
||||
const data = event.nativeEvent.data;
|
||||
|
||||
if (data.indexOf('error:') === 0) {
|
||||
console.error('CodeMirror:', data);
|
||||
return;
|
||||
}
|
||||
|
||||
const msg = JSON.parse(data);
|
||||
|
||||
const handlers: Record<string, Function> = {
|
||||
onLog: (event: any) => {
|
||||
console.info('CodeMirror:', ...event.value);
|
||||
},
|
||||
|
||||
onChange: (event: ChangeEvent) => {
|
||||
props.onChange(event);
|
||||
},
|
||||
|
||||
onUndoRedoDepthChange: (event: UndoRedoDepthChangeEvent) => {
|
||||
console.info('onUndoRedoDepthChange', event);
|
||||
props.onUndoRedoDepthChange(event);
|
||||
},
|
||||
};
|
||||
|
||||
if (handlers[msg.name]) {
|
||||
handlers[msg.name](msg.data);
|
||||
} else {
|
||||
console.info('Unsupported CodeMirror message:', msg);
|
||||
}
|
||||
}, [props.onChange]);
|
||||
|
||||
const onError = useCallback(() => {
|
||||
console.error('NoteEditor: webview error');
|
||||
});
|
||||
|
||||
// - `setSupportMultipleWindows` must be `true` for security reasons:
|
||||
// https://github.com/react-native-webview/react-native-webview/releases/tag/v11.0.0
|
||||
return <WebView
|
||||
style={props.style}
|
||||
ref={webviewRef}
|
||||
useWebKit={true}
|
||||
source={source}
|
||||
setSupportMultipleWindows={true}
|
||||
allowingReadAccessToURL={`file://${Setting.value('resourceDir')}`}
|
||||
originWhitelist={['file://*', './*', 'http://*', 'https://*']}
|
||||
allowFileAccess={true}
|
||||
injectedJavaScript={injectedJavaScript}
|
||||
onMessage={onMessage}
|
||||
onError={onError}
|
||||
/>;
|
||||
}
|
||||
|
||||
export default forwardRef(NoteEditor);
|
@ -1,10 +1,11 @@
|
||||
import AsyncActionQueue from '@joplin/lib/AsyncActionQueue';
|
||||
import UndoRedoService from '@joplin/lib/services/UndoRedoService';
|
||||
import uuid from '@joplin/lib/uuid';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import UndoRedoService from '@joplin/lib/services/UndoRedoService';
|
||||
import NoteBodyViewer from '../NoteBodyViewer/NoteBodyViewer';
|
||||
import checkPermissions from '../../utils/checkPermissions';
|
||||
import NoteEditor, { ChangeEvent, UndoRedoDepthChangeEvent } from '../NoteEditor/NoteEditor';
|
||||
|
||||
const FileViewer = require('react-native-file-viewer').default;
|
||||
const React = require('react');
|
||||
@ -42,6 +43,7 @@ const ImagePicker = require('react-native-image-picker').default;
|
||||
import SelectDateTimeDialog from '../SelectDateTimeDialog';
|
||||
import ShareExtension from '../../utils/ShareExtension.js';
|
||||
import CameraView from '../CameraView';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
const urlUtils = require('@joplin/lib/urlUtils');
|
||||
|
||||
const emptyArray: any[] = [];
|
||||
@ -95,6 +97,8 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
|
||||
this.styles_ = {};
|
||||
|
||||
this.editorRef = React.createRef();
|
||||
|
||||
const saveDialog = async () => {
|
||||
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' }]);
|
||||
@ -230,16 +234,38 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
this.body_selectionChange = this.body_selectionChange.bind(this);
|
||||
this.onBodyViewerLoadEnd = this.onBodyViewerLoadEnd.bind(this);
|
||||
this.onBodyViewerCheckboxChange = this.onBodyViewerCheckboxChange.bind(this);
|
||||
this.onBodyChange = this.onBodyChange.bind(this);
|
||||
this.onUndoRedoDepthChange = this.onUndoRedoDepthChange.bind(this);
|
||||
}
|
||||
|
||||
undoRedoService_stackChange() {
|
||||
this.setState({ undoRedoButtonState: {
|
||||
canUndo: this.undoRedoService_.canUndo,
|
||||
canRedo: this.undoRedoService_.canRedo,
|
||||
} });
|
||||
private useEditorBeta(): boolean {
|
||||
return this.props.useEditorBeta;
|
||||
}
|
||||
|
||||
async undoRedo(type: string) {
|
||||
private onBodyChange(event: ChangeEvent) {
|
||||
shared.noteComponent_change(this, 'body', event.value);
|
||||
this.scheduleSave();
|
||||
}
|
||||
|
||||
private onUndoRedoDepthChange(event: UndoRedoDepthChangeEvent) {
|
||||
if (this.useEditorBeta()) {
|
||||
this.setState({ undoRedoButtonState: {
|
||||
canUndo: !!event.undoDepth,
|
||||
canRedo: !!event.redoDepth,
|
||||
} });
|
||||
}
|
||||
}
|
||||
|
||||
private undoRedoService_stackChange() {
|
||||
if (!this.useEditorBeta()) {
|
||||
this.setState({ undoRedoButtonState: {
|
||||
canUndo: this.undoRedoService_.canUndo,
|
||||
canRedo: this.undoRedoService_.canRedo,
|
||||
} });
|
||||
}
|
||||
}
|
||||
|
||||
private async undoRedo(type: string) {
|
||||
const undoState = await this.undoRedoService_[type](this.undoState());
|
||||
if (!undoState) return;
|
||||
|
||||
@ -253,11 +279,25 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
}
|
||||
|
||||
screenHeader_undoButtonPress() {
|
||||
void this.undoRedo('undo');
|
||||
if (this.useEditorBeta()) {
|
||||
this.editorRef.current.undo();
|
||||
} else {
|
||||
void this.undoRedo('undo');
|
||||
}
|
||||
}
|
||||
|
||||
screenHeader_redoButtonPress() {
|
||||
void this.undoRedo('redo');
|
||||
if (this.useEditorBeta()) {
|
||||
this.editorRef.current.redo();
|
||||
} else {
|
||||
void this.undoRedo('redo');
|
||||
}
|
||||
}
|
||||
|
||||
undoState(noteBody: string = null) {
|
||||
return {
|
||||
body: noteBody === null ? this.state.note.body : noteBody,
|
||||
};
|
||||
}
|
||||
|
||||
styles() {
|
||||
@ -355,12 +395,6 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
return shared.isModified(this);
|
||||
}
|
||||
|
||||
undoState(noteBody: string = null) {
|
||||
return {
|
||||
body: noteBody === null ? this.state.note.body : noteBody,
|
||||
};
|
||||
}
|
||||
|
||||
async requestGeoLocationPermissions() {
|
||||
if (!Setting.value('trackLocation')) return;
|
||||
|
||||
@ -448,6 +482,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
} else {
|
||||
this.undoRedoService_.schedulePush(this.undoState());
|
||||
}
|
||||
|
||||
shared.noteComponent_change(this, 'body', text);
|
||||
this.scheduleSave();
|
||||
}
|
||||
@ -844,6 +879,11 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
output.push({
|
||||
title: _('Attach...'),
|
||||
onPress: async () => {
|
||||
if (this.state.mode === 'edit' && this.useEditorBeta()) {
|
||||
alert('Attaching files from the beta editor is not yet supported. You may do so from the viewer mode instead.');
|
||||
return;
|
||||
}
|
||||
|
||||
const buttons = [];
|
||||
|
||||
// On iOS, it will show "local files", which means certain files saved from the browser
|
||||
@ -1017,7 +1057,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
}
|
||||
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const note = this.state.note;
|
||||
const note: NoteEntity = this.state.note;
|
||||
const isTodo = !!Number(note.is_todo);
|
||||
|
||||
if (this.state.showCamera) {
|
||||
@ -1066,25 +1106,37 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
// abcd|
|
||||
// abcde|
|
||||
// abcde|f
|
||||
bodyComponent = (
|
||||
<TextInput
|
||||
autoCapitalize="sentences"
|
||||
|
||||
if (!this.useEditorBeta()) {
|
||||
bodyComponent = (
|
||||
<TextInput
|
||||
autoCapitalize="sentences"
|
||||
style={this.styles().bodyTextInput}
|
||||
ref="noteBodyTextField"
|
||||
multiline={true}
|
||||
value={note.body}
|
||||
onChangeText={(text: string) => this.body_changeText(text)}
|
||||
onSelectionChange={this.body_selectionChange}
|
||||
blurOnSubmit={false}
|
||||
selectionColor={theme.textSelectionColor}
|
||||
keyboardAppearance={theme.keyboardAppearance}
|
||||
placeholder={_('Add body')}
|
||||
placeholderTextColor={theme.colorFaded}
|
||||
// need some extra padding for iOS so that the keyboard won't cover last line of the note
|
||||
// see https://github.com/laurent22/joplin/issues/3607
|
||||
paddingBottom={ Platform.OS === 'ios' ? 40 : 0}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
bodyComponent = <NoteEditor
|
||||
ref={this.editorRef}
|
||||
themeId={this.props.themeId}
|
||||
initialText={note.body}
|
||||
onChange={this.onBodyChange}
|
||||
onUndoRedoDepthChange={this.onUndoRedoDepthChange}
|
||||
style={this.styles().bodyTextInput}
|
||||
ref="noteBodyTextField"
|
||||
multiline={true}
|
||||
value={note.body}
|
||||
onChangeText={(text: string) => this.body_changeText(text)}
|
||||
onSelectionChange={this.body_selectionChange}
|
||||
blurOnSubmit={false}
|
||||
selectionColor={theme.textSelectionColor}
|
||||
keyboardAppearance={theme.keyboardAppearance}
|
||||
placeholder={_('Add body')}
|
||||
placeholderTextColor={theme.colorFaded}
|
||||
// need some extra padding for iOS so that the keyboard won't cover last line of the note
|
||||
// see https://github.com/laurent22/joplin/issues/3607
|
||||
paddingBottom={ Platform.OS === 'ios' ? 40 : 0}
|
||||
/>
|
||||
);
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
const renderActionButton = () => {
|
||||
@ -1187,6 +1239,7 @@ const NoteScreen = connect((state: any) => {
|
||||
showSideMenu: state.showSideMenu,
|
||||
provisionalNoteIds: state.provisionalNoteIds,
|
||||
highlightedWords: state.highlightedWords,
|
||||
useEditorBeta: state.settings['editor.beta'],
|
||||
};
|
||||
})(NoteScreenComponent);
|
||||
|
||||
|
@ -5,8 +5,8 @@ const tasks = {
|
||||
encodeAssets: {
|
||||
fn: require('./tools/encodeAssets'),
|
||||
},
|
||||
buildReactNativeInjectedJs: {
|
||||
fn: require('./tools/buildReactNativeInjectedJs'),
|
||||
buildInjectedJs: {
|
||||
fn: require('./tools/buildInjectedJs'),
|
||||
},
|
||||
podInstall: {
|
||||
fn: require('./tools/podInstall'),
|
||||
@ -16,7 +16,7 @@ const tasks = {
|
||||
utils.registerGulpTasks(gulp, tasks);
|
||||
|
||||
gulp.task('build', gulp.series(
|
||||
'buildReactNativeInjectedJs',
|
||||
'buildInjectedJs',
|
||||
'encodeAssets',
|
||||
'podInstall'
|
||||
));
|
||||
|
1148
packages/app-mobile/package-lock.json
generated
1148
packages/app-mobile/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -10,11 +10,14 @@
|
||||
"tsc": "node node_modules/typescript/bin/tsc --project tsconfig.json",
|
||||
"watch": "node node_modules/typescript/bin/tsc --watch --project tsconfig.json",
|
||||
"clean": "node tools/clean.js",
|
||||
"buildInjectedJs": "gulp buildInjectedJs",
|
||||
"watchInjectedJs": "nodemon --verbose --watch components/NoteEditor/CodeMirror.ts --exec \"npm run buildInjectedJs\"",
|
||||
"postinstall": "jetify && npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@joplin/lib": "^1.0.9",
|
||||
"@joplin/renderer": "^1.0.17",
|
||||
"@joplin/lib": "^2.2.0",
|
||||
"@joplin/renderer": "^2.2.0",
|
||||
"@joplin/tools": "^2.2.0",
|
||||
"@react-native-community/clipboard": "^1.5.0",
|
||||
"@react-native-community/datetimepicker": "^3.0.3",
|
||||
"@react-native-community/geolocation": "^2.0.2",
|
||||
@ -62,7 +65,14 @@
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.11.6",
|
||||
"@babel/runtime": "^7.11.2",
|
||||
"@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": "^1.0.9",
|
||||
"@rollup/plugin-node-resolve": "^13.0.0",
|
||||
"@rollup/plugin-typescript": "^8.2.1",
|
||||
"@types/node": "^14.14.6",
|
||||
"@types/react": "^16.9.55",
|
||||
"@types/react-native": "^0.64.4",
|
||||
@ -71,6 +81,9 @@
|
||||
"gulp": "^4.0.2",
|
||||
"jetifier": "^1.6.5",
|
||||
"metro-react-native-babel-preset": "^0.63.0",
|
||||
"typescript": "^4.0.5"
|
||||
"nodemon": "^2.0.12",
|
||||
"rollup": "^2.53.1",
|
||||
"typescript": "^4.0.5",
|
||||
"uglify-js": "^3.13.10"
|
||||
}
|
||||
}
|
||||
|
33
packages/app-mobile/tools/buildInjectedJs.js
Normal file
33
packages/app-mobile/tools/buildInjectedJs.js
Normal file
@ -0,0 +1,33 @@
|
||||
// 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 { execCommand2, rootDir } = require('@joplin/tools/tool-utils');
|
||||
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 js = await fs.readFile(filePath, 'utf-8');
|
||||
const json = `module.exports = ${JSON.stringify(js)};`;
|
||||
const outputPath = `${outputDir}/${name}.js`;
|
||||
await fs.writeFile(outputPath, json);
|
||||
}
|
||||
|
||||
async function buildCodeMirrorBundle() {
|
||||
const sourceFile = `${mobileDir}/components/NoteEditor/CodeMirror.ts`;
|
||||
const fullBundleFile = `${mobileDir}/components/NoteEditor/CodeMirror.bundle.js`;
|
||||
await execCommand2(`./node_modules/rollup/dist/bin/rollup "${sourceFile}" --name codeMirrorBundle -f iife -o "${fullBundleFile}" -p @rollup/plugin-node-resolve -p @rollup/plugin-typescript`);
|
||||
await execCommand2(`./node_modules/uglify-js/bin/uglifyjs --compress -o "${codeMirrorBundleFile}" -- "${fullBundleFile}"`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await fs.mkdirp(outputDir);
|
||||
await buildCodeMirrorBundle();
|
||||
await copyJs('webviewLib', `${mobileDir}/node_modules/@joplin/lib/renderers/webviewLib.js`);
|
||||
await copyJs('CodeMirror.bundle', `${mobileDir}/components/NoteEditor/CodeMirror.bundle.min.js`);
|
||||
}
|
||||
|
||||
module.exports = main;
|
@ -1,23 +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 rnDir = `${__dirname}/..`;
|
||||
const outputDir = `${rnDir}/lib/rnInjectedJs`;
|
||||
|
||||
async function copyJs(name, filePath) {
|
||||
const js = await fs.readFile(filePath, 'utf-8');
|
||||
const json = `module.exports = ${JSON.stringify(js)};`;
|
||||
const outputPath = `${outputDir}/${name}.js`;
|
||||
await fs.writeFile(outputPath, json);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await fs.mkdirp(outputDir);
|
||||
await copyJs('webviewLib', `${rnDir}/node_modules/@joplin/lib/renderers/webviewLib.js`);
|
||||
}
|
||||
|
||||
module.exports = main;
|
@ -14,6 +14,7 @@ const Resource = require('@joplin/lib/models/Resource').default;
|
||||
|
||||
const injectedJs = {
|
||||
webviewLib: require('@joplin/lib/rnInjectedJs/webviewLib'),
|
||||
codeMirrorBundle: require('../lib/rnInjectedJs/CodeMirror.bundle'),
|
||||
};
|
||||
|
||||
function shimInit() {
|
||||
|
@ -785,10 +785,10 @@ class Setting extends BaseModel {
|
||||
value: false,
|
||||
type: SettingItemType.Bool,
|
||||
section: 'note',
|
||||
public: false, // mobilePlatform === 'ios',
|
||||
public: true,
|
||||
appTypes: [AppType.Mobile],
|
||||
label: () => 'Opt-in to the editor beta',
|
||||
description: () => 'This beta adds list continuation, Markdown preview, and Markdown shortcuts. If you find bugs, please report them in the Discourse forum.',
|
||||
description: () => 'This beta adds list continuation and syntax highlighting. If you find bugs, please report them in the Discourse forum.',
|
||||
},
|
||||
|
||||
newTodoFocus: {
|
||||
|
@ -1,5 +1,12 @@
|
||||
# Joplin Android app changelog
|
||||
|
||||
## [android-v2.2.1](https://github.com/laurent22/joplin/releases/tag/android-v2.2.1) (Pre-release) - 2021-07-13T17:37:38Z
|
||||
|
||||
- New: Added improved editor (beta)
|
||||
- Improved: Disable backup to Google Drive (#5114 by Roman Musin)
|
||||
- Improved: Interpret only valid search filters (#5103) (#3871 by [@JackGruber](https://github.com/JackGruber))
|
||||
- Improved: Removed old editor code (e01a175)
|
||||
|
||||
## [android-v2.1.4](https://github.com/laurent22/joplin/releases/tag/android-v2.1.4) - 2021-07-03T08:31:36Z
|
||||
|
||||
- Fixed: Fixes #5133: Items keep being uploaded to Joplin Server after a note has been shared.
|
||||
|
Loading…
Reference in New Issue
Block a user