New file properties modal

This commit is contained in:
benweet 2018-04-08 15:49:10 +01:00
parent fb13a52157
commit ab97d9d9fc
29 changed files with 348 additions and 214 deletions

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="app" :class="themeClasses"> <div class="app" :class="classes">
<splash-screen v-if="!ready"></splash-screen> <splash-screen v-if="!ready"></splash-screen>
<layout v-else></layout> <layout v-else></layout>
<modal v-if="showModal"></modal> <modal v-if="showModal"></modal>
@ -81,7 +81,7 @@ export default {
ready: false, ready: false,
}), }),
computed: { computed: {
themeClasses() { classes() {
const result = themeClasses[this.$store.getters['data/computedSettings'].colorTheme]; const result = themeClasses[this.$store.getters['data/computedSettings'].colorTheme];
return Array.isArray(result) ? result : themeClasses.light; return Array.isArray(result) ? result : themeClasses.light;
}, },

View File

@ -232,6 +232,19 @@ export default {
} }
} }
.modal__title {
font-weight: bold;
font-size: 1.5rem;
line-height: 1.4;
margin-top: 2.5rem;
}
.modal__sub-title {
opacity: 0.5;
font-size: 0.75rem;
margin-bottom: 1.5rem;
}
.modal__error { .modal__error {
color: #de2c00; color: #de2c00;
} }
@ -241,6 +254,12 @@ export default {
border-radius: $border-radius-base; border-radius: $border-radius-base;
margin: 1.2em 0; margin: 1.2em 0;
padding: 0.75em 1.25em; padding: 0.75em 1.25em;
font-size: 0.95em;
line-height: 1.6;
pre {
line-height: 1.5;
}
} }
.modal__button-bar { .modal__button-bar {
@ -266,6 +285,10 @@ export default {
} }
} }
.form-entry__label-info {
font-size: 0.75rem;
}
.form-entry__field { .form-entry__field {
border: 1px solid #d8d8d8; border: 1px solid #d8d8d8;
border-radius: $border-radius-base; border-radius: $border-radius-base;

View File

@ -36,8 +36,8 @@
<div class="navigation-bar__inner navigation-bar__inner--edit-pagedownButtons"> <div class="navigation-bar__inner navigation-bar__inner--edit-pagedownButtons">
<button class="navigation-bar__button button" @click="undo" v-title="'Undo'" :disabled="!canUndo"><icon-undo></icon-undo></button> <button class="navigation-bar__button button" @click="undo" v-title="'Undo'" :disabled="!canUndo"><icon-undo></icon-undo></button>
<button class="navigation-bar__button button" @click="redo" v-title="'Redo'" :disabled="!canRedo"><icon-redo></icon-redo></button> <button class="navigation-bar__button button" @click="redo" v-title="'Redo'" :disabled="!canRedo"><icon-redo></icon-redo></button>
<div v-for="button in pagedownButtons" :key="button.action"> <div v-for="button in pagedownButtons" :key="button.method">
<button class="navigation-bar__button button" v-if="button.action" @click="pagedownClick(button.action)" v-title="button.title"> <button class="navigation-bar__button button" v-if="button.method" @click="pagedownClick(button.method)" v-title="button.titleWithShortcut">
<component :is="button.iconClass"></component> <component :is="button.iconClass"></component>
</button> </button>
<div class="navigation-bar__spacer" v-else></div> <div class="navigation-bar__spacer" v-else></div>
@ -55,6 +55,28 @@ import animationSvc from '../services/animationSvc';
import tempFileSvc from '../services/tempFileSvc'; import tempFileSvc from '../services/tempFileSvc';
import utils from '../services/utils'; import utils from '../services/utils';
import pagedownButtons from '../data/pagedownButtons'; import pagedownButtons from '../data/pagedownButtons';
import store from '../store';
// According to mousetrap
const mod = /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'Meta' : 'Ctrl';
const getShortcut = (method) => {
let result = '';
Object.entries(store.getters['data/computedSettings'].shortcuts).some(([keys, shortcut]) => {
if (`${shortcut.method || shortcut}` !== method) {
return false;
}
result = keys.split('+').map(key => key.toLowerCase()).map((key) => {
if (key === 'mod') {
return mod;
}
// Capitalize
return key && `${key[0].toUpperCase()}${key.slice(1)}`;
}).join('+');
return true;
});
return result && ` ${result}`;
};
export default { export default {
data: () => ({ data: () => ({
@ -90,14 +112,11 @@ export default {
publishLocations: 'current', publishLocations: 'current',
}), }),
pagedownButtons() { pagedownButtons() {
return pagedownButtons.map((button) => { return pagedownButtons.map(button => ({
const title = button.title; ...button,
return { titleWithShortcut: `${button.title}${getShortcut(button.method)}`,
...button, iconClass: `icon-${button.icon}`,
title, }));
iconClass: `icon-${button.icon}`,
};
});
}, },
isSyncPossible() { isSyncPossible() {
return this.$store.getters['workspace/syncToken'] || return this.$store.getters['workspace/syncToken'] ||

View File

@ -44,27 +44,29 @@ export default class EditorClassApplier {
}; };
editorSvc.clEditor.on('contentChanged', this.restoreClass); editorSvc.clEditor.on('contentChanged', this.restoreClass);
nextTick(() => this.applyClass()); nextTick(() => this.restoreClass());
} }
applyClass() { applyClass() {
const offset = this.offsetGetter(); if (!this.stopped) {
if (offset && offset.start !== offset.end) { const offset = this.offsetGetter();
const range = editorSvc.clEditor.selectionMgr.createRange( if (offset && offset.start !== offset.end) {
Math.min(offset.start, offset.end), const range = editorSvc.clEditor.selectionMgr.createRange(
Math.max(offset.start, offset.end), Math.min(offset.start, offset.end),
); Math.max(offset.start, offset.end),
const properties = { );
...this.properties, const properties = {
className: this.classGetter().join(' '), ...this.properties,
}; className: this.classGetter().join(' '),
editorSvc.clEditor.watcher.noWatch(() => { };
utils.wrapRange(range, properties); editorSvc.clEditor.watcher.noWatch(() => {
}); utils.wrapRange(range, properties);
if (editorSvc.clEditor.selectionMgr.hasFocus()) { });
nextTickRestoreSelection(); if (editorSvc.clEditor.selectionMgr.hasFocus()) {
nextTickRestoreSelection();
}
this.lastEltCount = this.eltCollection.length;
} }
this.lastEltCount = this.eltCollection.length;
} }
} }
@ -80,5 +82,6 @@ export default class EditorClassApplier {
stop() { stop() {
editorSvc.clEditor.off('contentChanged', this.restoreClass); editorSvc.clEditor.off('contentChanged', this.restoreClass);
nextTick(() => this.removeClass()); nextTick(() => this.removeClass());
this.stopped = true;
} }
} }

View File

@ -32,30 +32,32 @@ export default class PreviewClassApplier {
}; };
editorSvc.$on('previewCtxWithDiffs', this.restoreClass); editorSvc.$on('previewCtxWithDiffs', this.restoreClass);
nextTick(() => this.applyClass()); nextTick(() => this.restoreClass());
} }
applyClass() { applyClass() {
const offset = this.offsetGetter(); if (!this.stopped) {
if (offset) { const offset = this.offsetGetter();
const offsetStart = editorSvc.getPreviewOffset( if (offset) {
offset.start, editorSvc.previewCtx.sectionDescList); const offsetStart = editorSvc.getPreviewOffset(
const offsetEnd = editorSvc.getPreviewOffset( offset.start, editorSvc.previewCtx.sectionDescList);
offset.end, editorSvc.previewCtx.sectionDescList); const offsetEnd = editorSvc.getPreviewOffset(
if (offsetStart != null && offsetEnd != null && offsetStart !== offsetEnd) { offset.end, editorSvc.previewCtx.sectionDescList);
const start = cledit.Utils.findContainer( if (offsetStart != null && offsetEnd != null && offsetStart !== offsetEnd) {
editorSvc.previewElt, Math.min(offsetStart, offsetEnd)); const start = cledit.Utils.findContainer(
const end = cledit.Utils.findContainer( editorSvc.previewElt, Math.min(offsetStart, offsetEnd));
editorSvc.previewElt, Math.max(offsetStart, offsetEnd)); const end = cledit.Utils.findContainer(
const range = document.createRange(); editorSvc.previewElt, Math.max(offsetStart, offsetEnd));
range.setStart(start.container, start.offsetInContainer); const range = document.createRange();
range.setEnd(end.container, end.offsetInContainer); range.setStart(start.container, start.offsetInContainer);
const properties = { range.setEnd(end.container, end.offsetInContainer);
...this.properties, const properties = {
className: this.classGetter().join(' '), ...this.properties,
}; className: this.classGetter().join(' '),
utils.wrapRange(range, properties); };
this.lastEltCount = this.eltCollection.length; utils.wrapRange(range, properties);
this.lastEltCount = this.eltCollection.length;
}
} }
} }
} }
@ -67,5 +69,6 @@ export default class PreviewClassApplier {
stop() { stop() {
editorSvc.$off('previewCtxWithDiffs', this.restoreClass); editorSvc.$off('previewCtxWithDiffs', this.restoreClass);
nextTick(() => this.removeClass()); nextTick(() => this.removeClass());
this.stopped = true;
} }
} }

