Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mobile: Add list continuation & Markdown preview to editor #2224

Merged
merged 56 commits into from Mar 25, 2020
Merged
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
00f8611
The basic editor is working! No list continuation still though
devonzuegel Dec 25, 2019
30e3153
List continuation is working! Now to delete when entering again and n…
devonzuegel Dec 25, 2019
6d569e8
Supports checkboxes + attempted at setting font
devonzuegel Dec 25, 2019
5220360
Editor font works now; now need to fix the delete (look at past state)
devonzuegel Dec 25, 2019
48eeaba
Fix deletion problem
devonzuegel Dec 25, 2019
222e6d6
Add ordered list handler
devonzuegel Dec 25, 2019
090e05b
Add comments
devonzuegel Dec 25, 2019
fce286a
Extract insertListLine
devonzuegel Dec 25, 2019
7221abc
End lists on enter for empty bullets
devonzuegel Dec 25, 2019
a129410
Add MarkdownView (renders badly though)
devonzuegel Dec 25, 2019
d3d5a75
Save edited text from MarkdownEditor
devonzuegel Dec 25, 2019
7e25807
Cleanup
devonzuegel Dec 25, 2019
cac2639
Refactor react-native-markdown-editor/
devonzuegel Dec 25, 2019
c59c632
Rename react-native-markdown-editor/ => MarkdownEditor/
devonzuegel Dec 25, 2019
68272da
Cleanup
devonzuegel Dec 25, 2019
0f4b750
Fix preview styles; still need to fix checkbox problem
devonzuegel Dec 26, 2019
a8b3908
Fix keyboard padding
devonzuegel Dec 26, 2019
0d66a8d
Change name back to #body_changeText
devonzuegel Mar 1, 2020
dac9fa9
Merge branch 'master' into list-continuation
devonzuegel Mar 1, 2020
bb6c4fb
Incorporate PR feedback from @laurent22
devonzuegel Mar 1, 2020
f5f6765
wip: Move MarkdownEditor/ from ReactNativeClient/lib/ to ReactNativeC…
devonzuegel Mar 1, 2020
73df347
Move MarkdownEditor/ from ReactNativeClient/lib/ to ReactNativeClient/
devonzuegel Mar 1, 2020
1a48526
Merge branch 'master' into list-continuation
laurent22 Mar 16, 2020
b4b68fd
Remove log statement
devonzuegel Mar 19, 2020
883c0cd
Focus TextInput in MarkdownEditor from grandparent
devonzuegel Mar 19, 2020
857c1dd
Make eslint happy
devonzuegel Mar 19, 2020
0729d36
Extract textInputRefName to shared variable
devonzuegel Mar 19, 2020
17ea526
Remove accidental #setState
devonzuegel Mar 19, 2020
690a627
Cleanup
devonzuegel Mar 19, 2020
9b14999
Cleanup
devonzuegel Mar 19, 2020
062c2fc
Run linter
devonzuegel Mar 19, 2020
ff0b209
Cleanup
devonzuegel Mar 20, 2020
96acd55
Update button order
devonzuegel Mar 21, 2020
de453c4
Improve styles for config descriptions
devonzuegel Mar 21, 2020
9b3b0a6
Allow descriptions to be added to BOOL type Setting configs
devonzuegel Mar 21, 2020
e244889
Add editorBeta Setting
devonzuegel Mar 21, 2020
a59964e
Move FailSafe details to description text
devonzuegel Mar 21, 2020
176d16e
Update descriptionText styles
devonzuegel Mar 21, 2020
082d433
Put the editor under the beta flag toggle
devonzuegel Mar 22, 2020
718eab4
Incorporate PR feedback from @laurent22
devonzuegel Mar 23, 2020
4c72146
Refactor Markdown editor focusing
devonzuegel Mar 23, 2020
5c5d964
Cleanup
devonzuegel Mar 23, 2020
14327c3
Reorder MarkdownEditor formats
devonzuegel Mar 23, 2020
6479245
Make applyListFormat behavior more intuitive
devonzuegel Mar 23, 2020
1dc7b7a
Add comment
devonzuegel Mar 23, 2020
8aadc53
Show MarkdownEditor with preview by default
devonzuegel Mar 23, 2020
8e580ef
Show preview by default, then hide on typing
devonzuegel Mar 23, 2020
ae4a022
Fix MarkdownEditor selection bug
devonzuegel Mar 23, 2020
5daa961
Cleanup
devonzuegel Mar 23, 2020
96a747a
Update Markdown button styles
devonzuegel Mar 24, 2020
4bcf3b6
Make Markdown button colors theme-conscious
devonzuegel Mar 24, 2020
cb71a91
Merge branch 'master' into list-continuation
devonzuegel Mar 24, 2020
a1bce92
Fix merge conflict resolution mistake
devonzuegel Mar 24, 2020
77b7d86
Fix broken import
devonzuegel Mar 24, 2020
9dd12b1
Delete package-lock.json
laurent22 Mar 24, 2020
1a735c7
Reset package-lock.json
devonzuegel Mar 24, 2020
File filter...
Filter file types
Jump to…
Jump to file
Failed to load files.

