Added keystrokes and shortcuts. Added templates
This commit is contained in:
parent
cc2f9fa204
commit
0b0bec15e2
@ -63,7 +63,7 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.md$/,
|
||||
test: /\.(md|yml|html)$/,
|
||||
loader: 'raw-loader'
|
||||
}
|
||||
]
|
||||
|
@ -12,9 +12,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"bezier-easing": "^1.1.0",
|
||||
"clipboard": "^1.7.1",
|
||||
"clunderscore": "^1.0.3",
|
||||
"diff-match-patch": "^1.0.0",
|
||||
"file-saver": "^1.3.3",
|
||||
"handlebars": "^4.0.10",
|
||||
"indexeddbshim": "^3.0.4",
|
||||
"js-yaml": "^3.9.1",
|
||||
"katex": "^0.7.1",
|
||||
"markdown-it": "^8.3.1",
|
||||
"markdown-it-abbr": "^1.0.4",
|
||||
@ -24,6 +28,7 @@
|
||||
"markdown-it-pandoc-renderer": "1.1.3",
|
||||
"markdown-it-sub": "^1.0.0",
|
||||
"markdown-it-sup": "^1.0.0",
|
||||
"mousetrap": "^1.6.1",
|
||||
"normalize-scss": "^7.0.0",
|
||||
"prismjs": "^1.6.0",
|
||||
"raw-loader": "^0.5.1",
|
||||
@ -79,7 +84,8 @@
|
||||
"webpack-bundle-analyzer": "^2.2.1",
|
||||
"webpack-dev-middleware": "^1.10.0",
|
||||
"webpack-hot-middleware": "^2.18.0",
|
||||
"webpack-merge": "^4.1.0"
|
||||
"webpack-merge": "^4.1.0",
|
||||
"worker-loader": "^0.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4.0.0",
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 6.7 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 174 KiB |
@ -7,10 +7,18 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue';
|
||||
import { mapState } from 'vuex';
|
||||
import Layout from './Layout';
|
||||
import Modal from './Modal';
|
||||
|
||||
// Global directives
|
||||
Vue.directive('focus', {
|
||||
inserted(el) {
|
||||
el.focus();
|
||||
},
|
||||
});
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Layout,
|
||||
@ -24,7 +32,7 @@ export default {
|
||||
return !this.$store.getters['content/current'].id;
|
||||
},
|
||||
showModal() {
|
||||
return !!this.$store.state.modal.content;
|
||||
return !!this.$store.state.modal.config;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
52
src/components/CodeEditor.vue
Normal file
52
src/components/CodeEditor.vue
Normal file
@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<pre class="code-editor textfield prism" :disabled="disabled"></pre>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Prism from 'prismjs';
|
||||
import cledit from '../libs/cledit';
|
||||
|
||||
export default {
|
||||
props: ['value', 'lang', 'disabled'],
|
||||
mounted() {
|
||||
const preElt = this.$el;
|
||||
let scrollElt = preElt;
|
||||
while (scrollElt && !scrollElt.classList.contains('modal')) {
|
||||
scrollElt = scrollElt.parentNode;
|
||||
}
|
||||
if (scrollElt) {
|
||||
const clEditor = cledit(preElt, scrollElt);
|
||||
clEditor.init({
|
||||
content: this.value,
|
||||
sectionHighlighter: section => Prism.highlight(section.text, Prism.languages[this.lang]),
|
||||
});
|
||||
clEditor.on('contentChanged', value => this.$emit('changed', value));
|
||||
clEditor.toggleEditable(!this.disabled);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import 'common/variables.scss';
|
||||
|
||||
.code-editor {
|
||||
margin: 0;
|
||||
font-family: $font-family-monospace;
|
||||
font-size: $font-size-monospace;
|
||||
font-variant-ligatures: no-common-ligatures;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
word-wrap: normal;
|
||||
height: auto;
|
||||
caret-color: #000;
|
||||
min-height: 160px;
|
||||
overflow: auto;
|
||||
padding: 0.2em 0.4em;
|
||||
|
||||
* {
|
||||
line-height: $line-height-base;
|
||||
font-size: inherit !important;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="editor">
|
||||
<pre class="editor__inner markdown-highlighting" :style="{padding: styles.editorPadding}"></pre>
|
||||
<pre class="editor__inner markdown-highlighting" :style="{padding: styles.editorPadding}" :class="{monospaced: computedSettings.editor.monospacedFontOnly}"></pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -9,9 +9,14 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
computed: mapGetters('layout', [
|
||||
computed: {
|
||||
...mapGetters('layout', [
|
||||
'styles',
|
||||
]),
|
||||
...mapGetters('data', [
|
||||
'computedSettings',
|
||||
]),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -24,13 +24,6 @@ import defaultContent from '../data/defaultContent.md';
|
||||
export default {
|
||||
name: 'explorer-node',
|
||||
props: ['node', 'depth'],
|
||||
directives: {
|
||||
focus: {
|
||||
inserted(el) {
|
||||
el.focus();
|
||||
},
|
||||
},
|
||||
},
|
||||
data: () => ({
|
||||
editingValue: '',
|
||||
}),
|
||||
|
84
src/components/FilePropertiesModal.vue
Normal file
84
src/components/FilePropertiesModal.vue
Normal file
@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div class="modal__inner-1 modal__inner-1--file-properties">
|
||||
<div class="modal__inner-2">
|
||||
<div class="tabs">
|
||||
<div class="tabs__tab" :class="{'tabs__tab--active': tab === 'custom'}" @click="tab = 'custom'">
|
||||
Current file properties
|
||||
</div>
|
||||
<div class="tabs__tab" :class="{'tabs__tab--active': tab === 'default'}" @click="tab = 'default'">
|
||||
Default properties
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-entry">
|
||||
<label class="form-entry__label">YAML</label>
|
||||
<div class="form-entry__field">
|
||||
<code-editor v-if="tab === 'custom'" lang="yaml" :value="customProperties" key="custom-properties" @changed="setCustomProperties"></code-editor>
|
||||
<code-editor v-else lang="yaml" :value="defaultProperties" disabled="true" key="default-properties"></code-editor>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal__error modal__error--file-properties">{{error}}</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.reject()">Cancel</button>
|
||||
<button class="button" @click="!error && config.resolve(strippedCustomProperties)">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import yaml from 'js-yaml';
|
||||
import { mapState } from 'vuex';
|
||||
import CodeEditor from './CodeEditor';
|
||||
import defaultProperties from '../data/defaultFileProperties.yml';
|
||||
|
||||
const emptyProperties = '# Add custom properties for the current file here to override the default properties.\n';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CodeEditor,
|
||||
},
|
||||
data: () => ({
|
||||
tab: 'custom',
|
||||
defaultProperties,
|
||||
customProperties: null,
|
||||
error: null,
|
||||
}),
|
||||
computed: {
|
||||
...mapState('modal', [
|
||||
'config',
|
||||
]),
|
||||
strippedCustomProperties() {
|
||||
return this.customProperties === emptyProperties ? '\n' : this.customProperties.replace(/\t/g, ' ');
|
||||
},
|
||||
},
|
||||
created() {
|
||||
const properties = this.$store.getters['content/current'].properties;
|
||||
this.setCustomProperties(properties === '\n' ? emptyProperties : properties);
|
||||
},
|
||||
methods: {
|
||||
setCustomProperties(value) {
|
||||
this.customProperties = value;
|
||||
try {
|
||||
yaml.safeLoad(this.strippedCustomProperties);
|
||||
this.error = null;
|
||||
} catch (e) {
|
||||
this.error = e.message;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import 'common/variables.scss';
|
||||
|
||||
.modal__inner-1--file-properties {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.modal__error--file-properties {
|
||||
white-space: pre-wrap;
|
||||
font-family: $font-family-monospace;
|
||||
font-size: $font-size-monospace;
|
||||
}
|
||||
</style>
|
73
src/components/GooglePhotoModal.vue
Normal file
73
src/components/GooglePhotoModal.vue
Normal file
@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div class="modal__inner-1 modal__inner-1--google-photo">
|
||||
<div class="modal__inner-2">
|
||||
<div class="google-photo__tumbnail" :style="{'background-image': thumbnailUrl}"></div>
|
||||
<div class="form-entry">
|
||||
<label class="form-entry__label" for="title">Title (optional)</label>
|
||||
<div class="form-entry__field">
|
||||
<input id="title" type="text" class="textfield" v-model.trim="title" @keyup.enter="resolve()">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-entry">
|
||||
<label class="form-entry__label" for="size">Size limit (optional)</label>
|
||||
<div class="form-entry__field">
|
||||
<input id="size" type="text" class="textfield" v-model="size" @keyup.enter="resolve()">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="reject()">Cancel</button>
|
||||
<button class="button" @click="resolve()">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
const makeThumbnail = (url, size) => `${url}=s${size}`;
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
title: '',
|
||||
size: '',
|
||||
}),
|
||||
computed: {
|
||||
thumbnailUrl() {
|
||||
return `url(${makeThumbnail(this.config.url, 320)})`;
|
||||
},
|
||||
...mapState('modal', [
|
||||
'config',
|
||||
]),
|
||||
},
|
||||
methods: {
|
||||
resolve() {
|
||||
let url = this.config.url;
|
||||
const size = parseInt(this.size, 10);
|
||||
if (!isNaN(size)) {
|
||||
url = makeThumbnail(url, size);
|
||||
}
|
||||
if (this.title) {
|
||||
url += ` "${this.title}"`;
|
||||
}
|
||||
const callback = this.config.callback;
|
||||
this.config.resolve();
|
||||
callback(url);
|
||||
},
|
||||
reject() {
|
||||
const callback = this.config.callback;
|
||||
this.config.reject();
|
||||
callback(null);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.google-photo__tumbnail {
|
||||
height: 160px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
</style>
|
87
src/components/HtmlExportModal.vue
Normal file
87
src/components/HtmlExportModal.vue
Normal file
@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div class="modal__inner-1 modal__inner-1--html-export">
|
||||
<div class="modal__inner-2">
|
||||
<div class="form-entry">
|
||||
<label class="form-entry__label" for="template">Template</label>
|
||||
<div class="form-entry__field">
|
||||
<select id="template" v-model="selectedTemplate" class="textfield">
|
||||
<option v-for="(template, id) in allTemplates" :key="id" v-bind:value="id">
|
||||
{{ template.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-entry__actions">
|
||||
<a href="javascript:void(0)" @click="configureTemplates">Configure templates</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button button--copy">Copy to clipboard</button>
|
||||
<button class="button" @click="config.reject()">Cancel</button>
|
||||
<button class="button" @click="resolve()">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
import Clipboard from 'clipboard';
|
||||
import exportSvc from '../services/exportSvc';
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
result: '',
|
||||
}),
|
||||
computed: {
|
||||
...mapState('modal', [
|
||||
'config',
|
||||
]),
|
||||
...mapGetters('data', [
|
||||
'allTemplates',
|
||||
]),
|
||||
selectedTemplate: {
|
||||
get() {
|
||||
return this.$store.getters['data/localSettings'].htmlExportLastTemplate;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.dispatch('data/patchLocalSettings', {
|
||||
htmlExportLastTemplate: value,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$watch('selectedTemplate', (selectedTemplate) => {
|
||||
const currentFile = this.$store.getters['file/current'];
|
||||
exportSvc.applyTemplate(currentFile.id, this.allTemplates[selectedTemplate])
|
||||
.then((res) => {
|
||||
this.result = res;
|
||||
});
|
||||
}, {
|
||||
immediate: true,
|
||||
});
|
||||
this.clipboard = new Clipboard(this.$el.querySelector('.button--copy'), {
|
||||
text: () => this.result,
|
||||
});
|
||||
},
|
||||
destroyed() {
|
||||
this.clipboard.destroy();
|
||||
},
|
||||
methods: {
|
||||
configureTemplates() {
|
||||
const reopen = () => this.$store.dispatch('modal/open', 'htmlExport');
|
||||
this.$store.dispatch('modal/open', {
|
||||
type: 'templates',
|
||||
selectedKey: this.selectedTemplate,
|
||||
})
|
||||
.then(templates => this.$store.dispatch('data/setTemplates', templates))
|
||||
.then(reopen, reopen);
|
||||
},
|
||||
resolve() {
|
||||
const currentFile = this.$store.getters['file/current'];
|
||||
exportSvc.exportToDisk(currentFile.id, 'html', this.allTemplates[this.selectedTemplate]);
|
||||
this.config.resolve();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
79
src/components/ImageModal.vue
Normal file
79
src/components/ImageModal.vue
Normal file
@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div class="modal__inner-1 modal__inner-1--image">
|
||||
<div class="modal__inner-2">
|
||||
<div class="form-entry">
|
||||
<label class="form-entry__label" for="url">URL</label>
|
||||
<div class="form-entry__field">
|
||||
<input id="url" type="text" class="textfield" v-model="url" @keyup.enter="resolve()">
|
||||
</div>
|
||||
</div>
|
||||
<menu-entry @click.native="openGooglePhotos(token)" v-for="token in googlePhotosTokens" :key="token.sub">
|
||||
<icon-google-photos slot="icon"></icon-google-photos>
|
||||
<div>Open from Google Photos</div>
|
||||
<span>{{token.name}}</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="addGooglePhotosAccount">
|
||||
<icon-google-photos slot="icon"></icon-google-photos>
|
||||
<span>Add Google Photos account</span>
|
||||
</menu-entry>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="reject()">Cancel</button>
|
||||
<button class="button" @click="resolve()">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import MenuEntry from './MenuEntry';
|
||||
import googleHelper from '../services/providers/helpers/googleHelper';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MenuEntry,
|
||||
},
|
||||
data: () => ({
|
||||
url: '',
|
||||
}),
|
||||
computed: {
|
||||
...mapState('modal', [
|
||||
'config',
|
||||
]),
|
||||
googlePhotosTokens() {
|
||||
const googleToken = this.$store.getters['data/googleTokens'];
|
||||
return Object.keys(googleToken)
|
||||
.map(sub => googleToken[sub])
|
||||
.filter(token => token.isPhotos)
|
||||
.sort((token1, token2) => token1.name.localeCompare(token2.name));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
resolve() {
|
||||
if (this.url) {
|
||||
const callback = this.config.callback;
|
||||
this.config.resolve();
|
||||
callback(this.url);
|
||||
}
|
||||
},
|
||||
reject() {
|
||||
const callback = this.config.callback;
|
||||
this.config.reject();
|
||||
callback(null);
|
||||
},
|
||||
addGooglePhotosAccount() {
|
||||
return googleHelper.addPhotosAccount();
|
||||
},
|
||||
openGooglePhotos(token) {
|
||||
const callback = this.config.callback;
|
||||
this.config.reject();
|
||||
googleHelper.openPicker(token, 'img')
|
||||
.then(res => res[0] && this.$store.dispatch('modal/open', {
|
||||
type: 'googlePhoto',
|
||||
url: res[0].url,
|
||||
callback,
|
||||
}));
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
43
src/components/LinkModal.vue
Normal file
43
src/components/LinkModal.vue
Normal file
@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div class="modal__inner-1 modal__inner-1--link" @keyup.enter="resolve()">
|
||||
<div class="modal__inner-2">
|
||||
<div class="form-entry">
|
||||
<label class="form-entry__label" for="url">URL</label>
|
||||
<div class="form-entry__field">
|
||||
<input id="url" type="text" class="textfield" v-model="url">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="reject()">Cancel</button>
|
||||
<button class="button" @click="resolve()">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
url: '',
|
||||
}),
|
||||
computed: mapState('modal', [
|
||||
'config',
|
||||
]),
|
||||
methods: {
|
||||
resolve() {
|
||||
if (this.url) {
|
||||
const callback = this.config.callback;
|
||||
this.config.resolve();
|
||||
callback(this.url);
|
||||
}
|
||||
},
|
||||
reject() {
|
||||
const callback = this.config.callback;
|
||||
this.config.reject();
|
||||
callback(null);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
@ -1,18 +1,18 @@
|
||||
<template>
|
||||
<div class="side-bar-item button flex flex--row flex--align-center">
|
||||
<div class="side-bar-item__icon flex flex--column flex--center">
|
||||
<a href="javascript:void(0)" class="menu-entry button flex flex--row flex--align-center">
|
||||
<div class="menu-entry__icon flex flex--column flex--center">
|
||||
<slot name="icon"></slot>
|
||||
</div>
|
||||
<div class="flex flex--column">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.side-bar-item {
|
||||
.menu-entry {
|
||||
text-align: left;
|
||||
padding: 10px 12px;
|
||||
padding: 10px;
|
||||
height: auto;
|
||||
|
||||
span {
|
||||
@ -24,7 +24,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.side-bar-item__icon {
|
||||
.menu-entry__icon {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
margin-right: 12px;
|
@ -1,10 +1,18 @@
|
||||
<template>
|
||||
<div class="modal" v-focus @keyup.esc="setContent()">
|
||||
<div class="modal__inner-1">
|
||||
<div class="modal" @keyup.esc="onEscape">
|
||||
<file-properties-modal v-if="config.type === 'fileProperties'"></file-properties-modal>
|
||||
<settings-modal v-else-if="config.type === 'settings'"></settings-modal>
|
||||
<templates-modal v-else-if="config.type === 'templates'"></templates-modal>
|
||||
<link-modal v-else-if="config.type === 'link'"></link-modal>
|
||||
<image-modal v-else-if="config.type === 'image'"></image-modal>
|
||||
<google-photo-modal v-else-if="config.type === 'googlePhoto'"></google-photo-modal>
|
||||
<html-export-modal v-else-if="config.type === 'htmlExport'"></html-export-modal>
|
||||
<div v-else class="modal__inner-1">
|
||||
<div class="modal__inner-2">
|
||||
<div class="modal__content-text" v-html="content.text"></div>
|
||||
<div class="modal__content" v-html="config.content"></div>
|
||||
<div class="modal__button-bar">
|
||||
<button v-for="button in content.buttons" :key="button.text" class="button" @click="button.onClick()">{{button.text}}</button>
|
||||
<button v-if="config.rejectText" class="button" @click="config.reject()">{{config.rejectText}}</button>
|
||||
<button v-if="config.resolveText" class="button" @click="config.resolve()">{{config.resolveText}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -13,27 +21,46 @@
|
||||
|
||||
<script>
|
||||
import { mapState, mapMutations } from 'vuex';
|
||||
import FilePropertiesModal from './FilePropertiesModal';
|
||||
import SettingsModal from './SettingsModal';
|
||||
import TemplatesModal from './TemplatesModal';
|
||||
import LinkModal from './LinkModal';
|
||||
import ImageModal from './ImageModal';
|
||||
import GooglePhotoModal from './GooglePhotoModal';
|
||||
import HtmlExportModal from './HtmlExportModal';
|
||||
import editorEngineSvc from '../services/editorEngineSvc';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
focus: {
|
||||
inserted(el) {
|
||||
const eltToFocus = el.querySelector('input.text-input') || el.querySelector('button.button');
|
||||
if (eltToFocus) {
|
||||
eltToFocus.focus();
|
||||
}
|
||||
},
|
||||
},
|
||||
components: {
|
||||
FilePropertiesModal,
|
||||
SettingsModal,
|
||||
TemplatesModal,
|
||||
LinkModal,
|
||||
ImageModal,
|
||||
GooglePhotoModal,
|
||||
HtmlExportModal,
|
||||
},
|
||||
computed: mapState('modal', [
|
||||
'content',
|
||||
'config',
|
||||
]),
|
||||
methods: {
|
||||
...mapMutations('modal', [
|
||||
'setContent',
|
||||
'setConfig',
|
||||
]),
|
||||
hideOnExternalEvent(evt) {
|
||||
if (this.content) {
|
||||
onEscape() {
|
||||
this.setConfig();
|
||||
editorEngineSvc.clEditor.focus();
|
||||
},
|
||||
onFocusInOut(evt) {
|
||||
const isFocusIn = evt.type === 'focusin';
|
||||
if (evt.target.parentNode && evt.target.parentNode.parentNode) {
|
||||
// Focus effect
|
||||
if (evt.target.parentNode.classList.contains('form-entry__field') &&
|
||||
evt.target.parentNode.parentNode.classList.contains('form-entry')) {
|
||||
evt.target.parentNode.parentNode.classList.toggle('form-entry--focused', isFocusIn);
|
||||
}
|
||||
}
|
||||
if (isFocusIn && this.config) {
|
||||
const modalInner = this.$el.querySelector('.modal__inner-2');
|
||||
let target = evt.target;
|
||||
while (target) {
|
||||
@ -42,15 +69,23 @@ export default {
|
||||
}
|
||||
target = target.parentNode;
|
||||
}
|
||||
this.setContent();
|
||||
this.setConfig();
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('focusin', this.hideOnExternalEvent);
|
||||
window.addEventListener('focusin', this.onFocusInOut);
|
||||
window.addEventListener('focusout', this.onFocusInOut);
|
||||
const eltToFocus = this.$el.querySelector('input.text-input')
|
||||
|| this.$el.querySelector('.textfield')
|
||||
|| this.$el.querySelector('button.button');
|
||||
if (eltToFocus) {
|
||||
eltToFocus.focus();
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('focusin', this.hideOnExternalEvent);
|
||||
window.removeEventListener('focusin', this.onFocusInOut);
|
||||
window.removeEventListener('focusout', this.onFocusInOut);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -62,22 +97,53 @@ export default {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(192, 192, 192, 0.8);
|
||||
background-color: rgba(180, 180, 180, 0.75);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.modal__inner-1 {
|
||||
margin: 0 auto;
|
||||
display: table;
|
||||
width: 100%;
|
||||
min-width: 320px;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.modal__inner-2 {
|
||||
margin: 50px 10px;
|
||||
margin: 50px 10px 100px;
|
||||
background-color: #fff;
|
||||
padding: 25px 50px;
|
||||
padding: 40px 50px 30px;
|
||||
border-radius: $border-radius-base;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: $border-radius-base;
|
||||
width: 100%;
|
||||
background-image: linear-gradient(to left, #ffe600, #ffe600 25%, #bbd500 25%, #bbd500 50%, #ff8a00 50%, #ff8a00 75%, #75b7fd 75%);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: $border-radius-base;
|
||||
width: 100%;
|
||||
background-image: linear-gradient(to right, #ffe600, #ffe600 25%, #bbd500 25%, #bbd500 50%, #ff8a00 50%, #ff8a00 75%, #75b7fd 75%);
|
||||
}
|
||||
}
|
||||
|
||||
.modal__content :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.modal__error {
|
||||
color: #de2c00;
|
||||
}
|
||||
|
||||
.modal__button-bar {
|
||||
|
@ -6,8 +6,8 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="navigation-bar__inner navigation-bar__inner--right navigation-bar__inner--button">
|
||||
<button class="navigation-bar__button button" @click="toggleSideBar()">
|
||||
<icon-menu></icon-menu>
|
||||
<button class="navigation-bar__button navigation-bar__button--stackedit button" @click="toggleSideBar()">
|
||||
<icon-stackedit></icon-stackedit>
|
||||
</button>
|
||||
</div>
|
||||
<div class="navigation-bar__inner navigation-bar__inner--right flex flex--row">
|
||||
@ -50,7 +50,7 @@
|
||||
<icon-format-quote-close></icon-format-quote-close>
|
||||
</button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('code')">
|
||||
<icon-code-braces></icon-code-braces>
|
||||
<icon-code-tags></icon-code-tags>
|
||||
</button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('link')">
|
||||
<icon-link-variant></icon-link-variant>
|
||||
@ -212,8 +212,19 @@ export default {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.navigation-bar__inner--button & {
|
||||
padding: 7px;
|
||||
padding: 6px;
|
||||
width: 38px;
|
||||
|
||||
&.navigation-bar__button--stackedit {
|
||||
opacity: 0.8;
|
||||
padding: 4px;
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
84
src/components/SettingsModal.vue
Normal file
84
src/components/SettingsModal.vue
Normal file
@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div class="modal__inner-1 modal__inner-1--settings">
|
||||
<div class="modal__inner-2">
|
||||
<div class="tabs">
|
||||
<div class="tabs__tab" :class="{'tabs__tab--active': tab === 'custom'}" @click="tab = 'custom'">
|
||||
Custom settings
|
||||
</div>
|
||||
<div class="tabs__tab" :class="{'tabs__tab--active': tab === 'default'}" @click="tab = 'default'">
|
||||
Default settings
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-entry">
|
||||
<label class="form-entry__label">YAML</label>
|
||||
<div class="form-entry__field">
|
||||
<code-editor v-if="tab === 'custom'" lang="yaml" :value="customSettings" key="custom-settings" @changed="setCustomSettings"></code-editor>
|
||||
<code-editor v-else lang="yaml" :value="defaultSettings" disabled="true" key="default-settings"></code-editor>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal__error modal__error--settings">{{error}}</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.reject()">Cancel</button>
|
||||
<button class="button" @click="!error && config.resolve(strippedCustomSettings)">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import yaml from 'js-yaml';
|
||||
import { mapState } from 'vuex';
|
||||
import CodeEditor from './CodeEditor';
|
||||
import defaultSettings from '../data/defaultSettings.yml';
|
||||
|
||||
const emptySettings = '# Add your custom settings here to override the default settings.\n';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CodeEditor,
|
||||
},
|
||||
data: () => ({
|
||||
tab: 'custom',
|
||||
defaultSettings,
|
||||
customSettings: null,
|
||||
error: null,
|
||||
}),
|
||||
computed: {
|
||||
...mapState('modal', [
|
||||
'config',
|
||||
]),
|
||||
strippedCustomSettings() {
|
||||
return this.customSettings === emptySettings ? '\n' : this.customSettings.replace(/\t/g, ' ');
|
||||
},
|
||||
},
|
||||
created() {
|
||||
const settings = this.$store.getters['data/settings'];
|
||||
this.setCustomSettings(settings === '\n' ? emptySettings : settings);
|
||||
},
|
||||
methods: {
|
||||
setCustomSettings(value) {
|
||||
this.customSettings = value;
|
||||
try {
|
||||
yaml.safeLoad(this.strippedCustomSettings);
|
||||
this.error = null;
|
||||
} catch (e) {
|
||||
this.error = e.message;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import 'common/variables.scss';
|
||||
|
||||
.modal__inner-1--settings {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.modal__error--settings {
|
||||
white-space: pre-wrap;
|
||||
font-family: $font-family-monospace;
|
||||
font-size: $font-size-monospace;
|
||||
}
|
||||
</style>
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="side-bar flex flex--column">
|
||||
<div class="side-title flex flex--row">
|
||||
<button v-if="panel !== 'menu'" class="side-title__button button" @click="panel = 'menu'">
|
||||
<button v-if="panel !== 'menu'" class="side-title__button button" @click="setPanel('menu')">
|
||||
<icon-arrow-left></icon-arrow-left>
|
||||
</button>
|
||||
<div class="side-title__title">
|
||||
@ -12,30 +12,110 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="side-bar__inner">
|
||||
<!-- Main menu -->
|
||||
<div v-if="panel === 'menu'" class="side-bar__panel side-bar__panel--menu">
|
||||
<side-bar-item v-if="!loginToken" @click.native="signin">
|
||||
<menu-entry v-if="!loginToken" @click.native="signin">
|
||||
<icon-login slot="icon"></icon-login>
|
||||
<div>Sign in with Google</div>
|
||||
<span>Have all your files and settings backed up and synced.</span>
|
||||
</side-bar-item>
|
||||
<!-- <side-bar-item @click.native="signin">
|
||||
<icon-login slot="icon"></icon-login>
|
||||
<div>Sign in on CouchDB</div>
|
||||
<span>Save and collaborate on a CouchDB hosted by you.</span>
|
||||
</side-bar-item> -->
|
||||
<span>Back up and sync all your files, folders and settings.</span>
|
||||
</menu-entry>
|
||||
<hr v-if="!loginToken">
|
||||
<side-bar-item @click.native="panel = 'toc'">
|
||||
<menu-entry @click.native="setPanel('sync')">
|
||||
<icon-sync slot="icon"></icon-sync>
|
||||
<div>Synchronize</div>
|
||||
<span>Open, save, collaborate in the Cloud.</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="setPanel('publish')">
|
||||
<icon-upload slot="icon"></icon-upload>
|
||||
<div>Publish</div>
|
||||
<span>Export to the web.</span>
|
||||
</menu-entry>
|
||||
<hr>
|
||||
<menu-entry @click.native="fileProperties">
|
||||
<icon-view-list slot="icon"></icon-view-list>
|
||||
<div>File properties</div>
|
||||
<span>Add publication metadata and configure extensions.</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="setPanel('toc')">
|
||||
<icon-toc slot="icon"></icon-toc>
|
||||
Table of contents
|
||||
</side-bar-item>
|
||||
<side-bar-item @click.native="panel = 'help'">
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="setPanel('help')">
|
||||
<icon-help-circle slot="icon"></icon-help-circle>
|
||||
Markdown cheat sheet
|
||||
</side-bar-item>
|
||||
</menu-entry>
|
||||
<hr>
|
||||
<menu-entry @click.native="importFile">
|
||||
<icon-hard-disk slot="icon"></icon-hard-disk>
|
||||
Import from disk
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="setPanel('export')">
|
||||
<icon-hard-disk slot="icon"></icon-hard-disk>
|
||||
Export to disk
|
||||
</menu-entry>
|
||||
<hr>
|
||||
<menu-entry @click.native="setPanel('more')">
|
||||
More...
|
||||
</menu-entry>
|
||||
</div>
|
||||
<!-- Sync menu -->
|
||||
<div v-else-if="panel === 'sync'" class="side-bar__panel side-bar__panel--menu">
|
||||
<div v-for="token in googleDriveTokens" :key="token.sub">
|
||||
<menu-entry @click.native="openGoogleDrive(token)">
|
||||
<icon-google-drive slot="icon"></icon-google-drive>
|
||||
<div>Open from Google Drive</div>
|
||||
<span>{{token.name}}</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="saveGoogleDrive(token)">
|
||||
<icon-google-drive slot="icon"></icon-google-drive>
|
||||
<div>Save on Google Drive</div>
|
||||
<span>{{token.name}}</span>
|
||||
</menu-entry>
|
||||
<hr>
|
||||
</div>
|
||||
<menu-entry @click.native="addGoogleDriveAccount">
|
||||
<icon-google-drive slot="icon"></icon-google-drive>
|
||||
<span>Add Google Drive account</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
<!-- More menu -->
|
||||
<div v-else-if="panel === 'more'" class="side-bar__panel side-bar__panel--menu">
|
||||
<menu-entry @click.native="settings">
|
||||
<icon-settings slot="icon"></icon-settings>
|
||||
<div>Settings</div>
|
||||
<span>Tweak application and keyboard shortcuts.</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="templates">
|
||||
<icon-code-braces slot="icon"></icon-code-braces>
|
||||
<div>Templates</div>
|
||||
<span>Configure Handlebars templates for your exports.</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="reset">
|
||||
<icon-logout slot="icon"></icon-logout>
|
||||
<div>Reset application</div>
|
||||
<span>Sign out and clean local data.</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
<!-- Export menu -->
|
||||
<div v-else-if="panel === 'export'" class="side-bar__panel side-bar__panel--menu">
|
||||
<menu-entry @click.native="exportMarkdown">
|
||||
<icon-download slot="icon"></icon-download>
|
||||
Export as Markdown
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="exportHtml">
|
||||
<icon-download slot="icon"></icon-download>
|
||||
Export as HTML
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="exportPdf">
|
||||
<icon-download slot="icon"></icon-download>
|
||||
Export as PDF
|
||||
</menu-entry>
|
||||
</div>
|
||||
<!-- Help -->
|
||||
<div v-else-if="panel === 'help'" class="side-bar__panel side-bar__panel--help">
|
||||
<pre class="markdown-highlighting" v-html="markdownSample"></pre>
|
||||
</div>
|
||||
<!-- TOC -->
|
||||
<div class="side-bar__panel side-bar__panel--toc" :class="{'side-bar__panel--hidden': panel !== 'toc'}">
|
||||
<toc>
|
||||
</toc>
|
||||
@ -47,49 +127,98 @@
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import Toc from './Toc';
|
||||
import SideBarItem from './SideBarItem';
|
||||
import MenuEntry from './MenuEntry';
|
||||
import markdownSample from '../data/markdownSample.md';
|
||||
import markdownConversionSvc from '../services/markdownConversionSvc';
|
||||
import googleHelper from '../services/providers/helpers/googleHelper';
|
||||
import googleDriveProvider from '../services/providers/googleDriveProvider';
|
||||
import syncSvc from '../services/syncSvc';
|
||||
import localDbSvc from '../services/localDbSvc';
|
||||
import exportSvc from '../services/exportSvc';
|
||||
|
||||
const panelNames = {
|
||||
menu: 'Menu',
|
||||
help: 'Markdown cheat sheet',
|
||||
toc: 'Table of contents',
|
||||
sync: 'Synchronize',
|
||||
publish: 'Publish',
|
||||
export: 'Export to disk',
|
||||
more: 'More',
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Toc,
|
||||
SideBarItem,
|
||||
MenuEntry,
|
||||
},
|
||||
data: () => ({
|
||||
panel: 'menu',
|
||||
panelNames: {
|
||||
menu: 'Menu',
|
||||
toc: 'Table of Contents',
|
||||
help: 'Markdown cheat sheet',
|
||||
},
|
||||
markdownSample: markdownConversionSvc.highlight(markdownSample),
|
||||
}),
|
||||
computed: {
|
||||
...mapGetters('data', [
|
||||
'loginToken',
|
||||
]),
|
||||
panel() {
|
||||
return this.$store.getters['data/localSettings'].sideBarPanel;
|
||||
},
|
||||
panelName() {
|
||||
return panelNames[this.panel];
|
||||
},
|
||||
googleDriveTokens() {
|
||||
const googleToken = this.$store.getters['data/googleTokens'];
|
||||
return Object.keys(googleToken)
|
||||
.map(sub => googleToken[sub])
|
||||
.filter(token => token.isDrive)
|
||||
.sort((token1, token2) => token1.name.localeCompare(token2.name));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions('data', [
|
||||
'toggleSideBar',
|
||||
]),
|
||||
...mapActions('data', {
|
||||
setPanel: 'setSideBarPanel',
|
||||
}),
|
||||
signin() {
|
||||
googleHelper.startOauth2([
|
||||
'openid',
|
||||
'https://www.googleapis.com/auth/drive.appdata',
|
||||
]).then(() => syncSvc.requestSync());
|
||||
return googleHelper.signin()
|
||||
.then(() => syncSvc.requestSync());
|
||||
},
|
||||
importFile() {
|
||||
return this.$store.dispatch('modal/notImplemented');
|
||||
},
|
||||
fileProperties() {
|
||||
return this.$store.dispatch('modal/open', 'fileProperties')
|
||||
.then(properties => this.$store.dispatch('content/patchCurrent', { properties }));
|
||||
},
|
||||
settings() {
|
||||
return this.$store.dispatch('modal/open', 'settings')
|
||||
.then(settings => this.$store.dispatch('data/setSettings', settings));
|
||||
},
|
||||
templates() {
|
||||
return this.$store.dispatch('modal/open', 'templates')
|
||||
.then(templates => this.$store.dispatch('data/setTemplates', templates));
|
||||
},
|
||||
reset() {
|
||||
return this.$store.dispatch('modal/reset')
|
||||
.then(() => localDbSvc.removeDb());
|
||||
},
|
||||
addGoogleDriveAccount() {
|
||||
return googleHelper.addGoogleDriveAccount();
|
||||
},
|
||||
openGoogleDrive(token) {
|
||||
return googleHelper.openPicker(token, 'doc')
|
||||
.then(files => this.$store.dispatch('queue/enqueue',
|
||||
() => googleDriveProvider.openFiles(token, files)));
|
||||
},
|
||||
exportMarkdown() {
|
||||
const currentFile = this.$store.getters['file/current'];
|
||||
return exportSvc.exportToDisk(currentFile.id, 'md');
|
||||
},
|
||||
exportHtml() {
|
||||
return this.$store.dispatch('modal/open', 'htmlExport');
|
||||
},
|
||||
exportPdf() {
|
||||
return this.$store.dispatch('modal/notImplemented');
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -103,7 +232,7 @@ export default {
|
||||
height: 100%;
|
||||
|
||||
hr {
|
||||
margin: 5px 10px;
|
||||
margin: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,7 +253,7 @@ export default {
|
||||
}
|
||||
|
||||
.side-bar__panel--menu {
|
||||
padding: 5px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.side-bar__panel--help {
|
||||
|
156
src/components/TemplatesModal.vue
Normal file
156
src/components/TemplatesModal.vue
Normal file
@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<div class="modal__inner-1 modal__inner-1--templates">
|
||||
<div class="modal__inner-2">
|
||||
<div class="form-entry">
|
||||
<label class="form-entry__label" for="template">Template</label>
|
||||
<div class="form-entry__field">
|
||||
<input v-if="isEditing" id="template" type="text" class="textfield" v-focus @blur="submitEdit()" @keyup.enter="submitEdit()" @keyup.esc.stop="submitEdit(true)" v-model="editingName">
|
||||
<select v-else id="template" v-model="selectedId" class="textfield">
|
||||
<option v-for="(template, id) in templates" :key="id" v-bind:value="id">
|
||||
{{ template.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-entry__actions flex flex--row flex--end">
|
||||
<button class="form-entry__button button" @click="create">
|
||||
<icon-file-plus></icon-file-plus>
|
||||
</button>
|
||||
<button class="form-entry__button button" @click="copy">
|
||||
<icon-file-multiple></icon-file-multiple>
|
||||
</button>
|
||||
<button v-if="!isReadOnly" class="form-entry__button button" @click="isEditing = true">
|
||||
<icon-pen></icon-pen>
|
||||
</button>
|
||||
<button v-if="!isReadOnly" class="form-entry__button button" @click="remove">
|
||||
<icon-delete></icon-delete>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-entry">
|
||||
<label class="form-entry__label">Value</label>
|
||||
<div class="form-entry__field" v-for="(template, id) in templates" :key="id" v-if="id === selectedId">
|
||||
<code-editor lang="handlebars" :value="template.value" :disabled="isReadOnly" @changed="template.value = $event"></code-editor>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isReadOnly">
|
||||
<a href="javascript:void(0)" v-if="!showHelpers" @click="showHelpers = true">Add helpers ▾</a>
|
||||
<div class="form-entry" v-else>
|
||||
<br>
|
||||
<label class="form-entry__label">Helpers</label>
|
||||
<div class="form-entry__field" v-for="(template, id) in templates" :key="id" v-if="id === selectedId">
|
||||
<code-editor lang="javascript" :value="template.helpers" @changed="template.helpers = $event"></code-editor>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.reject()">Cancel</button>
|
||||
<button class="button" @click="resolve()">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import utils from '../services/utils';
|
||||
import CodeEditor from './CodeEditor';
|
||||
import emptyTemplateValue from '../data/emptyTemplateValue.html';
|
||||
import emptyTemplateHelpers from '!raw-loader!../data/emptyTemplateHelpers.js'; // eslint-disable-line
|
||||
|
||||
function fillEmptyFields(template) {
|
||||
if (template.value === '\n') {
|
||||
template.value = emptyTemplateValue;
|
||||
}
|
||||
if (template.helpers === '\n') {
|
||||
template.helpers = emptyTemplateHelpers;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CodeEditor,
|
||||
},
|
||||
data: () => ({
|
||||
selectedId: '',
|
||||
templates: {},
|
||||
showHelpers: false,
|
||||
isEditing: false,
|
||||
editingName: '',
|
||||
}),
|
||||
computed: {
|
||||
...mapState('modal', [
|
||||
'config',
|
||||
]),
|
||||
isReadOnly() {
|
||||
return this.templates[this.selectedId].isAdditional;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.$watch(
|
||||
() => this.$store.getters['data/allTemplates'],
|
||||
(allTemplates) => {
|
||||
const templates = utils.sortObject(
|
||||
utils.deepCopy(allTemplates),
|
||||
(key, template) => template.name,
|
||||
);
|
||||
Object.keys(templates).forEach(id => fillEmptyFields(templates[id]));
|
||||
this.templates = templates;
|
||||
this.selectedId = this.$store.state.modal.config.selectedId;
|
||||
if (!templates[this.selectedId]) {
|
||||
this.selectedId = Object.keys(templates)[0];
|
||||
}
|
||||
this.isEditing = false;
|
||||
}, { immediate: true });
|
||||
this.$watch('selectedId', (selectedId) => {
|
||||
const template = this.templates[selectedId];
|
||||
this.showHelpers = template.helpers !== emptyTemplateHelpers;
|
||||
this.editingName = template.name;
|
||||
}, { immediate: true });
|
||||
},
|
||||
methods: {
|
||||
create() {
|
||||
const template = {
|
||||
name: 'New template',
|
||||
value: '\n',
|
||||
helpers: '\n',
|
||||
};
|
||||
fillEmptyFields(template);
|
||||
this.selectedId = utils.uid();
|
||||
this.templates[this.selectedId] = template;
|
||||
this.isEditing = true;
|
||||
},
|
||||
copy() {
|
||||
const template = utils.deepCopy(this.templates[this.selectedId]);
|
||||
template.name += ' copy';
|
||||
delete template.isAdditional;
|
||||
this.selectedId = utils.uid();
|
||||
this.templates[this.selectedId] = template;
|
||||
this.isEditing = true;
|
||||
},
|
||||
remove() {
|
||||
delete this.templates[this.selectedId];
|
||||
this.selectedId = Object.keys(this.templates)[0];
|
||||
},
|
||||
submitEdit(cancel) {
|
||||
const template = this.templates[this.selectedId];
|
||||
if (!cancel && this.editingName) {
|
||||
template.name = this.editingName.slice(0, 250);
|
||||
} else {
|
||||
this.editingName = template.name;
|
||||
}
|
||||
setTimeout(() => { // For the form-entry to get the blur event
|
||||
this.isEditing = false;
|
||||
}, 1);
|
||||
},
|
||||
resolve() {
|
||||
this.config.resolve(this.templates);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.modal__inner-1--templates {
|
||||
max-width: 720px;
|
||||
}
|
||||
</style>
|
@ -59,7 +59,7 @@ export default {
|
||||
color: rgba(0, 0, 0, 0.75);
|
||||
cursor: pointer;
|
||||
font-size: 10px;
|
||||
padding: 5px 20px 40px;
|
||||
padding: 10px 20px 40px;
|
||||
white-space: nowrap;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
@ -75,6 +75,7 @@ export default {
|
||||
* {
|
||||
margin: 0.2em 0;
|
||||
padding: 0.2em 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
|
@ -80,8 +80,9 @@ textarea {
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: #333;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
background-color: rgba(0, 0, 0, 0.067);
|
||||
outline: 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
@ -96,6 +97,76 @@ textarea {
|
||||
}
|
||||
}
|
||||
|
||||
.form-entry {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.form-entry__label {
|
||||
display: block;
|
||||
font-size: 0.9rem;
|
||||
color: #a0a0a0;
|
||||
|
||||
.form-entry--focused & {
|
||||
color: darken($link-color, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
.form-entry__field {
|
||||
border: 1px solid #d8d8d8;
|
||||
border-radius: $border-radius-base;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.form-entry--focused & {
|
||||
border-color: $link-color;
|
||||
}
|
||||
}
|
||||
|
||||
.form-entry__actions {
|
||||
text-align: right;
|
||||
margin: 0.25em;
|
||||
}
|
||||
|
||||
.form-entry__button {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
padding: 6px;
|
||||
display: inline-block;
|
||||
background-color: transparent;
|
||||
opacity: 0.75;
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.textfield {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
font-family: inherit;
|
||||
font-weight: 400;
|
||||
font-size: 1.05em;
|
||||
padding: 0 0.6rem;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
color: inherit;
|
||||
height: 2.6rem;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
cursor: not-allowed;
|
||||
background-color: #f8f8f8;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
@ -162,6 +233,7 @@ textarea {
|
||||
&:focus,
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
@ -173,3 +245,44 @@ textarea {
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
border-bottom: 1px solid $hr-color;
|
||||
margin-bottom: 2em;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
.tabs__tab {
|
||||
width: 50%;
|
||||
float: left;
|
||||
text-align: center;
|
||||
line-height: 2.5em;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
border-top-left-radius: $border-radius-base;
|
||||
border-top-right-radius: $border-radius-base;
|
||||
color: $link-color;
|
||||
font-weight: 400;
|
||||
font-size: 1.1em;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: rgba(0, 0, 0, 0.067);
|
||||
}
|
||||
}
|
||||
|
||||
.tabs__tab--active {
|
||||
border-bottom: 2px solid $link-color;
|
||||
color: inherit;
|
||||
cursor: auto;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ $line-height-title: 1.33;
|
||||
$font-size-monospace: 0.85em;
|
||||
$code-bg: rgba(0, 0, 0, 0.05);
|
||||
$code-border-radius: 2px;
|
||||
$link-color: #4a80cf;
|
||||
$link-color: #0c93e4;
|
||||
$border-radius-base: 2px;
|
||||
$hr-color: rgba(128, 128, 128, 0.2);
|
||||
$navbar-color: rgba(255, 255, 255, 0.67);
|
||||
|
25
src/data/defaultFileProperties.yml
Normal file
25
src/data/defaultFileProperties.yml
Normal file
@ -0,0 +1,25 @@
|
||||
### File properties can contain metadata used for your publications (Wordpress, Blogger...).
|
||||
|
||||
### For example, you can specify a blog post title:
|
||||
#title: My article
|
||||
|
||||
### Extension configuration
|
||||
extensions:
|
||||
|
||||
# Markdown extensions
|
||||
markdown:
|
||||
abbr: true
|
||||
breaks: false
|
||||
deflist: true
|
||||
del: true
|
||||
fence: true
|
||||
footnote: true
|
||||
linkify: true
|
||||
sub: true
|
||||
sup: true
|
||||
table: true
|
||||
typographer: true
|
||||
|
||||
# Katex extension
|
||||
katex:
|
||||
enabled: true
|
@ -6,4 +6,6 @@ export default () => ({
|
||||
showSideBar: false,
|
||||
showExplorer: false,
|
||||
focusMode: false,
|
||||
sideBarPanel: 'menu',
|
||||
htmlExportLastTemplate: 'styledHtml',
|
||||
});
|
||||
|
81
src/data/defaultSettings.yml
Normal file
81
src/data/defaultSettings.yml
Normal file
@ -0,0 +1,81 @@
|
||||
# Adjust font size in editor and preview
|
||||
fontSizeFactor: 1
|
||||
# Adjust maximum text width in editor and preview
|
||||
maxWidthFactor: 1
|
||||
# Synchronize editor and preview scrollbars
|
||||
scrollSync: true
|
||||
|
||||
# Editor settings
|
||||
editor:
|
||||
# Display images in the editor
|
||||
inlineImages: true
|
||||
# Use monospaced font only
|
||||
monospacedFontOnly: false
|
||||
|
||||
# Keyboard shortcuts (see https://craig.is/killing/mice)
|
||||
shortcuts:
|
||||
-
|
||||
keys: mod+s
|
||||
method: sync
|
||||
-
|
||||
keys: mod+shift+b
|
||||
method: bold
|
||||
-
|
||||
keys: mod+shift+i
|
||||
method: italic
|
||||
-
|
||||
keys: mod+shift+l
|
||||
method: link
|
||||
-
|
||||
keys: mod+shift+l
|
||||
method: link
|
||||
-
|
||||
keys: mod+shift+q
|
||||
method: quote
|
||||
-
|
||||
keys: mod+shift+k
|
||||
method: code
|
||||
-
|
||||
keys: mod+shift+g
|
||||
method: image
|
||||
-
|
||||
keys: mod+shift+o
|
||||
method: olist
|
||||
-
|
||||
keys: mod+shift+o
|
||||
method: olist
|
||||
-
|
||||
keys: mod+shift+u
|
||||
method: ulist
|
||||
-
|
||||
keys: mod+shift+h
|
||||
method: heading
|
||||
-
|
||||
keys: mod+shift+r
|
||||
method: hr
|
||||
-
|
||||
keys: = = > space
|
||||
method: expand
|
||||
params:
|
||||
- '==> '
|
||||
- '⇒ '
|
||||
-
|
||||
keys: < = = space
|
||||
method: expand
|
||||
params:
|
||||
- '<== '
|
||||
- '⇐ '
|
||||
|
||||
# Default content for newly created files
|
||||
newFileContent: |
|
||||
|
||||
|
||||
|
||||
> Written with [StackEdit](https://stackedit.io/).
|
||||
|
||||
# Default properties for newly created files
|
||||
newFileProperties: |
|
||||
# extensions:
|
||||
# markdown:
|
||||
# breaks: true
|
||||
|
@ -1,6 +1,7 @@
|
||||
export default () => ({
|
||||
id: null,
|
||||
type: 'syncLocation',
|
||||
provider: null,
|
||||
fileId: null,
|
||||
hash: 0,
|
||||
});
|
||||
|
10
src/data/emptyTemplateHelpers.js
Normal file
10
src/data/emptyTemplateHelpers.js
Normal file
@ -0,0 +1,10 @@
|
||||
/* Add your custom Handlebars helpers here.
|
||||
|
||||
For example:
|
||||
Handlebars.registerHelper('transform', function (options) {
|
||||
var result = options.fn(this);
|
||||
return new Handlebars.SafeString(
|
||||
result.replace(/<pre[^>]*>/g, '<pre class="prettyprint">')
|
||||
);
|
||||
});
|
||||
*/
|
24
src/data/emptyTemplateValue.html
Normal file
24
src/data/emptyTemplateValue.html
Normal file
@ -0,0 +1,24 @@
|
||||
<!-- Specify your Handlebars template here.
|
||||
|
||||
The following JavaScript context will be passed to the template:
|
||||
{
|
||||
files: [{
|
||||
name: 'The filename',
|
||||
content: {
|
||||
text: 'The file content',
|
||||
html: '<p>The file content</p>',
|
||||
yamlProperties: 'The file properties in YAML format',
|
||||
properties: {
|
||||
// Computed file properties object
|
||||
},
|
||||
toc: [
|
||||
// Table Of Contents tree
|
||||
]
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
You can use Handlebars built-in helpers and some custom StackEdit helpers:
|
||||
- {{#tocToHtml files.0.content.toc}}{{/tocToHtml}} will produce a nice TOC.
|
||||
- {{#tocToHtml files.0.content.toc 3}}{{/tocToHtml}} will limit the TOC depth to 3.
|
||||
-->
|
1
src/data/plainHtmlTemplate.html
Normal file
1
src/data/plainHtmlTemplate.html
Normal file
@ -0,0 +1 @@
|
||||
{{{files.0.content.html}}}
|
16
src/data/styledHtmlTemplate.html
Normal file
16
src/data/styledHtmlTemplate.html
Normal file
@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{files.0.name}}</title>
|
||||
<link rel="stylesheet" href="http://app.classeur.io/base-min.css" />
|
||||
<script type="text/javascript" src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="export-container">{{{files.0.content.html}}}</div>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -58,7 +58,7 @@ function texMath(state, silent) {
|
||||
}
|
||||
|
||||
extensionSvc.onGetOptions((options, properties) => {
|
||||
options.math = properties['ext:katex'] !== 'false';
|
||||
options.math = properties.extensions.katex.enabled;
|
||||
});
|
||||
|
||||
extensionSvc.onInitConverter(2, (markdown, options) => {
|
||||
|
@ -47,19 +47,8 @@ const inlineBaseRules2 = [
|
||||
'text_collapse',
|
||||
];
|
||||
|
||||
extensionSvc.onGetOptions((options, properties) => {
|
||||
options.abbr = properties['ext:markdown:abbr'] !== 'false';
|
||||
options.breaks = properties['ext:markdown:breaks'] === 'true';
|
||||
options.deflist = properties['ext:markdown:deflist'] !== 'false';
|
||||
options.del = properties['ext:markdown:del'] !== 'false';
|
||||
options.fence = properties['ext:markdown:fence'] !== 'false';
|
||||
options.footnote = properties['ext:markdown:footnote'] !== 'false';
|
||||
options.linkify = properties['ext:markdown:linkify'] !== 'false';
|
||||
options.sub = properties['ext:markdown:sub'] !== 'false';
|
||||
options.sup = properties['ext:markdown:sup'] !== 'false';
|
||||
options.table = properties['ext:markdown:table'] !== 'false';
|
||||
options.typographer = properties['ext:markdown:typographer'] !== 'false';
|
||||
});
|
||||
extensionSvc.onGetOptions(
|
||||
(options, properties) => Object.assign(options, properties.extensions.markdown));
|
||||
|
||||
extensionSvc.onInitConverter(0, (markdown, options) => {
|
||||
markdown.set({
|
||||
|
5
src/icons/CodeTags.vue
Normal file
5
src/icons/CodeTags.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="100%" height="100%" viewBox="2.00 2.00 20.00 20.00">
|
||||
<path fill="#0D0E0E" fill-opacity="1" stroke-linejoin="round" d="M 14.6,16.6L 19.2,12L 14.6,7.4L 16,6L 22,12L 16,18L 14.6,16.6 Z M 9.4,16.6L 4.8,12L 9.4,7.4L 8,6L 2,12L 8,18L 9.4,16.6 Z "/>
|
||||
</svg>
|
||||
</template>
|
5
src/icons/Download.vue
Normal file
5
src/icons/Download.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00">
|
||||
<path fill="#000000" fill-opacity="1" stroke-width="0.2" stroke-linejoin="round" d="M 4.9994,19.9981L 18.9994,19.9981L 18.9994,17.9981L 4.9994,17.9981M 18.9994,8.99807L 14.9994,8.99807L 14.9994,2.99807L 8.9994,2.99807L 8.9994,8.99807L 4.9994,8.99807L 11.9994,15.9981L 18.9994,8.99807 Z "/>
|
||||
</svg>
|
||||
</template>
|
5
src/icons/FileMultiple.vue
Normal file
5
src/icons/FileMultiple.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00">
|
||||
<path d="M14.63,7.617l4.821,0l-4.821,-4.821l0,4.821Zm-6.136,-6.136l7.012,0l5.259,5.26l0,10.518c0,0.968 -0.785,1.753 -1.753,1.753l-10.527,0c-0.968,0 -1.744,-0.785 -1.744,-1.753l0.009,-14.024c0,-0.968 0.775,-1.754 1.744,-1.754Zm-3.506,3.507l0,15.777l14.024,0l0,1.754l-14.024,0c-0.965,0 -1.753,-0.789 -1.753,-1.754l0,-15.777l1.753,0Z"/>
|
||||
</svg>
|
||||
</template>
|
9
src/icons/GoogleDrive.vue
Normal file
9
src/icons/GoogleDrive.vue
Normal file
@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="100%" height="100%" viewBox="0 0 133156 115341">
|
||||
<g>
|
||||
<polygon style="fill:#3777E3" points="22194,115341 44385,76894 133156,76894 110963,115341 "/>
|
||||
<polygon style="fill:#FFCF63" points="88772,76894 133156,76894 88772,0 44385,0 "/>
|
||||
<polygon style="fill:#11A861" points="0,76894 22194,115341 66578,38447 44385,0 "/>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
12
src/icons/GooglePhotos.vue
Normal file
12
src/icons/GooglePhotos.vue
Normal file
@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="100%" height="100%" viewBox="0 0 512 511">
|
||||
<path d="M255.912,0.08c1.4,0.8 2.6,2 3.7,3.2c41.3,41.5 82.7,83 123.899,124.6c-26,25.6 -51.6,51.6 -77.399,77.3c-9.7,9.8 -19.601,19.4 -29.2,29.4c-7.2,-17.4 -14.1,-34.9 -21,-52.4c0,-18.2 0.1,-36.4 0,-54.7c-0.1,-42.4 -0.2,-84.9 0,-127.4l0,0Z" style="fill:#dc4b3e;fill-rule:nonzero;stroke:#dd4b39;stroke-width:0.09px;"/>
|
||||
<path d="M127.812,127.48l128.1,0c0.1,18.3 0,36.5 0,54.7c-7.1,17.2 -14,34.5 -20.8,51.9c-2.2,-1.2 -3.8,-3 -5.5,-4.8l-101.4,-101.4l-0.4,-0.4Z" style="fill:#ff9e0e;fill-rule:nonzero;stroke:#ef851c;stroke-width:0.09px;"/>
|
||||
<path d="M383.511,127.88l0.4,-0.3c-0.1,42.6 -0.1,85.3 0,127.9l-55.1,0c-17.2,-7.2 -34.601,-13.8 -51.9,-20.9c9.6,-10 19.5,-19.6 29.2,-29.4c25.801,-25.7 51.4,-51.7 77.4,-77.3l0,0Z" style="fill:#af195a;fill-rule:nonzero;stroke:#7e3794;stroke-width:0.09px;"/>
|
||||
<path d="M106.912,148.98c7.2,-6.9 13.9,-14.3 21.3,-21.1l101.4,101.4c1.7,1.8 3.3,3.6 5.5,4.8c-2.3,1.7 -5.2,2.3 -7.8,3.5c-14.801,6 -29.801,11.6 -44.5,18c-18.301,-0.2 -36.601,-0.1 -54.9,-0.1c-42.6,-0.1 -85.2,0.2 -127.8,-0.1c35.5,-35.6 71.2,-71 106.8,-106.4l0,0Z" style="fill:#ffc112;fill-rule:nonzero;stroke:#ffbb1b;stroke-width:0.09px;"/>
|
||||
<path d="M127.912,255.48c18.3,0 36.6,-0.1 54.9,0.1c17.3,7.1 34.6,13.8 51.899,20.8c-28.399,28.8 -57.099,57.2 -85.599,85.9c-7.2,6.8 -13.7,14.3 -21.3,20.7c0,-42.5 -0.1,-85 0.1,-127.5Z" style="fill:#17a05e;fill-rule:nonzero;stroke:#1a8763;stroke-width:0.09px;"/>
|
||||
<path d="M328.812,255.48l55.1,0c42.5,0.1 85.1,-0.1 127.6,0.1c-27.3,27.7 -55,55.1 -82.399,82.6c-15.2,15.1 -30.2,30.399 -45.4,45.3c-34,-34.4 -68.5,-68.4 -102.6,-102.8c-1.4,-1.5 -2.9,-2.8 -4.601,-3.8c2.9,-1.801 6.101,-2.7 9.2,-4c14.4,-5.8 28.799,-11.4 43.1,-17.4l0,0Z" style="fill:#4587f4;fill-rule:nonzero;stroke:#427fed;stroke-width:0.09px;"/>
|
||||
<path d="M234.712,276.38c7.3,17.399 13.9,35 21.2,52.399c-0.1,18.2 0,36.5 -0.1,54.7l0,88c-0.2,13.1 0.3,26.2 -0.2,39.2c-2.101,-1 -3.4,-2.9 -5.101,-4.5c-40.899,-41.099 -81.699,-82.199 -122.699,-123.199c7.6,-6.4 14.1,-13.9 21.3,-20.7c28.5,-28.7 57.2,-57.1 85.6,-85.9Z" style="fill:#8dc44d;fill-rule:nonzero;stroke:#65b045;stroke-width:0.09px;"/>
|
||||
<path d="M276.511,276.88c1.7,1 3.2,2.3 4.601,3.8c34.1,34.4 68.6,68.4 102.6,102.8c-42.7,-0.1 -85.3,0.1 -127.899,0c0.1,-18.2 0,-36.5 0.1,-54.7c6.699,-17.3 13.899,-34.5 20.598,-51.9l0,0Z" style="fill:#3569d6;fill-rule:nonzero;stroke:#43459d;stroke-width:0.09px;"/>
|
||||
</svg>
|
||||
</template>
|
5
src/icons/HardDisk.vue
Normal file
5
src/icons/HardDisk.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00">
|
||||
<path fill="#000000" fill-opacity="1" stroke-linejoin="round" d="M 6,2L 18,2C 19.1046,2 20,2.89543 20,4L 20,20C 20,21.1046 19.1046,22 18,22L 6,22C 4.89543,22 4,21.1046 4,20L 4,4C 4,2.89543 4.89543,2 6,2 Z M 12,4.00001C 8.68629,4.00001 5.99999,6.6863 5.99999,10C 5.99999,13.3137 8.68629,16 12.1022,15.9992L 11.2221,13.7674C 10.946,13.2891 11.1099,12.6775 11.5882,12.4013L 12.4542,11.9013C 12.9325,11.6252 13.5441,11.7891 13.8202,12.2674L 15.7446,14.6884C 17.1194,13.5889 18,11.8973 18,10C 18,6.6863 15.3137,4.00001 12,4.00001 Z M 12,9.00001C 12.5523,9.00001 13,9.44773 13,10C 13,10.5523 12.5523,11 12,11C 11.4477,11 11,10.5523 11,10C 11,9.44773 11.4477,9.00001 12,9.00001 Z M 7,18C 6.44771,18 6,18.4477 6,19C 6,19.5523 6.44771,20 7,20C 7.55228,20 8,19.5523 8,19C 8,18.4477 7.55228,18 7,18 Z M 12.0882,13.2674L 14.5757,19.5759L 17.1738,18.0759L 12.9542,12.7674L 12.0882,13.2674 Z "/>
|
||||
</svg>
|
||||
</template>
|
5
src/icons/Logout.vue
Normal file
5
src/icons/Logout.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00">
|
||||
<path fill="#000000" fill-opacity="1" stroke-width="0.2" stroke-linejoin="round" d="M 16.9999,17.25L 16.9999,14L 9.99998,14L 9.99998,10L 16.9999,10L 16.9999,6.75L 22.2499,12L 16.9999,17.25 Z M 13,2.00002C 14.1046,2.00002 15,2.89545 15,4.00002L 15,8L 13,8L 13,4.00002L 4,4.00004L 4,20L 13,20L 13,16L 15,16L 15,20C 15,21.1046 14.1046,22 13,22L 4,22C 2.89543,22 2,21.1046 2,20L 2,4.00004C 2,2.89547 2.89543,2.00006 4,2.00006L 13,2.00002 Z "/>
|
||||
</svg>
|
||||
</template>
|
@ -1,5 +0,0 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00">
|
||||
<path fill="#000000" fill-opacity="1" stroke-width="0.2" stroke-linejoin="round" d="M 3,6L 21,6L 21,8L 3,8L 3,6 Z M 3,11L 21,11L 21,13L 3,13L 3,11 Z M 3,16L 21,16L 21,18L 3,18L 3,16 Z "/>
|
||||
</svg>
|
||||
</template>
|
19
src/icons/Stackedit.vue
Normal file
19
src/icons/Stackedit.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="100%" height="100%" viewBox="0 0 126 126">
|
||||
<path d="M103.5,0c3.762,0.003 7.395,0.971 10.725,2.716c0.966,0.506 1.808,1.221 2.712,1.831l-53.937,40.453l-53.937,-40.453c4.402,-3.289 8.02,-4.273 13.437,-4.547l81,0Z" style="fill:#ffe600;fill-rule:nonzero;"/>
|
||||
<path d="M9.063,4.547l53.937,40.453l-58.04,72.55c-3.44,-4.417 -4.681,-8.528 -4.96,-14.05l0,-81c0.064,-5.221 1.801,-10.265 5.138,-14.312c0.914,-1.109 2.033,-2.033 3.05,-3.05l0.875,-0.591Z" style="fill:#bbd500;fill-rule:nonzero;"/>
|
||||
<path d="M63,45l58.04,72.549l-0.178,0.263c-4.901,5.465 -10.068,7.82 -17.362,8.188l-81,0c-5.221,-0.065 -10.265,-1.801 -14.312,-5.138c-1.109,-0.915 -2.033,-2.034 -3.05,-3.05l-0.177,-0.262l58.039,-72.55Z" style="fill:#ff8a00;fill-rule:nonzero;"/>
|
||||
<path d="M116.937,4.547c3.844,2.631 6.684,6.83 8.051,11.262c0.441,1.427 0.673,2.914 0.896,4.391c0.114,0.759 0.077,1.533 0.116,2.3l0,81c-0.023,3.748 -0.939,7.415 -2.716,10.725c-0.632,1.178 -1.496,2.216 -2.245,3.325l-58.039,-72.55l53.937,-40.453Z" style="fill:#75b7fd;fill-rule:nonzero;"/>
|
||||
<path d="M32.063,12l61.874,0c7.767,0 14.063,6.296 14.063,14.063l0,61.874c0,7.767 -6.296,14.063 -14.063,14.063l-61.875,0c-7.766,0 -14.062,-6.296 -14.062,-14.063l0,-61.875c0,-7.766 6.296,-14.062 14.062,-14.062Z" style="fill:#fff;fill-rule:nonzero;"/>
|
||||
<g>
|
||||
<g>
|
||||
<rect x="49.927" y="23.501" width="8.137" height="58.668" style="fill:#737373;"/>
|
||||
<rect x="67.937" y="23.501" width="8.137" height="58.668" style="fill:#737373;"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M84.047,46.623l0,-10.239l-42.094,0l0,10.239l42.094,0Z" style="fill:#737373;"/>
|
||||
<path d="M84.047,69.287l0,-10.24l-42.094,0l0,10.24l42.094,0Z" style="fill:#737373;"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
5
src/icons/Upload.vue
Normal file
5
src/icons/Upload.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00">
|
||||
<path fill="#000000" fill-opacity="1" stroke-width="0.2" stroke-linejoin="round" d="M 8.99939,15.998L 8.99939,9.99805L 4.99939,9.99805L 11.9994,2.99805L 18.9994,9.99805L 14.9994,9.99805L 14.9994,15.998L 8.99939,15.998 Z M 4.99937,19.9981L 4.99937,17.9981L 18.9994,17.9981L 18.9994,19.9981L 4.99937,19.9981 Z "/>
|
||||
</svg>
|
||||
</template>
|
5
src/icons/ViewList.vue
Normal file
5
src/icons/ViewList.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00">
|
||||
<path fill="#000000" fill-opacity="1" stroke-linejoin="round" d="M 9,5L 9,9L 21,9L 21,5M 9,19L 21,19L 21,15L 9,15M 9,14L 21,14L 21,10L 9,10M 4,9L 8,9L 8,5L 4,5M 4,19L 8,19L 8,15L 4,15M 4,14L 8,14L 8,10L 4,10L 4,14 Z "/>
|
||||
</svg>
|
||||
</template>
|
@ -2,7 +2,6 @@ import Vue from 'vue';
|
||||
import FormatBold from './FormatBold';
|
||||
import FormatItalic from './FormatItalic';
|
||||
import FormatQuoteClose from './FormatQuoteClose';
|
||||
import CodeBraces from './CodeBraces';
|
||||
import LinkVariant from './LinkVariant';
|
||||
import FileImage from './FileImage';
|
||||
import Table from './Table';
|
||||
@ -15,9 +14,9 @@ import StatusBar from './StatusBar';
|
||||
import NavigationBar from './NavigationBar';
|
||||
import SidePreview from './SidePreview';
|
||||
import Eye from './Eye';
|
||||
import Menu from './Menu';
|
||||
import Settings from './Settings';
|
||||
import FilePlus from './FilePlus';
|
||||
import FileMultiple from './FileMultiple';
|
||||
import FolderPlus from './FolderPlus';
|
||||
import Delete from './Delete';
|
||||
import Close from './Close';
|
||||
@ -28,13 +27,23 @@ import ArrowLeft from './ArrowLeft';
|
||||
import HelpCircle from './HelpCircle';
|
||||
import Toc from './Toc';
|
||||
import Login from './Login';
|
||||
import Logout from './Logout';
|
||||
import Sync from './Sync';
|
||||
import SyncOff from './SyncOff';
|
||||
import Upload from './Upload';
|
||||
import ViewList from './ViewList';
|
||||
import HardDisk from './HardDisk';
|
||||
import Download from './Download';
|
||||
import CodeTags from './CodeTags';
|
||||
import CodeBraces from './CodeBraces';
|
||||
// Providers
|
||||
import Stackedit from './Stackedit';
|
||||
import GoogleDrive from './GoogleDrive';
|
||||
import GooglePhotos from './GooglePhotos';
|
||||
|
||||
Vue.component('iconFormatBold', FormatBold);
|
||||
Vue.component('iconFormatItalic', FormatItalic);
|
||||
Vue.component('iconFormatQuoteClose', FormatQuoteClose);
|
||||
Vue.component('iconCodeBraces', CodeBraces);
|
||||
Vue.component('iconLinkVariant', LinkVariant);
|
||||
Vue.component('iconFileImage', FileImage);
|
||||
Vue.component('iconTable', Table);
|
||||
@ -47,9 +56,9 @@ Vue.component('iconStatusBar', StatusBar);
|
||||
Vue.component('iconNavigationBar', NavigationBar);
|
||||
Vue.component('iconSidePreview', SidePreview);
|
||||
Vue.component('iconEye', Eye);
|
||||
Vue.component('iconMenu', Menu);
|
||||
Vue.component('iconSettings', Settings);
|
||||
Vue.component('iconFilePlus', FilePlus);
|
||||
Vue.component('iconFileMultiple', FileMultiple);
|
||||
Vue.component('iconFolderPlus', FolderPlus);
|
||||
Vue.component('iconDelete', Delete);
|
||||
Vue.component('iconClose', Close);
|
||||
@ -60,5 +69,16 @@ Vue.component('iconArrowLeft', ArrowLeft);
|
||||
Vue.component('iconHelpCircle', HelpCircle);
|
||||
Vue.component('iconToc', Toc);
|
||||
Vue.component('iconLogin', Login);
|
||||
Vue.component('iconLogout', Logout);
|
||||
Vue.component('iconSync', Sync);
|
||||
Vue.component('iconSyncOff', SyncOff);
|
||||
Vue.component('iconUpload', Upload);
|
||||
Vue.component('iconViewList', ViewList);
|
||||
Vue.component('iconHardDisk', HardDisk);
|
||||
Vue.component('iconDownload', Download);
|
||||
Vue.component('iconCodeTags', CodeTags);
|
||||
Vue.component('iconCodeBraces', CodeBraces);
|
||||
// Providers
|
||||
Vue.component('iconStackedit', Stackedit);
|
||||
Vue.component('iconGoogleDrive', GoogleDrive);
|
||||
Vue.component('iconGooglePhotos', GooglePhotos);
|
||||
|
@ -51,6 +51,8 @@ function SelectionMgr(editor) {
|
||||
if (adjustScroll) {
|
||||
var adjustment = scrollElt.clientHeight / 2 * editor.options.getCursorFocusRatio()
|
||||
var cursorTop = this.cursorCoordinates.top + this.cursorCoordinates.height / 2
|
||||
// Adjust cursorTop with contentElt position relative to scrollElt
|
||||
cursorTop += contentElt.getBoundingClientRect().top - scrollElt.getBoundingClientRect().top + scrollElt.scrollTop;
|
||||
var minScrollTop = cursorTop - adjustment
|
||||
var maxScrollTop = cursorTop + adjustment - scrollElt.clientHeight
|
||||
if (scrollElt.scrollTop > minScrollTop) {
|
||||
|
@ -139,7 +139,13 @@ class Animation {
|
||||
this.$end.endCb = typeof endCb === 'function' && endCb;
|
||||
this.$end.stepCb = typeof stepCb === 'function' && stepCb;
|
||||
this.$startTime = Date.now() + this.$end.delay;
|
||||
this.loop(this.$end.duration && useTransition);
|
||||
if (!this.$end.duration) {
|
||||
this.loop(false);
|
||||
} else if (useTransition) {
|
||||
this.loop(true);
|
||||
} else {
|
||||
this.$requestId = window.requestAnimationFrame(() => this.loop(false));
|
||||
}
|
||||
return this.elt;
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,6 @@ const allowDebounce = (action, wait) => {
|
||||
};
|
||||
|
||||
const diffMatchPatch = new DiffMatchPatch();
|
||||
let lastContentId = null;
|
||||
let instantPreview = true;
|
||||
let tokens;
|
||||
const anchorHash = {};
|
||||
@ -153,8 +152,8 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||
*/
|
||||
initPrism() {
|
||||
const options = {
|
||||
insideFences: markdownConversionSvc.defaultOptions.insideFences,
|
||||
...this.options,
|
||||
insideFences: markdownConversionSvc.defaultOptions.insideFences,
|
||||
};
|
||||
this.prismGrammars = markdownGrammarSvc.makeGrammars(options);
|
||||
},
|
||||
@ -185,10 +184,6 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||
},
|
||||
};
|
||||
editorEngineSvc.initClEditor(options);
|
||||
editorEngineSvc.clEditor.toggleEditable(true);
|
||||
const contentId = store.getters['content/current'].id;
|
||||
// Switch off the editor when no content is loaded
|
||||
editorEngineSvc.clEditor.toggleEditable(!!contentId);
|
||||
this.restoreScrollPosition();
|
||||
},
|
||||
|
||||
@ -213,7 +208,6 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||
let insertBeforePreviewElt = this.previewElt.firstChild;
|
||||
let insertBeforeTocElt = this.tocElt.firstChild;
|
||||
let previewHtml = '';
|
||||
let heading;
|
||||
this.conversionCtx.htmlSectionDiff.forEach((item) => {
|
||||
for (let i = 0; i < item[1].length; i += 1) {
|
||||
const section = this.conversionCtx.sectionList[sectionIdx];
|
||||
@ -237,13 +231,13 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||
insertBeforeTocElt = insertBeforeTocElt.nextSibling;
|
||||
this.tocElt.removeChild(sectionTocElt);
|
||||
} else if (item[0] === 1) {
|
||||
const html = this.conversionCtx.htmlSectionList[sectionIdx];
|
||||
const html = htmlSanitizer.sanitizeHtml(this.conversionCtx.htmlSectionList[sectionIdx]);
|
||||
sectionIdx += 1;
|
||||
|
||||
// Create preview section element
|
||||
sectionPreviewElt = document.createElement('div');
|
||||
sectionPreviewElt.className = 'cl-preview-section modified';
|
||||
sectionPreviewElt.innerHTML = htmlSanitizer.sanitizeHtml(html);
|
||||
sectionPreviewElt.innerHTML = html;
|
||||
if (insertBeforePreviewElt) {
|
||||
this.previewElt.insertBefore(sectionPreviewElt, insertBeforePreviewElt);
|
||||
} else {
|
||||
@ -254,17 +248,11 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||
// Create TOC section element
|
||||
sectionTocElt = document.createElement('div');
|
||||
sectionTocElt.className = 'cl-toc-section modified';
|
||||
let headingElt = sectionPreviewElt.querySelector('h1, h2, h3, h4, h5, h6');
|
||||
heading = undefined;
|
||||
const headingElt = sectionPreviewElt.querySelector('h1, h2, h3, h4, h5, h6');
|
||||
if (headingElt) {
|
||||
heading = {
|
||||
title: headingElt.textContent,
|
||||
anchor: headingElt.id,
|
||||
level: parseInt(headingElt.tagName.slice(1), 10),
|
||||
};
|
||||
headingElt = headingElt.cloneNode(true);
|
||||
headingElt.removeAttribute('id');
|
||||
sectionTocElt.appendChild(headingElt);
|
||||
const clonedElt = headingElt.cloneNode(true);
|
||||
clonedElt.removeAttribute('id');
|
||||
sectionTocElt.appendChild(clonedElt);
|
||||
}
|
||||
if (insertBeforeTocElt) {
|
||||
this.tocElt.insertBefore(sectionTocElt, insertBeforeTocElt);
|
||||
@ -272,23 +260,13 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||
this.tocElt.appendChild(sectionTocElt);
|
||||
}
|
||||
|
||||
const clonedElt = sectionPreviewElt.cloneNode(true);
|
||||
// Unwrap tables
|
||||
clonedElt.querySelectorAll('.table-wrapper').cl_each((elt) => {
|
||||
while (elt.firstChild) {
|
||||
elt.parentNode.appendChild(elt.firstChild);
|
||||
}
|
||||
elt.parentNode.removeChild(elt);
|
||||
});
|
||||
|
||||
previewHtml += clonedElt.innerHTML;
|
||||
previewHtml += html;
|
||||
newSectionDescList.push({
|
||||
section,
|
||||
editorElt: section.elt,
|
||||
previewElt: sectionPreviewElt,
|
||||
tocElt: sectionTocElt,
|
||||
html: clonedElt.innerHTML,
|
||||
heading,
|
||||
html,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -454,74 +432,6 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||
.start();
|
||||
},
|
||||
|
||||
/**
|
||||
* Apply the template to the file content
|
||||
*/
|
||||
// applyTemplate({ state, commit, dispatch, rootState }, template) {
|
||||
// function groupToc(array, level = 1) {
|
||||
// const result = [];
|
||||
// let currentItem;
|
||||
|
||||
// function pushCurrentItem() {
|
||||
// if (currentItem) {
|
||||
// if (currentItem.children.length > 0) {
|
||||
// currentItem.children = groupToc(currentItem.children, level + 1);
|
||||
// }
|
||||
// result.push(currentItem);
|
||||
// }
|
||||
// }
|
||||
// array.forEach((item) => {
|
||||
// if (item.level !== level) {
|
||||
// currentItem = currentItem || {
|
||||
// children: [],
|
||||
// };
|
||||
// currentItem.children.push(item);
|
||||
// } else {
|
||||
// pushCurrentItem();
|
||||
// currentItem = item;
|
||||
// }
|
||||
// });
|
||||
// pushCurrentItem();
|
||||
// return result;
|
||||
// }
|
||||
|
||||
// let toc = [];
|
||||
// state.sectionDescList.cl_each((sectionDesc) => {
|
||||
// if (sectionDesc.heading) {
|
||||
// toc.push({
|
||||
// title: sectionDesc.heading.title,
|
||||
// level: sectionDesc.heading.level,
|
||||
// anchor: sectionDesc.heading.anchor,
|
||||
// children: [],
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
// toc = groupToc(toc);
|
||||
|
||||
// const view = {
|
||||
// file: {
|
||||
// name: rootState.file.currentFile.name,
|
||||
// content: {
|
||||
// properties: rootState.file.currentFile.content.properties,
|
||||
// text: rootState.file.currentFile.content.text,
|
||||
// html: state.previewHtml,
|
||||
// toc,
|
||||
// },
|
||||
// },
|
||||
// };
|
||||
// const worker = new window.Worker(clVersion.getAssetPath('templateWorker-min.js'));
|
||||
// worker.postMessage([template, view, clSettingSvc.values.handlebarsHelpers]);
|
||||
// return new Promise((resolve, reject) => {
|
||||
// worker.addEventListener('message', (e) => {
|
||||
// resolve(e.data.toString());
|
||||
// });
|
||||
// setTimeout(() => {
|
||||
// worker.terminate();
|
||||
// reject('Template generation timeout.');
|
||||
// }, 10000);
|
||||
// });
|
||||
// },
|
||||
|
||||
/**
|
||||
* Pass the elements to the store and initialize the editor.
|
||||
*/
|
||||
@ -531,7 +441,6 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||
this.tocElt = tocElt;
|
||||
|
||||
editorEngineSvc.createClEditor(editorElt);
|
||||
editorEngineSvc.clEditor.toggleEditable(false);
|
||||
editorEngineSvc.clEditor.on('contentChanged', (content, diffs, sectionList) => {
|
||||
const parsingCtx = {
|
||||
...this.parsingCtx,
|
||||
@ -543,18 +452,20 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||
input: Object.create(editorEngineSvc.clEditor),
|
||||
});
|
||||
this.pagedownEditor.run();
|
||||
// state.pagedownEditor.hooks.set('insertLinkDialog', (callback) => {
|
||||
// clEditorSvc.linkDialogCallback = callback
|
||||
// clEditorLayoutSvc.currentControl = 'linkDialog'
|
||||
// scope.$evalAsync()
|
||||
// return true
|
||||
// })
|
||||
// state.pagedownEditor.hooks.set('insertImageDialog', (callback) => {
|
||||
// clEditorSvc.imageDialogCallback = callback
|
||||
// clEditorLayoutSvc.currentControl = 'imageDialog'
|
||||
// scope.$evalAsync()
|
||||
// return true
|
||||
// })
|
||||
this.pagedownEditor.hooks.set('insertLinkDialog', (callback) => {
|
||||
store.dispatch('modal/open', {
|
||||
type: 'link',
|
||||
callback,
|
||||
});
|
||||
return true;
|
||||
});
|
||||
this.pagedownEditor.hooks.set('insertImageDialog', (callback) => {
|
||||
store.dispatch('modal/open', {
|
||||
type: 'image',
|
||||
callback,
|
||||
});
|
||||
return true;
|
||||
});
|
||||
|
||||
this.editorElt.parentNode.addEventListener('scroll', () => this.saveContentState(true));
|
||||
|
||||
@ -636,7 +547,7 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||
}, 100);
|
||||
|
||||
let imgEltsToCache = [];
|
||||
if (store.state.editor.inlineImages) {
|
||||
if (store.getters['data/computedSettings'].editor.inlineImages) {
|
||||
editorEngineSvc.clEditor.highlighter.on('sectionHighlighted', (section) => {
|
||||
section.elt.getElementsByClassName('token img').cl_each((imgTokenElt) => {
|
||||
const srcElt = imgTokenElt.querySelector('.token.cl-src');
|
||||
@ -683,10 +594,6 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||
|
||||
this.$emit('inited');
|
||||
|
||||
// scope.$watch('editorLayoutSvc.isEditorOpen', function (isOpen) {
|
||||
// clEditorSvc.cledit.toggleEditable(isOpen)
|
||||
// })
|
||||
|
||||
// scope.$watch('editorLayoutSvc.currentControl', function (currentControl) {
|
||||
// !currentControl && setTimeout(function () {
|
||||
// !scope.isDialogOpen && clEditorSvc.cledit && clEditorSvc.cledit.focus()
|
||||
@ -705,6 +612,8 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||
// })
|
||||
|
||||
// Watch file content changes
|
||||
let lastContentId = null;
|
||||
let lastProperties;
|
||||
store.watch(
|
||||
() => store.getters['content/current'].hash,
|
||||
() => {
|
||||
@ -717,13 +626,16 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||
initClEditor = true;
|
||||
}
|
||||
// Track properties changes
|
||||
const options = extensionSvc.getOptions(content.properties, true);
|
||||
if (content.properties !== lastProperties) {
|
||||
lastProperties = content.properties;
|
||||
const options = extensionSvc.getOptions(store.getters['content/currentProperties']);
|
||||
if (JSON.stringify(options) !== JSON.stringify(editorSvc.options)) {
|
||||
editorSvc.options = options;
|
||||
editorSvc.initPrism();
|
||||
editorSvc.initConverter();
|
||||
initClEditor = true;
|
||||
}
|
||||
}
|
||||
if (initClEditor) {
|
||||
editorSvc.initClEditor();
|
||||
}
|
||||
@ -733,6 +645,13 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
// Disable editor if hidden or if no content is loaded
|
||||
store.watch(
|
||||
() => store.getters['content/current'].id && store.getters['layout/styles'].showEditor,
|
||||
editable => editorEngineSvc.clEditor.toggleEditable(!!editable), {
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
store.watch(() => store.getters['layout/styles'],
|
||||
() => editorSvc.measureSectionDimensions(false, true));
|
||||
},
|
||||
|
121
src/services/exportSvc.js
Normal file
121
src/services/exportSvc.js
Normal file
@ -0,0 +1,121 @@
|
||||
import FileSaver from 'file-saver';
|
||||
import TemplateWorker from 'worker-loader!./templateWorker.js'; // eslint-disable-line
|
||||
import localDbSvc from './localDbSvc';
|
||||
import markdownConversionSvc from './markdownConversionSvc';
|
||||
import extensionSvc from './extensionSvc';
|
||||
import utils from './utils';
|
||||
import store from '../store';
|
||||
import htmlSanitizer from '../libs/htmlSanitizer';
|
||||
|
||||
function groupHeadings(headings, level = 1) {
|
||||
const result = [];
|
||||
let currentItem;
|
||||
|
||||
function pushCurrentItem() {
|
||||
if (currentItem) {
|
||||
if (currentItem.children.length > 0) {
|
||||
currentItem.children = groupHeadings(currentItem.children, level + 1);
|
||||
}
|
||||
result.push(currentItem);
|
||||
}
|
||||
}
|
||||
headings.forEach((heading) => {
|
||||
if (heading.level !== level) {
|
||||
currentItem = currentItem || {
|
||||
children: [],
|
||||
};
|
||||
currentItem.children.push(heading);
|
||||
} else {
|
||||
pushCurrentItem();
|
||||
currentItem = heading;
|
||||
}
|
||||
});
|
||||
pushCurrentItem();
|
||||
return result;
|
||||
}
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Apply the template to the file content
|
||||
*/
|
||||
applyTemplate(fileId, template = {
|
||||
value: '{{{files.0.content.text}}}',
|
||||
helpers: '',
|
||||
}) {
|
||||
const file = store.state.file.itemMap[fileId];
|
||||
return localDbSvc.loadItem(`${fileId}/content`)
|
||||
.then((content) => {
|
||||
const properties = utils.computeProperties(content.properties);
|
||||
const options = extensionSvc.getOptions(properties);
|
||||
const converter = markdownConversionSvc.createConverter(options, true);
|
||||
const parsingCtx = markdownConversionSvc.parseSections(converter, content.text);
|
||||
const conversionCtx = markdownConversionSvc.convert(parsingCtx);
|
||||
const html = conversionCtx.htmlSectionList.map(htmlSanitizer.sanitizeHtml).join('');
|
||||
const elt = document.createElement('div');
|
||||
elt.innerHTML = html;
|
||||
|
||||
// Unwrap tables
|
||||
elt.querySelectorAll('.table-wrapper').cl_each((wrapperElt) => {
|
||||
while (wrapperElt.firstChild) {
|
||||
wrapperElt.parentNode.appendChild(wrapperElt.firstChild);
|
||||
}
|
||||
wrapperElt.parentNode.removeChild(wrapperElt);
|
||||
});
|
||||
|
||||
// Make TOC
|
||||
const headings = elt.querySelectorAll('h1,h2,h3,h4,h5,h6').cl_map(headingElt => ({
|
||||
title: headingElt.textContent,
|
||||
anchor: headingElt.id,
|
||||
level: parseInt(headingElt.tagName.slice(1), 10),
|
||||
children: [],
|
||||
}));
|
||||
const toc = groupHeadings(headings);
|
||||
const view = {
|
||||
files: [{
|
||||
name: file.name,
|
||||
content: {
|
||||
text: content.text,
|
||||
properties,
|
||||
yamlProperties: content.properties,
|
||||
html: elt.innerHTML,
|
||||
toc,
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
// Run template conversion in a Worker to prevent attacks from helpers
|
||||
const worker = new TemplateWorker();
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
worker.terminate();
|
||||
reject('Template generation timeout.');
|
||||
}, 10000);
|
||||
worker.addEventListener('message', (e) => {
|
||||
clearTimeout(timeoutId);
|
||||
worker.terminate();
|
||||
// e.data can contain unsafe data if helpers attempts to call postMessage
|
||||
const [err, result] = e.data;
|
||||
if (err) {
|
||||
reject(err.toString());
|
||||
} else {
|
||||
resolve(result.toString());
|
||||
}
|
||||
});
|
||||
worker.postMessage([template.value, view, template.helpers]);
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Export a file to disk.
|
||||
*/
|
||||
exportToDisk(fileId, type, template) {
|
||||
const file = store.state.file.itemMap[fileId];
|
||||
return this.applyTemplate(fileId, template)
|
||||
.then((res) => {
|
||||
const blob = new Blob([res], {
|
||||
type: 'text/plain;charset=utf-8',
|
||||
});
|
||||
FileSaver.saveAs(blob, `${file.name}.${type}`);
|
||||
});
|
||||
},
|
||||
};
|
@ -27,10 +27,10 @@ export default {
|
||||
}, {});
|
||||
},
|
||||
|
||||
initConverter(markdown, options, isCurrentFile) {
|
||||
initConverter(markdown, options) {
|
||||
// Use forEach as it's a sparsed array
|
||||
initConverterListeners.forEach((listener) => {
|
||||
listener(markdown, options, isCurrentFile);
|
||||
listener(markdown, options);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -255,7 +255,7 @@ export default {
|
||||
// Put item in the store
|
||||
dbItem.tx = undefined;
|
||||
store.commit(`${dbItem.type}/setItem`, dbItem);
|
||||
resolve();
|
||||
resolve(dbItem);
|
||||
}
|
||||
};
|
||||
}, () => onError());
|
||||
@ -266,6 +266,8 @@ export default {
|
||||
* Unload from the store contents that haven't been opened recently
|
||||
*/
|
||||
unloadContents() {
|
||||
return this.sync()
|
||||
.then(() => {
|
||||
// Keep only last opened files in memory
|
||||
const lastOpenedFileIds = new Set(store.getters['data/lastOpenedIds']);
|
||||
Object.keys(contentTypes).forEach((type) => {
|
||||
@ -277,5 +279,21 @@ export default {
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Drop the database
|
||||
*/
|
||||
removeDb() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.deleteDatabase('stackedit-db');
|
||||
request.onerror = reject;
|
||||
request.onsuccess = resolve;
|
||||
})
|
||||
.then(() => {
|
||||
localStorage.removeItem('localDbVersion');
|
||||
window.location.reload();
|
||||
}, () => console.error('Could not delete local database.'));
|
||||
},
|
||||
};
|
||||
|
@ -129,13 +129,13 @@ const markdownConversionSvc = {
|
||||
* Creates a converter and init it with extensions.
|
||||
* @returns {Object} A converter.
|
||||
*/
|
||||
createConverter(options, isCurrentFile) {
|
||||
createConverter(options) {
|
||||
// Let the listeners add the rules
|
||||
const converter = new MarkdownIt('zero');
|
||||
converter.core.ruler.enable([], true);
|
||||
converter.block.ruler.enable([], true);
|
||||
converter.inline.ruler.enable([], true);
|
||||
extensionSvc.initConverter(converter, options, isCurrentFile);
|
||||
extensionSvc.initConverter(converter, options);
|
||||
Object.keys(startSectionBlockTypeMap).forEach((type) => {
|
||||
const rule = converter.renderer.rules[type] || converter.renderer.renderToken;
|
||||
converter.renderer.rules[type] = (tokens, idx, opts, env, self) => {
|
||||
|
@ -243,13 +243,6 @@ export default {
|
||||
rest: latex,
|
||||
},
|
||||
};
|
||||
rest['latex block'] = {
|
||||
pattern: /\\begin\{([a-z]*\*?)\}[\s\S]*?\\?\\end\{\1\}/g,
|
||||
inside: {
|
||||
keyword: /\\(begin|end)/,
|
||||
rest: latex,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (options.footnote) {
|
||||
rest.inlinefn = {
|
||||
|
@ -1 +1,3 @@
|
||||
import './shortcuts';
|
||||
import './keystrokes';
|
||||
import './scrollSync';
|
||||
|
174
src/services/optional/keystrokes.js
Normal file
174
src/services/optional/keystrokes.js
Normal file
@ -0,0 +1,174 @@
|
||||
import cledit from '../../libs/cledit';
|
||||
import editorSvc from '../editorSvc';
|
||||
import editorEngineSvc from '../editorEngineSvc';
|
||||
|
||||
const Keystroke = cledit.Keystroke;
|
||||
const indentRegexp = /^ {0,3}>[ ]*|^[ \t]*[*+-][ \t]|^([ \t]*)\d+\.[ \t]|^\s+/;
|
||||
let clearNewline;
|
||||
let lastSelection;
|
||||
|
||||
function fixNumberedList(state, indent) {
|
||||
if (state.selection || indent === undefined) {
|
||||
return;
|
||||
}
|
||||
const spaceIndent = indent.replace(/\t/g, ' ');
|
||||
const indentRegex = new RegExp(`^[ \\s]*$|^${spaceIndent}(\\d+\\.[ \\t])?(( )?.*)$`);
|
||||
|
||||
function getHits(lines) {
|
||||
let hits = [];
|
||||
let pendingHits = [];
|
||||
|
||||
function flush() {
|
||||
if (!pendingHits.hasHit && pendingHits.hasNoIndent) {
|
||||
return false;
|
||||
}
|
||||
hits = hits.concat(pendingHits);
|
||||
pendingHits = [];
|
||||
return true;
|
||||
}
|
||||
|
||||
lines.some((line) => {
|
||||
const match = line.replace(
|
||||
/^[ \t]*/, wholeMatch => wholeMatch.replace(/\t/g, ' ')).match(indentRegex);
|
||||
if (!match || line.match(/^#+ /)) { // Line not empty, not indented, or title
|
||||
flush();
|
||||
return true;
|
||||
}
|
||||
pendingHits.push({
|
||||
line,
|
||||
match,
|
||||
});
|
||||
if (match[2] !== undefined) {
|
||||
if (match[1]) {
|
||||
pendingHits.hasHit = true;
|
||||
} else if (!match[3]) {
|
||||
pendingHits.hasNoIndent = true;
|
||||
}
|
||||
} else if (!flush()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return hits;
|
||||
}
|
||||
|
||||
function formatHits(hits) {
|
||||
let num;
|
||||
return hits.map((hit) => {
|
||||
if (hit.match[1]) {
|
||||
if (!num) {
|
||||
num = parseInt(hit.match[1], 10);
|
||||
}
|
||||
const result = indent + num + hit.match[1].slice(-2) + hit.match[2];
|
||||
num += 1;
|
||||
return result;
|
||||
}
|
||||
return hit.line;
|
||||
});
|
||||
}
|
||||
|
||||
const before = state.before.split('\n');
|
||||
before.unshift(''); // Add an extra line (fixes #184)
|
||||
const after = state.after.split('\n');
|
||||
let currentLine = before.pop() || '';
|
||||
const currentPos = currentLine.length;
|
||||
currentLine += after.shift() || '';
|
||||
let lines = before.concat(currentLine).concat(after);
|
||||
let idx = before.length - getHits(before.slice().reverse()).length; // Prevents starting from 0
|
||||
while (idx <= before.length + 1) {
|
||||
const hits = formatHits(getHits(lines.slice(idx)));
|
||||
if (!hits.length) {
|
||||
idx += 1;
|
||||
} else {
|
||||
lines = lines.slice(0, idx).concat(hits).concat(lines.slice(idx + hits.length));
|
||||
idx += hits.length;
|
||||
}
|
||||
}
|
||||
currentLine = lines[before.length];
|
||||
state.before = lines.slice(1, before.length); // As we've added an extra line
|
||||
state.before.push(currentLine.slice(0, currentPos));
|
||||
state.before = state.before.join('\n');
|
||||
state.after = [currentLine.slice(currentPos)].concat(lines.slice(before.length + 1));
|
||||
state.after = state.after.join('\n');
|
||||
}
|
||||
|
||||
function enterKeyHandler(evt, state) {
|
||||
if (evt.which !== 13) {
|
||||
// Not enter
|
||||
clearNewline = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
evt.preventDefault();
|
||||
const lf = state.before.lastIndexOf('\n') + 1;
|
||||
const previousLine = state.before.slice(lf);
|
||||
const indentMatch = previousLine.match(indentRegexp) || [''];
|
||||
if (clearNewline && !state.selection && state.before.length === lastSelection) {
|
||||
state.before = state.before.substring(0, lf);
|
||||
state.selection = '';
|
||||
clearNewline = false;
|
||||
fixNumberedList(state, indentMatch[1]);
|
||||
return true;
|
||||
}
|
||||
clearNewline = false;
|
||||
const indent = indentMatch[0];
|
||||
if (indent.length) {
|
||||
clearNewline = true;
|
||||
}
|
||||
|
||||
editorEngineSvc.clEditor.undoMgr.setCurrentMode('single');
|
||||
|
||||
state.before += `\n${indent}`;
|
||||
state.selection = '';
|
||||
lastSelection = state.before.length;
|
||||
fixNumberedList(state, indentMatch[1]);
|
||||
return true;
|
||||
}
|
||||
|
||||
function tabKeyHandler(evt, state) {
|
||||
if (evt.which !== 9 || evt.metaKey || evt.ctrlKey) {
|
||||
// Not tab
|
||||
return false;
|
||||
}
|
||||
|
||||
const strSplice = (str, i, remove, add) =>
|
||||
str.slice(0, i) + (add || '') + str.slice(i + (+remove || 0));
|
||||
|
||||
evt.preventDefault();
|
||||
const isInverse = evt.shiftKey;
|
||||
const lf = state.before.lastIndexOf('\n') + 1;
|
||||
const previousLine = state.before.slice(lf) + state.selection + state.after;
|
||||
const indentMatch = previousLine.match(indentRegexp);
|
||||
if (isInverse) {
|
||||
const previousChar = state.before.slice(-1);
|
||||
if (/\s/.test(state.before.charAt(lf))) {
|
||||
state.before = strSplice(state.before, lf, 1);
|
||||
if (indentMatch) {
|
||||
fixNumberedList(state, indentMatch[1]);
|
||||
if (indentMatch[1]) {
|
||||
fixNumberedList(state, indentMatch[1].slice(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
const selection = previousChar + state.selection;
|
||||
state.selection = selection.replace(/\n[ \t]/gm, '\n');
|
||||
if (previousChar) {
|
||||
state.selection = state.selection.slice(1);
|
||||
}
|
||||
} else if (state.selection || indentMatch) {
|
||||
state.before = strSplice(state.before, lf, 0, '\t');
|
||||
state.selection = state.selection.replace(/\n(?=.)/g, '\n\t');
|
||||
if (indentMatch) {
|
||||
fixNumberedList(state, indentMatch[1]);
|
||||
fixNumberedList(state, `\t${indentMatch[1]}`);
|
||||
}
|
||||
} else {
|
||||
state.before += '\t';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
editorSvc.$on('inited', () => {
|
||||
editorEngineSvc.clEditor.addKeystroke(new Keystroke(enterKeyHandler, 50));
|
||||
editorEngineSvc.clEditor.addKeystroke(new Keystroke(tabKeyHandler, 50));
|
||||
});
|
@ -35,7 +35,7 @@ function throttle(func, wait) {
|
||||
const doScrollSync = () => {
|
||||
const localSkipAnimation = skipAnimation || !store.getters['layout/styles'].showSidePreview;
|
||||
skipAnimation = false;
|
||||
if (!store.state.editor.scrollSync || !sectionDescList || sectionDescList.length === 0) {
|
||||
if (!store.getters['data/computedSettings'].scrollSync || !sectionDescList || sectionDescList.length === 0) {
|
||||
return;
|
||||
}
|
||||
let editorScrollTop = editorScrollerElt.scrollTop;
|
||||
@ -117,7 +117,7 @@ const forceScrollSync = () => {
|
||||
doScrollSync();
|
||||
}
|
||||
};
|
||||
store.watch(state => state.editor.scrollSync, forceScrollSync);
|
||||
store.watch(() => store.getters['data/computedSettings'].scrollSync, forceScrollSync);
|
||||
|
||||
editorSvc.$on('inited', () => {
|
||||
editorScrollerElt = editorSvc.editorElt.parentNode;
|
||||
|
48
src/services/optional/shortcuts.js
Normal file
48
src/services/optional/shortcuts.js
Normal file
@ -0,0 +1,48 @@
|
||||
import Mousetrap from 'mousetrap';
|
||||
import store from '../../store';
|
||||
import editorSvc from '../../services/editorSvc';
|
||||
import syncSvc from '../../services/syncSvc';
|
||||
|
||||
// Skip shortcuts if modal is open or editor is hidden
|
||||
Mousetrap.prototype.stopCallback = () => store.state.modal.config || !store.getters['layout/styles'].showEditor;
|
||||
|
||||
const pagedownHandler = name => () => editorSvc.pagedownEditor.uiManager.doClick(name);
|
||||
|
||||
const methods = {
|
||||
bold: pagedownHandler('bold'),
|
||||
italic: pagedownHandler('italic'),
|
||||
link: pagedownHandler('link'),
|
||||
quote: pagedownHandler('quote'),
|
||||
code: pagedownHandler('code'),
|
||||
image: pagedownHandler('image'),
|
||||
olist: pagedownHandler('olist'),
|
||||
ulist: pagedownHandler('ulist'),
|
||||
heading: pagedownHandler('heading'),
|
||||
hr: pagedownHandler('hr'),
|
||||
sync: () => syncSvc.isSyncPossible() && syncSvc.requestSync(),
|
||||
};
|
||||
|
||||
store.watch(
|
||||
() => store.getters['data/computedSettings'],
|
||||
(computedSettings) => {
|
||||
Mousetrap.reset();
|
||||
|
||||
const shortcuts = computedSettings.shortcuts;
|
||||
shortcuts.forEach((shortcut) => {
|
||||
if (shortcut.keys) {
|
||||
const method = shortcut.method || shortcut;
|
||||
let params = shortcut.params || [];
|
||||
if (!Array.isArray(params)) {
|
||||
params = [params];
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(methods, method)) {
|
||||
Mousetrap.bind(shortcut.keys.toString(), () => {
|
||||
methods[method].apply(null, params);
|
||||
return false; // preventDefault
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}, {
|
||||
immediate: true,
|
||||
});
|
@ -1,30 +0,0 @@
|
||||
import store from '../../store';
|
||||
import googleHelper from './helpers/googleHelper';
|
||||
import providerUtils from './providerUtils';
|
||||
|
||||
export default {
|
||||
downloadContent(token, syncLocation) {
|
||||
return googleHelper.downloadFile(token, syncLocation.gdriveFileId)
|
||||
.then(content => providerUtils.parseContent(content));
|
||||
},
|
||||
uploadContent(token, item, syncLocation, ifNotTooLate) {
|
||||
const file = store.state.file.itemMap[syncLocation.fileId];
|
||||
const name = (file && file.name) || 'Untitled';
|
||||
const parents = [];
|
||||
if (syncLocation.gdriveParentId) {
|
||||
parents.push(syncLocation.gdriveParentId);
|
||||
}
|
||||
return googleHelper.saveFile(
|
||||
token,
|
||||
name,
|
||||
parents,
|
||||
providerUtils.serializeContent(item),
|
||||
syncLocation && syncLocation.gdriveId,
|
||||
ifNotTooLate,
|
||||
)
|
||||
.then(gdriveFile => ({
|
||||
...syncLocation,
|
||||
gdriveId: gdriveFile.id,
|
||||
}));
|
||||
},
|
||||
};
|
@ -37,10 +37,9 @@ export default {
|
||||
}
|
||||
},
|
||||
saveItem(token, item, syncData, ifNotTooLate) {
|
||||
return googleHelper.saveFile(
|
||||
return googleHelper.saveAppDataFile(
|
||||
token,
|
||||
JSON.stringify(item),
|
||||
['appDataFolder'],
|
||||
JSON.stringify(item), ['appDataFolder'],
|
||||
null,
|
||||
syncData && syncData.id,
|
||||
ifNotTooLate,
|
||||
@ -88,8 +87,7 @@ export default {
|
||||
id: item.id,
|
||||
type: item.type,
|
||||
hash: item.hash,
|
||||
}),
|
||||
['appDataFolder'],
|
||||
}), ['appDataFolder'],
|
||||
JSON.stringify(item),
|
||||
syncData && syncData.id,
|
||||
ifNotTooLate,
|
85
src/services/providers/googleDriveProvider.js
Normal file
85
src/services/providers/googleDriveProvider.js
Normal file
@ -0,0 +1,85 @@
|
||||
import store from '../../store';
|
||||
import googleHelper from './helpers/googleHelper';
|
||||
import providerUtils from './providerUtils';
|
||||
import utils from '../utils';
|
||||
|
||||
const defaultFilename = 'Untitled';
|
||||
|
||||
export default {
|
||||
downloadContent(token, syncLocation) {
|
||||
return googleHelper.downloadFile(token, syncLocation.driveFileId)
|
||||
.then(content => providerUtils.parseContent(content));
|
||||
},
|
||||
uploadContent(token, item, syncLocation, ifNotTooLate) {
|
||||
const file = store.state.file.itemMap[syncLocation.fileId];
|
||||
const name = (file && file.name) || defaultFilename;
|
||||
const parents = [];
|
||||
if (syncLocation.driveParentId) {
|
||||
parents.push(syncLocation.driveParentId);
|
||||
}
|
||||
return googleHelper.saveFile(
|
||||
token,
|
||||
name,
|
||||
parents,
|
||||
providerUtils.serializeContent(item),
|
||||
syncLocation && syncLocation.driveFileId,
|
||||
ifNotTooLate,
|
||||
)
|
||||
.then(driveFile => ({
|
||||
...syncLocation,
|
||||
driveFileId: driveFile.id,
|
||||
}));
|
||||
},
|
||||
openFiles(token, files) {
|
||||
const openOneFile = () => {
|
||||
const file = files.pop();
|
||||
if (!file) {
|
||||
return null;
|
||||
}
|
||||
let syncLocation;
|
||||
// Try to find an existing sync location
|
||||
store.getters['syncLocation/items'].some((existingSyncLocation) => {
|
||||
if (existingSyncLocation.driveFileId === file.id) {
|
||||
syncLocation = existingSyncLocation;
|
||||
}
|
||||
return syncLocation;
|
||||
});
|
||||
if (syncLocation) {
|
||||
// Sync location already exists, just open the file
|
||||
this.$store.commit('file/setCurrentId', syncLocation.fileId);
|
||||
return openOneFile();
|
||||
}
|
||||
// Sync location does not exist, download content from Google Drive and create the file
|
||||
syncLocation = {
|
||||
driveFileId: file.id,
|
||||
provider: 'googleDrive',
|
||||
sub: token.sub,
|
||||
};
|
||||
return this.downloadContent(token, syncLocation)
|
||||
.then((content) => {
|
||||
const id = utils.uid();
|
||||
delete content.history;
|
||||
store.commit('content/setItem', {
|
||||
...content,
|
||||
id: `${id}/content`,
|
||||
});
|
||||
store.commit('file/setItem', {
|
||||
id,
|
||||
name: (file.name || defaultFilename).slice(0, 250),
|
||||
parentId: store.getters['file/current'].parentId,
|
||||
});
|
||||
store.commit('syncLocation/setItem', {
|
||||
...syncLocation,
|
||||
id: utils.uid(),
|
||||
fileId: id,
|
||||
});
|
||||
store.commit('file/setCurrentId', id);
|
||||
}, () => {
|
||||
console.error(`Could not open file ${file.id}.`);
|
||||
})
|
||||
.then(() => openOneFile());
|
||||
};
|
||||
return Promise.resolve()
|
||||
.then(() => openOneFile());
|
||||
},
|
||||
};
|
@ -4,29 +4,18 @@ import store from '../../../store';
|
||||
const clientId = '241271498917-t4t7d07qis7oc0ahaskbif3ft6tk63cd.apps.googleusercontent.com';
|
||||
const appsDomain = null;
|
||||
const tokenExpirationMargin = 5 * 60 * 1000; // 5 min (Google tokens expire after 1h)
|
||||
let gapi;
|
||||
let google;
|
||||
|
||||
// const scopeMap = {
|
||||
// profile: [
|
||||
// 'https://www.googleapis.com/auth/userinfo.profile',
|
||||
// ],
|
||||
// gdrive: [
|
||||
// 'https://www.googleapis.com/auth/drive.install',
|
||||
// store.getters['data/settings'].gdriveFullAccess === true ?
|
||||
// 'https://www.googleapis.com/auth/drive' :
|
||||
// 'https://www.googleapis.com/auth/drive.file',
|
||||
// ],
|
||||
// blogger: [
|
||||
// 'https://www.googleapis.com/auth/blogger',
|
||||
// ],
|
||||
// picasa: [
|
||||
// 'https://www.googleapis.com/auth/photos',
|
||||
// ],
|
||||
// };
|
||||
|
||||
const gdriveAppDataScopes = ['https://www.googleapis.com/auth/drive.appdata'];
|
||||
const getGdriveScopes = () => [store.getters['data/settings'].gdriveFullAccess === true
|
||||
const driveAppDataScopes = ['https://www.googleapis.com/auth/drive.appdata'];
|
||||
const getDriveScopes = token => [token.driveFullAccess
|
||||
? 'https://www.googleapis.com/auth/drive'
|
||||
: 'https://www.googleapis.com/auth/drive.file'];
|
||||
: 'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive.install'];
|
||||
// const bloggerScopes = ['https://www.googleapis.com/auth/blogger'];
|
||||
const photosScopes = ['https://www.googleapis.com/auth/photos'];
|
||||
|
||||
const libraries = ['picker'];
|
||||
|
||||
const request = (token, options) => utils.request({
|
||||
...options,
|
||||
@ -101,7 +90,7 @@ export default {
|
||||
'https://accounts.google.com/o/oauth2/v2/auth', {
|
||||
client_id: clientId,
|
||||
response_type: 'token',
|
||||
scope: scopes.join(' '),
|
||||
scope: ['openid', ...scopes].join(' '), // Need openid for user info
|
||||
hd: appsDomain,
|
||||
login_hint: sub,
|
||||
prompt: silent ? 'none' : null,
|
||||
@ -128,7 +117,12 @@ export default {
|
||||
accessToken: data.accessToken,
|
||||
expiresOn: Date.now() + (data.expiresIn * 1000),
|
||||
sub: res.body.sub,
|
||||
isLogin: !store.getters['data/loginToken'],
|
||||
isLogin: !store.getters['data/loginToken'] &&
|
||||
scopes.indexOf('https://www.googleapis.com/auth/drive.appdata') !== -1,
|
||||
isDrive: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1 ||
|
||||
scopes.indexOf('https://www.googleapis.com/auth/drive.file') !== -1,
|
||||
isPhotos: scopes.indexOf('https://www.googleapis.com/auth/photos') !== -1,
|
||||
driveFullAccess: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1,
|
||||
};
|
||||
}))
|
||||
// Call the tokeninfo endpoint
|
||||
@ -140,11 +134,14 @@ export default {
|
||||
token.name = res.body.displayName;
|
||||
const existingToken = store.getters['data/googleTokens'][token.sub];
|
||||
if (existingToken) {
|
||||
if (!sub) {
|
||||
throw new Error('Google account already linked.');
|
||||
}
|
||||
// Add isLogin and nextPageToken to token
|
||||
token.isLogin = existingToken.isLogin;
|
||||
// We probably retrieved a new token with restricted scopes.
|
||||
// That's no problem, token will be refreshed later with merged scopes.
|
||||
// Save flags
|
||||
token.isLogin = existingToken.isLogin || token.isLogin;
|
||||
token.isDrive = existingToken.isDrive || token.isDrive;
|
||||
token.isPhotos = existingToken.isPhotos || token.isPhotos;
|
||||
token.driveFullAccess = existingToken.driveFullAccess || token.driveFullAccess;
|
||||
// Save nextPageToken
|
||||
token.nextPageToken = existingToken.nextPageToken;
|
||||
}
|
||||
// Add token to googleTokens
|
||||
@ -162,28 +159,49 @@ export default {
|
||||
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
if (mergedScopes.length === lastToken.scopes.length) {
|
||||
if (mergedScopes.length === lastToken.scopes.length &&
|
||||
lastToken.expiresOn > Date.now() + tokenExpirationMargin
|
||||
) {
|
||||
return lastToken;
|
||||
}
|
||||
// New scopes are requested, popup an authorize window
|
||||
return this.startOauth2(mergedScopes, sub);
|
||||
})
|
||||
.then((refreshedToken) => {
|
||||
if (refreshedToken.expiresOn > Date.now() + tokenExpirationMargin) {
|
||||
// Token is fresh enough
|
||||
return refreshedToken;
|
||||
}
|
||||
// Token is almost outdated, try to take one in background
|
||||
// New scopes are requested or existing token is going to expire.
|
||||
// Try to get a new token in background
|
||||
return this.startOauth2(mergedScopes, sub, true)
|
||||
// If it fails try to popup a window
|
||||
.catch(() => this.startOauth2(mergedScopes, sub));
|
||||
});
|
||||
},
|
||||
loadClientScript() {
|
||||
if (gapi) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return utils.loadScript('https://apis.google.com/js/api.js')
|
||||
.then(() => Promise.all(libraries.map(
|
||||
library => new Promise((resolve, reject) => window.gapi.load(library, {
|
||||
callback: resolve,
|
||||
onerror: reject,
|
||||
timeout: 30000,
|
||||
ontimeout: reject,
|
||||
})))))
|
||||
.then(() => {
|
||||
gapi = window.gapi;
|
||||
google = window.google;
|
||||
});
|
||||
},
|
||||
signin() {
|
||||
return this.startOauth2(driveAppDataScopes);
|
||||
},
|
||||
addDriveAccount(fullAccess = false) {
|
||||
return this.startOauth2(getDriveScopes({ driveFullAccess: fullAccess }));
|
||||
},
|
||||
addPhotosAccount() {
|
||||
return this.startOauth2(photosScopes);
|
||||
},
|
||||
getChanges(token) {
|
||||
const result = {
|
||||
changes: [],
|
||||
};
|
||||
return this.refreshToken(gdriveAppDataScopes, token)
|
||||
return this.refreshToken(driveAppDataScopes, token)
|
||||
.then((refreshedToken) => {
|
||||
const getPage = (pageToken = '1') => request(refreshedToken, {
|
||||
method: 'GET',
|
||||
@ -207,27 +225,86 @@ export default {
|
||||
});
|
||||
},
|
||||
saveFile(token, name, parents, media, fileId, ifNotTooLate) {
|
||||
return this.refreshToken(getGdriveScopes(), token)
|
||||
return this.refreshToken(getDriveScopes(token), token)
|
||||
.then(refreshedToken => saveFile(refreshedToken, name, parents, media, fileId, ifNotTooLate));
|
||||
},
|
||||
saveAppDataFile(token, name, parents, media, fileId, ifNotTooLate) {
|
||||
return this.refreshToken(gdriveAppDataScopes, token)
|
||||
return this.refreshToken(driveAppDataScopes, token)
|
||||
.then(refreshedToken => saveFile(refreshedToken, name, parents, media, fileId, ifNotTooLate));
|
||||
},
|
||||
downloadFile(token, id) {
|
||||
return this.refreshToken(getGdriveScopes(), token)
|
||||
return this.refreshToken(getDriveScopes(token), token)
|
||||
.then(refreshedToken => downloadFile(refreshedToken, id));
|
||||
},
|
||||
downloadAppDataFile(token, id) {
|
||||
return this.refreshToken(gdriveAppDataScopes, token)
|
||||
return this.refreshToken(driveAppDataScopes, token)
|
||||
.then(refreshedToken => downloadFile(refreshedToken, id));
|
||||
},
|
||||
removeAppDataFile(token, id, ifNotTooLate = cb => res => cb(res)) {
|
||||
return this.refreshToken(gdriveAppDataScopes, token)
|
||||
return this.refreshToken(driveAppDataScopes, token)
|
||||
// Refreshing a token can take a while if an oauth window pops up, so check if it's too late
|
||||
.then(ifNotTooLate(refreshedToken => request(refreshedToken, {
|
||||
method: 'DELETE',
|
||||
url: `https://www.googleapis.com/drive/v3/files/${id}`,
|
||||
})));
|
||||
},
|
||||
openPicker(token, type = 'doc') {
|
||||
const scopes = type === 'img' ? photosScopes : getDriveScopes(token);
|
||||
return this.loadClientScript()
|
||||
.then(() => this.refreshToken(scopes, token))
|
||||
.then(refreshedToken => new Promise((resolve) => {
|
||||
let picker;
|
||||
const pickerBuilder = new google.picker.PickerBuilder()
|
||||
.setOAuthToken(refreshedToken.accessToken)
|
||||
.setCallback((data) => {
|
||||
switch (data[google.picker.Response.ACTION]) {
|
||||
case google.picker.Action.PICKED:
|
||||
case google.picker.Action.CANCEL:
|
||||
resolve(data.docs || []);
|
||||
picker.dispose();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
switch (type) {
|
||||
default:
|
||||
case 'doc': {
|
||||
const view = new google.picker.DocsView(google.picker.ViewId.DOCS);
|
||||
view.setParent('root');
|
||||
view.setIncludeFolders(true);
|
||||
view.setMimeTypes([
|
||||
'text/plain',
|
||||
'text/x-markdown',
|
||||
'application/octet-stream',
|
||||
].join(','));
|
||||
pickerBuilder.enableFeature(google.picker.Feature.NAV_HIDDEN);
|
||||
pickerBuilder.enableFeature(google.picker.Feature.MULTISELECT_ENABLED);
|
||||
pickerBuilder.addView(view);
|
||||
break;
|
||||
}
|
||||
case 'folder': {
|
||||
const view = new google.picker.DocsView(google.picker.ViewId.FOLDERS);
|
||||
view.setParent('root');
|
||||
view.setIncludeFolders(true);
|
||||
view.setSelectFolderEnabled(true);
|
||||
view.setMimeTypes('application/vnd.google-apps.folder');
|
||||
pickerBuilder.enableFeature(google.picker.Feature.NAV_HIDDEN);
|
||||
pickerBuilder.addView(view);
|
||||
break;
|
||||
}
|
||||
case 'img': {
|
||||
let view = new google.picker.PhotosView();
|
||||
view.setType('flat');
|
||||
pickerBuilder.addView(view);
|
||||
view = new google.picker.PhotosView();
|
||||
view.setType('ofuser');
|
||||
pickerBuilder.addView(view);
|
||||
pickerBuilder.addView(google.picker.ViewId.PHOTO_UPLOAD);
|
||||
break;
|
||||
}
|
||||
}
|
||||
picker = pickerBuilder.build();
|
||||
picker.setVisible(true);
|
||||
}));
|
||||
},
|
||||
};
|
||||
|
@ -26,13 +26,14 @@ export default {
|
||||
}
|
||||
if (Object.keys(data).length) {
|
||||
const serializedData = b64Encode(JSON.stringify(data)).replace(/(.{50})/g, '$1\n');
|
||||
result += `<!--stackedit_data:\n${serializedData}-->`;
|
||||
result += `<!--stackedit_data:\n${serializedData}\n-->`;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
parseContent(serializedContent) {
|
||||
const result = emptyContent();
|
||||
result.text = serializedContent;
|
||||
result.history = [];
|
||||
const extractedData = dataExtractor.exec(serializedContent);
|
||||
if (extractedData) {
|
||||
try {
|
||||
|
@ -4,7 +4,8 @@ import welcomeFile from '../data/welcomeFile.md';
|
||||
import utils from './utils';
|
||||
import diffUtils from './diffUtils';
|
||||
import userActivitySvc from './userActivitySvc';
|
||||
import gdriveAppDataProvider from './providers/gdriveAppDataProvider';
|
||||
import googleDriveAppDataProvider from './providers/googleDriveAppDataProvider';
|
||||
import googleDriveProvider from './providers/googleDriveProvider';
|
||||
|
||||
const lastSyncActivityKey = 'lastSyncActivity';
|
||||
let lastSyncActivity;
|
||||
@ -38,17 +39,21 @@ function setLastSyncActivity() {
|
||||
|
||||
function getSyncProvider(syncLocation) {
|
||||
switch (syncLocation.provider) {
|
||||
case 'gdriveAppData':
|
||||
case 'googleDriveAppData':
|
||||
default:
|
||||
return gdriveAppDataProvider;
|
||||
return googleDriveAppDataProvider;
|
||||
case 'googleDrive':
|
||||
return googleDriveProvider;
|
||||
}
|
||||
}
|
||||
|
||||
function getSyncToken(syncLocation) {
|
||||
switch (syncLocation.provider) {
|
||||
case 'gdriveAppData':
|
||||
case 'googleDriveAppData':
|
||||
default:
|
||||
return store.getters['data/loginToken'];
|
||||
case 'googleDrive':
|
||||
return store.getters['data/googleTokens'][syncLocation.sub];
|
||||
}
|
||||
}
|
||||
|
||||
@ -135,7 +140,7 @@ function syncFile(fileId) {
|
||||
...store.getters['syncLocation/groupedByFileId'][fileId] || [],
|
||||
];
|
||||
if (isDataSyncPossible()) {
|
||||
syncLocations.push({ id: 'main', provider: 'gdriveAppData', fileId });
|
||||
syncLocations.unshift({ id: 'main', provider: 'googleDriveAppData', fileId });
|
||||
}
|
||||
let result;
|
||||
syncLocations.some((syncLocation) => {
|
||||
@ -200,7 +205,8 @@ function syncFile(fileId) {
|
||||
}
|
||||
}
|
||||
|
||||
// Store server content if any, and merged content which will be sent if different
|
||||
// Store last sent if it's in the server history,
|
||||
// and merged content which will be sent if different
|
||||
const newSyncedContent = utils.deepCopy(syncedContent);
|
||||
const newSyncHistoryItem = newSyncedContent.syncHistory[syncLocation.id] || [];
|
||||
newSyncedContent.syncHistory[syncLocation.id] = newSyncHistoryItem;
|
||||
@ -248,10 +254,12 @@ function syncFile(fileId) {
|
||||
|
||||
return syncOneContentLocation();
|
||||
})
|
||||
.then(() => localDbSvc.unloadContents(), (err) => {
|
||||
localDbSvc.unloadContents();
|
||||
.then(
|
||||
() => localDbSvc.unloadContents(),
|
||||
err => localDbSvc.unloadContents()
|
||||
.then(() => {
|
||||
throw err;
|
||||
})
|
||||
}))
|
||||
.catch((err) => {
|
||||
if (err && err.message === 'TOO_LATE') {
|
||||
// Restart sync
|
||||
@ -263,11 +271,11 @@ function syncFile(fileId) {
|
||||
|
||||
function sync() {
|
||||
const googleToken = store.getters['data/loginToken'];
|
||||
return gdriveAppDataProvider.getChanges(googleToken)
|
||||
return googleDriveAppDataProvider.getChanges(googleToken)
|
||||
.then((changes) => {
|
||||
// Apply changes
|
||||
applyChanges(changes);
|
||||
gdriveAppDataProvider.setAppliedChanges(googleToken, changes);
|
||||
googleDriveAppDataProvider.setAppliedChanges(googleToken, changes);
|
||||
|
||||
// Prevent from sending items too long after changes have been retrieved
|
||||
const syncStartTime = Date.now();
|
||||
@ -292,7 +300,7 @@ function sync() {
|
||||
const item = storeItemMap[id];
|
||||
const existingSyncData = syncDataByItemId[id];
|
||||
if (!existingSyncData || existingSyncData.hash !== item.hash) {
|
||||
result = gdriveAppDataProvider.saveItem(
|
||||
result = googleDriveAppDataProvider.saveItem(
|
||||
googleToken,
|
||||
// Use deepCopy to freeze objects
|
||||
utils.deepCopy(item),
|
||||
@ -327,7 +335,8 @@ function sync() {
|
||||
) {
|
||||
// Use deepCopy to freeze objects
|
||||
const syncDataToRemove = utils.deepCopy(existingSyncData);
|
||||
result = gdriveAppDataProvider.removeItem(googleToken, syncDataToRemove, ifNotTooLate)
|
||||
result = googleDriveAppDataProvider
|
||||
.removeItem(googleToken, syncDataToRemove, ifNotTooLate)
|
||||
.then(() => {
|
||||
const syncDataCopy = { ...store.getters['data/syncData'] };
|
||||
delete syncDataCopy[syncDataToRemove.id];
|
||||
@ -396,7 +405,7 @@ function requestSync() {
|
||||
clearInterval(intervalId);
|
||||
if (!isSyncPossible()) {
|
||||
// Cancel sync
|
||||
reject();
|
||||
reject('Sync not possible.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
98
src/services/templateWorker.js
Normal file
98
src/services/templateWorker.js
Normal file
@ -0,0 +1,98 @@
|
||||
// This WebWorker provides a safe environment to run user scripts
|
||||
// See http://stackoverflow.com/questions/10653809/making-webworkers-a-safe-environment/10796616
|
||||
|
||||
import Handlebars from 'handlebars';
|
||||
|
||||
// Classeur own helpers
|
||||
Handlebars.registerHelper('tocToHtml', (toc, depth = 6) => {
|
||||
function arrayToHtml(arr) {
|
||||
if (!arr || !arr.length || arr[0].level > depth) {
|
||||
return '';
|
||||
}
|
||||
const ulHtml = arr.map((item) => {
|
||||
let result = '<li>';
|
||||
if (item.anchor && item.title) {
|
||||
result += `<a href="#${item.anchor}">${item.title}</a>`;
|
||||
}
|
||||
result += arrayToHtml(item.children);
|
||||
return `${result}</li>`;
|
||||
}).join('\n');
|
||||
return `\n<ul>\n${ulHtml}\n</ul>\n`;
|
||||
}
|
||||
return new Handlebars.SafeString(arrayToHtml(toc));
|
||||
});
|
||||
|
||||
const whiteList = {
|
||||
self: 1,
|
||||
onmessage: 1,
|
||||
postMessage: 1,
|
||||
global: 1,
|
||||
whiteList: 1,
|
||||
eval: 1,
|
||||
Array: 1,
|
||||
Boolean: 1,
|
||||
Date: 1,
|
||||
Function: 1,
|
||||
Number: 1,
|
||||
Object: 1,
|
||||
RegExp: 1,
|
||||
String: 1,
|
||||
Error: 1,
|
||||
EvalError: 1,
|
||||
RangeError: 1,
|
||||
ReferenceError: 1,
|
||||
SyntaxError: 1,
|
||||
TypeError: 1,
|
||||
URIError: 1,
|
||||
decodeURI: 1,
|
||||
decodeURIComponent: 1,
|
||||
encodeURI: 1,
|
||||
encodeURIComponent: 1,
|
||||
isFinite: 1,
|
||||
isNaN: 1,
|
||||
parseFloat: 1,
|
||||
parseInt: 1,
|
||||
Infinity: 1,
|
||||
JSON: 1,
|
||||
Math: 1,
|
||||
NaN: 1,
|
||||
undefined: 1,
|
||||
safeEval: 1,
|
||||
close: 1,
|
||||
Handlebars: 1,
|
||||
};
|
||||
|
||||
let global = self;
|
||||
while (global !== Object.prototype) {
|
||||
Object.getOwnPropertyNames(global).forEach((prop) => { // eslint-disable-line no-loop-func
|
||||
if (!Object.prototype.hasOwnProperty.call(whiteList, prop)) {
|
||||
try {
|
||||
Object.defineProperty(global, prop, {
|
||||
get() {
|
||||
throw new Error(`Security Exception: cannot access ${prop}`);
|
||||
},
|
||||
configurable: false,
|
||||
});
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
});
|
||||
global = Object.getPrototypeOf(global);
|
||||
}
|
||||
|
||||
function safeEval(code) {
|
||||
eval(`"use strict";\n${code}`); // eslint-disable-line no-eval
|
||||
}
|
||||
|
||||
self.onmessage = (evt) => {
|
||||
try {
|
||||
const template = Handlebars.compile(evt.data[0]);
|
||||
const context = evt.data[1];
|
||||
safeEval(evt.data[2]);
|
||||
self.postMessage([null, template(context)]);
|
||||
} catch (e) {
|
||||
self.postMessage([e.toString()]);
|
||||
}
|
||||
close();
|
||||
};
|
@ -1,8 +1,14 @@
|
||||
import yaml from 'js-yaml';
|
||||
import defaultProperties from '../data/defaultFileProperties.yml';
|
||||
|
||||
// For sortObject
|
||||
const collator = new Intl.Collator(undefined, { sensitivity: 'base' });
|
||||
|
||||
// For uid()
|
||||
const crypto = window.crypto || window.msCrypto;
|
||||
const alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
|
||||
const radix = alphabet.length;
|
||||
const array = new Uint32Array(20);
|
||||
const array = new Uint32Array(16);
|
||||
|
||||
// For addQueryParams()
|
||||
const urlParser = window.document.createElement('a');
|
||||
@ -10,10 +16,8 @@ const urlParser = window.document.createElement('a');
|
||||
// For loadScript()
|
||||
const scriptLoadingPromises = Object.create(null);
|
||||
|
||||
// For startOauth2()
|
||||
const origin = `${location.protocol}//${location.host}`;
|
||||
|
||||
export default {
|
||||
origin: `${location.protocol}//${location.host}`,
|
||||
types: ['contentState', 'syncedContent', 'content', 'file', 'folder', 'syncLocation', 'data'],
|
||||
deepCopy(obj) {
|
||||
return obj == null ? obj : JSON.parse(JSON.stringify(obj));
|
||||
@ -30,6 +34,15 @@ export default {
|
||||
}, {});
|
||||
});
|
||||
},
|
||||
sortObject(obj, sortFunc = key => key) {
|
||||
const result = {};
|
||||
const compare = (key1, key2) => collator.compare(
|
||||
sortFunc(key1, obj[key1]), sortFunc(key2, obj[key2]));
|
||||
Object.keys(obj).sort(compare).forEach((key) => {
|
||||
result[key] = obj[key];
|
||||
});
|
||||
return result;
|
||||
},
|
||||
uid() {
|
||||
crypto.getRandomValues(array);
|
||||
return array.cl_map(value => alphabet[value % radix]).join('');
|
||||
@ -44,6 +57,27 @@ export default {
|
||||
}
|
||||
return hash;
|
||||
},
|
||||
computeProperties(yamlProperties) {
|
||||
const customProperties = yaml.safeLoad(yamlProperties);
|
||||
const properties = yaml.safeLoad(defaultProperties);
|
||||
const override = (obj, opt) => {
|
||||
const objType = Object.prototype.toString.call(obj);
|
||||
const optType = Object.prototype.toString.call(opt);
|
||||
if (objType !== optType) {
|
||||
return obj;
|
||||
} else if (objType !== '[object Object]') {
|
||||
return opt === undefined ? obj : opt;
|
||||
}
|
||||
Object.keys({
|
||||
...obj,
|
||||
...opt,
|
||||
}).forEach((key) => {
|
||||
obj[key] = override(obj[key], opt[key]);
|
||||
});
|
||||
return obj;
|
||||
};
|
||||
return override(properties, customProperties);
|
||||
},
|
||||
randomize(value) {
|
||||
return Math.floor((1 + (Math.random() * 0.2)) * value);
|
||||
},
|
||||
@ -85,17 +119,15 @@ export default {
|
||||
// Build the authorize URL
|
||||
const state = this.uid();
|
||||
params.state = state;
|
||||
params.redirect_uri = `${origin}/oauth2/callback.html`;
|
||||
params.redirect_uri = `${this.origin}/oauth2/callback.html`;
|
||||
const authorizeUrl = this.addQueryParams(url, params);
|
||||
if (silent) {
|
||||
// Use an iframe as wnd for silent mode
|
||||
oauth2Context.iframeElt = document.createElement('iframe');
|
||||
oauth2Context.iframeElt.style.position = 'absolute';
|
||||
oauth2Context.iframeElt.style.left = '-9999px';
|
||||
oauth2Context.iframeElt.onload = () => {
|
||||
oauth2Context.closeTimeout = setTimeout(() => oauth2Context.clean(), 5 * 1000);
|
||||
};
|
||||
oauth2Context.iframeElt.onerror = () => oauth2Context.clean();
|
||||
oauth2Context.closeTimeout = setTimeout(() => oauth2Context.clean('Unknown error.'), 5 * 1000);
|
||||
oauth2Context.iframeElt.onerror = () => oauth2Context.clean('Unknown error.');
|
||||
oauth2Context.iframeElt.src = authorizeUrl;
|
||||
document.body.appendChild(oauth2Context.iframeElt);
|
||||
oauth2Context.wnd = oauth2Context.iframeElt.contentWindow;
|
||||
@ -106,7 +138,7 @@ export default {
|
||||
if (!oauth2Context.wnd) {
|
||||
return Promise.reject();
|
||||
}
|
||||
oauth2Context.closeTimeout = setTimeout(() => oauth2Context.clean(), 120 * 1000);
|
||||
oauth2Context.closeTimeout = setTimeout(() => oauth2Context.clean('Timeout.'), 120 * 1000);
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
oauth2Context.clean = (errorMsg) => {
|
||||
@ -129,7 +161,7 @@ export default {
|
||||
|
||||
oauth2Context.msgHandler = (event) => {
|
||||
if (event.source === oauth2Context.wnd &&
|
||||
event.origin === origin &&
|
||||
event.origin === this.origin &&
|
||||
event.data &&
|
||||
event.data.state === state
|
||||
) {
|
||||
@ -145,9 +177,9 @@ export default {
|
||||
|
||||
oauth2Context.checkClosedInterval = !silent && setInterval(() => {
|
||||
if (oauth2Context.wnd.closed) {
|
||||
oauth2Context.clean();
|
||||
oauth2Context.clean('Authorize window was closed');
|
||||
}
|
||||
}, 200);
|
||||
}, 250);
|
||||
});
|
||||
},
|
||||
request(configParam) {
|
||||
|
@ -10,7 +10,6 @@ import folder from './modules/folder';
|
||||
import syncLocation from './modules/syncLocation';
|
||||
import data from './modules/data';
|
||||
import layout from './modules/layout';
|
||||
import editor from './modules/editor';
|
||||
import explorer from './modules/explorer';
|
||||
import modal from './modules/modal';
|
||||
import queue from './modules/queue';
|
||||
@ -48,7 +47,6 @@ const store = new Vuex.Store({
|
||||
syncLocation,
|
||||
data,
|
||||
layout,
|
||||
editor,
|
||||
explorer,
|
||||
modal,
|
||||
queue,
|
||||
|
@ -1,5 +1,6 @@
|
||||
import moduleTemplate from './moduleTemplate';
|
||||
import empty from '../../data/emptyContent';
|
||||
import utils from '../../services/utils';
|
||||
|
||||
const module = moduleTemplate(empty);
|
||||
|
||||
@ -7,6 +8,7 @@ module.getters = {
|
||||
...module.getters,
|
||||
current: (state, getters, rootState, rootGetters) =>
|
||||
state.itemMap[`${rootGetters['file/current'].id}/content`] || empty(),
|
||||
currentProperties: (state, getters) => utils.computeProperties(getters.current.properties),
|
||||
};
|
||||
|
||||
module.actions = {
|
||||
|
@ -1,11 +1,17 @@
|
||||
import yaml from 'js-yaml';
|
||||
import moduleTemplate from './moduleTemplate';
|
||||
import utils from '../../services/utils';
|
||||
import defaultSettings from '../../data/defaultSettings.yml';
|
||||
import defaultLocalSettings from '../../data/defaultLocalSettings';
|
||||
import plainHtmlTemplate from '../../data/plainHtmlTemplate.html';
|
||||
import styledHtmlTemplate from '../../data/styledHtmlTemplate.html';
|
||||
|
||||
const itemTemplate = (id, data = {}) => ({ id, type: 'data', data, hash: 0 });
|
||||
|
||||
const empty = (id) => {
|
||||
switch (id) {
|
||||
case 'settings':
|
||||
return itemTemplate(id, '\n');
|
||||
case 'localSettings':
|
||||
return itemTemplate(id, defaultLocalSettings());
|
||||
default:
|
||||
@ -37,12 +43,66 @@ module.actions.toggleNavigationBar = localSettingsToggler('showNavigationBar');
|
||||
module.actions.toggleEditor = localSettingsToggler('showEditor');
|
||||
module.actions.toggleSidePreview = localSettingsToggler('showSidePreview');
|
||||
module.actions.toggleStatusBar = localSettingsToggler('showStatusBar');
|
||||
module.actions.toggleSideBar = localSettingsToggler('showSideBar');
|
||||
module.actions.toggleSideBar = ({ getters, dispatch }, value) => {
|
||||
dispatch('setSideBarPanel'); // Reset side bar
|
||||
dispatch('patchLocalSettings', {
|
||||
showSideBar: value === undefined ? !getters.localSettings.showSideBar : value,
|
||||
});
|
||||
};
|
||||
module.actions.toggleExplorer = localSettingsToggler('showExplorer');
|
||||
module.actions.toggleFocusMode = localSettingsToggler('focusMode');
|
||||
module.actions.setSideBarPanel = ({ dispatch }, value) => dispatch('patchLocalSettings', {
|
||||
sideBarPanel: value === undefined ? 'menu' : value,
|
||||
});
|
||||
|
||||
// Settings
|
||||
module.getters.settings = getter('settings');
|
||||
module.getters.computedSettings = (state, getters) => {
|
||||
const customSettings = yaml.safeLoad(getters.settings);
|
||||
const settings = yaml.safeLoad(defaultSettings);
|
||||
const override = (obj, opt) => {
|
||||
const objType = Object.prototype.toString.call(obj);
|
||||
const optType = Object.prototype.toString.call(opt);
|
||||
if (objType !== optType) {
|
||||
return obj;
|
||||
} else if (objType !== '[object Object]') {
|
||||
return opt;
|
||||
}
|
||||
Object.keys(obj).forEach((key) => {
|
||||
obj[key] = override(obj[key], opt[key]);
|
||||
});
|
||||
return obj;
|
||||
};
|
||||
return override(settings, customSettings);
|
||||
};
|
||||
module.actions.setSettings = setter('settings');
|
||||
|
||||
// Templates
|
||||
module.getters.templates = getter('templates');
|
||||
const makeAdditionalTemplate = (name, value, helpers = '\n') => ({
|
||||
name,
|
||||
value,
|
||||
helpers,
|
||||
isAdditional: true,
|
||||
});
|
||||
const additionalTemplates = {
|
||||
plainHtml: makeAdditionalTemplate('Plain HTML', plainHtmlTemplate),
|
||||
styledHtml: makeAdditionalTemplate('Styled HTML', styledHtmlTemplate),
|
||||
};
|
||||
module.getters.allTemplates = (state, getters) => ({
|
||||
...getters.templates,
|
||||
...additionalTemplates,
|
||||
});
|
||||
module.actions.setTemplates = ({ commit }, data) => {
|
||||
const dataToCommit = {
|
||||
...data,
|
||||
};
|
||||
// We don't store additional templates
|
||||
Object.keys(additionalTemplates).forEach((id) => {
|
||||
delete dataToCommit[id];
|
||||
});
|
||||
commit('setItem', itemTemplate('templates', dataToCommit));
|
||||
};
|
||||
|
||||
// Last opened
|
||||
module.getters.lastOpened = getter('lastOpened');
|
||||
|
@ -1,16 +0,0 @@
|
||||
const setter = propertyName => (state, value) => {
|
||||
state[propertyName] = value;
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: {
|
||||
// Configuration
|
||||
inlineImages: true,
|
||||
scrollSync: true,
|
||||
},
|
||||
mutations: {
|
||||
setInlineImages: setter('inlineImages'),
|
||||
setScrollSync: setter('scrollSync'),
|
||||
},
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
const editorMinWidth = 280;
|
||||
const editorMinWidth = 320;
|
||||
const minPadding = 20;
|
||||
const previewButtonWidth = 55;
|
||||
const editorTopPadding = 10;
|
||||
@ -15,7 +15,7 @@ const constants = {
|
||||
statusBarHeight: 20,
|
||||
};
|
||||
|
||||
function computeStyles(state, localSettings, styles = {
|
||||
function computeStyles(state, computedSettings, localSettings, styles = {
|
||||
showNavigationBar: !localSettings.showEditor || localSettings.showNavigationBar,
|
||||
showStatusBar: localSettings.showStatusBar,
|
||||
showEditor: localSettings.showEditor,
|
||||
@ -49,7 +49,7 @@ function computeStyles(state, localSettings, styles = {
|
||||
if (styles.showSidePreview && doublePanelWidth / 2 < editorMinWidth) {
|
||||
styles.showSidePreview = false;
|
||||
styles.showPreview = false;
|
||||
return computeStyles(state, localSettings, styles);
|
||||
return computeStyles(state, computedSettings, localSettings, styles);
|
||||
}
|
||||
|
||||
styles.fontSize = 18;
|
||||
@ -61,14 +61,14 @@ function computeStyles(state, localSettings, styles = {
|
||||
if (doublePanelWidth < 1040) {
|
||||
styles.textWidth = 830;
|
||||
}
|
||||
styles.textWidth *= state.editorWidthFactor;
|
||||
styles.textWidth *= computedSettings.maxWidthFactor;
|
||||
if (doublePanelWidth < styles.textWidth) {
|
||||
styles.textWidth = doublePanelWidth;
|
||||
}
|
||||
if (styles.textWidth < 640) {
|
||||
styles.fontSize -= 1;
|
||||
}
|
||||
styles.fontSize *= state.fontSizeFactor;
|
||||
styles.fontSize *= computedSettings.fontSizeFactor;
|
||||
|
||||
const bottomPadding = Math.floor(styles.innerHeight / 2);
|
||||
const panelWidth = Math.floor(doublePanelWidth / 2);
|
||||
@ -98,21 +98,13 @@ function computeStyles(state, localSettings, styles = {
|
||||
return styles;
|
||||
}
|
||||
|
||||
const setter = propertyName => (state, value) => {
|
||||
state[propertyName] = value;
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: {
|
||||
editorWidthFactor: 1,
|
||||
fontSizeFactor: 1,
|
||||
bodyWidth: 0,
|
||||
bodyHeight: 0,
|
||||
},
|
||||
mutations: {
|
||||
setEditorWidthFactor: setter('editorWidthFactor'),
|
||||
setFontSizeFactor: setter('fontSizeFactor'),
|
||||
updateBodySize: (state) => {
|
||||
state.bodyWidth = document.body.clientWidth;
|
||||
state.bodyHeight = document.body.clientHeight;
|
||||
@ -121,8 +113,9 @@ export default {
|
||||
getters: {
|
||||
constants: () => constants,
|
||||
styles: (state, getters, rootState, rootGetters) => {
|
||||
const computedSettings = rootGetters['data/computedSettings'];
|
||||
const localSettings = rootGetters['data/localSettings'];
|
||||
return computeStyles(state, localSettings);
|
||||
return computeStyles(state, computedSettings, localSettings);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -1,49 +1,54 @@
|
||||
const confirmButtons = yesText => [{
|
||||
text: 'No',
|
||||
}, {
|
||||
text: yesText || 'Yes',
|
||||
resolve: true,
|
||||
}];
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: {
|
||||
content: null,
|
||||
config: null,
|
||||
},
|
||||
mutations: {
|
||||
setContent: (state, value) => {
|
||||
state.content = value;
|
||||
setConfig: (state, value) => {
|
||||
state.config = value;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
open({ commit }, content) {
|
||||
open({ commit }, param) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!content.buttons) {
|
||||
content.buttons = [{
|
||||
text: 'OK',
|
||||
resolve: true,
|
||||
}];
|
||||
}
|
||||
content.buttons.forEach((button) => {
|
||||
button.onClick = () => {
|
||||
commit('setContent');
|
||||
if (button.resolve) {
|
||||
resolve(button.resolve);
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
let config = param;
|
||||
if (typeof config === 'string') {
|
||||
config = {
|
||||
type: config,
|
||||
};
|
||||
});
|
||||
commit('setContent', content);
|
||||
}
|
||||
config.resolve = (result) => {
|
||||
if (config.onResolve) {
|
||||
config.onResolve(result);
|
||||
}
|
||||
commit('setConfig');
|
||||
resolve(result);
|
||||
};
|
||||
config.reject = (error) => {
|
||||
commit('setConfig');
|
||||
reject(error);
|
||||
};
|
||||
commit('setConfig', config);
|
||||
});
|
||||
},
|
||||
notImplemented: ({ dispatch }) => dispatch('open', {
|
||||
content: '<p>Sorry, this feature is not available yet...</p>',
|
||||
rejectText: 'Ok',
|
||||
}),
|
||||
fileDeletion: ({ dispatch }, item) => dispatch('open', {
|
||||
text: `<p>You are about to delete the file <b>${item.name}</b>. Are you sure ?</p>`,
|
||||
buttons: confirmButtons('Yes, delete'),
|
||||
content: `<p>You are about to delete the file <b>${item.name}</b>. Are you sure?</p>`,
|
||||
resolveText: 'Yes, delete',
|
||||
rejectText: 'No',
|
||||
}),
|
||||
folderDeletion: ({ dispatch }, item) => dispatch('open', {
|
||||
text: `<p>You are about to delete the folder <b>${item.name}</b> and all its files. Are you sure ?</p>`,
|
||||
buttons: confirmButtons('Yes, delete'),
|
||||
content: `<p>You are about to delete the folder <b>${item.name}</b> and all its files. Are you sure?</p>`,
|
||||
resolveText: 'Yes, delete',
|
||||
rejectText: 'No',
|
||||
}),
|
||||
reset: ({ dispatch }) => dispatch('open', {
|
||||
content: '<p>This will clean your local files and settings. Are you sure?</p>',
|
||||
resolveText: 'Yes, clean',
|
||||
rejectText: 'No',
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
93
yarn.lock
93
yarn.lock
@ -55,6 +55,15 @@ ajv@^4.11.2, ajv@^4.7.0, ajv@^4.9.1:
|
||||
co "^4.6.0"
|
||||
json-stable-stringify "^1.0.1"
|
||||
|
||||
ajv@^5.0.0:
|
||||
version "5.2.2"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.2.tgz#47c68d69e86f5d953103b0074a9430dc63da5e39"
|
||||
dependencies:
|
||||
co "^4.6.0"
|
||||
fast-deep-equal "^1.0.0"
|
||||
json-schema-traverse "^0.3.0"
|
||||
json-stable-stringify "^1.0.1"
|
||||
|
||||
align-text@^0.1.1, align-text@^0.1.3:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117"
|
||||
@ -205,6 +214,10 @@ async-foreach@^0.1.3:
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542"
|
||||
|
||||
async@^1.4.0:
|
||||
version "1.5.2"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
|
||||
|
||||
async@^2.1.2, async@^2.1.5:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-2.4.1.tgz#62a56b279c98a11d0987096a01cc3eeb8eb7bbd7"
|
||||
@ -1122,7 +1135,7 @@ cli-width@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a"
|
||||
|
||||
clipboard@^1.5.5:
|
||||
clipboard@^1.5.5, clipboard@^1.7.1:
|
||||
version "1.7.1"
|
||||
resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-1.7.1.tgz#360d6d6946e99a7a1fef395e42ba92b5e9b5a16b"
|
||||
dependencies:
|
||||
@ -1599,7 +1612,7 @@ debug@2.6.7:
|
||||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
debug@^2.1.1, debug@^2.2.0, debug@^2.6.0, debug@^2.6.8:
|
||||
debug@^2.1.1, debug@^2.2.0, debug@^2.6.0:
|
||||
version "2.6.8"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc"
|
||||
dependencies:
|
||||
@ -2084,6 +2097,10 @@ esprima@^3.1.1:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
|
||||
|
||||
esprima@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804"
|
||||
|
||||
esquery@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.0.tgz#cfba8b57d7fba93f17298a8a006a04cda13d80fa"
|
||||
@ -2235,6 +2252,10 @@ fancy-log@^1.1.0:
|
||||
chalk "^1.1.1"
|
||||
time-stamp "^1.0.0"
|
||||
|
||||
fast-deep-equal@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
|
||||
|
||||
fast-levenshtein@~2.0.4:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
|
||||
@ -2263,6 +2284,10 @@ file-loader@^0.11.1:
|
||||
dependencies:
|
||||
loader-utils "^1.0.2"
|
||||
|
||||
file-saver@^1.3.3:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-1.3.3.tgz#cdd4c44d3aa264eac2f68ec165bc791c34af1232"
|
||||
|
||||
filename-regex@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26"
|
||||
@ -2739,6 +2764,16 @@ gzip-size@^3.0.0:
|
||||
dependencies:
|
||||
duplexer "^0.1.1"
|
||||
|
||||
handlebars@^4.0.10:
|
||||
version "4.0.10"
|
||||
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.10.tgz#3d30c718b09a3d96f23ea4cc1f403c4d3ba9ff4f"
|
||||
dependencies:
|
||||
async "^1.4.0"
|
||||
optimist "^0.6.1"
|
||||
source-map "^0.4.4"
|
||||
optionalDependencies:
|
||||
uglify-js "^2.6"
|
||||
|
||||
har-schema@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e"
|
||||
@ -3289,6 +3324,13 @@ js-yaml@^3.4.3, js-yaml@^3.5.1:
|
||||
argparse "^1.0.7"
|
||||
esprima "^3.1.1"
|
||||
|
||||
js-yaml@^3.9.1:
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.9.1.tgz#08775cebdfdd359209f0d2acd383c8f86a6904a0"
|
||||
dependencies:
|
||||
argparse "^1.0.7"
|
||||
esprima "^4.0.0"
|
||||
|
||||
js-yaml@~3.7.0:
|
||||
version "3.7.0"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.7.0.tgz#5c967ddd837a9bfdca5f2de84253abe8a1c03b80"
|
||||
@ -3312,6 +3354,10 @@ json-loader@^0.5.4:
|
||||
version "0.5.4"
|
||||
resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.4.tgz#8baa1365a632f58a3c46d20175fc6002c96e37de"
|
||||
|
||||
json-schema-traverse@^0.3.0:
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340"
|
||||
|
||||
json-schema@0.2.3:
|
||||
version "0.2.3"
|
||||
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
|
||||
@ -3878,6 +3924,10 @@ minimist@^1.1.0, minimist@^1.1.3, minimist@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
|
||||
|
||||
minimist@~0.0.1:
|
||||
version "0.0.10"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
|
||||
|
||||
mixin-object@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e"
|
||||
@ -3891,6 +3941,10 @@ mkdirp@0.5.1, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkd
|
||||
dependencies:
|
||||
minimist "0.0.8"
|
||||
|
||||
mousetrap@^1.6.1:
|
||||
version "1.6.1"
|
||||
resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.1.tgz#2a085f5c751294c75e7e81f6ec2545b29cbf42d9"
|
||||
|
||||
ms@0.7.1:
|
||||
version "0.7.1"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098"
|
||||
@ -4224,6 +4278,13 @@ opn@^4.0.2:
|
||||
object-assign "^4.0.1"
|
||||
pinkie-promise "^2.0.0"
|
||||
|
||||
optimist@^0.6.1:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
|
||||
dependencies:
|
||||
minimist "~0.0.1"
|
||||
wordwrap "~0.0.2"
|
||||
|
||||
optimize-css-assets-webpack-plugin@^1.3.0:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-1.3.2.tgz#eb27456e21eefbd8080f31e8368c59684e585a2c"
|
||||
@ -5259,6 +5320,12 @@ sax@~1.2.1:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.2.tgz#fd8631a23bc7826bef5d871bdb87378c95647828"
|
||||
|
||||
schema-utils@^0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.3.0.tgz#f5877222ce3e931edae039f17eb3716e7137f8cf"
|
||||
dependencies:
|
||||
ajv "^5.0.0"
|
||||
|
||||
scss-tokenizer@^0.2.3:
|
||||
version "0.2.3"
|
||||
resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1"
|
||||
@ -5394,7 +5461,7 @@ source-map@0.5.x, source-map@^0.5.0, source-map@^0.5.1, source-map@^0.5.3, sourc
|
||||
version "0.5.6"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
|
||||
|
||||
source-map@^0.4.2:
|
||||
source-map@^0.4.2, source-map@^0.4.4:
|
||||
version "0.4.4"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b"
|
||||
dependencies:
|
||||
@ -5870,6 +5937,15 @@ uglify-js@3.0.x:
|
||||
commander "~2.9.0"
|
||||
source-map "~0.5.1"
|
||||
|
||||
uglify-js@^2.6:
|
||||
version "2.8.29"
|
||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd"
|
||||
dependencies:
|
||||
source-map "~0.5.1"
|
||||
yargs "~3.10.0"
|
||||
optionalDependencies:
|
||||
uglify-to-browserify "~1.0.0"
|
||||
|
||||
uglify-js@^2.8.27:
|
||||
version "2.8.27"
|
||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.27.tgz#47787f912b0f242e5b984343be8e35e95f694c9c"
|
||||
@ -6222,10 +6298,21 @@ wordwrap@0.0.2:
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
|
||||
|
||||
wordwrap@~0.0.2:
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
|
||||
|
||||
wordwrap@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
|
||||
|
||||
worker-loader@^0.8.1:
|
||||
version "0.8.1"
|
||||
resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-0.8.1.tgz#e8e995331ea34df5bf68296824bfb7f0ad578d43"
|
||||
dependencies:
|
||||
loader-utils "^1.0.2"
|
||||
schema-utils "^0.3.0"
|
||||
|
||||
wrap-ansi@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
|
||||
|
Loading…
Reference in New Issue
Block a user