You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-11-26 22:41:17 +02:00
Mobile: Add Markdown toolbar (#6753)
This commit is contained in:
@@ -5,36 +5,36 @@ import EditLinkDialog from './EditLinkDialog';
|
||||
import { defaultSearchState, SearchPanel } from './SearchPanel';
|
||||
|
||||
const React = require('react');
|
||||
const { forwardRef, useImperativeHandle } = require('react');
|
||||
const { useEffect, useMemo, useState, useCallback, useRef } = require('react');
|
||||
import { forwardRef, RefObject, useImperativeHandle } from 'react';
|
||||
import { useEffect, useMemo, useState, useCallback, useRef } from 'react';
|
||||
const { WebView } = require('react-native-webview');
|
||||
const { View } = require('react-native');
|
||||
import { View, ViewStyle } from 'react-native';
|
||||
const { editorFont } = require('../global-style');
|
||||
|
||||
import SelectionFormatting from './SelectionFormatting';
|
||||
import {
|
||||
EditorSettings,
|
||||
EditorControl,
|
||||
|
||||
ChangeEvent, UndoRedoDepthChangeEvent, Selection, SelectionChangeEvent,
|
||||
ListType,
|
||||
SearchState,
|
||||
EditorSettings, EditorControl,
|
||||
ChangeEvent, UndoRedoDepthChangeEvent, Selection, SelectionChangeEvent, ListType, SearchState,
|
||||
} from './types';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import MarkdownToolbar from './MarkdownToolbar/MarkdownToolbar';
|
||||
|
||||
type ChangeEventHandler = (event: ChangeEvent)=> void;
|
||||
type UndoRedoDepthChangeHandler = (event: UndoRedoDepthChangeEvent)=> void;
|
||||
type SelectionChangeEventHandler = (event: SelectionChangeEvent)=> void;
|
||||
type OnAttachCallback = ()=> void;
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
initialText: string;
|
||||
initialSelection?: Selection;
|
||||
style: any;
|
||||
style: ViewStyle;
|
||||
contentStyle?: ViewStyle;
|
||||
|
||||
onChange: ChangeEventHandler;
|
||||
onSelectionChange: SelectionChangeEventHandler;
|
||||
onUndoRedoDepthChange: UndoRedoDepthChangeHandler;
|
||||
onAttach: OnAttachCallback;
|
||||
}
|
||||
|
||||
function fontFamilyFromSettings() {
|
||||
@@ -106,6 +106,106 @@ function editorTheme(themeId: number) {
|
||||
};
|
||||
}
|
||||
|
||||
type OnInjectJSCallback = (js: string)=> void;
|
||||
type OnSetVisibleCallback = (visible: boolean)=> void;
|
||||
type OnSearchStateChangeCallback = (state: SearchState)=> void;
|
||||
const useEditorControl = (
|
||||
injectJS: OnInjectJSCallback, setLinkDialogVisible: OnSetVisibleCallback,
|
||||
setSearchState: OnSearchStateChangeCallback, searchStateRef: RefObject<SearchState>
|
||||
): EditorControl => {
|
||||
return useMemo(() => {
|
||||
return {
|
||||
undo() {
|
||||
injectJS('cm.undo();');
|
||||
},
|
||||
redo() {
|
||||
injectJS('cm.redo();');
|
||||
},
|
||||
select(anchor: number, head: number) {
|
||||
injectJS(
|
||||
`cm.select(${JSON.stringify(anchor)}, ${JSON.stringify(head)});`
|
||||
);
|
||||
},
|
||||
insertText(text: string) {
|
||||
injectJS(`cm.insertText(${JSON.stringify(text)});`);
|
||||
},
|
||||
|
||||
toggleBolded() {
|
||||
injectJS('cm.toggleBolded();');
|
||||
},
|
||||
toggleItalicized() {
|
||||
injectJS('cm.toggleItalicized();');
|
||||
},
|
||||
toggleList(listType: ListType) {
|
||||
injectJS(`cm.toggleList(${JSON.stringify(listType)});`);
|
||||
},
|
||||
toggleCode() {
|
||||
injectJS('cm.toggleCode();');
|
||||
},
|
||||
toggleMath() {
|
||||
injectJS('cm.toggleMath();');
|
||||
},
|
||||
toggleHeaderLevel(level: number) {
|
||||
injectJS(`cm.toggleHeaderLevel(${level});`);
|
||||
},
|
||||
increaseIndent() {
|
||||
injectJS('cm.increaseIndent();');
|
||||
},
|
||||
decreaseIndent() {
|
||||
injectJS('cm.decreaseIndent();');
|
||||
},
|
||||
updateLink(label: string, url: string) {
|
||||
injectJS(`cm.updateLink(
|
||||
${JSON.stringify(label)},
|
||||
${JSON.stringify(url)}
|
||||
);`);
|
||||
},
|
||||
scrollSelectionIntoView() {
|
||||
injectJS('cm.scrollSelectionIntoView();');
|
||||
},
|
||||
showLinkDialog() {
|
||||
setLinkDialogVisible(true);
|
||||
},
|
||||
hideLinkDialog() {
|
||||
setLinkDialogVisible(false);
|
||||
},
|
||||
hideKeyboard() {
|
||||
injectJS('document.activeElement?.blur();');
|
||||
},
|
||||
searchControl: {
|
||||
findNext() {
|
||||
injectJS('cm.searchControl.findNext();');
|
||||
},
|
||||
findPrevious() {
|
||||
injectJS('cm.searchControl.findPrevious();');
|
||||
},
|
||||
replaceCurrent() {
|
||||
injectJS('cm.searchControl.replaceCurrent();');
|
||||
},
|
||||
replaceAll() {
|
||||
injectJS('cm.searchControl.replaceAll();');
|
||||
},
|
||||
setSearchState(state: SearchState) {
|
||||
injectJS(`cm.searchControl.setSearchState(${JSON.stringify(state)})`);
|
||||
setSearchState(state);
|
||||
},
|
||||
showSearch() {
|
||||
setSearchState({
|
||||
...searchStateRef.current,
|
||||
dialogVisible: true,
|
||||
});
|
||||
},
|
||||
hideSearch() {
|
||||
setSearchState({
|
||||
...searchStateRef.current,
|
||||
dialogVisible: false,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
}, [injectJS, searchStateRef, setLinkDialogVisible, setSearchState]);
|
||||
};
|
||||
|
||||
function NoteEditor(props: Props, ref: any) {
|
||||
const [source, setSource] = useState(undefined);
|
||||
const webviewRef = useRef(null);
|
||||
@@ -172,10 +272,19 @@ function NoteEditor(props: Props, ref: any) {
|
||||
const css = useCss(props.themeId);
|
||||
const html = useHtml(css);
|
||||
const [selectionState, setSelectionState] = useState(new SelectionFormatting());
|
||||
const [searchState, setSearchState] = useState(defaultSearchState);
|
||||
const [linkDialogVisible, setLinkDialogVisible] = useState(false);
|
||||
const [searchState, setSearchState] = useState(defaultSearchState);
|
||||
|
||||
// / Runs [js] in the context of the CodeMirror frame.
|
||||
// Having a [searchStateRef] allows [editorControl] to not be re-created
|
||||
// whenever [searchState] changes.
|
||||
const searchStateRef = useRef(defaultSearchState);
|
||||
|
||||
// Keep the reference and the [searchState] in sync
|
||||
useEffect(() => {
|
||||
searchStateRef.current = searchState;
|
||||
}, [searchState]);
|
||||
|
||||
// Runs [js] in the context of the CodeMirror frame.
|
||||
const injectJS = (js: string) => {
|
||||
webviewRef.current.injectJavaScript(`
|
||||
try {
|
||||
@@ -189,96 +298,9 @@ function NoteEditor(props: Props, ref: any) {
|
||||
true;`);
|
||||
};
|
||||
|
||||
|
||||
const editorControl: EditorControl = {
|
||||
undo() {
|
||||
injectJS('cm.undo();');
|
||||
},
|
||||
redo() {
|
||||
injectJS('cm.redo();');
|
||||
},
|
||||
select(anchor: number, head: number) {
|
||||
injectJS(
|
||||
`cm.select(${JSON.stringify(anchor)}, ${JSON.stringify(head)});`
|
||||
);
|
||||
},
|
||||
insertText(text: string) {
|
||||
injectJS(`cm.insertText(${JSON.stringify(text)});`);
|
||||
},
|
||||
|
||||
toggleBolded() {
|
||||
injectJS('cm.toggleBolded();');
|
||||
},
|
||||
toggleItalicized() {
|
||||
injectJS('cm.toggleItalicized();');
|
||||
},
|
||||
toggleList(listType: ListType) {
|
||||
injectJS(`cm.toggleList(${JSON.stringify(listType)});`);
|
||||
},
|
||||
toggleCode() {
|
||||
injectJS('cm.toggleCode();');
|
||||
},
|
||||
toggleMath() {
|
||||
injectJS('cm.toggleMath();');
|
||||
},
|
||||
toggleHeaderLevel(level: number) {
|
||||
injectJS(`cm.toggleHeaderLevel(${level});`);
|
||||
},
|
||||
increaseIndent() {
|
||||
injectJS('cm.increaseIndent();');
|
||||
},
|
||||
decreaseIndent() {
|
||||
injectJS('cm.decreaseIndent();');
|
||||
},
|
||||
updateLink(label: string, url: string) {
|
||||
injectJS(`cm.updateLink(
|
||||
${JSON.stringify(label)},
|
||||
${JSON.stringify(url)}
|
||||
);`);
|
||||
},
|
||||
scrollSelectionIntoView() {
|
||||
injectJS('cm.scrollSelectionIntoView();');
|
||||
},
|
||||
showLinkDialog() {
|
||||
setLinkDialogVisible(true);
|
||||
},
|
||||
hideLinkDialog() {
|
||||
setLinkDialogVisible(false);
|
||||
},
|
||||
hideKeyboard() {
|
||||
injectJS('document.activeElement?.blur();');
|
||||
},
|
||||
searchControl: {
|
||||
findNext() {
|
||||
injectJS('cm.searchControl.findNext();');
|
||||
},
|
||||
findPrevious() {
|
||||
injectJS('cm.searchControl.findPrevious();');
|
||||
},
|
||||
replaceCurrent() {
|
||||
injectJS('cm.searchControl.replaceCurrent();');
|
||||
},
|
||||
replaceAll() {
|
||||
injectJS('cm.searchControl.replaceAll();');
|
||||
},
|
||||
setSearchState(state: SearchState) {
|
||||
injectJS(`cm.searchControl.setSearchState(${JSON.stringify(state)})`);
|
||||
setSearchState(state);
|
||||
},
|
||||
showSearch() {
|
||||
const newSearchState: SearchState = Object.assign({}, searchState);
|
||||
newSearchState.dialogVisible = true;
|
||||
|
||||
setSearchState(newSearchState);
|
||||
},
|
||||
hideSearch() {
|
||||
const newSearchState: SearchState = Object.assign({}, searchState);
|
||||
newSearchState.dialogVisible = false;
|
||||
|
||||
setSearchState(newSearchState);
|
||||
},
|
||||
},
|
||||
};
|
||||
const editorControl = useEditorControl(
|
||||
injectJS, setLinkDialogVisible, setSearchState, searchStateRef
|
||||
);
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
return editorControl;
|
||||
@@ -358,13 +380,12 @@ function NoteEditor(props: Props, ref: any) {
|
||||
} else {
|
||||
console.info('Unsupported CodeMirror message:', msg);
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [props.onChange]);
|
||||
}, [props.onSelectionChange, props.onUndoRedoDepthChange, props.onChange, editorControl]);
|
||||
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
const onError = useCallback(() => {
|
||||
console.error('NoteEditor: webview error');
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
// - `setSupportMultipleWindows` must be `true` for security reasons:
|
||||
@@ -385,7 +406,8 @@ function NoteEditor(props: Props, ref: any) {
|
||||
<View style={{
|
||||
flexGrow: 1,
|
||||
flexShrink: 0,
|
||||
minHeight: '40%',
|
||||
minHeight: '30%',
|
||||
...props.contentStyle,
|
||||
}}>
|
||||
<WebView
|
||||
style={{
|
||||
@@ -411,6 +433,19 @@ function NoteEditor(props: Props, ref: any) {
|
||||
searchControl={editorControl.searchControl}
|
||||
searchState={searchState}
|
||||
/>
|
||||
|
||||
<MarkdownToolbar
|
||||
style={{
|
||||
// Don't show the markdown toolbar if there isn't enough space
|
||||
// for it:
|
||||
flexShrink: 1,
|
||||
}}
|
||||
editorSettings={editorSettings}
|
||||
editorControl={editorControl}
|
||||
selectionState={selectionState}
|
||||
searchState={searchState}
|
||||
onAttach={props.onAttach}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user