Always

Just for now

@@ -0,0 +1,23 @@
import applyWrapFormat from './applyWrapFormat';
import applyWrapFormatNewLines from './applyWrapFormatNewLines';
import applyListFormat from './applyListFormat';
import applyWebLinkFormat from './applyWebLinkFormat';

export default [
{ key: 'B', title: 'B', wrapper: '**', onPress: applyWrapFormat, style: { fontWeight: 'bold' } },
{ key: 'I', title: 'I', wrapper: '*', onPress: applyWrapFormat, style: { fontStyle: 'italic' } },
{ key: 'Link', title: 'Link', onPress: applyWebLinkFormat },
{ key: 'List', title: 'List', prefix: '-', onPress: applyListFormat },
{
key: 'S',
title: 'S',
wrapper: '~~',
onPress: applyWrapFormat,
style: { textDecorationLine: 'line-through' },
},
{ key: '</>', title: '</>', wrapper: '`', onPress: applyWrapFormat },
{ key: 'Pre', title: 'Pre', wrapper: '```', onPress: applyWrapFormatNewLines },
{ key: 'H1', title: 'H1', prefix: '#', onPress: applyListFormat },
{ key: 'H2', title: 'H2', prefix: '##', onPress: applyListFormat },
{ key: 'H3', title: 'H3', prefix: '###', onPress: applyListFormat },
];
@@ -0,0 +1,162 @@
/**
* Inspired by https://github.com/kunall17/MarkdownEditor
*/

import React from 'react';
import {
View,
StyleSheet,
TextInput,
Platform,
KeyboardAvoidingView,
TouchableOpacity,
Image,
} from 'react-native';
import { renderFormatButtons } from './renderButtons';
import { NoteBodyViewer } from 'lib/components/note-body-viewer.js';

const styles = StyleSheet.create({
buttonContainer: {
flex: 0,
flexDirection: 'row',
},
screen: { // Wrapper around the editor and the preview
flex: 1,
flexDirection: 'column',
alignItems: 'stretch',
},
});

const MarkdownPreviewButton = (props) =>
<TouchableOpacity
onPress={props.convertMarkdown}
style={{ padding: 8, borderRightWidth: 1, borderColor: props.borderColor }}>
<Image
style={{ tintColor: props.color, padding: 8 }}
source={require('./static/visibility.png')}
resizeMode="cover"
/>
</TouchableOpacity>;