View File

@ -38,7 +38,6 @@ import UserName from '../UserName';
import EditorClassApplier from '../common/EditorClassApplier'; import EditorClassApplier from '../common/EditorClassApplier';
import PreviewClassApplier from '../common/PreviewClassApplier'; import PreviewClassApplier from '../common/PreviewClassApplier';
import utils from '../../services/utils'; import utils from '../../services/utils';
import editorSvc from '../../services/editorSvc';
import googleHelper from '../../services/providers/helpers/googleHelper'; import googleHelper from '../../services/providers/helpers/googleHelper';
import syncSvc from '../../services/syncSvc'; import syncSvc from '../../services/syncSvc';
@ -137,22 +136,20 @@ export default {
previewClassAppliers.forEach(previewClassApplier => previewClassApplier.stop()); previewClassAppliers.forEach(previewClassApplier => previewClassApplier.stop());
previewClassAppliers = []; previewClassAppliers = [];
if (revisionContent) { if (revisionContent) {
editorSvc.$once('previewCtxWithDiffs', () => { let offset = 0;
let offset = 0; revisionContent.diffs.forEach(([type, text]) => {
revisionContent.diffs.forEach(([type, text]) => { if (type) {
if (type) { const classes = ['revision-diff', `revision-diff--${type > 0 ? 'insert' : 'delete'}`];
const classes = ['revision-diff', `revision-diff--${type > 0 ? 'insert' : 'delete'}`]; const offsets = {
const offsets = { start: offset,
start: offset, end: offset + text.length,
end: offset + text.length, };
}; editorClassAppliers.push(new EditorClassApplier(
editorClassAppliers.push(new EditorClassApplier( [`revision-diff--${utils.uid()}`, ...classes], offsets));
[`revision-diff--${utils.uid()}`, ...classes], offsets)); previewClassAppliers.push(new PreviewClassApplier(
previewClassAppliers.push(new PreviewClassApplier( [`revision-diff--${utils.uid()}`, ...classes], offsets));
[`revision-diff--${utils.uid()}`, ...classes], offsets)); }
} offset += text.length;
offset += text.length;
});
}); });
} }
}, },

