mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-26 18:58:21 +02:00
Mobile: Add toolbar, list continuation and Markdown preview to editor (#2224)
* The basic editor is working! No list continuation still though * List continuation is working! Now to delete when entering again and not typing on line + handle ordered lists * Supports checkboxes + attempted at setting font * Editor font works now; now need to fix the delete (look at past state) * Fix deletion problem * Add ordered list handler * Add comments * Extract insertListLine * End lists on enter for empty bullets * Add MarkdownView (renders badly though) * Save edited text from MarkdownEditor * Cleanup * Refactor react-native-markdown-editor/ * Rename react-native-markdown-editor/ => MarkdownEditor/ * Cleanup * Fix preview styles; still need to fix checkbox problem * Fix keyboard padding * Change name back to #body_changeText * Incorporate PR feedback from @laurent22 * wip: Move MarkdownEditor/ from ReactNativeClient/lib/ to ReactNativeClient/ * Move MarkdownEditor/ from ReactNativeClient/lib/ to ReactNativeClient/ * Remove log statement * Focus TextInput in MarkdownEditor from grandparent * Make eslint happy * Extract textInputRefName to shared variable * Remove accidental #setState * Cleanup * Cleanup * Run linter * Cleanup * Update button order * Improve styles for config descriptions * Allow descriptions to be added to BOOL type Setting configs * Add editorBeta Setting * Move FailSafe details to description text * Update descriptionText styles * Put the editor under the beta flag toggle * Incorporate PR feedback from @laurent22 * Refactor Markdown editor focusing * Cleanup * Reorder MarkdownEditor formats * Make applyListFormat behavior more intuitive * Add comment * Show MarkdownEditor with preview by default * Show preview by default, then hide on typing * Fix MarkdownEditor selection bug * Cleanup * Update Markdown button styles * Make Markdown button colors theme-conscious * Fix merge conflict resolution mistake * Fix broken import * Delete package-lock.json * Reset package-lock.json Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
This commit is contained in:
parent
a548d695f2
commit
b6d4fd16c9
23
ReactNativeClient/MarkdownEditor/Formats.js
Normal file
23
ReactNativeClient/MarkdownEditor/Formats.js
Normal file
@ -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 },
|
||||||
|
];
|
162
ReactNativeClient/MarkdownEditor/MarkdownEditor.js
Normal file
162
ReactNativeClient/MarkdownEditor/MarkdownEditor.js
Normal file
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
43
ReactNativeClient/MarkdownEditor/applyListFormat.js
Normal file
43
ReactNativeClient/MarkdownEditor/applyListFormat.js
Normal file
@ -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);
|
||||||
|
});
|
||||||
|
};
|
38
ReactNativeClient/MarkdownEditor/applyWebLinkFormat.js
Normal file
38
ReactNativeClient/MarkdownEditor/applyWebLinkFormat.js
Normal file
@ -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);
|
||||||
|
});
|
||||||
|
};
|
27
ReactNativeClient/MarkdownEditor/applyWrapFormat.js
Normal file
27
ReactNativeClient/MarkdownEditor/applyWrapFormat.js
Normal file
@ -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);
|
||||||
|
});
|
||||||
|
};
|
55
ReactNativeClient/MarkdownEditor/applyWrapFormatNewLines.js
Normal file
55
ReactNativeClient/MarkdownEditor/applyWrapFormatNewLines.js
Normal file
@ -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);
|
||||||
|
});
|
||||||
|
};
|
13
ReactNativeClient/MarkdownEditor/index.js
Normal file
13
ReactNativeClient/MarkdownEditor/index.js
Normal file
@ -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,
|
||||||
|
};
|
32
ReactNativeClient/MarkdownEditor/renderButtons.js
Normal file
32
ReactNativeClient/MarkdownEditor/renderButtons.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { FlatList, TouchableOpacity, Text } from 'react-native';
|
||||||
|
|
||||||
|
import Formats from './Formats';
|
||||||
|
|
||||||
|
const defaultStyles = { padding: 8, fontSize: 16 };
|
||||||
|
|
||||||
|
const defaultMarkdownButton = ({ item, getState, setState, color }) => {
|
||||||
|
return (
|
||||||
|
<TouchableOpacity onPress={() => item.onPress({ getState, setState, item })}>
|
||||||
|
<Text style={[defaultStyles, item.style, { color: color }]}>
|
||||||
|
{item.title}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const renderFormatButtons = ({ getState, setState, color }, formats, markdownButton) => {
|
||||||
|
const list = (
|
||||||
|
<FlatList
|
||||||
|
data={formats ? formats : Formats}
|
||||||
|
keyboardShouldPersistTaps="always"
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
renderItem={({ item, index }) =>
|
||||||
|
markdownButton
|
||||||
|
? markdownButton({ item, getState, setState })
|
||||||
|
: defaultMarkdownButton({ item, getState, setState, color })}
|
||||||
|
horizontal
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
return list;
|
||||||
|
};
|
BIN
ReactNativeClient/MarkdownEditor/static/visibility.png
Normal file
BIN
ReactNativeClient/MarkdownEditor/static/visibility.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 309 B |
9
ReactNativeClient/MarkdownEditor/utils.js
Normal file
9
ReactNativeClient/MarkdownEditor/utils.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import regexValidator from './webLinkValidator';
|
||||||
|
|
||||||
|
export const replaceBetween = (text: string, selection: Object, what: string) =>
|
||||||
|
text.substring(0, selection.start) + what + text.substring(selection.end);
|
||||||
|
|
||||||
|
export const isStringWebLink = (text: string): boolean => {
|
||||||
|
const pattern = regexValidator;
|
||||||
|
return pattern.test(text);
|
||||||
|
};
|
104
ReactNativeClient/MarkdownEditor/webLinkValidator.js
Normal file
104
ReactNativeClient/MarkdownEditor/webLinkValidator.js
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
// prettier-ignore
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
//
|
||||||
|
// Regular Expression for URL validation
|
||||||
|
//
|
||||||
|
// Author: Diego Perini
|
||||||
|
// Updated: 2010/12/05
|
||||||
|
// License: MIT
|
||||||
|
//
|
||||||
|
// Copyright (c) 2010-2013 Diego Perini (http://www.iport.it)
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person
|
||||||
|
// obtaining a copy of this software and associated documentation
|
||||||
|
// files (the "Software"), to deal in the Software without
|
||||||
|
// restriction, including without limitation the rights to use,
|
||||||
|
// copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the
|
||||||
|
// Software is furnished to do so, subject to the following
|
||||||
|
// conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be
|
||||||
|
// included in all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||||
|
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||||
|
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
|
// OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
//
|
||||||
|
// the regular expression composed & commented
|
||||||
|
// could be easily tweaked for RFC compliance,
|
||||||
|
// it was expressly modified to fit & satisfy
|
||||||
|
// these test for an URL shortener:
|
||||||
|
//
|
||||||
|
// http://mathiasbynens.be/demo/url-regex
|
||||||
|
//
|
||||||
|
// Notes on possible differences from a standard/generic validation:
|
||||||
|
//
|
||||||
|
// - utf-8 char class take in consideration the full Unicode range
|
||||||
|
// - TLDs have been made mandatory so single names like "localhost" fails
|
||||||
|
// - protocols have been restricted to ftp, http and https only as requested
|
||||||
|
//
|
||||||
|
// Changes:
|
||||||
|
//
|
||||||
|
// - IP address dotted notation validation, range: 1.0.0.0 - 223.255.255.255
|
||||||
|
// first and last IP address of each class is considered invalid
|
||||||
|
// (since they are broadcast/network addresses)
|
||||||
|
//
|
||||||
|
// - Added exclusion of private, reserved and/or local networks ranges
|
||||||
|
//
|
||||||
|
// - Made starting path slash optional (http://example.com?foo=bar)
|
||||||
|
//
|
||||||
|
// - Allow a dot (.) at the end of hostnames (http://example.com.)
|
||||||
|
//
|
||||||
|
// Compressed one-line versions:
|
||||||
|
//
|
||||||
|
// Javascript version
|
||||||
|
//
|
||||||
|
// /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))\.?)(?::\d{2,5})?(?:[/?#]\S*)?$/i
|
||||||
|
//
|
||||||
|
// PHP version
|
||||||
|
//
|
||||||
|
// _^(?:(?:https?|ftp)://)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\x{00a1}-\x{ffff}0-9]-*)*[a-z\x{00a1}-\x{ffff}0-9]+)(?:\.(?:[a-z\x{00a1}-\x{ffff}0-9]-*)*[a-z\x{00a1}-\x{ffff}0-9]+)*(?:\.(?:[a-z\x{00a1}-\x{ffff}]{2,}))\.?)(?::\d{2,5})?(?:[/?#]\S*)?$_iuS
|
||||||
|
//
|
||||||
|
export default new RegExp(
|
||||||
|
"^" +
|
||||||
|
// protocol identifier
|
||||||
|
"(?:(?:https?|ftp)://)" +
|
||||||
|
// user:pass authentication
|
||||||
|
"(?:\\S+(?::\\S*)?@)?" +
|
||||||
|
"(?:" +
|
||||||
|
// IP address exclusion
|
||||||
|
// private & local networks
|
||||||
|
"(?!(?:10|127)(?:\\.\\d{1,3}){3})" +
|
||||||
|
"(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})" +
|
||||||
|
"(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})" +
|
||||||
|
// IP address dotted notation octets
|
||||||
|
// excludes loopback network 0.0.0.0
|
||||||
|
// excludes reserved space >= 224.0.0.0
|
||||||
|
// excludes network & broacast addresses
|
||||||
|
// (first & last IP address of each class)
|
||||||
|
"(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" +
|
||||||
|
"(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" +
|
||||||
|
"(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" +
|
||||||
|
"|" +
|
||||||
|
// host name
|
||||||
|
"(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)" +
|
||||||
|
// domain name
|
||||||
|
"(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*" +
|
||||||
|
// TLD identifier
|
||||||
|
"(?:\\.(?:[a-z\\u00a1-\\uffff]{2,}))" +
|
||||||
|
// TLD may end with dot
|
||||||
|
"\\.?" +
|
||||||
|
")" +
|
||||||
|
// port number
|
||||||
|
"(?::\\d{2,5})?" +
|
||||||
|
// resource path
|
||||||
|
"(?:[/?#]\\S*)?" +
|
||||||
|
"$", "i"
|
||||||
|
);
|
@ -55,6 +55,8 @@ class NoteBodyViewer extends Component {
|
|||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}, 100);
|
}, 100);
|
||||||
},
|
},
|
||||||
|
paddingTop: '.8em', // Extra top padding on the rendered MD so it doesn't touch the border
|
||||||
|
paddingBottom: this.props.paddingBottom || '0',
|
||||||
highlightedKeywords: this.props.highlightedKeywords,
|
highlightedKeywords: this.props.highlightedKeywords,
|
||||||
resources: this.props.noteResources, // await shared.attachedResources(bodyToRender),
|
resources: this.props.noteResources, // await shared.attachedResources(bodyToRender),
|
||||||
codeTheme: theme.codeThemeCss,
|
codeTheme: theme.codeThemeCss,
|
||||||
|
@ -190,8 +190,8 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
|||||||
paddingRight: 5,
|
paddingRight: 5,
|
||||||
},
|
},
|
||||||
descriptionText: {
|
descriptionText: {
|
||||||
color: theme.color,
|
color: theme.colorFaded,
|
||||||
fontSize: theme.fontSize,
|
fontSize: theme.fontSizeSmaller,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
sliderUnits: {
|
sliderUnits: {
|
||||||
@ -200,8 +200,8 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
|||||||
marginRight: 10,
|
marginRight: 10,
|
||||||
},
|
},
|
||||||
settingDescriptionText: {
|
settingDescriptionText: {
|
||||||
color: theme.color,
|
color: theme.colorFaded,
|
||||||
fontSize: theme.fontSize,
|
fontSize: theme.fontSizeSmaller,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
paddingLeft: theme.marginLeft,
|
paddingLeft: theme.marginLeft,
|
||||||
paddingRight: theme.marginRight,
|
paddingRight: theme.marginRight,
|
||||||
@ -341,6 +341,9 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
|||||||
const md = Setting.settingMetadata(key);
|
const md = Setting.settingMetadata(key);
|
||||||
const settingDescription = md.description ? md.description() : '';
|
const settingDescription = md.description ? md.description() : '';
|
||||||
|
|
||||||
|
const descriptionComp = !settingDescription ? null : <Text style={this.styles().settingDescriptionText}>{settingDescription}</Text>;
|
||||||
|
const containerStyle = !settingDescription ? this.styles().settingContainer : this.styles().settingContainerNoBottomBorder;
|
||||||
|
|
||||||
if (md.isEnum) {
|
if (md.isEnum) {
|
||||||
value = value.toString();
|
value = value.toString();
|
||||||
|
|
||||||
@ -351,9 +354,6 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
|||||||
items.push({ label: settingOptions[k], value: k.toString() });
|
items.push({ label: settingOptions[k], value: k.toString() });
|
||||||
}
|
}
|
||||||
|
|
||||||
const descriptionComp = !settingDescription ? null : <Text style={this.styles().settingDescriptionText}>{settingDescription}</Text>;
|
|
||||||
const containerStyle = !settingDescription ? this.styles().settingContainer : this.styles().settingContainerNoBottomBorder;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View key={key} style={{ flexDirection: 'column', borderBottomWidth: 1, borderBottomColor: theme.dividerColor }}>
|
<View key={key} style={{ flexDirection: 'column', borderBottomWidth: 1, borderBottomColor: theme.dividerColor }}>
|
||||||
<View style={containerStyle}>
|
<View style={containerStyle}>
|
||||||
@ -386,11 +386,14 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
|||||||
);
|
);
|
||||||
} else if (md.type == Setting.TYPE_BOOL) {
|
} else if (md.type == Setting.TYPE_BOOL) {
|
||||||
return (
|
return (
|
||||||
<View key={key} style={this.styles().switchSettingContainer}>
|
<View key={key}>
|
||||||
<Text key="label" style={this.styles().switchSettingText}>
|
<View style={containerStyle}>
|
||||||
{md.label()}
|
<Text key="label" style={this.styles().switchSettingText}>
|
||||||
</Text>
|
{md.label()}
|
||||||
<Switch key="control" style={this.styles().switchSettingControl} value={value} onValueChange={value => updateSettingValue(key, value)} />
|
</Text>
|
||||||
|
<Switch key="control" style={this.styles().switchSettingControl} value={value} onValueChange={value => updateSettingValue(key, value)} />
|
||||||
|
</View>
|
||||||
|
{descriptionComp}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
} else if (md.type == Setting.TYPE_INT) {
|
} else if (md.type == Setting.TYPE_INT) {
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const { Platform, Clipboard, Keyboard, View, TextInput, StyleSheet, Linking, Image, Share } = require('react-native');
|
const { ScrollView, Platform, Clipboard, Keyboard, View, TextInput, StyleSheet, Linking, Image, Share } = require('react-native');
|
||||||
const { connect } = require('react-redux');
|
const { connect } = require('react-redux');
|
||||||
const { uuid } = require('lib/uuid.js');
|
const { uuid } = require('lib/uuid.js');
|
||||||
|
const { MarkdownEditor } = require('../../../MarkdownEditor/index.js');
|
||||||
const RNFS = require('react-native-fs');
|
const RNFS = require('react-native-fs');
|
||||||
const Note = require('lib/models/Note.js');
|
const Note = require('lib/models/Note.js');
|
||||||
const BaseItem = require('lib/models/BaseItem.js');
|
const BaseItem = require('lib/models/BaseItem.js');
|
||||||
@ -71,6 +72,8 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
HACK_webviewLoadingState: 0,
|
HACK_webviewLoadingState: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.markdownEditorRef = React.createRef(); // For focusing the Markdown editor
|
||||||
|
|
||||||
this.doFocusUpdate_ = false;
|
this.doFocusUpdate_ = false;
|
||||||
|
|
||||||
// iOS doesn't support multiline text fields properly so disable it
|
// iOS doesn't support multiline text fields properly so disable it
|
||||||
@ -207,6 +210,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
if (this.styles_[cacheKey]) return this.styles_[cacheKey];
|
if (this.styles_[cacheKey]) return this.styles_[cacheKey];
|
||||||
this.styles_ = {};
|
this.styles_ = {};
|
||||||
|
|
||||||
|
// TODO: Clean up these style names and nesting
|
||||||
const styles = {
|
const styles = {
|
||||||
bodyTextInput: {
|
bodyTextInput: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@ -222,8 +226,12 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
paddingLeft: theme.marginLeft,
|
paddingLeft: theme.marginLeft,
|
||||||
paddingRight: theme.marginRight,
|
paddingRight: theme.marginRight,
|
||||||
paddingTop: theme.marginTop,
|
},
|
||||||
paddingBottom: theme.marginBottom,
|
noteBodyViewerPreview: {
|
||||||
|
borderTopColor: theme.dividerColor,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderBottomColor: theme.dividerColor,
|
||||||
|
borderBottomWidth: 1,
|
||||||
},
|
},
|
||||||
checkbox: {
|
checkbox: {
|
||||||
color: theme.color,
|
color: theme.color,
|
||||||
@ -232,6 +240,10 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
paddingTop: 10, // Added for iOS (Not needed for Android??)
|
paddingTop: 10, // Added for iOS (Not needed for Android??)
|
||||||
paddingBottom: 10, // Added for iOS (Not needed for Android??)
|
paddingBottom: 10, // Added for iOS (Not needed for Android??)
|
||||||
},
|
},
|
||||||
|
markdownButtons: {
|
||||||
|
borderColor: theme.dividerColor,
|
||||||
|
color: theme.htmlLinkColor,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
styles.titleContainer = {
|
styles.titleContainer = {
|
||||||
@ -767,8 +779,14 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
let fieldToFocus = this.state.note.is_todo ? 'title' : 'body';
|
let fieldToFocus = this.state.note.is_todo ? 'title' : 'body';
|
||||||
if (this.state.mode === 'view') fieldToFocus = '';
|
if (this.state.mode === 'view') fieldToFocus = '';
|
||||||
|
|
||||||
if (fieldToFocus === 'title' && this.refs.titleTextField) this.refs.titleTextField.focus();
|
if (fieldToFocus === 'title' && this.refs.titleTextField) {
|
||||||
if (fieldToFocus === 'body' && this.refs.noteBodyTextField) this.refs.noteBodyTextField.focus();
|
this.refs.titleTextField.focus();
|
||||||
|
}
|
||||||
|
if (fieldToFocus === 'body' && this.markdownEditorRef.current) {
|
||||||
|
if (this.markdownEditorRef.current) {
|
||||||
|
this.markdownEditorRef.current.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async folderPickerOptions_valueChanged(itemValue) {
|
async folderPickerOptions_valueChanged(itemValue) {
|
||||||
@ -822,7 +840,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let bodyComponent = null;
|
let bodyComponent = null;
|
||||||
if (this.state.mode == 'view') {
|
if (this.state.mode == 'view' && !Setting.value('editor.beta')) {
|
||||||
const onCheckboxChange = newBody => {
|
const onCheckboxChange = newBody => {
|
||||||
this.saveOneProperty('body', newBody);
|
this.saveOneProperty('body', newBody);
|
||||||
};
|
};
|
||||||
@ -843,6 +861,9 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
ref="noteBodyViewer"
|
ref="noteBodyViewer"
|
||||||
style={this.styles().noteBodyViewer}
|
style={this.styles().noteBodyViewer}
|
||||||
webViewStyle={theme}
|
webViewStyle={theme}
|
||||||
|
// Extra bottom padding to make it possible to scroll past the
|
||||||
|
// action button (so that it doesn't overlap the text)
|
||||||
|
paddingBottom='3.8em'
|
||||||
note={note}
|
note={note}
|
||||||
noteResources={this.state.noteResources}
|
noteResources={this.state.noteResources}
|
||||||
highlightedKeywords={keywords}
|
highlightedKeywords={keywords}
|
||||||
@ -865,9 +886,66 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
} else {
|
} else {
|
||||||
// autoFocus={fieldToFocus === 'body'}
|
// autoFocus={fieldToFocus === 'body'}
|
||||||
|
|
||||||
// Note: blurOnSubmit is necessary to get multiline to work.
|
// Currently keyword highlighting is supported only when FTS is available.
|
||||||
// See https://github.com/facebook/react-native/issues/12717#issuecomment-327001997
|
let keywords = [];
|
||||||
bodyComponent = <TextInput autoCapitalize="sentences" style={this.styles().bodyTextInput} ref="noteBodyTextField" multiline={true} value={note.body} onChangeText={text => this.body_changeText(text)} blurOnSubmit={false} selectionColor={theme.textSelectionColor} placeholder={_('Add body')} placeholderTextColor={theme.colorFaded} />;
|
if (this.props.searchQuery && !!this.props.ftsEnabled) {
|
||||||
|
const parsedQuery = SearchEngine.instance().parseQuery(this.props.searchQuery);
|
||||||
|
keywords = SearchEngine.instance().allParsedQueryTerms(parsedQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCheckboxChange = newBody => {
|
||||||
|
this.saveOneProperty('body', newBody);
|
||||||
|
};
|
||||||
|
|
||||||
|
bodyComponent = Setting.value('editor.beta')
|
||||||
|
// Note: blurOnSubmit is necessary to get multiline to work.
|
||||||
|
// See https://github.com/facebook/react-native/issues/12717#issuecomment-327001997
|
||||||
|
? <MarkdownEditor
|
||||||
|
ref={this.markdownEditorRef} // For focusing the Markdown editor
|
||||||
|
editorFont={editorFont(this.props.editorFont)}
|
||||||
|
style={this.styles().bodyTextInput}
|
||||||
|
previewStyles={this.styles().noteBodyViewer}
|
||||||
|
value={note.body}
|
||||||
|
borderColor={this.styles().markdownButtons.borderColor}
|
||||||
|
markdownButtonsColor={this.styles().markdownButtons.color}
|
||||||
|
saveText={text => this.body_changeText(text)}
|
||||||
|
blurOnSubmit={false}
|
||||||
|
selectionColor={theme.textSelectionColor}
|
||||||
|
placeholder={_('Add body')}
|
||||||
|
placeholderTextColor={theme.colorFaded}
|
||||||
|
noteBodyViewer={{
|
||||||
|
onJoplinLinkClick: this.onJoplinLinkClick_,
|
||||||
|
ref: 'noteBodyViewer',
|
||||||
|
style: {
|
||||||
|
...this.styles().noteBodyViewer,
|
||||||
|
...this.styles().noteBodyViewerPreview,
|
||||||
|
},
|
||||||
|
webViewStyle: theme,
|
||||||
|
note: note,
|
||||||
|
noteResources: this.state.noteResources,
|
||||||
|
highlightedKeywords: keywords,
|
||||||
|
theme: this.props.theme,
|
||||||
|
noteHash: this.props.noteHash,
|
||||||
|
onCheckboxChange: newBody => {
|
||||||
|
onCheckboxChange(newBody);
|
||||||
|
},
|
||||||
|
onMarkForDownload: this.onMarkForDownload,
|
||||||
|
onLoadEnd: () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.setState({ HACK_webviewLoadingState: 1 });
|
||||||
|
setTimeout(() => {
|
||||||
|
this.setState({ HACK_webviewLoadingState: 0 });
|
||||||
|
}, 50);
|
||||||
|
}, 5);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
|
||||||
|
/>
|
||||||
|
: (
|
||||||
|
<ScrollView persistentScrollbar>
|
||||||
|
<TextInput autoCapitalize="sentences" style={this.styles().bodyTextInput} ref="noteBodyTextField" multiline={true} value={note.body} onChangeText={text => this.body_changeText(text)} blurOnSubmit={false} selectionColor={theme.textSelectionColor} placeholder={_('Add body')} placeholderTextColor={theme.colorFaded} />
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderActionButton = () => {
|
const renderActionButton = () => {
|
||||||
@ -913,7 +991,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
<ScreenHeader folderPickerOptions={this.folderPickerOptions()} menuOptions={this.menuOptions()} showSaveButton={showSaveButton} saveButtonDisabled={saveButtonDisabled} onSaveButtonPress={this.saveNoteButton_press} showSideMenuButton={false} showSearchButton={false} />
|
<ScreenHeader folderPickerOptions={this.folderPickerOptions()} menuOptions={this.menuOptions()} showSaveButton={showSaveButton} saveButtonDisabled={saveButtonDisabled} onSaveButtonPress={this.saveNoteButton_press} showSideMenuButton={false} showSearchButton={false} />
|
||||||
{titleComp}
|
{titleComp}
|
||||||
{bodyComponent}
|
{bodyComponent}
|
||||||
{actionButtonComp}
|
{!Setting.value('editor.beta') && actionButtonComp}
|
||||||
|
|
||||||
<SelectDateTimeDialog shown={this.state.alarmDialogShown} date={dueDate} onAccept={this.onAlarmDialogAccept} onReject={this.onAlarmDialogReject} />
|
<SelectDateTimeDialog shown={this.state.alarmDialogShown} date={dueDate} onAccept={this.onAlarmDialogAccept} onReject={this.onAlarmDialogReject} />
|
||||||
|
|
||||||
|
@ -178,9 +178,11 @@ class MdToHtml {
|
|||||||
// files. Otherwise some of them might be in the cssStrings property.
|
// files. Otherwise some of them might be in the cssStrings property.
|
||||||
externalAssetsOnly: false,
|
externalAssetsOnly: false,
|
||||||
postMessageSyntax: 'postMessage',
|
postMessageSyntax: 'postMessage',
|
||||||
|
paddingTop: '0',
|
||||||
|
paddingBottom: '0',
|
||||||
highlightedKeywords: [],
|
highlightedKeywords: [],
|
||||||
codeTheme: 'atom-one-light.css',
|
codeTheme: 'atom-one-light.css',
|
||||||
theme: Object.assign({}, defaultNoteStyle, theme),
|
theme: Object.assign({ paddingTop: '16px' }, defaultNoteStyle, theme),
|
||||||
plugins: {},
|
plugins: {},
|
||||||
}, options);
|
}, options);
|
||||||
|
|
||||||
|
@ -18,7 +18,8 @@ module.exports = function(theme) {
|
|||||||
line-height: ${theme.htmlLineHeight};
|
line-height: ${theme.htmlLineHeight};
|
||||||
background-color: ${theme.htmlBackgroundColor};
|
background-color: ${theme.htmlBackgroundColor};
|
||||||
font-family: ${fontFamily};
|
font-family: ${fontFamily};
|
||||||
padding-bottom: ${theme.bodyPaddingBottom};
|
padding-bottom: ${theme.paddingBottom};
|
||||||
|
padding-top: ${theme.paddingTop};
|
||||||
}
|
}
|
||||||
strong {
|
strong {
|
||||||
color: ${theme.colorBright};
|
color: ${theme.colorBright};
|
||||||
|
@ -316,6 +316,17 @@ class Setting extends BaseModel {
|
|||||||
},
|
},
|
||||||
'folders.sortOrder.reverse': { value: false, type: Setting.TYPE_BOOL, public: true, label: () => _('Reverse sort order'), appTypes: ['cli'] },
|
'folders.sortOrder.reverse': { value: false, type: Setting.TYPE_BOOL, public: true, label: () => _('Reverse sort order'), appTypes: ['cli'] },
|
||||||
trackLocation: { value: true, type: Setting.TYPE_BOOL, section: 'note', public: true, label: () => _('Save geo-location with notes') },
|
trackLocation: { value: true, type: Setting.TYPE_BOOL, section: 'note', public: true, label: () => _('Save geo-location with notes') },
|
||||||
|
|
||||||
|
'editor.beta': {
|
||||||
|
value: false,
|
||||||
|
type: Setting.TYPE_BOOL,
|
||||||
|
section: 'note',
|
||||||
|
public: true,
|
||||||
|
appTypes: ['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.',
|
||||||
|
},
|
||||||
|
|
||||||
newTodoFocus: {
|
newTodoFocus: {
|
||||||
value: 'title',
|
value: 'title',
|
||||||
type: Setting.TYPE_STRING,
|
type: Setting.TYPE_STRING,
|
||||||
@ -552,7 +563,15 @@ class Setting extends BaseModel {
|
|||||||
label: () => _('Ignore TLS certificate errors'),
|
label: () => _('Ignore TLS certificate errors'),
|
||||||
},
|
},
|
||||||
|
|
||||||
'sync.wipeOutFailSafe': { value: true, type: Setting.TYPE_BOOL, advanced: true, public: true, section: 'sync', label: () => _('Fail-safe: Do not wipe out local data when sync target is empty (often the result of a misconfiguration or bug)') },
|
'sync.wipeOutFailSafe': {
|
||||||
|
value: true,
|
||||||
|
type: Setting.TYPE_BOOL,
|
||||||
|
advanced: true,
|
||||||
|
public: true,
|
||||||
|
section: 'sync',
|
||||||
|
label: () => _('Fail-safe'),
|
||||||
|
description: () => _('Fail-safe: Do not wipe out local data when sync target is empty (often the result of a misconfiguration or bug)'),
|
||||||
|
},
|
||||||
|
|
||||||
'api.token': { value: null, type: Setting.TYPE_STRING, public: false },
|
'api.token': { value: null, type: Setting.TYPE_STRING, public: false },
|
||||||
'api.port': { value: null, type: Setting.TYPE_INT, public: true, appTypes: ['cli'], description: () => _('Specify the port that should be used by the API server. If not set, a default will be used.') },
|
'api.port': { value: null, type: Setting.TYPE_INT, public: true, appTypes: ['cli'], description: () => _('Specify the port that should be used by the API server. If not set, a default will be used.') },
|
||||||
|
Loading…
x
Reference in New Issue
Block a user