export default class MarkdownEditor extends React.Component {
constructor(props) {
super(props);
this.state = {
text: props.value,
selection: { start: 0, end: 0 },
// Show preview by default
showPreview: props.showPreview ? props.showPreview : true,
};
this.textAreaRef = React.createRef(); // For focusing the textarea
}
textInput: TextInput;

changeText = (selection: {start: number, end: number}) => (input: string) => {
let result = input;
const cursor = selection.start;
const isOnNewline = '\n' === input.slice(cursor - 1, cursor);
const isDeletion = input.length < this.state.text.length;
if (isOnNewline && !isDeletion) {
const prevLines = input.slice(0, cursor - 1).split('\n');
const prevLine = prevLines[prevLines.length - 1];

const insertListLine = (bullet) => ([
prevLines.join('\n'), // Previous text
`\n${bullet} `, // Current line with new bullet point
input.slice(cursor, input.length), // Following text
].join(''));

const insertedEndListLine = [
// Previous text (all but last bullet line, which we remove)
prevLines.slice(0, prevLines.length - 1).join('\n') ,
'\n\n', // Two newlines to get out of the list
input.slice(cursor, input.length), // Following text
].join('');

// Add new ordered list line item
if (prevLine.startsWith('- ') && !prevLine.startsWith('- [ ')) {
// If the bullet on the previous line isn't empty, add a new bullet.
if (prevLine.trim() !== '-') {
result = insertListLine('-');
} else {
result = insertedEndListLine;
}
}

// Add new checklist line item
if ((prevLine.startsWith('- [ ] ') || prevLine.startsWith('- [x] '))) {
// If the bullet on the previous line isn't empty, add a new bullet.
if (prevLine.trim() !== '- [ ]' && prevLine.trim() !== '- [x]') {
result = insertListLine('- [ ]');
} else {
result = insertedEndListLine;
}
}

// Add new ordered list item
if (/^\d+\./.test(prevLine)) {
// If the bullet on the previous line isn't empty, add a new bullet.
const digit = Number(prevLine.match(/^\d+/)[0]);
if (prevLine.trim() !== `${digit}.`) {
result = insertListLine(`${digit + 1}.`);
} else {
result = insertedEndListLine;
}
}
}
// Hide Markdown preview on text change
this.setState({ text: result, showPreview: false });
this.props.saveText(result);
if (this.props.onMarkdownChange) this.props.onMarkdownChange(input);
};

onSelectionChange = event => {
this.setState({ selection: event.nativeEvent.selection });
};

focus = () => this.textAreaRef.current.focus()

convertMarkdown = () => this.setState({ showPreview: !this.state.showPreview })

render() {
const WrapperView = Platform.OS === 'ios' ? KeyboardAvoidingView : View;
const { Formats, markdownButton } = this.props;
const { text, selection, showPreview } = this.state;
return (
<WrapperView style={styles.screen}>
<TextInput
{...this.props}
multiline
autoCapitalize="sentences"
underlineColorAndroid="transparent"
onChangeText={this.changeText(selection)}
onSelectionChange={this.onSelectionChange}
value={text}
ref={this.textAreaRef}
selection={selection}
/>
{showPreview && <NoteBodyViewer {...this.props.noteBodyViewer} />}
<View style={styles.buttonContainer}>
<MarkdownPreviewButton
convertMarkdown={this.convertMarkdown}
borderColor={this.props.borderColor}
color={this.props.markdownButtonsColor}
/>
{renderFormatButtons(
{
color: this.props.markdownButtonsColor,
getState: () => this.state,
setState: (state, callback) => {
// Hide Markdown preview on text change
this.setState({ showPreview: false });
this.setState(state, callback);
},
},
Formats,
markdownButton,
)}
</View>
</WrapperView>
);
}
}
@@ -0,0 +1,43 @@
import { replaceBetween } from './utils';

export default ({ getState, item, setState }) => {
let { text } = getState();
const { selection } = getState();
text = text || '';
let newText;
let newSelection;

// Ignore multi-character selections.
// NOTE: I was on the fence about whether more appropriate behavior would be
// to add the list prefix (e.g. '-', '1.', '#', '##', '###') at the
// beginning of the line where the selection begins, but for now I think
// it's more natural to just ignore it in this case. If after using this
// editor for a while it turns out the other way is more natural, that's
// fine by me!
if (selection.start !== selection.end) {
return;
}

const spaceForPrefix = item.prefix.length + 1;
const isNewLine = text.substring(selection.start - 1, selection.start) === '\n';
if (isNewLine) { // We're at the start of a line
newText = replaceBetween(text, selection, `${item.prefix} `);
newSelection = { start: selection.start + spaceForPrefix, end: selection.start + spaceForPrefix };
} else { // We're in the middle of a line
// NOTE: It may be more natural for the prefix (e.g. '-', '1.', '#', '##')
// to be prepended at the beginning of the line where the selection is,
// rather than creating a new line (which is the behavior implemented here).
// If the other way is more natural, that's fine by me!
newText = replaceBetween(text, selection, `\n${item.prefix} `);
newSelection = {
start: selection.start + spaceForPrefix + 1,
end: selection.start + spaceForPrefix + 1,
};
}

setState({ text: newText }, () => {
setTimeout(() => {
setState({ selection: newSelection });
}, 300);
});
};
@@ -0,0 +1,38 @@
import { isStringWebLink, replaceBetween } from './utils';

