You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-06-15 23:00:36 +02:00
Desktop: Added support for templates (#1647)
* First pass of adding support for templates * remove default value from template prompt * Add template placeholder text * Add mustache templates with datetime support for new notes * Moved template code to utils, added separate prompt for templates * Add templates to menu and allow for keyboad only use * update template prompt for dark theme * update with laurents suggestions, add refresh button * revert template command, remove new note prompt
This commit is contained in:
committed by
Laurent Cozic
parent
e29fb3eb66
commit
cd5d412c69
@ -26,11 +26,13 @@ const ResourceService = require('lib/services/ResourceService');
|
|||||||
const ClipperServer = require('lib/ClipperServer');
|
const ClipperServer = require('lib/ClipperServer');
|
||||||
const ExternalEditWatcher = require('lib/services/ExternalEditWatcher');
|
const ExternalEditWatcher = require('lib/services/ExternalEditWatcher');
|
||||||
const { bridge } = require('electron').remote.require('./bridge');
|
const { bridge } = require('electron').remote.require('./bridge');
|
||||||
|
const { shell } = require('electron');
|
||||||
const Menu = bridge().Menu;
|
const Menu = bridge().Menu;
|
||||||
const MenuItem = bridge().MenuItem;
|
const MenuItem = bridge().MenuItem;
|
||||||
const PluginManager = require('lib/services/PluginManager');
|
const PluginManager = require('lib/services/PluginManager');
|
||||||
const RevisionService = require('lib/services/RevisionService');
|
const RevisionService = require('lib/services/RevisionService');
|
||||||
const MigrationService = require('lib/services/MigrationService');
|
const MigrationService = require('lib/services/MigrationService');
|
||||||
|
const TemplateUtils = require('lib/TemplateUtils');
|
||||||
|
|
||||||
const pluginClasses = [
|
const pluginClasses = [
|
||||||
require('./plugins/GotoAnything.min'),
|
require('./plugins/GotoAnything.min'),
|
||||||
@ -209,7 +211,7 @@ class Application extends BaseApplication {
|
|||||||
// The bridge runs within the main process, with its own instance of locale.js
|
// The bridge runs within the main process, with its own instance of locale.js
|
||||||
// so it needs to be set too here.
|
// so it needs to be set too here.
|
||||||
bridge().setLocale(Setting.value('locale'));
|
bridge().setLocale(Setting.value('locale'));
|
||||||
this.refreshMenu();
|
await this.refreshMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action.type == 'SETTING_UPDATE_ONE' && action.key == 'showTrayIcon' || action.type == 'SETTING_UPDATE_ALL') {
|
if (action.type == 'SETTING_UPDATE_ONE' && action.key == 'showTrayIcon' || action.type == 'SETTING_UPDATE_ALL') {
|
||||||
@ -246,10 +248,10 @@ class Application extends BaseApplication {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshMenu() {
|
async refreshMenu() {
|
||||||
const screen = this.lastMenuScreen_;
|
const screen = this.lastMenuScreen_;
|
||||||
this.lastMenuScreen_ = null;
|
this.lastMenuScreen_ = null;
|
||||||
this.updateMenu(screen);
|
await this.updateMenu(screen);
|
||||||
}
|
}
|
||||||
|
|
||||||
focusElement_(target) {
|
focusElement_(target) {
|
||||||
@ -260,7 +262,7 @@ class Application extends BaseApplication {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateMenu(screen) {
|
async updateMenu(screen) {
|
||||||
if (this.lastMenuScreen_ === screen) return;
|
if (this.lastMenuScreen_ === screen) return;
|
||||||
|
|
||||||
const sortNoteFolderItems = (type) => {
|
const sortNoteFolderItems = (type) => {
|
||||||
@ -328,6 +330,7 @@ class Application extends BaseApplication {
|
|||||||
const exportItems = [];
|
const exportItems = [];
|
||||||
const preferencesItems = [];
|
const preferencesItems = [];
|
||||||
const toolsItemsFirst = [];
|
const toolsItemsFirst = [];
|
||||||
|
const templateItems = [];
|
||||||
const ioService = new InteropService();
|
const ioService = new InteropService();
|
||||||
const ioModules = ioService.modules();
|
const ioModules = ioService.modules();
|
||||||
for (let i = 0; i < ioModules.length; i++) {
|
for (let i = 0; i < ioModules.length; i++) {
|
||||||
@ -504,6 +507,57 @@ class Application extends BaseApplication {
|
|||||||
screens: ['Main'],
|
screens: ['Main'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const templateDirExists = await shim.fsDriver().exists(Setting.value('templateDir'));
|
||||||
|
|
||||||
|
templateItems.push({
|
||||||
|
label: _('Create note from template'),
|
||||||
|
visible: templateDirExists,
|
||||||
|
click: () => {
|
||||||
|
this.dispatch({
|
||||||
|
type: 'WINDOW_COMMAND',
|
||||||
|
name: 'selectTemplate',
|
||||||
|
noteType: 'note',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: _('Create to-do from template'),
|
||||||
|
visible: templateDirExists,
|
||||||
|
click: () => {
|
||||||
|
this.dispatch({
|
||||||
|
type: 'WINDOW_COMMAND',
|
||||||
|
name: 'selectTemplate',
|
||||||
|
noteType: 'todo',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: _('Insert template'),
|
||||||
|
visible: templateDirExists,
|
||||||
|
accelerator: 'CommandOrControl+Alt+I',
|
||||||
|
click: () => {
|
||||||
|
this.dispatch({
|
||||||
|
type: 'WINDOW_COMMAND',
|
||||||
|
name: 'selectTemplate',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: _('Open template directory'),
|
||||||
|
click: () => {
|
||||||
|
const templateDir = Setting.value('templateDir');
|
||||||
|
if (!templateDirExists) shim.fsDriver().mkdir(templateDir);
|
||||||
|
shell.openItem(templateDir);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: _('Refresh templates'),
|
||||||
|
click: async () => {
|
||||||
|
const templates = await TemplateUtils.loadTemplates(Setting.value('templateDir'));
|
||||||
|
|
||||||
|
this.store().dispatch({
|
||||||
|
type: 'TEMPLATE_UPDATE_ALL',
|
||||||
|
templates: templates
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const toolsItems = toolsItemsFirst.concat(preferencesItems);
|
const toolsItems = toolsItemsFirst.concat(preferencesItems);
|
||||||
|
|
||||||
function _checkForUpdates(ctx) {
|
function _checkForUpdates(ctx) {
|
||||||
@ -563,6 +617,13 @@ class Application extends BaseApplication {
|
|||||||
shim.isMac() ? noItem : newNotebookItem, {
|
shim.isMac() ? noItem : newNotebookItem, {
|
||||||
type: 'separator',
|
type: 'separator',
|
||||||
visible: shim.isMac() ? false : true
|
visible: shim.isMac() ? false : true
|
||||||
|
}, {
|
||||||
|
label: _('Templates'),
|
||||||
|
visible: shim.isMac() ? false : true,
|
||||||
|
submenu: templateItems,
|
||||||
|
}, {
|
||||||
|
type: 'separator',
|
||||||
|
visible: shim.isMac() ? false : true
|
||||||
}, {
|
}, {
|
||||||
label: _('Import'),
|
label: _('Import'),
|
||||||
visible: shim.isMac() ? false : true,
|
visible: shim.isMac() ? false : true,
|
||||||
@ -615,6 +676,11 @@ class Application extends BaseApplication {
|
|||||||
selector: 'performClose:',
|
selector: 'performClose:',
|
||||||
}, {
|
}, {
|
||||||
type: 'separator',
|
type: 'separator',
|
||||||
|
}, {
|
||||||
|
label: _('Templates'),
|
||||||
|
submenu: templateItems,
|
||||||
|
}, {
|
||||||
|
type: 'separator',
|
||||||
}, {
|
}, {
|
||||||
label: _('Import'),
|
label: _('Import'),
|
||||||
submenu: importItems,
|
submenu: importItems,
|
||||||
@ -1080,6 +1146,13 @@ class Application extends BaseApplication {
|
|||||||
css: cssString
|
css: cssString
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const templates = await TemplateUtils.loadTemplates(Setting.value('templateDir'));
|
||||||
|
|
||||||
|
this.store().dispatch({
|
||||||
|
type: 'TEMPLATE_UPDATE_ALL',
|
||||||
|
templates: templates
|
||||||
|
});
|
||||||
|
|
||||||
// Note: Auto-update currently doesn't work in Linux: it downloads the update
|
// Note: Auto-update currently doesn't work in Linux: it downloads the update
|
||||||
// but then doesn't install it on exit.
|
// but then doesn't install it on exit.
|
||||||
if (shim.isWindows() || shim.isMac()) {
|
if (shim.isWindows() || shim.isMac()) {
|
||||||
|
@ -74,12 +74,13 @@ class MainScreenComponent extends React.Component {
|
|||||||
async doCommand(command) {
|
async doCommand(command) {
|
||||||
if (!command) return;
|
if (!command) return;
|
||||||
|
|
||||||
const createNewNote = async (title, isTodo) => {
|
const createNewNote = async (template, isTodo) => {
|
||||||
const folderId = Setting.value('activeFolderId');
|
const folderId = Setting.value('activeFolderId');
|
||||||
if (!folderId) return;
|
if (!folderId) return;
|
||||||
|
|
||||||
const newNote = {
|
const newNote = {
|
||||||
parent_id: folderId,
|
parent_id: folderId,
|
||||||
|
template: template,
|
||||||
is_todo: isTodo ? 1 : 0,
|
is_todo: isTodo ? 1 : 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -272,6 +273,30 @@ class MainScreenComponent extends React.Component {
|
|||||||
eventManager.emit('alarmChange', { noteId: note.id });
|
eventManager.emit('alarmChange', { noteId: note.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.setState({ promptOptions: null });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (command.name === 'selectTemplate') {
|
||||||
|
this.setState({
|
||||||
|
promptOptions: {
|
||||||
|
label: _('Template file:'),
|
||||||
|
inputType: 'dropdown',
|
||||||
|
value: this.props.templates[0], // Need to start with some value
|
||||||
|
autocomplete: this.props.templates,
|
||||||
|
onClose: async (answer) => {
|
||||||
|
if (answer) {
|
||||||
|
if (command.noteType === 'note' || command.noteType === 'todo') {
|
||||||
|
createNewNote(answer.value, command.noteType === 'todo');
|
||||||
|
} else {
|
||||||
|
this.props.dispatch({
|
||||||
|
type: 'WINDOW_COMMAND',
|
||||||
|
name: 'insertTemplate',
|
||||||
|
value: answer.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({ promptOptions: null });
|
this.setState({ promptOptions: null });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -523,6 +548,7 @@ const mapStateToProps = (state) => {
|
|||||||
selectedNoteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null,
|
selectedNoteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null,
|
||||||
plugins: state.plugins,
|
plugins: state.plugins,
|
||||||
noteDevToolsVisible: state.noteDevToolsVisible,
|
noteDevToolsVisible: state.noteDevToolsVisible,
|
||||||
|
templates: state.templates,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -40,6 +40,7 @@ const DecryptionWorker = require('lib/services/DecryptionWorker');
|
|||||||
const ModelCache = require('lib/services/ModelCache');
|
const ModelCache = require('lib/services/ModelCache');
|
||||||
const NoteTextViewer = require('./NoteTextViewer.min');
|
const NoteTextViewer = require('./NoteTextViewer.min');
|
||||||
const NoteRevisionViewer = require('./NoteRevisionViewer.min');
|
const NoteRevisionViewer = require('./NoteRevisionViewer.min');
|
||||||
|
const TemplateUtils = require('lib/TemplateUtils');
|
||||||
|
|
||||||
require('brace/mode/markdown');
|
require('brace/mode/markdown');
|
||||||
// https://ace.c9.io/build/kitchen-sink.html
|
// https://ace.c9.io/build/kitchen-sink.html
|
||||||
@ -452,14 +453,18 @@ class NoteTextComponent extends React.Component {
|
|||||||
const stateNoteId = this.state.note ? this.state.note.id : null;
|
const stateNoteId = this.state.note ? this.state.note.id : null;
|
||||||
let noteId = null;
|
let noteId = null;
|
||||||
let note = null;
|
let note = null;
|
||||||
|
let newNote = null;
|
||||||
let loadingNewNote = true;
|
let loadingNewNote = true;
|
||||||
let parentFolder = null;
|
let parentFolder = null;
|
||||||
let noteTags = [];
|
let noteTags = [];
|
||||||
let scrollPercent = 0;
|
let scrollPercent = 0;
|
||||||
|
|
||||||
if (props.newNote) {
|
if (props.newNote) {
|
||||||
note = Object.assign({}, props.newNote);
|
// assign new note and prevent body from being null
|
||||||
|
note = Object.assign({}, props.newNote, {body: ''});
|
||||||
this.lastLoadedNoteId_ = null;
|
this.lastLoadedNoteId_ = null;
|
||||||
|
if (note.template)
|
||||||
|
note.body = TemplateUtils.render(note.template);
|
||||||
} else {
|
} else {
|
||||||
noteId = props.noteId;
|
noteId = props.noteId;
|
||||||
|
|
||||||
@ -1012,6 +1017,8 @@ class NoteTextComponent extends React.Component {
|
|||||||
fn = this.commandShowLocalSearch;
|
fn = this.commandShowLocalSearch;
|
||||||
} else if (command.name === 'textCode') {
|
} else if (command.name === 'textCode') {
|
||||||
fn = this.commandTextCode;
|
fn = this.commandTextCode;
|
||||||
|
} else if (command.name === 'insertTemplate') {
|
||||||
|
fn = () => { return this.commandTemplate(command.value); };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1349,6 +1356,10 @@ class NoteTextComponent extends React.Component {
|
|||||||
this.wrapSelectionWithStrings('`', '`');
|
this.wrapSelectionWithStrings('`', '`');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
commandTemplate(value) {
|
||||||
|
this.wrapSelectionWithStrings(TemplateUtils.render(value));
|
||||||
|
}
|
||||||
|
|
||||||
addListItem(string1, string2 = '', defaultText = '') {
|
addListItem(string1, string2 = '', defaultText = '') {
|
||||||
const currentLine = this.selectionRangeCurrentLine();
|
const currentLine = this.selectionRangeCurrentLine();
|
||||||
let newLine = '\n'
|
let newLine = '\n'
|
||||||
@ -1920,6 +1931,7 @@ const mapStateToProps = (state) => {
|
|||||||
customCss: state.customCss,
|
customCss: state.customCss,
|
||||||
lastEditorScrollPercents: state.lastEditorScrollPercents,
|
lastEditorScrollPercents: state.lastEditorScrollPercents,
|
||||||
historyNotes: state.historyNotes,
|
historyNotes: state.historyNotes,
|
||||||
|
templates: state.templates,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ const { themeStyle } = require('../theme.js');
|
|||||||
const { time } = require('lib/time-utils.js');
|
const { time } = require('lib/time-utils.js');
|
||||||
const Datetime = require('react-datetime');
|
const Datetime = require('react-datetime');
|
||||||
const CreatableSelect = require('react-select/lib/Creatable').default;
|
const CreatableSelect = require('react-select/lib/Creatable').default;
|
||||||
|
const Select = require('react-select').default;
|
||||||
const makeAnimated = require('react-select/lib/animated').default;
|
const makeAnimated = require('react-select/lib/animated').default;
|
||||||
|
|
||||||
class PromptDialog extends React.Component {
|
class PromptDialog extends React.Component {
|
||||||
@ -101,7 +102,7 @@ class PromptDialog extends React.Component {
|
|||||||
borderColor: theme.dividerColor,
|
borderColor: theme.dividerColor,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.styles_.tagList = {
|
this.styles_.select = {
|
||||||
control: (provided) => (Object.assign(provided, {
|
control: (provided) => (Object.assign(provided, {
|
||||||
minWidth: width * 0.2,
|
minWidth: width * 0.2,
|
||||||
maxWidth: width * 0.5,
|
maxWidth: width * 0.5,
|
||||||
@ -115,6 +116,10 @@ class PromptDialog extends React.Component {
|
|||||||
fontFamily: theme.fontFamily,
|
fontFamily: theme.fontFamily,
|
||||||
backgroundColor: theme.backgroundColor,
|
backgroundColor: theme.backgroundColor,
|
||||||
})),
|
})),
|
||||||
|
option: (provided) => (Object.assign(provided, {
|
||||||
|
color: theme.color,
|
||||||
|
fontFamily: theme.fontFamily,
|
||||||
|
})),
|
||||||
multiValueLabel: (provided) => (Object.assign(provided, {
|
multiValueLabel: (provided) => (Object.assign(provided, {
|
||||||
fontFamily: theme.fontFamily,
|
fontFamily: theme.fontFamily,
|
||||||
})),
|
})),
|
||||||
@ -123,14 +128,22 @@ class PromptDialog extends React.Component {
|
|||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.styles_.tagListTheme = (tagTheme) => (Object.assign(tagTheme, {
|
this.styles_.selectTheme = (tagTheme) => (Object.assign(tagTheme, {
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
colors: Object.assign(tagTheme.colors, {
|
colors: Object.assign(tagTheme.colors, {
|
||||||
primary: theme.raisedBackgroundColor,
|
primary: theme.raisedBackgroundColor,
|
||||||
primary25: theme.raisedBackgroundColor,
|
primary25: theme.raisedBackgroundColor,
|
||||||
neutral0: theme.backgroundColor,
|
neutral0: theme.backgroundColor,
|
||||||
|
neutral5: theme.backgroundColor,
|
||||||
neutral10: theme.raisedBackgroundColor,
|
neutral10: theme.raisedBackgroundColor,
|
||||||
|
neutral20: theme.raisedBackgroundColor,
|
||||||
|
neutral30: theme.raisedBackgroundColor,
|
||||||
|
neutral40: theme.color,
|
||||||
|
neutral50: theme.color,
|
||||||
|
neutral60: theme.color,
|
||||||
|
neutral70: theme.color,
|
||||||
neutral80: theme.color,
|
neutral80: theme.color,
|
||||||
|
neutral90: theme.color,
|
||||||
danger: theme.backgroundColor,
|
danger: theme.backgroundColor,
|
||||||
dangerLight: theme.colorError2,
|
dangerLight: theme.colorError2,
|
||||||
}),
|
}),
|
||||||
@ -179,14 +192,19 @@ class PromptDialog extends React.Component {
|
|||||||
this.setState({ answer: momentObject });
|
this.setState({ answer: momentObject });
|
||||||
}
|
}
|
||||||
|
|
||||||
const onTagsChange = (newTags) => {
|
const onSelectChange = (newValue) => {
|
||||||
this.setState({ answer: newTags });
|
this.setState({ answer: newValue });
|
||||||
this.focusInput_ = true;
|
this.focusInput_ = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onKeyDown = (event) => {
|
const onKeyDown = (event) => {
|
||||||
if (event.key === 'Enter' && this.props.inputType !== 'tags') {
|
if (event.key === 'Enter') {
|
||||||
|
if (this.props.inputType !== 'tags' && this.props.inputType !== 'dropdown') {
|
||||||
onClose(true);
|
onClose(true);
|
||||||
|
} else if (this.answerInput_.current && !this.answerInput_.current.state.menuIsOpen) {
|
||||||
|
// The menu will be open if the user is selecting a new item
|
||||||
|
onClose(true);
|
||||||
|
}
|
||||||
} else if (event.key === 'Escape') {
|
} else if (event.key === 'Escape') {
|
||||||
onClose(false);
|
onClose(false);
|
||||||
}
|
}
|
||||||
@ -206,8 +224,8 @@ class PromptDialog extends React.Component {
|
|||||||
/>
|
/>
|
||||||
} else if (this.props.inputType === 'tags') {
|
} else if (this.props.inputType === 'tags') {
|
||||||
inputComp = <CreatableSelect
|
inputComp = <CreatableSelect
|
||||||
styles={styles.tagList}
|
styles={styles.select}
|
||||||
theme={styles.tagListTheme}
|
theme={styles.selectTheme}
|
||||||
ref={this.answerInput_}
|
ref={this.answerInput_}
|
||||||
value={this.state.answer}
|
value={this.state.answer}
|
||||||
placeholder=""
|
placeholder=""
|
||||||
@ -216,7 +234,20 @@ class PromptDialog extends React.Component {
|
|||||||
isClearable={false}
|
isClearable={false}
|
||||||
backspaceRemovesValue={true}
|
backspaceRemovesValue={true}
|
||||||
options={this.props.autocomplete}
|
options={this.props.autocomplete}
|
||||||
onChange={onTagsChange}
|
onChange={onSelectChange}
|
||||||
|
onKeyDown={(event) => onKeyDown(event)}
|
||||||
|
/>
|
||||||
|
} else if (this.props.inputType === 'dropdown') {
|
||||||
|
inputComp = <Select
|
||||||
|
styles={styles.select}
|
||||||
|
theme={styles.selectTheme}
|
||||||
|
ref={this.answerInput_}
|
||||||
|
components={makeAnimated()}
|
||||||
|
value={this.props.answer}
|
||||||
|
defaultValue={this.props.defaultValue}
|
||||||
|
isClearable={false}
|
||||||
|
options={this.props.autocomplete}
|
||||||
|
onChange={onSelectChange}
|
||||||
onKeyDown={(event) => onKeyDown(event)}
|
onKeyDown={(event) => onKeyDown(event)}
|
||||||
/>
|
/>
|
||||||
} else {
|
} else {
|
||||||
|
5
ElectronClient/app/package-lock.json
generated
5
ElectronClient/app/package-lock.json
generated
@ -4453,6 +4453,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"mustache": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mustache/-/mustache-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-jFI/4UVRsRYdUbuDTKT7KzfOp7FiD5WzYmmwNwXyUVypC0xjoTL78Fqc0jHUPIvvGD+6DQSPHIt1NE7D1ArsqA=="
|
||||||
|
},
|
||||||
"nan": {
|
"nan": {
|
||||||
"version": "2.10.0",
|
"version": "2.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz",
|
||||||
|
@ -125,6 +125,7 @@
|
|||||||
"mime": "^2.3.1",
|
"mime": "^2.3.1",
|
||||||
"moment": "^2.22.2",
|
"moment": "^2.22.2",
|
||||||
"multiparty": "^4.2.1",
|
"multiparty": "^4.2.1",
|
||||||
|
"mustache": "^3.0.1",
|
||||||
"node-fetch": "^1.7.3",
|
"node-fetch": "^1.7.3",
|
||||||
"node-notifier": "^5.2.1",
|
"node-notifier": "^5.2.1",
|
||||||
"promise": "^8.0.1",
|
"promise": "^8.0.1",
|
||||||
|
@ -47,6 +47,18 @@ table td, table th {
|
|||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: rgba(100, 100, 100, 0.7);
|
background: rgba(100, 100, 100, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fade_out {
|
||||||
|
-webkit-transition: 0.15s;
|
||||||
|
transition: 0.15s;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade_in {
|
||||||
|
-webkit-transition: 0.3s;
|
||||||
|
transition: 0.3s;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
/* By default, the Ice Editor displays invalid characters, such as non-breaking spaces
|
/* By default, the Ice Editor displays invalid characters, such as non-breaking spaces
|
||||||
as red boxes, but since those are actually valid characters and common in imported
|
as red boxes, but since those are actually valid characters and common in imported
|
||||||
Evernote data, we hide them here. */
|
Evernote data, we hide them here. */
|
||||||
|
19
README.md
19
README.md
@ -316,6 +316,25 @@ It is generally recommended to enter the notes as Markdown as it makes the notes
|
|||||||
|
|
||||||
Rendered markdown can be customized by placing a userstyle file in the profile directory `~/.config/joplin-desktop/userstyle.css` (This path might be different on your device - check at the top of the Config screen for the exact path). This file supports standard CSS syntax. Joplin ***must*** be restarted for the new css to be applied, please ensure that Joplin is not closing to the tray, but is actually exiting. Note that this file is used only when display the notes, **not when printing or exporting to PDF**. This is because printing has a lot more restrictions (for example, printing white text over a black background is usually not wanted), so special rules are applied to make it look good when printing, and a userstyle.css would interfer with that.
|
Rendered markdown can be customized by placing a userstyle file in the profile directory `~/.config/joplin-desktop/userstyle.css` (This path might be different on your device - check at the top of the Config screen for the exact path). This file supports standard CSS syntax. Joplin ***must*** be restarted for the new css to be applied, please ensure that Joplin is not closing to the tray, but is actually exiting. Note that this file is used only when display the notes, **not when printing or exporting to PDF**. This is because printing has a lot more restrictions (for example, printing white text over a black background is usually not wanted), so special rules are applied to make it look good when printing, and a userstyle.css would interfer with that.
|
||||||
|
|
||||||
|
## New Note Templates
|
||||||
|
|
||||||
|
Templates can be used for new notes by creating a templates folder in `~/.config/joplin-desktop/` and placing markdown template files into it. For example creating the file `hours.md` in the directory `~/.config/joplin-desktop/templates/` with the contents:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
Date: {{date}}
|
||||||
|
Hours:
|
||||||
|
Details:
|
||||||
|
```
|
||||||
|
|
||||||
|
When creating a new note you will now be prompted to insert a template that contains the above text (and {{date}} replaced with today's date). Templates can also be inserted from the menu (File->Templates).
|
||||||
|
|
||||||
|
The currently supported template variables are:
|
||||||
|
| Variable | Description | Example |
|
||||||
|
| {{date}} | Today's date formatted based on the settings format | 2019-01-01 |
|
||||||
|
| {{time}} | Current time formatted based on the settings format | 13:00 |
|
||||||
|
| {{datetime}} | Current date and time formatted based on the settings format | 01/01/19 1:00 PM |
|
||||||
|
| {{#custom_datetime}} | Current date and/or time formatted based on a supplied string (using [moment.js](https://momentjs.com/) formatting) | {{#custom_datetime}}M d{{/custom_datetime}} |
|
||||||
|
|
||||||
# Searching
|
# Searching
|
||||||
|
|
||||||
Joplin implements the SQLite Full Text Search (FTS4) extension. It means the content of all the notes is indexed in real time and search queries return results very fast. Both [Simple FTS Queries](https://www.sqlite.org/fts3.html#simple_fts_queries) and [Full-Text Index Queries](https://www.sqlite.org/fts3.html#full_text_index_queries) are supported. See below for the list of supported queries:
|
Joplin implements the SQLite Full Text Search (FTS4) extension. It means the content of all the notes is indexed in real time and search queries return results very fast. Both [Simple FTS Queries](https://www.sqlite.org/fts3.html#simple_fts_queries) and [Full-Text Index Queries](https://www.sqlite.org/fts3.html#full_text_index_queries) are supported. See below for the list of supported queries:
|
||||||
|
@ -546,6 +546,7 @@ class BaseApplication {
|
|||||||
|
|
||||||
Setting.setConstant('env', initArgs.env);
|
Setting.setConstant('env', initArgs.env);
|
||||||
Setting.setConstant('profileDir', profileDir);
|
Setting.setConstant('profileDir', profileDir);
|
||||||
|
Setting.setConstant('templateDir', profileDir + '/templates');
|
||||||
Setting.setConstant('resourceDirName', resourceDirName);
|
Setting.setConstant('resourceDirName', resourceDirName);
|
||||||
Setting.setConstant('resourceDir', resourceDir);
|
Setting.setConstant('resourceDir', resourceDir);
|
||||||
Setting.setConstant('tempDir', tempDir);
|
Setting.setConstant('tempDir', tempDir);
|
||||||
|
59
ReactNativeClient/lib/TemplateUtils.js
Normal file
59
ReactNativeClient/lib/TemplateUtils.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
const fs = require('fs-extra');
|
||||||
|
const { shim } = require('lib/shim.js');
|
||||||
|
const { time } = require('lib/time-utils.js');
|
||||||
|
const Mustache = require('mustache');
|
||||||
|
|
||||||
|
const TemplateUtils = {};
|
||||||
|
|
||||||
|
// new template variables can be added here
|
||||||
|
// If there are too many, this should be moved to a new file
|
||||||
|
const view = {
|
||||||
|
date: time.formatMsToLocal(new Date().getTime(), time.dateFormat()),
|
||||||
|
time: time.formatMsToLocal(new Date().getTime(), time.timeFormat()),
|
||||||
|
datetime: time.formatMsToLocal(new Date().getTime()),
|
||||||
|
custom_datetime: () => { return (text, render) => {
|
||||||
|
return render(time.formatMsToLocal(new Date().getTime(), text));
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mustache escapes strings (including /) with the html code by default
|
||||||
|
// This isn't useful for markdown so it's disabled
|
||||||
|
Mustache.escape = (text) => { return text; }
|
||||||
|
|
||||||
|
TemplateUtils.render = function(input) {
|
||||||
|
return Mustache.render(input, view);
|
||||||
|
}
|
||||||
|
|
||||||
|
TemplateUtils.loadTemplates = async function(filePath) {
|
||||||
|
let templates = [];
|
||||||
|
let files = [];
|
||||||
|
|
||||||
|
if (await shim.fsDriver().exists(filePath)) {
|
||||||
|
try {
|
||||||
|
files = await shim.fsDriver().readDirStats(filePath);
|
||||||
|
} catch (error) {
|
||||||
|
let msg = error.message ? error.message : '';
|
||||||
|
msg = 'Could not read template names from ' + filePath + '\n' + msg;
|
||||||
|
error.message = msg;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
files.forEach(async (file) => {
|
||||||
|
if (file.path.endsWith('.md')) {
|
||||||
|
try {
|
||||||
|
let fileString = await shim.fsDriver().readFile(filePath + '/' + file.path, 'utf-8');
|
||||||
|
templates.push({label: file.path, value: fileString});
|
||||||
|
} catch (error) {
|
||||||
|
let msg = error.message ? error.message : '';
|
||||||
|
msg = 'Could not load template ' + file.path + '\n' + msg;
|
||||||
|
error.message = msg;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return templates;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = TemplateUtils;
|
@ -648,6 +648,7 @@ Setting.constants_ = {
|
|||||||
resourceDirName: '',
|
resourceDirName: '',
|
||||||
resourceDir: '',
|
resourceDir: '',
|
||||||
profileDir: '',
|
profileDir: '',
|
||||||
|
templateDir: '',
|
||||||
tempDir: '',
|
tempDir: '',
|
||||||
openDevTools: false,
|
openDevTools: false,
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,7 @@ const defaultState = {
|
|||||||
hasDisabledSyncItems: false,
|
hasDisabledSyncItems: false,
|
||||||
newNote: null,
|
newNote: null,
|
||||||
customCss: '',
|
customCss: '',
|
||||||
|
templates: [],
|
||||||
collapsedFolderIds: [],
|
collapsedFolderIds: [],
|
||||||
clipperServer: {
|
clipperServer: {
|
||||||
startState: 'idle',
|
startState: 'idle',
|
||||||
@ -714,6 +715,12 @@ const reducer = (state = defaultState, action) => {
|
|||||||
newState.customCss = action.css;
|
newState.customCss = action.css;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'TEMPLATE_UPDATE_ALL':
|
||||||
|
|
||||||
|
newState = Object.assign({}, state);
|
||||||
|
newState.templates = action.templates;
|
||||||
|
break;
|
||||||
|
|
||||||
case 'SET_NOTE_TAGS':
|
case 'SET_NOTE_TAGS':
|
||||||
newState = Object.assign({}, state);
|
newState = Object.assign({}, state);
|
||||||
newState.selectedNoteTags = action.items;
|
newState.selectedNoteTags = action.items;
|
||||||
|
Reference in New Issue
Block a user