View File

@ -2,26 +2,76 @@
<modal-inner class="modal__inner-1--file-properties" aria-label="File properties"> <modal-inner class="modal__inner-1--file-properties" aria-label="File properties">
<div class="modal__content"> <div class="modal__content">
<div class="tabs flex flex--row"> <div class="tabs flex flex--row">
<tab :active="tab === 'custom'" @click="tab = 'custom'"> <tab :active="tab === 'simple'" @click="setSimpleTab()">
Current file properties Simple properties
</tab> </tab>
<tab :active="tab === 'default'" @click="tab = 'default'"> <tab :active="tab === 'yaml'" @click="setYamlTab()">
Default properties YAML properties
</tab> </tab>
</div> </div>
<div class="form-entry" v-if="tab === 'custom'" role="tabpanel" aria-label="Current file properties"> <div v-if="tab === 'simple'">
<label class="form-entry__label">YAML</label> <div class="modal__title">Extensions</div>
<div class="form-entry__field"> <div class="modal__sub-title">Configure the Markdown engine.</div>
<code-editor lang="yaml" :value="customProperties" key="custom-properties" @changed="setCustomProperties"></code-editor> <form-entry label="Preset">
<select slot="field" class="textfield" v-model="preset" @keydown.enter="resolve()">
<option v-for="(preset, id) in presets" :key="id" :value="preset">
{{ preset }}
</option>
</select>
</form-entry>
<div class="modal__title">Metadata</div>
<div class="modal__sub-title">Add info to your publications (Wordpress, Blogger...).</div>
<form-entry label="Title">
<input slot="field" class="textfield" type="text" v-model.trim="title" @keydown.enter="resolve()">
</form-entry>
<form-entry label="Author">
<input slot="field" class="textfield" type="text" v-model.trim="author" @keydown.enter="resolve()">
</form-entry>
<form-entry label="Tags" info="comma-separated">
<input slot="field" class="textfield" type="text" v-model.trim="tags" @keydown.enter="resolve()">
</form-entry>
<form-entry label="Categories" info="comma-separated">
<input slot="field" class="textfield" type="text" v-model.trim="categories" @keydown.enter="resolve()">
</form-entry>
<form-entry label="Excerpt">
<input slot="field" class="textfield" type="text" v-model.trim="excerpt" @keydown.enter="resolve()">
</form-entry>
<form-entry label="Featured image">
<input slot="field" class="textfield" type="text" v-model.trim="featuredImage" @keydown.enter="resolve()">
</form-entry>
<form-entry label="Status">
<input slot="field" class="textfield" type="text" v-model.trim="status" @keydown.enter="resolve()">
</form-entry>
<form-entry label="Date" info="YYYY-MM-DD">
<input slot="field" class="textfield" type="text" v-model.trim="date" @keydown.enter="resolve()">
</form-entry>
</div>
<div v-if="tab === 'yaml'">
<div class="form-entry" role="tabpanel" aria-label="YAML properties">
<label class="form-entry__label">YAML</label>
<div class="form-entry__field">
<code-editor lang="yaml" :value="yamlProperties" key="custom-properties" @changed="setYamlProperties"></code-editor>
</div>
</div>
<div class="modal__error modal__error--file-properties">{{error}}</div>
<div class="modal__info">
<p><strong>ProTip:</strong> You can manually toggle extensions:</p>
<pre class=" language-yaml"><code class="prism language-yaml"><span class="token key atrule">extensions</span><span class="token punctuation">:</span>
<span class="token key atrule">emoji</span><span class="token punctuation">:</span>
<span class="token comment"># Enable emoji shortcuts like :) :-(</span>
<span class="token key atrule">shortcuts</span><span class="token punctuation">:</span> <span class="token boolean important">true</span>
</code></pre>
<p>Use preset <code>zero</code> to make your own configuration:</p>
<pre class=" language-yaml"><code class="prism language-yaml"><span class="token key atrule">extensions</span><span class="token punctuation">:</span>
<span class="token key atrule">preset</span><span class="token punctuation">:</span> zero
<span class="token key atrule">markdown</span><span class="token punctuation">:</span>
<span class="token key atrule">table</span><span class="token punctuation">:</span> <span class="token boolean important">true</span>
<span class="token key atrule">katex</span><span class="token punctuation">:</span>
<span class="token key atrule">enabled</span><span class="token punctuation">:</span> <span class="token boolean important">true</span>
</code></pre>
<p>For the full list of options, see <a href="https://github.com/benweet/stackedit/blob/master/src/data/presets.js" target="_blank">here</a>.</p>
</div> </div>
</div> </div>
<div class="form-entry" v-else-if="tab === 'default'" role="tabpanel" aria-label="Default properties">
<label class="form-entry__label">YAML</label>
<div class="form-entry__field">
<code-editor lang="yaml" :value="defaultProperties" key="default-properties" disabled="true"></code-editor>
</div>
</div>
<div class="modal__error modal__error--file-properties">{{error}}</div>
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button> <button class="button" @click="config.reject()">Cancel</button>
@ -35,56 +85,135 @@ import yaml from 'js-yaml';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import ModalInner from './common/ModalInner'; import ModalInner from './common/ModalInner';
import Tab from './common/Tab'; import Tab from './common/Tab';
import FormEntry from './common/FormEntry';
import CodeEditor from '../CodeEditor'; import CodeEditor from '../CodeEditor';
import utils from '../../services/utils'; import utils from '../../services/utils';
import defaultProperties from '../../data/defaultFileProperties.yml'; import presets from '../../data/presets';
const emptyProperties = `# Add custom properties for the current file here const simpleProperties = {
# to override the default properties. title: '',
`; author: '',
tags: '',
categories: '',
excerpt: '',
featuredImage: '',
status: '',
date: '',
};
export default { export default {
components: { components: {
ModalInner, ModalInner,
Tab, Tab,
FormEntry,
CodeEditor, CodeEditor,
}, },
data: () => ({ data: () => ({
contentId: null, contentId: null,
tab: 'custom', yamlProperties: null,
defaultProperties, preset: '',
customProperties: null,
error: null, error: null,
...simpleProperties,
}), }),
computed: { computed: {
...mapGetters('modal', [ ...mapGetters('modal', [
'config', 'config',
]), ]),
strippedCustomProperties() { presets: () => Object.keys(presets).sort(),
return this.customProperties === emptyProperties ? '\n' : this.customProperties.replace(/\t/g, ' '); tab: {
get() {
return this.$store.getters['data/localSettings'].filePropertiesTab;
},
set(value) {
this.$store.dispatch('data/patchLocalSettings', {
filePropertiesTab: value,
});
},
}, },
}, },
created() { created() {
const content = this.$store.getters['content/current']; const content = this.$store.getters['content/current'];
this.contentId = content.id; this.contentId = content.id;
const properties = content.properties; this.setYamlProperties(content.properties);
this.setCustomProperties(properties === '\n' ? emptyProperties : properties); if (this.tab !== 'yaml') {
this.setSimpleTab();
}
}, },
methods: { methods: {
setCustomProperties(value) { yamlToSimple() {
this.customProperties = value; const properties = this.properties || {};
const extensions = properties.extensions || {};
this.preset = extensions.preset;
if (this.presets.indexOf(this.preset) === -1) {
this.preset = 'default';
}
Object.keys(simpleProperties).forEach((name) => {
this[name] = `${properties[name] || ''}`;
});
},
simpleToYaml() {
let hasChanged = false;
const properties = this.properties || {};
const extensions = properties.extensions || {};
if (this.preset !== extensions.preset) {
if (this.preset !== 'default') {
extensions.preset = this.preset;
hasChanged = true;
} else if (extensions.preset) {
delete extensions.preset;
hasChanged = true;
}
}
Object.keys(simpleProperties).forEach((name) => {
if (this[name] !== properties[name]) {
if (this[name]) {
properties[name] = this[name];
hasChanged = true;
} else if (properties[name]) {
delete properties[name];
hasChanged = true;
}
}
});
if (hasChanged) {
if (Object.keys(extensions).length) {
properties.extensions = extensions;
} else {
delete properties.extensions;
}
this.setYamlProperties(Object.keys(properties).length
? yaml.safeDump(properties)
: '\n');
}
},
setSimpleTab() {
this.tab = 'simple';
this.yamlToSimple();
},
setYamlTab() {
this.tab = 'yaml';
this.simpleToYaml();
},
setYamlProperties(value) {
this.yamlProperties = value;
try { try {
yaml.safeLoad(this.strippedCustomProperties); this.properties = yaml.safeLoad(value);
this.error = null; this.error = null;
} catch (e) { } catch (e) {
this.error = e.message; this.error = e.message;
} }
}, },
resolve() { resolve() {
if (!this.error) { if (this.tab === 'simple') {
// Compute YAML properties
this.simpleToYaml();
}
if (this.error) {
this.setYamlTab();
} else {
this.$store.commit('content/patchItem', { this.$store.commit('content/patchItem', {
id: this.contentId, id: this.contentId,
properties: utils.sanitizeText(this.strippedCustomProperties), properties: utils.sanitizeText(this.yamlProperties),
}); });
this.config.resolve(); this.config.resolve();
} }
@ -97,7 +226,7 @@ export default {
@import '../common/variables.scss'; @import '../common/variables.scss';
.modal__inner-1--file-properties { .modal__inner-1--file-properties {
max-width: 600px; max-width: 540px;
} }
.modal__error--file-properties { .modal__error--file-properties {

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="form-entry" :error="error"> <div class="form-entry" :error="error">
<label class="form-entry__label" :for="uid">{{label}}</label> <label class="form-entry__label" :for="uid">{{label}}<span class="form-entry__label-info" v-if="info"> &mdash; {{info}}</span></label>
<div class="form-entry__field"> <div class="form-entry__field">
<slot name="field"></slot> <slot name="field"></slot>
</div> </div>
@ -12,7 +12,7 @@
import utils from '../../../services/utils'; import utils from '../../../services/utils';
export default { export default {
props: ['label', 'error'], props: ['label', 'info', 'error'],
data: () => ({ data: () => ({
uid: utils.uid(), uid: utils.uid(),
}), }),

View File

@ -11,7 +11,7 @@
<b>Example:</b> http://example.blogger.com/ <b>Example:</b> http://example.blogger.com/
</div> </div>
</form-entry> </form-entry>
<form-entry label="Existing page ID (optional)"> <form-entry label="Existing page ID" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="pageId" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="pageId" @keydown.enter="resolve()">
</form-entry> </form-entry>
<form-entry label="Template"> <form-entry label="Template">

View File

@ -11,7 +11,7 @@
<b>Example:</b> http://example.blogger.com/ <b>Example:</b> http://example.blogger.com/
</div> </div>
</form-entry> </form-entry>
<form-entry label="Existing post ID (optional)"> <form-entry label="Existing post ID" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="postId" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="postId" @keydown.enter="resolve()">
</form-entry> </form-entry>
<form-entry label="Template"> <form-entry label="Template">

View File

@ -15,7 +15,7 @@
</label> </label>
</div> </div>
</div> </div>
<form-entry label="Existing Gist ID (optional)"> <form-entry label="Existing Gist ID" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="gistId" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="gistId" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
If the file exists in the Gist, it will be replaced. If the file exists in the Gist, it will be replaced.

View File

@ -15,7 +15,7 @@
</label> </label>
</div> </div>
</div> </div>
<form-entry label="Existing Gist ID (optional)"> <form-entry label="Existing Gist ID" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="gistId" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="gistId" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
If the file exists in the Gist, it will be replaced. If the file exists in the Gist, it will be replaced.

View File

@ -11,7 +11,7 @@
<b>Example:</b> https://github.com/benweet/stackedit <b>Example:</b> https://github.com/benweet/stackedit
</div> </div>
</form-entry> </form-entry>
<form-entry label="Branch (optional)"> <form-entry label="Branch" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
If not provided, the <code>master</code> branch will be used. If not provided, the <code>master</code> branch will be used.

View File

@ -11,7 +11,7 @@
<b>Example:</b> https://github.com/benweet/stackedit <b>Example:</b> https://github.com/benweet/stackedit
</div> </div>
</form-entry> </form-entry>
<form-entry label="Branch (optional)"> <form-entry label="Branch" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
If not provided, the master branch will be used. If not provided, the master branch will be used.

View File

@ -11,7 +11,7 @@
<b>Example:</b> https://github.com/benweet/stackedit <b>Example:</b> https://github.com/benweet/stackedit
</div> </div>
</form-entry> </form-entry>
<form-entry label="Branch (optional)"> <form-entry label="Branch" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
If not provided, the <code>master</code> branch will be used. If not provided, the <code>master</code> branch will be used.

View File

@ -5,7 +5,7 @@
<icon-provider provider-id="googleDrive"></icon-provider> <icon-provider provider-id="googleDrive"></icon-provider>
</div> </div>
<p>This will publish <b>{{currentFileName}}</b> to your <b>Google Drive</b> account.</p> <p>This will publish <b>{{currentFileName}}</b> to your <b>Google Drive</b> account.</p>
<form-entry label="Folder ID (optional)"> <form-entry label="Folder ID" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="folderId" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="folderId" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
If no folder ID is supplied, the file will be created in your root folder. If no folder ID is supplied, the file will be created in your root folder.
@ -14,7 +14,7 @@
<a href="javascript:void(0)" @click="openFolder">Choose folder</a> <a href="javascript:void(0)" @click="openFolder">Choose folder</a>
</div> </div>
</form-entry> </form-entry>
<form-entry label="Existing file ID (optional)"> <form-entry label="Existing file ID" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="fileId" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="fileId" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
This will overwrite the file on the server. This will overwrite the file on the server.

View File

@ -5,7 +5,7 @@
<icon-provider provider-id="googleDrive"></icon-provider> <icon-provider provider-id="googleDrive"></icon-provider>
</div> </div>
<p>This will save <b>{{currentFileName}}</b> to your <b>Google Drive</b> account and keep it synchronized.</p> <p>This will save <b>{{currentFileName}}</b> to your <b>Google Drive</b> account and keep it synchronized.</p>
<form-entry label="Folder ID (optional)"> <form-entry label="Folder ID" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="folderId" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="folderId" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
If no folder ID is supplied, the file will be created in your root folder. If no folder ID is supplied, the file will be created in your root folder.
@ -14,7 +14,7 @@
<a href="javascript:void(0)" @click="openFolder">Choose folder</a> <a href="javascript:void(0)" @click="openFolder">Choose folder</a>
</div> </div>
</form-entry> </form-entry>
<form-entry label="Existing file ID (optional)"> <form-entry label="Existing file ID" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="fileId" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="fileId" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
This will overwrite the file on the server. This will overwrite the file on the server.

View File

@ -5,7 +5,7 @@
<icon-provider provider-id="googleDrive"></icon-provider> <icon-provider provider-id="googleDrive"></icon-provider>
</div> </div>
<p>This will create a workspace synchronized with a <b>Google Drive</b> folder.</p> <p>This will create a workspace synchronized with a <b>Google Drive</b> folder.</p>
<form-entry label="Folder ID (optional)"> <form-entry label="Folder ID" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="folderId" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="folderId" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
If no folder ID is supplied, a new workspace folder will be created in your root folder. If no folder ID is supplied, a new workspace folder will be created in your root folder.

View File

@ -2,10 +2,10 @@
<modal-inner class="modal__inner-1--google-photo" aria-label="Import Google Photo"> <modal-inner class="modal__inner-1--google-photo" aria-label="Import Google Photo">
<div class="modal__content"> <div class="modal__content">
<div class="google-photo__tumbnail" :style="{backgroundImage: thumbnailUrl}"></div> <div class="google-photo__tumbnail" :style="{backgroundImage: thumbnailUrl}"></div>
<form-entry label="Title (optional)"> <form-entry label="Title" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="title" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="title" @keydown.enter="resolve()">
</form-entry> </form-entry>
<form-entry label="Size limit (optional)"> <form-entry label="Size limit" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="size" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="size" @keydown.enter="resolve()">
</form-entry> </form-entry>
</div> </div>

View File

@ -12,7 +12,7 @@
<b>Jetpack plugin</b> is required for self-hosted sites. <b>Jetpack plugin</b> is required for self-hosted sites.
</div> </div>
</form-entry> </form-entry>
<form-entry label="Existing post ID (optional)"> <form-entry label="Existing post ID" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="postId" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="postId" @keydown.enter="resolve()">
</form-entry> </form-entry>
<form-entry label="Template"> <form-entry label="Template">

View File

@ -11,10 +11,10 @@
https://example.zendesk.com/hc/en-us/sections/<b>21857469</b>-Section-name https://example.zendesk.com/hc/en-us/sections/<b>21857469</b>-Section-name
</div> </div>
</form-entry> </form-entry>
<form-entry label="Existing article ID (optional)"> <form-entry label="Existing article ID" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="articleId" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="articleId" @keydown.enter="resolve()">
</form-entry> </form-entry>
<form-entry label="Locale (optional)"> <form-entry label="Locale" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="locale" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="locale" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
<b>Default:</b> en-us <b>Default:</b> en-us

View File

@ -1,63 +0,0 @@
## File properties can contain metadata used
## for your publications (Wordpress, Blogger...).
## For example:
#title: My article
#author:
#tags: Tag 1, Tag 2
#categories: Categorie 1, Categorie 2
#excerpt:
#featuredImage:
#status: draft
#date: YYYY-MM-DD HH:MM:SS
## Extensions configuration
## Preset can be `default`, `commonmark`, `gfm` or `zero`.
## Use preset `zero` to manually switch on extensions.
extensions:
preset: default
## Markdown extension
#markdown:
#abbr: true
#breaks: true
#deflist: true
#del: true
#fence: true
#footnote: true
#imgsize: true
#linkify: true
#mark: true
#sub: true
#sup: true
#table: true
#tasklist: true
#typographer: true
## Emoji extension
#emoji:
#enabled: true
## Enable shortcuts like :) :-(
## These are disabled by default.
#shortcuts: false
## Katex extension
## Render LaTeX mathematical expressions by using:
## $...$ for inline formulas
## $$...$$ for displayed formulas.
## See https://math.meta.stackexchange.com/questions/5020
#katex:
#enabled: true
## Mermaid extension
## Convert code blocks starting with ```mermaid
## into diagrams and flowcharts.
## See https://mermaidjs.github.io/
#mermaid:
#enabled: true

View File

@ -1,5 +1,6 @@
export default () => ({ export default () => ({
welcomeFileHashes: {}, welcomeFileHashes: {},
filePropertiesTab: '',
htmlExportTemplate: 'styledHtml', htmlExportTemplate: 'styledHtml',
pdfExportTemplate: 'styledHtml', pdfExportTemplate: 'styledHtml',
pandocExportFormat: 'pdf', pandocExportFormat: 'pdf',

View File

@ -24,15 +24,18 @@ shortcuts:
mod+alt+f: replace mod+alt+f: replace
mod+g: replace mod+g: replace
mod+shift+b: bold mod+shift+b: bold
mod+shift+i: italic mod+shift+c: clist
mod+shift+l: link
mod+shift+q: quote
mod+shift+k: code mod+shift+k: code
mod+shift+g: image
mod+shift+o: olist
mod+shift+u: ulist
mod+shift+h: heading mod+shift+h: heading
mod+shift+r: hr mod+shift+r: hr
mod+shift+g: image
mod+shift+i: italic
mod+shift+l: link
mod+shift+o: olist
mod+shift+q: quote
mod+shift+s: strikethrough
mod+shift+t: table
mod+shift+u: ulist
'= = > space': '= = > space':
method: expand method: expand
params: params:
@ -84,6 +87,5 @@ newFileContent: |
# Default properties for new files # Default properties for new files
newFileProperties: | newFileProperties: |
# extensions: # extensions:
# markdown: # preset: gfm
# breaks: false

View File

@ -1,49 +1,49 @@
export default [{}, { export default [{}, {
action: 'bold', method: 'bold',
title: 'Bold', title: 'Bold',
icon: 'format-bold', icon: 'format-bold',
}, { }, {
action: 'italic', method: 'italic',
title: 'Italic', title: 'Italic',
icon: 'format-italic', icon: 'format-italic',
}, { }, {
action: 'heading', method: 'heading',
title: 'Heading', title: 'Heading',
icon: 'format-size', icon: 'format-size',
}, { }, {
action: 'strikethrough', method: 'strikethrough',
title: 'Strikethrough', title: 'Strikethrough',
icon: 'format-strikethrough', icon: 'format-strikethrough',
}, {}, { }, {}, {
action: 'ulist', method: 'ulist',
title: 'Unordered list', title: 'Unordered list',
icon: 'format-list-bulleted', icon: 'format-list-bulleted',
}, { }, {
action: 'olist', method: 'olist',
title: 'Ordered list', title: 'Ordered list',
icon: 'format-list-numbers', icon: 'format-list-numbers',
}, { }, {
action: 'clist', method: 'clist',
title: 'Check list', title: 'Check list',
icon: 'format-list-checks', icon: 'format-list-checks',
}, {}, { }, {}, {
action: 'quote', method: 'quote',
title: 'Blockquote', title: 'Blockquote',
icon: 'format-quote-close', icon: 'format-quote-close',
}, { }, {
action: 'code', method: 'code',
title: 'Code', title: 'Code',
icon: 'code-tags', icon: 'code-tags',
}, { }, {
action: 'table', method: 'table',
title: 'Table', title: 'Table',
icon: 'table', icon: 'table',
}, { }, {
action: 'link', method: 'link',
title: 'Link', title: 'Link',
icon: 'link-variant', icon: 'link-variant',
}, { }, {
action: 'image', method: 'image',
title: 'Image', title: 'Image',
icon: 'file-image', icon: 'file-image',
}]; }];

View File

@ -1,4 +1,5 @@
const zero = { const zero = {
// Markdown extensions
markdown: { markdown: {
abbr: false, abbr: false,
breaks: false, breaks: false,
@ -15,13 +16,28 @@ const zero = {
tasklist: false, tasklist: false,
typographer: false, typographer: false,
}, },
// Emoji extension
emoji: { emoji: {
enabled: false, enabled: false,
// Enable shortcuts like :) :-(
shortcuts: false, shortcuts: false,
}, },
/*
Katex extension
Render LaTeX mathematical expressions using:
$...$ for inline formulas
$$...$$ for displayed formulas.
See https://math.meta.stackexchange.com/questions/5020
*/
katex: { katex: {
enabled: false, enabled: false,
}, },
/*
Mermaid extension
Convert code blocks starting with ```mermaid
into diagrams and flowcharts.
See https://mermaidjs.github.io/
*/
mermaid: { mermaid: {
enabled: false, enabled: false,
}, },

View File

@ -29,6 +29,7 @@ const methods = {
image: pagedownHandler('image'), image: pagedownHandler('image'),
olist: pagedownHandler('olist'), olist: pagedownHandler('olist'),
ulist: pagedownHandler('ulist'), ulist: pagedownHandler('ulist'),
clist: pagedownHandler('clist'),
heading: pagedownHandler('heading'), heading: pagedownHandler('heading'),
hr: pagedownHandler('hr'), hr: pagedownHandler('hr'),
sync() { sync() {
@ -66,8 +67,7 @@ store.watch(
(computedSettings) => { (computedSettings) => {
Mousetrap.reset(); Mousetrap.reset();
const shortcuts = computedSettings.shortcuts; Object.entries(computedSettings.shortcuts).forEach(([key, shortcut]) => {
Object.entries(shortcuts).forEach(([key, shortcut]) => {
if (shortcut) { if (shortcut) {
const method = `${shortcut.method || shortcut}`; const method = `${shortcut.method || shortcut}`;
let params = shortcut.params || []; let params = shortcut.params || [];

View File

@ -1,6 +1,5 @@
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import '../libs/clunderscore'; import '../libs/clunderscore';
import defaultPropertiesYaml from '../data/defaultFileProperties.yml';
import presets from '../data/presets'; import presets from '../data/presets';
const origin = `${location.protocol}//${location.host}`; const origin = `${location.protocol}//${location.host}`;
@ -56,13 +55,14 @@ const deepCopy = (obj) => {
return JSON.parse(JSON.stringify(obj)); return JSON.parse(JSON.stringify(obj));
}; };
// Build presets // Compute presets
const computedPresets = {};
Object.keys(presets).forEach((key) => { Object.keys(presets).forEach((key) => {
let preset = deepCopy(presets[key][0]); let preset = deepCopy(presets[key][0]);
if (presets[key][1]) { if (presets[key][1]) {
preset = deepOverride(preset, presets[key][1]); preset = deepOverride(preset, presets[key][1]);
} }
presets[key] = preset; computedPresets[key] = preset;
}); });
export default { export default {
@ -154,13 +154,17 @@ export default {
c => `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`).join('')); c => `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`).join(''));
}, },
computeProperties(yamlProperties) { computeProperties(yamlProperties) {
const customProperties = yaml.safeLoad(yamlProperties); let properties = {};
const defaultProperties = yaml.safeLoad(defaultPropertiesYaml); try {
const properties = deepOverride(defaultProperties, customProperties); properties = yaml.safeLoad(yamlProperties) || {};
const preset = deepCopy(presets[properties.extensions.preset] || presets.default); } catch (e) {
const extensions = deepOverride(preset, properties.extensions); // Ignore
extensions.preset = properties.extensions.preset; }
properties.extensions = extensions; const extensions = properties.extensions || {};
const computedPreset = deepCopy(computedPresets[extensions.preset] || computedPresets.default);
const computedExtensions = deepOverride(computedPreset, properties.extensions);
computedExtensions.preset = extensions.preset;
properties.extensions = computedExtensions;
return properties; return properties;
}, },
randomize(value) { randomize(value) {

View File

@ -3,7 +3,7 @@ import pagedownButtons from '../data/pagedownButtons';
let buttonCount = 0; let buttonCount = 0;
let spacerCount = 0; let spacerCount = 0;
pagedownButtons.forEach((button) => { pagedownButtons.forEach((button) => {
if (button.action) { if (button.method) {
buttonCount += 1; buttonCount += 1;
} else { } else {
spacerCount += 1; spacerCount += 1;