export const writeUrlTextHere = 'https://example.com';
export const writeTextHereString = 'Write some text here';

// eslint-disable-next-line no-unused-vars
export default ({ getState, item, setState }) => {
const { selection, text } = getState();
let newText;
let newSelection;
const selectedText = text.substring(selection.start, selection.end);
if (selection.start !== selection.end) {
if (isStringWebLink(selectedText)) {
newText = replaceBetween(text, selection, `[${writeTextHereString}](${selectedText})`);
newSelection = {
start: selection.start + 1,
end: selection.start + 1 + writeTextHereString.length,
};
} else {
newText = replaceBetween(text, selection, `[${selectedText}](${writeUrlTextHere})`);
newSelection = {
start: selection.end + 3,
end: selection.end + 3 + writeUrlTextHere.length,
};
}
} else {
newText = replaceBetween(text, selection, `[${writeTextHereString}](${writeUrlTextHere})`);
newSelection = {
start: selection.start + 1,
end: selection.start + 1 + writeTextHereString.length,
};
}
setState({ text: newText }, () => {
setTimeout(() => {
setState({ selection: newSelection });
}, 25);
});
};
@@ -0,0 +1,27 @@
import { replaceBetween } from './utils';

export default ({ getState, item, setState }) => {
const { text, selection } = getState();
const newText = replaceBetween(
text,
selection,
item.wrapper.concat(text.substring(selection.start, selection.end), item.wrapper),
);
let newPosition;
if (selection.start === selection.end) {
newPosition = selection.end + item.wrapper.length;
} else {
newPosition = selection.end + item.wrapper.length * 2;
}
const extra = {
selection: {
start: newPosition,
end: newPosition,
},
};
setState({ text: newText }, () => {
setTimeout(() => {
setState({ ...extra });
}, 25);
});
};
@@ -0,0 +1,55 @@
import { replaceBetween } from './utils';

export default ({ getState, item, setState }) => {
const { text, selection } = getState();
let newText = replaceBetween(
text,
selection,
`\n${item.wrapper.concat(
'\n',
text.substring(selection.start, selection.end),
'\n',
item.wrapper,
'\n',
)}`,
);
let newPosition;
if (selection.start === selection.end) {
newPosition = selection.end + item.wrapper.length + 2; // +2 For two new lines
newText = replaceBetween(
text,
selection,
`\n${item.wrapper.concat(
'\n',
text.substring(selection.start, selection.end),
'\n',
item.wrapper,
'\n',
)}`,
);
} else {
newPosition = selection.end + item.wrapper.length * 2 + 3; // +3 For three new lines
newText = replaceBetween(
text,
selection,
`${item.wrapper.concat(
'\n',
text.substring(selection.start, selection.end),
'\n',
item.wrapper,
'\n',
)}`,
);
}
const extra = {
selection: {
start: newPosition,
end: newPosition,
},
};
setState({ text: newText }, () => {
setTimeout(() => {
setState({ ...extra });
}, 25);
});
};
@@ -0,0 +1,13 @@
import applyWrapFormat from './applyWrapFormat';
import applyWrapFormatNewLines from './applyWrapFormatNewLines';
import applyListFormat from './applyListFormat';
import applyWebLinkFormat from './applyWebLinkFormat';
import MarkdownEditor from './MarkdownEditor';

module.exports = {
MarkdownEditor,
applyWrapFormat,
applyWrapFormatNewLines,
applyListFormat,
applyWebLinkFormat,
};
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.