Merge branch 'dev'
This commit is contained in:
commit
beac7fb1a3
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
@ -18,6 +18,7 @@ import SplashScreen from './SplashScreen';
|
|||||||
import syncSvc from '../services/syncSvc';
|
import syncSvc from '../services/syncSvc';
|
||||||
import networkSvc from '../services/networkSvc';
|
import networkSvc from '../services/networkSvc';
|
||||||
import sponsorSvc from '../services/sponsorSvc';
|
import sponsorSvc from '../services/sponsorSvc';
|
||||||
|
import tempFileSvc from '../services/tempFileSvc';
|
||||||
import timeSvc from '../services/timeSvc';
|
import timeSvc from '../services/timeSvc';
|
||||||
import store from '../store';
|
import store from '../store';
|
||||||
|
|
||||||
@ -94,6 +95,7 @@ export default {
|
|||||||
networkSvc.init();
|
networkSvc.init();
|
||||||
sponsorSvc.init();
|
sponsorSvc.init();
|
||||||
this.ready = true;
|
this.ready = true;
|
||||||
|
tempFileSvc.setReady();
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (err && err.message !== 'reload') {
|
if (err && err.message !== 'reload') {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="button-bar">
|
<div class="button-bar">
|
||||||
<div class="button-bar__inner button-bar__inner--top">
|
<div class="button-bar__inner button-bar__inner--top">
|
||||||
<button class="button-bar__button button" :class="{ 'button-bar__button--on': layoutSettings.showNavigationBar }" @click="toggleNavigationBar()" v-title="'Toggle navigation bar'">
|
<button class="button-bar__button button" :class="{ 'button-bar__button--on': layoutSettings.showNavigationBar }" v-if="!light" @click="toggleNavigationBar()" v-title="'Toggle navigation bar'">
|
||||||
<icon-navigation-bar></icon-navigation-bar>
|
<icon-navigation-bar></icon-navigation-bar>
|
||||||
</button>
|
</button>
|
||||||
<button class="button-bar__button button" :class="{ 'button-bar__button--on': layoutSettings.showSidePreview }" tour-step-anchor="editor" @click="toggleSidePreview()" v-title="'Toggle side preview'">
|
<button class="button-bar__button button" :class="{ 'button-bar__button--on': layoutSettings.showSidePreview }" tour-step-anchor="editor" @click="toggleSidePreview()" v-title="'Toggle side preview'">
|
||||||
@ -26,12 +26,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters, mapActions } from 'vuex';
|
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
computed: mapGetters('data', [
|
computed: {
|
||||||
'layoutSettings',
|
...mapState([
|
||||||
]),
|
'light',
|
||||||
|
]),
|
||||||
|
...mapGetters('data', [
|
||||||
|
'layoutSettings',
|
||||||
|
]),
|
||||||
|
},
|
||||||
methods: mapActions('data', [
|
methods: mapActions('data', [
|
||||||
'toggleNavigationBar',
|
'toggleNavigationBar',
|
||||||
'toggleEditor',
|
'toggleEditor',
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<pre class="editor__inner markdown-highlighting" :style="{padding: styles.editorPadding}" :class="{monospaced: computedSettings.editor.monospacedFontOnly}"></pre>
|
<pre class="editor__inner markdown-highlighting" :style="{padding: styles.editorPadding}" :class="{monospaced: computedSettings.editor.monospacedFontOnly}"></pre>
|
||||||
<div class="gutter" :style="{left: styles.editorGutterLeft + 'px'}">
|
<div class="gutter" :style="{left: styles.editorGutterLeft + 'px'}">
|
||||||
<comment-list v-if="styles.editorGutterWidth"></comment-list>
|
<comment-list v-if="styles.editorGutterWidth"></comment-list>
|
||||||
<editor-new-discussion-button></editor-new-discussion-button>
|
<editor-new-discussion-button v-if="!isCurrentTemp"></editor-new-discussion-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -19,6 +19,9 @@ export default {
|
|||||||
EditorNewDiscussionButton,
|
EditorNewDiscussionButton,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapGetters('file', [
|
||||||
|
'isCurrentTemp',
|
||||||
|
]),
|
||||||
...mapGetters('layout', [
|
...mapGetters('layout', [
|
||||||
'styles',
|
'styles',
|
||||||
]),
|
]),
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
<icon-close></icon-close>
|
<icon-close></icon-close>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="explorer__tree" :class="{'explorer__tree--new-item': !newChildNode.isNil}" tabindex="0">
|
<div class="explorer__tree" :class="{'explorer__tree--new-item': !newChildNode.isNil}" v-if="!light" tabindex="0" @keyup.delete="deleteItem()">
|
||||||
<explorer-node :node="rootNode" :depth="0"></explorer-node>
|
<explorer-node :node="rootNode" :depth="0"></explorer-node>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -34,6 +34,9 @@ export default {
|
|||||||
ExplorerNode,
|
ExplorerNode,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapState([
|
||||||
|
'light',
|
||||||
|
]),
|
||||||
...mapState('explorer', [
|
...mapState('explorer', [
|
||||||
'newChildNode',
|
'newChildNode',
|
||||||
]),
|
]),
|
||||||
@ -58,7 +61,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.$store.watch(
|
this.$watch(
|
||||||
() => this.$store.getters['file/current'].id,
|
() => this.$store.getters['file/current'].id,
|
||||||
(currentFileId) => {
|
(currentFileId) => {
|
||||||
this.$store.commit('explorer/setSelectedId', currentFileId);
|
this.$store.commit('explorer/setSelectedId', currentFileId);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="explorer-node" :class="{'explorer-node--selected': isSelected, 'explorer-node--open': isOpen, 'explorer-node--drag-target': isDragTargetFolder}" @dragover.prevent @dragenter.stop="setDragTarget(node.item.id)" @dragleave.stop="isDragTarget && setDragTargetId()" @drop.prevent.stop="onDrop" @contextmenu="onContextMenu">
|
<div class="explorer-node" :class="{'explorer-node--selected': isSelected, 'explorer-node--open': isOpen, 'explorer-node--drag-target': isDragTargetFolder}" @dragover.prevent @dragenter.stop="node.noDrop || setDragTarget(node.item.id)" @dragleave.stop="isDragTarget && setDragTargetId()" @drop.prevent.stop="onDrop" @contextmenu="onContextMenu">
|
||||||
<div class="explorer-node__item-editor" v-if="isEditing" :class="['explorer-node__item-editor--' + node.item.type]" :style="{paddingLeft: leftPadding}" draggable="true" @dragstart.stop.prevent>
|
<div class="explorer-node__item-editor" v-if="isEditing" :class="['explorer-node__item-editor--' + node.item.type]" :style="{paddingLeft: leftPadding}" draggable="true" @dragstart.stop.prevent>
|
||||||
<input type="text" class="text-input" v-focus @blur="submitEdit()" @keydown.enter="submitEdit()" @keydown.esc="submitEdit(true)" v-model="editingNodeName">
|
<input type="text" class="text-input" v-focus @blur="submitEdit()" @keydown.enter="submitEdit()" @keydown.esc="submitEdit(true)" v-model="editingNodeName">
|
||||||
</div>
|
</div>
|
||||||
@ -169,7 +169,7 @@ export default {
|
|||||||
perform: () => this.newItem(false),
|
perform: () => this.newItem(false),
|
||||||
}, {
|
}, {
|
||||||
name: 'New folder',
|
name: 'New folder',
|
||||||
disabled: !this.node.isFolder || this.node.isTrash,
|
disabled: !this.node.isFolder || this.node.isTrash || this.node.isTemp,
|
||||||
perform: () => this.newItem(true),
|
perform: () => this.newItem(true),
|
||||||
}, {
|
}, {
|
||||||
type: 'separator',
|
type: 'separator',
|
||||||
@ -179,7 +179,6 @@ export default {
|
|||||||
perform: () => this.setEditingId(this.node.item.id),
|
perform: () => this.setEditingId(this.node.item.id),
|
||||||
}, {
|
}, {
|
||||||
name: 'Delete',
|
name: 'Delete',
|
||||||
disabled: this.node.isTrash || this.node.item.parentId === 'trash',
|
|
||||||
perform: () => this.deleteItem(),
|
perform: () => this.deleteItem(),
|
||||||
}],
|
}],
|
||||||
})
|
})
|
||||||
|
@ -44,7 +44,7 @@
|
|||||||
<side-bar></side-bar>
|
<side-bar></side-bar>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<tour v-if="!layoutSettings.welcomeTourFinished"></tour>
|
<tour v-if="!light && !layoutSettings.welcomeTourFinished"></tour>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -78,6 +78,9 @@ export default {
|
|||||||
FindReplace,
|
FindReplace,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapState([
|
||||||
|
'light',
|
||||||
|
]),
|
||||||
...mapState('content', [
|
...mapState('content', [
|
||||||
'revisionContent',
|
'revisionContent',
|
||||||
]),
|
]),
|
||||||
@ -116,8 +119,13 @@ export default {
|
|||||||
editorSvc.init(editorElt, previewElt, tocElt);
|
editorSvc.init(editorElt, previewElt, tocElt);
|
||||||
|
|
||||||
// Focus on the editor every time reader mode is disabled
|
// Focus on the editor every time reader mode is disabled
|
||||||
this.$watch(() => this.styles.showEditor,
|
const focus = () => {
|
||||||
showEditor => showEditor && editorSvc.clEditor.focus());
|
if (this.styles.showEditor) {
|
||||||
|
editorSvc.clEditor.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
setTimeout(focus, 100);
|
||||||
|
this.$watch(() => this.styles.showEditor, focus);
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
window.removeEventListener('resize', this.updateStyle);
|
window.removeEventListener('resize', this.updateStyle);
|
||||||
|
@ -1,40 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal" @keydown.esc="onEscape" @keydown.tab="onTab">
|
<div class="modal" @keydown.esc="onEscape" @keydown.tab="onTab">
|
||||||
<file-properties-modal v-if="config.type === 'fileProperties'"></file-properties-modal>
|
<component v-if="currentModalComponent" :is="currentModalComponent"></component>
|
||||||
<settings-modal v-else-if="config.type === 'settings'"></settings-modal>
|
|
||||||
<templates-modal v-else-if="config.type === 'templates'"></templates-modal>
|
|
||||||
<about-modal v-else-if="config.type === 'about'"></about-modal>
|
|
||||||
<html-export-modal v-else-if="config.type === 'htmlExport'"></html-export-modal>
|
|
||||||
<pdf-export-modal v-else-if="config.type === 'pdfExport'"></pdf-export-modal>
|
|
||||||
<pandoc-export-modal v-else-if="config.type === 'pandocExport'"></pandoc-export-modal>
|
|
||||||
<link-modal v-else-if="config.type === 'link'"></link-modal>
|
|
||||||
<image-modal v-else-if="config.type === 'image'"></image-modal>
|
|
||||||
<sync-management-modal v-else-if="config.type === 'syncManagement'"></sync-management-modal>
|
|
||||||
<publish-management-modal v-else-if="config.type === 'publishManagement'"></publish-management-modal>
|
|
||||||
<workspace-management-modal v-else-if="config.type === 'workspaceManagement'"></workspace-management-modal>
|
|
||||||
<sponsor-modal v-else-if="config.type === 'sponsor'"></sponsor-modal>
|
|
||||||
<!-- Providers -->
|
|
||||||
<google-photo-modal v-else-if="config.type === 'googlePhoto'"></google-photo-modal>
|
|
||||||
<google-drive-account-modal v-else-if="config.type === 'googleDriveAccount'"></google-drive-account-modal>
|
|
||||||
<google-drive-save-modal v-else-if="config.type === 'googleDriveSave'"></google-drive-save-modal>
|
|
||||||
<google-drive-workspace-modal v-else-if="config.type === 'googleDriveWorkspace'"></google-drive-workspace-modal>
|
|
||||||
<google-drive-publish-modal v-else-if="config.type === 'googleDrivePublish'"></google-drive-publish-modal>
|
|
||||||
<dropbox-account-modal v-else-if="config.type === 'dropboxAccount'"></dropbox-account-modal>
|
|
||||||
<dropbox-save-modal v-else-if="config.type === 'dropboxSave'"></dropbox-save-modal>
|
|
||||||
<dropbox-publish-modal v-else-if="config.type === 'dropboxPublish'"></dropbox-publish-modal>
|
|
||||||
<github-account-modal v-else-if="config.type === 'githubAccount'"></github-account-modal>
|
|
||||||
<github-open-modal v-else-if="config.type === 'githubOpen'"></github-open-modal>
|
|
||||||
<github-save-modal v-else-if="config.type === 'githubSave'"></github-save-modal>
|
|
||||||
<github-publish-modal v-else-if="config.type === 'githubPublish'"></github-publish-modal>
|
|
||||||
<gist-sync-modal v-else-if="config.type === 'gistSync'"></gist-sync-modal>
|
|
||||||
<gist-publish-modal v-else-if="config.type === 'gistPublish'"></gist-publish-modal>
|
|
||||||
<wordpress-publish-modal v-else-if="config.type === 'wordpressPublish'"></wordpress-publish-modal>
|
|
||||||
<blogger-publish-modal v-else-if="config.type === 'bloggerPublish'"></blogger-publish-modal>
|
|
||||||
<blogger-page-publish-modal v-else-if="config.type === 'bloggerPagePublish'"></blogger-page-publish-modal>
|
|
||||||
<zendesk-account-modal v-else-if="config.type === 'zendeskAccount'"></zendesk-account-modal>
|
|
||||||
<zendesk-publish-modal v-else-if="config.type === 'zendeskPublish'"></zendesk-publish-modal>
|
|
||||||
<couchdb-workspace-modal v-else-if="config.type === 'couchdbWorkspace'"></couchdb-workspace-modal>
|
|
||||||
<couchdb-credentials-modal v-else-if="config.type === 'couchdbCredentials'"></couchdb-credentials-modal>
|
|
||||||
<modal-inner v-else aria-label="Dialog">
|
<modal-inner v-else aria-label="Dialog">
|
||||||
<div class="modal__content" v-html="config.content"></div>
|
<div class="modal__content" v-html="config.content"></div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
@ -129,9 +95,22 @@ export default {
|
|||||||
CouchdbWorkspaceModal,
|
CouchdbWorkspaceModal,
|
||||||
CouchdbCredentialsModal,
|
CouchdbCredentialsModal,
|
||||||
},
|
},
|
||||||
computed: mapGetters('modal', [
|
computed: {
|
||||||
'config',
|
...mapGetters('modal', [
|
||||||
]),
|
'config',
|
||||||
|
]),
|
||||||
|
currentModalComponent() {
|
||||||
|
if (this.config.type) {
|
||||||
|
let componentName = this.config.type[0].toUpperCase();
|
||||||
|
componentName += this.config.type.slice(1);
|
||||||
|
componentName += 'Modal';
|
||||||
|
if (this.$options.components[componentName]) {
|
||||||
|
return componentName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onEscape() {
|
onEscape() {
|
||||||
this.config.reject();
|
this.config.reject();
|
||||||
|
@ -2,11 +2,13 @@
|
|||||||
<nav class="navigation-bar" :class="{'navigation-bar--editor': styles.showEditor && !revisionContent}">
|
<nav class="navigation-bar" :class="{'navigation-bar--editor': styles.showEditor && !revisionContent}">
|
||||||
<!-- Explorer -->
|
<!-- Explorer -->
|
||||||
<div class="navigation-bar__inner navigation-bar__inner--left navigation-bar__inner--button">
|
<div class="navigation-bar__inner navigation-bar__inner--left navigation-bar__inner--button">
|
||||||
<button class="navigation-bar__button button" tour-step-anchor="explorer" @click="toggleExplorer()" v-title="'Toggle explorer'"><icon-folder></icon-folder></button>
|
<button class="navigation-bar__button button" v-if="light" @click="close()" v-title="'Close StackEdit'"><icon-close></icon-close></button>
|
||||||
|
<button class="navigation-bar__button button" v-else tour-step-anchor="explorer" @click="toggleExplorer()" v-title="'Toggle explorer'"><icon-folder></icon-folder></button>
|
||||||
</div>
|
</div>
|
||||||
<!-- Side bar -->
|
<!-- Side bar -->
|
||||||
<div class="navigation-bar__inner navigation-bar__inner--right navigation-bar__inner--button">
|
<div class="navigation-bar__inner navigation-bar__inner--right navigation-bar__inner--button">
|
||||||
<button class="navigation-bar__button navigation-bar__button--stackedit button" tour-step-anchor="menu" @click="toggleSideBar()" v-title="'Toggle side bar'"><icon-provider provider-id="stackedit"></icon-provider></button>
|
<a class="navigation-bar__button navigation-bar__button--stackedit button" v-if="light" href="app" target="_blank" v-title="'Open StackEdit'"><icon-provider provider-id="stackedit"></icon-provider></a>
|
||||||
|
<button class="navigation-bar__button navigation-bar__button--stackedit button" v-else tour-step-anchor="menu" @click="toggleSideBar()" v-title="'Toggle side bar'"><icon-provider provider-id="stackedit"></icon-provider></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="navigation-bar__inner navigation-bar__inner--right navigation-bar__inner--title flex flex--row">
|
<div class="navigation-bar__inner navigation-bar__inner--right navigation-bar__inner--title flex flex--row">
|
||||||
<!-- Spinner -->
|
<!-- Spinner -->
|
||||||
@ -57,6 +59,7 @@ import editorSvc from '../services/editorSvc';
|
|||||||
import syncSvc from '../services/syncSvc';
|
import syncSvc from '../services/syncSvc';
|
||||||
import publishSvc from '../services/publishSvc';
|
import publishSvc from '../services/publishSvc';
|
||||||
import animationSvc from '../services/animationSvc';
|
import animationSvc from '../services/animationSvc';
|
||||||
|
import tempFileSvc from '../services/tempFileSvc';
|
||||||
import utils from '../services/utils';
|
import utils from '../services/utils';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -68,6 +71,7 @@ export default {
|
|||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
...mapState([
|
...mapState([
|
||||||
|
'light',
|
||||||
'offline',
|
'offline',
|
||||||
]),
|
]),
|
||||||
...mapState('queue', [
|
...mapState('queue', [
|
||||||
@ -104,9 +108,7 @@ export default {
|
|||||||
}
|
}
|
||||||
this.titleFakeElt.textContent = this.title;
|
this.titleFakeElt.textContent = this.title;
|
||||||
const width = this.titleFakeElt.getBoundingClientRect().width + 2; // 2px for the caret
|
const width = this.titleFakeElt.getBoundingClientRect().width + 2; // 2px for the caret
|
||||||
return width < this.styles.titleMaxWidth
|
return Math.min(width, this.styles.titleMaxWidth);
|
||||||
? width
|
|
||||||
: this.styles.titleMaxWidth;
|
|
||||||
},
|
},
|
||||||
titleScrolling() {
|
titleScrolling() {
|
||||||
const result = this.titleHover && !this.titleFocus;
|
const result = this.titleHover && !this.titleFocus;
|
||||||
@ -178,9 +180,12 @@ export default {
|
|||||||
}
|
}
|
||||||
this.titleInputElt.blur();
|
this.titleInputElt.blur();
|
||||||
},
|
},
|
||||||
|
close() {
|
||||||
|
tempFileSvc.close();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.$store.watch(
|
this.$watch(
|
||||||
() => this.$store.getters['file/current'].name,
|
() => this.$store.getters['file/current'].name,
|
||||||
(name) => {
|
(name) => {
|
||||||
this.title = name;
|
this.title = name;
|
||||||
@ -213,7 +218,7 @@ export default {
|
|||||||
float: left;
|
float: left;
|
||||||
|
|
||||||
&.navigation-bar__inner--button {
|
&.navigation-bar__inner--button {
|
||||||
margin-right: 15px;
|
margin-right: 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -255,6 +260,7 @@ $button-size: 36px;
|
|||||||
.navigation-bar__button {
|
.navigation-bar__button {
|
||||||
width: $button-size;
|
width: $button-size;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
|
transition: opacity 0.25s;
|
||||||
|
|
||||||
.navigation-bar__inner--button & {
|
.navigation-bar__inner--button & {
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
@ -290,7 +296,7 @@ $button-size: 36px;
|
|||||||
|
|
||||||
.navigation-bar__title {
|
.navigation-bar__title {
|
||||||
margin: 0 4px;
|
margin: 0 4px;
|
||||||
font-size: 22px;
|
font-size: 21px;
|
||||||
|
|
||||||
.layout--revision & {
|
.layout--revision & {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="gutter" :style="{left: styles.previewGutterLeft + 'px'}">
|
<div class="gutter" :style="{left: styles.previewGutterLeft + 'px'}">
|
||||||
<comment-list v-if="styles.previewGutterWidth"></comment-list>
|
<comment-list v-if="styles.previewGutterWidth"></comment-list>
|
||||||
<preview-new-discussion-button></preview-new-discussion-button>
|
<preview-new-discussion-button v-if="!isCurrentTemp"></preview-new-discussion-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!styles.showEditor" class="preview__corner">
|
<div v-if="!styles.showEditor" class="preview__corner">
|
||||||
@ -32,9 +32,14 @@ export default {
|
|||||||
data: () => ({
|
data: () => ({
|
||||||
previewTop: true,
|
previewTop: true,
|
||||||
}),
|
}),
|
||||||
computed: mapGetters('layout', [
|
computed: {
|
||||||
'styles',
|
...mapGetters('file', [
|
||||||
]),
|
'isCurrentTemp',
|
||||||
|
]),
|
||||||
|
...mapGetters('layout', [
|
||||||
|
'styles',
|
||||||
|
]),
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions('data', [
|
...mapActions('data', [
|
||||||
'toggleEditor',
|
'toggleEditor',
|
||||||
|
@ -59,7 +59,7 @@ export default {
|
|||||||
created() {
|
created() {
|
||||||
editorSvc.$on('sectionList', () => this.computeText());
|
editorSvc.$on('sectionList', () => this.computeText());
|
||||||
editorSvc.$on('selectionRange', () => this.computeText());
|
editorSvc.$on('selectionRange', () => this.computeText());
|
||||||
editorSvc.$on('previewText', () => this.computeHtml());
|
editorSvc.$on('previewCtx', () => this.computeHtml());
|
||||||
editorSvc.$on('previewSelectionRange', () => this.computeHtml());
|
editorSvc.$on('previewSelectionRange', () => this.computeHtml());
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -92,7 +92,7 @@ export default {
|
|||||||
this.htmlSelection = true;
|
this.htmlSelection = true;
|
||||||
if (!text) {
|
if (!text) {
|
||||||
this.htmlSelection = false;
|
this.htmlSelection = false;
|
||||||
text = editorSvc.previewText;
|
text = editorSvc.previewCtx.text;
|
||||||
}
|
}
|
||||||
if (text != null) {
|
if (text != null) {
|
||||||
this.htmlStats.forEach((stat) => {
|
this.htmlStats.forEach((stat) => {
|
||||||
|
@ -30,7 +30,7 @@ export default {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const y = e.clientY - tocElt.getBoundingClientRect().top;
|
const y = e.clientY - tocElt.getBoundingClientRect().top;
|
||||||
|
|
||||||
editorSvc.sectionDescList.some((sectionDesc) => {
|
editorSvc.previewCtx.sectionDescList.some((sectionDesc) => {
|
||||||
if (y >= sectionDesc.tocDimension.endOffset) {
|
if (y >= sectionDesc.tocDimension.endOffset) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -64,7 +64,7 @@ export default {
|
|||||||
const updateMaskY = () => {
|
const updateMaskY = () => {
|
||||||
const scrollPosition = editorSvc.getScrollPosition();
|
const scrollPosition = editorSvc.getScrollPosition();
|
||||||
if (scrollPosition) {
|
if (scrollPosition) {
|
||||||
const sectionDesc = editorSvc.sectionDescList[scrollPosition.sectionIdx];
|
const sectionDesc = editorSvc.previewCtx.sectionDescList[scrollPosition.sectionIdx];
|
||||||
this.maskY = sectionDesc.tocDimension.startOffset +
|
this.maskY = sectionDesc.tocDimension.startOffset +
|
||||||
(scrollPosition.posInSection * sectionDesc.tocDimension.height);
|
(scrollPosition.posInSection * sectionDesc.tocDimension.height);
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ export default class PreviewClassApplier {
|
|||||||
this.lastEltCount = this.eltCollection.length;
|
this.lastEltCount = this.eltCollection.length;
|
||||||
|
|
||||||
this.restoreClass = () => {
|
this.restoreClass = () => {
|
||||||
if (!editorSvc.sectionDescWithDiffsList) {
|
if (!editorSvc.previewCtxWithDiffs) {
|
||||||
this.removeClass();
|
this.removeClass();
|
||||||
} else if (!this.eltCollection.length || this.eltCollection.length !== this.lastEltCount) {
|
} else if (!this.eltCollection.length || this.eltCollection.length !== this.lastEltCount) {
|
||||||
this.removeClass();
|
this.removeClass();
|
||||||
@ -31,15 +31,17 @@ export default class PreviewClassApplier {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
editorSvc.$on('sectionDescWithDiffsList', this.restoreClass);
|
editorSvc.$on('previewCtxWithDiffs', this.restoreClass);
|
||||||
nextTick(() => this.applyClass());
|
nextTick(() => this.applyClass());
|
||||||
}
|
}
|
||||||
|
|
||||||
applyClass() {
|
applyClass() {
|
||||||
const offset = this.offsetGetter();
|
const offset = this.offsetGetter();
|
||||||
if (offset) {
|
if (offset) {
|
||||||
const offsetStart = editorSvc.getPreviewOffset(offset.start, editorSvc.sectionDescList);
|
const offsetStart = editorSvc.getPreviewOffset(
|
||||||
const offsetEnd = editorSvc.getPreviewOffset(offset.end, editorSvc.sectionDescList);
|
offset.start, editorSvc.previewCtx.sectionDescList);
|
||||||
|
const offsetEnd = editorSvc.getPreviewOffset(
|
||||||
|
offset.end, editorSvc.previewCtx.sectionDescList);
|
||||||
if (offsetStart != null && offsetEnd != null && offsetStart !== offsetEnd) {
|
if (offsetStart != null && offsetEnd != null && offsetStart !== offsetEnd) {
|
||||||
const start = cledit.Utils.findContainer(
|
const start = cledit.Utils.findContainer(
|
||||||
editorSvc.previewElt, Math.min(offsetStart, offsetEnd));
|
editorSvc.previewElt, Math.min(offsetStart, offsetEnd));
|
||||||
@ -63,8 +65,7 @@ export default class PreviewClassApplier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
editorSvc.$off('previewHtml', this.restoreClass);
|
editorSvc.$off('previewCtxWithDiffs', this.restoreClass);
|
||||||
editorSvc.$off('sectionDescWithDiffsList', this.restoreClass);
|
|
||||||
nextTick(() => this.removeClass());
|
nextTick(() => this.removeClass());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -163,9 +163,9 @@ export default {
|
|||||||
() => this.updateSticky(),
|
() => this.updateSticky(),
|
||||||
{ immediate: true });
|
{ immediate: true });
|
||||||
|
|
||||||
// Move preview discussions once sectionDescWithDiffsList have been calculated
|
// Move preview discussions once previewCtxWithDiffs has been calculated
|
||||||
if (!editorSvc.sectionDescWithDiffsList) {
|
if (!editorSvc.previewCtxWithDiffs) {
|
||||||
editorSvc.$once('sectionDescWithDiffsList', () => {
|
editorSvc.$once('previewCtxWithDiffs', () => {
|
||||||
this.updateTops();
|
this.updateTops();
|
||||||
this.updateSticky();
|
this.updateSticky();
|
||||||
});
|
});
|
||||||
|
@ -48,6 +48,7 @@ export default {
|
|||||||
'previousDiscussionId',
|
'previousDiscussionId',
|
||||||
'nextDiscussionId',
|
'nextDiscussionId',
|
||||||
'currentFileDiscussions',
|
'currentFileDiscussions',
|
||||||
|
'currentDiscussionLastCommentId',
|
||||||
]),
|
]),
|
||||||
...mapGetters('layout', [
|
...mapGetters('layout', [
|
||||||
'constants',
|
'constants',
|
||||||
@ -59,7 +60,7 @@ export default {
|
|||||||
return this.nextDiscussionId && this.nextDiscussionId !== this.currentDiscussionId;
|
return this.nextDiscussionId && this.nextDiscussionId !== this.currentDiscussionId;
|
||||||
},
|
},
|
||||||
showRemove() {
|
showRemove() {
|
||||||
return this.currentFileDiscussions[this.currentDiscussionId];
|
return this.currentDiscussionLastCommentId;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -28,7 +28,7 @@ export default {
|
|||||||
) {
|
) {
|
||||||
this.selection = editorSvc.getTrimmedSelection();
|
this.selection = editorSvc.getTrimmedSelection();
|
||||||
if (this.selection) {
|
if (this.selection) {
|
||||||
const text = editorSvc.previewTextWithDiffsList;
|
const text = editorSvc.previewCtxWithDiffs.text;
|
||||||
offset = editorSvc.getPreviewOffset(this.selection.end);
|
offset = editorSvc.getPreviewOffset(this.selection.end);
|
||||||
while (offset && text[offset - 1] === '\n') {
|
while (offset && text[offset - 1] === '\n') {
|
||||||
offset -= 1;
|
offset -= 1;
|
||||||
|
@ -4,6 +4,12 @@
|
|||||||
<p>You have to <a href="javascript:void(0)" @click="signin">sign in with Google</a> to enable revision history.</p>
|
<p>You have to <a href="javascript:void(0)" @click="signin">sign in with Google</a> to enable revision history.</p>
|
||||||
<p><b>Note:</b> This will sync your main workspace.</p>
|
<p><b>Note:</b> This will sync your main workspace.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="side-bar__info" v-if="loading">
|
||||||
|
<p>Loading history…</p>
|
||||||
|
</div>
|
||||||
|
<div class="side-bar__info" v-else-if="!revisionsWithSpacer.length">
|
||||||
|
<p><b>{{currentFileName}}</b> has no history.</p>
|
||||||
|
</div>
|
||||||
<div class="revision" v-for="revision in revisionsWithSpacer" :key="revision.id">
|
<div class="revision" v-for="revision in revisionsWithSpacer" :key="revision.id">
|
||||||
<div class="history__spacer" v-if="revision.spacer"></div>
|
<div class="history__spacer" v-if="revision.spacer"></div>
|
||||||
<a class="revision__button button flex flex--row" href="javascript:void(0)" @click="open(revision)">
|
<a class="revision__button button flex flex--row" href="javascript:void(0)" @click="open(revision)">
|
||||||
@ -53,12 +59,16 @@ export default {
|
|||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
allRevisions: [],
|
allRevisions: [],
|
||||||
|
loading: false,
|
||||||
showCount: pageSize,
|
showCount: pageSize,
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters('workspace', [
|
...mapGetters('workspace', [
|
||||||
'syncToken',
|
'syncToken',
|
||||||
]),
|
]),
|
||||||
|
currentFileName() {
|
||||||
|
return this.$store.getters['file/current'].name;
|
||||||
|
},
|
||||||
revisions() {
|
revisions() {
|
||||||
return this.allRevisions.slice(0, this.showCount);
|
return this.allRevisions.slice(0, this.showCount);
|
||||||
},
|
},
|
||||||
@ -127,7 +137,7 @@ export default {
|
|||||||
previewClassAppliers.forEach(previewClassApplier => previewClassApplier.stop());
|
previewClassAppliers.forEach(previewClassApplier => previewClassApplier.stop());
|
||||||
previewClassAppliers = [];
|
previewClassAppliers = [];
|
||||||
if (revisionContent) {
|
if (revisionContent) {
|
||||||
editorSvc.$once('sectionDescWithDiffsList', () => {
|
editorSvc.$once('previewCtxWithDiffs', () => {
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
revisionContent.diffs.forEach(([type, text]) => {
|
revisionContent.diffs.forEach(([type, text]) => {
|
||||||
if (type) {
|
if (type) {
|
||||||
@ -177,7 +187,9 @@ export default {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (revisionsPromise) {
|
if (revisionsPromise) {
|
||||||
|
this.loading = true;
|
||||||
revisionsPromise.then((revisions) => {
|
revisionsPromise.then((revisions) => {
|
||||||
|
this.loading = false;
|
||||||
this.allRevisions = revisions;
|
this.allRevisions = revisions;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -61,14 +61,14 @@
|
|||||||
Markdown cheat sheet
|
Markdown cheat sheet
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<hr>
|
<hr>
|
||||||
<menu-entry @click.native="setPanel('export')">
|
|
||||||
<icon-content-save slot="icon"></icon-content-save>
|
|
||||||
Export to disk
|
|
||||||
</menu-entry>
|
|
||||||
<menu-entry @click.native="setPanel('import')">
|
<menu-entry @click.native="setPanel('import')">
|
||||||
<icon-content-save slot="icon"></icon-content-save>
|
<icon-content-save slot="icon"></icon-content-save>
|
||||||
Import from disk
|
Import from disk
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="setPanel('export')">
|
||||||
|
<icon-content-save slot="icon"></icon-content-save>
|
||||||
|
Export to disk
|
||||||
|
</menu-entry>
|
||||||
<menu-entry @click.native="print">
|
<menu-entry @click.native="print">
|
||||||
<icon-printer slot="icon"></icon-printer>
|
<icon-printer slot="icon"></icon-printer>
|
||||||
Print
|
Print
|
||||||
|
@ -1,99 +1,104 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="side-bar__panel side-bar__panel--menu">
|
<div class="side-bar__panel side-bar__panel--menu">
|
||||||
<div class="side-bar__info" v-if="noToken">
|
<div class="side-bar__info" v-if="isCurrentTemp">
|
||||||
<p>You have to <b>link an account</b> to start publishing files.</p>
|
<p><b>{{currentFileName}}</b> can not be published as it's a temporary file.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="side-bar__info" v-if="publishLocations.length">
|
<div v-else>
|
||||||
<p><b>{{currentFileName}}</b> is already published.</p>
|
<div class="side-bar__info" v-if="noToken">
|
||||||
<menu-entry @click.native="requestPublish">
|
<p>You have to <b>link an account</b> to start publishing files.</p>
|
||||||
<icon-upload slot="icon"></icon-upload>
|
</div>
|
||||||
<div>Publish now</div>
|
<div class="side-bar__info" v-if="publishLocations.length">
|
||||||
<span>Update current file publications.</span>
|
<p><b>{{currentFileName}}</b> is already published.</p>
|
||||||
</menu-entry>
|
<menu-entry @click.native="requestPublish">
|
||||||
<menu-entry @click.native="managePublish">
|
<icon-upload slot="icon"></icon-upload>
|
||||||
<icon-view-list slot="icon"></icon-view-list>
|
<div>Publish now</div>
|
||||||
<div>File publication</div>
|
<span>Update current file publications.</span>
|
||||||
<span>Manage current file publication locations.</span>
|
</menu-entry>
|
||||||
</menu-entry>
|
<menu-entry @click.native="managePublish">
|
||||||
</div>
|
<icon-view-list slot="icon"></icon-view-list>
|
||||||
<hr>
|
<div>File publication</div>
|
||||||
<div v-for="token in googleDriveTokens" :key="token.sub">
|
<span>Manage current file publication locations.</span>
|
||||||
<menu-entry @click.native="publishGoogleDrive(token)">
|
</menu-entry>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div v-for="token in googleDriveTokens" :key="token.sub">
|
||||||
|
<menu-entry @click.native="publishGoogleDrive(token)">
|
||||||
|
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
|
||||||
|
<div>Publish to Google Drive</div>
|
||||||
|
<span>{{token.name}}</span>
|
||||||
|
</menu-entry>
|
||||||
|
</div>
|
||||||
|
<div v-for="token in dropboxTokens" :key="token.sub">
|
||||||
|
<menu-entry @click.native="publishDropbox(token)">
|
||||||
|
<icon-provider slot="icon" provider-id="dropbox"></icon-provider>
|
||||||
|
<div>Publish to Dropbox</div>
|
||||||
|
<span>{{token.name}}</span>
|
||||||
|
</menu-entry>
|
||||||
|
</div>
|
||||||
|
<div v-for="token in githubTokens" :key="token.sub">
|
||||||
|
<menu-entry @click.native="publishGithub(token)">
|
||||||
|
<icon-provider slot="icon" provider-id="github"></icon-provider>
|
||||||
|
<div>Publish to GitHub</div>
|
||||||
|
<span>{{token.name}}</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="publishGist(token)">
|
||||||
|
<icon-provider slot="icon" provider-id="gist"></icon-provider>
|
||||||
|
<div>Publish to Gist</div>
|
||||||
|
<span>{{token.name}}</span>
|
||||||
|
</menu-entry>
|
||||||
|
</div>
|
||||||
|
<div v-for="token in wordpressTokens" :key="token.sub">
|
||||||
|
<menu-entry @click.native="publishWordpress(token)">
|
||||||
|
<icon-provider slot="icon" provider-id="wordpress"></icon-provider>
|
||||||
|
<div>Publish to WordPress</div>
|
||||||
|
<span>{{token.name}}</span>
|
||||||
|
</menu-entry>
|
||||||
|
</div>
|
||||||
|
<div v-for="token in bloggerTokens" :key="token.sub">
|
||||||
|
<menu-entry @click.native="publishBlogger(token)">
|
||||||
|
<icon-provider slot="icon" provider-id="blogger"></icon-provider>
|
||||||
|
<div>Publish to Blogger</div>
|
||||||
|
<span>{{token.name}}</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="publishBloggerPage(token)">
|
||||||
|
<icon-provider slot="icon" provider-id="bloggerPage"></icon-provider>
|
||||||
|
<div>Publish to Blogger Page</div>
|
||||||
|
<span>{{token.name}}</span>
|
||||||
|
</menu-entry>
|
||||||
|
</div>
|
||||||
|
<div v-for="token in zendeskTokens" :key="token.sub">
|
||||||
|
<menu-entry @click.native="publishZendesk(token)">
|
||||||
|
<icon-provider slot="icon" provider-id="zendesk"></icon-provider>
|
||||||
|
<div>Publish to Zendesk Help Center</div>
|
||||||
|
<span>{{token.name}} — {{token.subdomain}}</span>
|
||||||
|
</menu-entry>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<menu-entry @click.native="addGoogleDriveAccount">
|
||||||
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
|
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
|
||||||
<div>Publish to Google Drive</div>
|
<span>Add Google Drive account</span>
|
||||||
<span>{{token.name}}</span>
|
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
</div>
|
<menu-entry @click.native="addDropboxAccount">
|
||||||
<div v-for="token in dropboxTokens" :key="token.sub">
|
|
||||||
<menu-entry @click.native="publishDropbox(token)">
|
|
||||||
<icon-provider slot="icon" provider-id="dropbox"></icon-provider>
|
<icon-provider slot="icon" provider-id="dropbox"></icon-provider>
|
||||||
<div>Publish to Dropbox</div>
|
<span>Add Dropbox account</span>
|
||||||
<span>{{token.name}}</span>
|
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
</div>
|
<menu-entry @click.native="addGithubAccount">
|
||||||
<div v-for="token in githubTokens" :key="token.sub">
|
|
||||||
<menu-entry @click.native="publishGithub(token)">
|
|
||||||
<icon-provider slot="icon" provider-id="github"></icon-provider>
|
<icon-provider slot="icon" provider-id="github"></icon-provider>
|
||||||
<div>Publish to GitHub</div>
|
<span>Add GitHub account</span>
|
||||||
<span>{{token.name}}</span>
|
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<menu-entry @click.native="publishGist(token)">
|
<menu-entry @click.native="addWordpressAccount">
|
||||||
<icon-provider slot="icon" provider-id="gist"></icon-provider>
|
|
||||||
<div>Publish to Gist</div>
|
|
||||||
<span>{{token.name}}</span>
|
|
||||||
</menu-entry>
|
|
||||||
</div>
|
|
||||||
<div v-for="token in wordpressTokens" :key="token.sub">
|
|
||||||
<menu-entry @click.native="publishWordpress(token)">
|
|
||||||
<icon-provider slot="icon" provider-id="wordpress"></icon-provider>
|
<icon-provider slot="icon" provider-id="wordpress"></icon-provider>
|
||||||
<div>Publish to WordPress</div>
|
<span>Add WordPress account</span>
|
||||||
<span>{{token.name}}</span>
|
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
</div>
|
<menu-entry @click.native="addBloggerAccount">
|
||||||
<div v-for="token in bloggerTokens" :key="token.sub">
|
|
||||||
<menu-entry @click.native="publishBlogger(token)">
|
|
||||||
<icon-provider slot="icon" provider-id="blogger"></icon-provider>
|
<icon-provider slot="icon" provider-id="blogger"></icon-provider>
|
||||||
<div>Publish to Blogger</div>
|
<span>Add Blogger account</span>
|
||||||
<span>{{token.name}}</span>
|
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<menu-entry @click.native="publishBloggerPage(token)">
|
<menu-entry @click.native="addZendeskAccount">
|
||||||
<icon-provider slot="icon" provider-id="bloggerPage"></icon-provider>
|
|
||||||
<div>Publish to Blogger Page</div>
|
|
||||||
<span>{{token.name}}</span>
|
|
||||||
</menu-entry>
|
|
||||||
</div>
|
|
||||||
<div v-for="token in zendeskTokens" :key="token.sub">
|
|
||||||
<menu-entry @click.native="publishZendesk(token)">
|
|
||||||
<icon-provider slot="icon" provider-id="zendesk"></icon-provider>
|
<icon-provider slot="icon" provider-id="zendesk"></icon-provider>
|
||||||
<div>Publish to Zendesk Help Center</div>
|
<span>Add Zendesk account</span>
|
||||||
<span>{{token.name}} — {{token.subdomain}}</span>
|
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
|
||||||
<menu-entry @click.native="addGoogleDriveAccount">
|
|
||||||
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
|
|
||||||
<span>Add Google Drive account</span>
|
|
||||||
</menu-entry>
|
|
||||||
<menu-entry @click.native="addDropboxAccount">
|
|
||||||
<icon-provider slot="icon" provider-id="dropbox"></icon-provider>
|
|
||||||
<span>Add Dropbox account</span>
|
|
||||||
</menu-entry>
|
|
||||||
<menu-entry @click.native="addGithubAccount">
|
|
||||||
<icon-provider slot="icon" provider-id="github"></icon-provider>
|
|
||||||
<span>Add GitHub account</span>
|
|
||||||
</menu-entry>
|
|
||||||
<menu-entry @click.native="addWordpressAccount">
|
|
||||||
<icon-provider slot="icon" provider-id="wordpress"></icon-provider>
|
|
||||||
<span>Add WordPress account</span>
|
|
||||||
</menu-entry>
|
|
||||||
<menu-entry @click.native="addBloggerAccount">
|
|
||||||
<icon-provider slot="icon" provider-id="blogger"></icon-provider>
|
|
||||||
<span>Add Blogger account</span>
|
|
||||||
</menu-entry>
|
|
||||||
<menu-entry @click.native="addZendeskAccount">
|
|
||||||
<icon-provider slot="icon" provider-id="zendesk"></icon-provider>
|
|
||||||
<span>Add Zendesk account</span>
|
|
||||||
</menu-entry>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -126,6 +131,9 @@ export default {
|
|||||||
...mapState('queue', [
|
...mapState('queue', [
|
||||||
'isPublishRequested',
|
'isPublishRequested',
|
||||||
]),
|
]),
|
||||||
|
...mapGetters('file', [
|
||||||
|
'isCurrentTemp',
|
||||||
|
]),
|
||||||
...mapGetters('publishLocation', {
|
...mapGetters('publishLocation', {
|
||||||
publishLocations: 'current',
|
publishLocations: 'current',
|
||||||
}),
|
}),
|
||||||
|
@ -1,76 +1,81 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="side-bar__panel side-bar__panel--menu">
|
<div class="side-bar__panel side-bar__panel--menu">
|
||||||
<div class="side-bar__info" v-if="noToken">
|
<div class="side-bar__info" v-if="isCurrentTemp">
|
||||||
<p>You have to <b>link an account</b> to start syncing files.</p>
|
<p><b>{{currentFileName}}</b> can not be synchronized as it's a temporary file.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="side-bar__info" v-if="syncLocations.length">
|
<div v-else>
|
||||||
<p><b>{{currentFileName}}</b> is already synchronized.</p>
|
<div class="side-bar__info" v-if="noToken">
|
||||||
<menu-entry @click.native="requestSync">
|
<p>You have to <b>link an account</b> to start syncing files.</p>
|
||||||
<icon-sync slot="icon"></icon-sync>
|
</div>
|
||||||
<div>Synchronize now</div>
|
<div class="side-bar__info" v-if="syncLocations.length">
|
||||||
<span>Download / upload file changes.</span>
|
<p><b>{{currentFileName}}</b> is already synchronized.</p>
|
||||||
</menu-entry>
|
<menu-entry @click.native="requestSync">
|
||||||
<menu-entry @click.native="manageSync">
|
<icon-sync slot="icon"></icon-sync>
|
||||||
<icon-view-list slot="icon"></icon-view-list>
|
<div>Synchronize now</div>
|
||||||
<div>File synchronization</div>
|
<span>Download / upload file changes.</span>
|
||||||
<span>Manage current file synchronized locations.</span>
|
</menu-entry>
|
||||||
</menu-entry>
|
<menu-entry @click.native="manageSync">
|
||||||
</div>
|
<icon-view-list slot="icon"></icon-view-list>
|
||||||
<hr>
|
<div>File synchronization</div>
|
||||||
<div v-for="token in googleDriveTokens" :key="token.sub">
|
<span>Manage current file synchronized locations.</span>
|
||||||
<menu-entry @click.native="openGoogleDrive(token)">
|
</menu-entry>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div v-for="token in googleDriveTokens" :key="token.sub">
|
||||||
|
<menu-entry @click.native="openGoogleDrive(token)">
|
||||||
|
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
|
||||||
|
<div>Open from Google Drive</div>
|
||||||
|
<span>{{token.name}}</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="saveGoogleDrive(token)">
|
||||||
|
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
|
||||||
|
<div>Save on Google Drive</div>
|
||||||
|
<span>{{token.name}}</span>
|
||||||
|
</menu-entry>
|
||||||
|
</div>
|
||||||
|
<div v-for="token in dropboxTokens" :key="token.sub">
|
||||||
|
<menu-entry @click.native="openDropbox(token)">
|
||||||
|
<icon-provider slot="icon" provider-id="dropbox"></icon-provider>
|
||||||
|
<div>Open from Dropbox</div>
|
||||||
|
<span>{{token.name}}</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="saveDropbox(token)">
|
||||||
|
<icon-provider slot="icon" provider-id="dropbox"></icon-provider>
|
||||||
|
<div>Save on Dropbox</div>
|
||||||
|
<span>{{token.name}}</span>
|
||||||
|
</menu-entry>
|
||||||
|
</div>
|
||||||
|
<div v-for="token in githubTokens" :key="token.sub">
|
||||||
|
<menu-entry @click.native="openGithub(token)">
|
||||||
|
<icon-provider slot="icon" provider-id="github"></icon-provider>
|
||||||
|
<div>Open from GitHub</div>
|
||||||
|
<span>{{token.name}}</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="saveGithub(token)">
|
||||||
|
<icon-provider slot="icon" provider-id="github"></icon-provider>
|
||||||
|
<div>Save on GitHub</div>
|
||||||
|
<span>{{token.name}}</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="saveGist(token)">
|
||||||
|
<icon-provider slot="icon" provider-id="gist"></icon-provider>
|
||||||
|
<div>Save on Gist</div>
|
||||||
|
<span>{{token.name}}</span>
|
||||||
|
</menu-entry>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<menu-entry @click.native="addGoogleDriveAccount">
|
||||||
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
|
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
|
||||||
<div>Open from Google Drive</div>
|
<span>Add Google Drive account</span>
|
||||||
<span>{{token.name}}</span>
|
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<menu-entry @click.native="saveGoogleDrive(token)">
|
<menu-entry @click.native="addDropboxAccount">
|
||||||
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
|
|
||||||
<div>Save on Google Drive</div>
|
|
||||||
<span>{{token.name}}</span>
|
|
||||||
</menu-entry>
|
|
||||||
</div>
|
|
||||||
<div v-for="token in dropboxTokens" :key="token.sub">
|
|
||||||
<menu-entry @click.native="openDropbox(token)">
|
|
||||||
<icon-provider slot="icon" provider-id="dropbox"></icon-provider>
|
<icon-provider slot="icon" provider-id="dropbox"></icon-provider>
|
||||||
<div>Open from Dropbox</div>
|
<span>Add Dropbox account</span>
|
||||||
<span>{{token.name}}</span>
|
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<menu-entry @click.native="saveDropbox(token)">
|
<menu-entry @click.native="addGithubAccount">
|
||||||
<icon-provider slot="icon" provider-id="dropbox"></icon-provider>
|
<icon-provider slot="icon" provider-id="github"></icon-provider>
|
||||||
<div>Save on Dropbox</div>
|
<span>Add GitHub account</span>
|
||||||
<span>{{token.name}}</span>
|
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
</div>
|
</div>
|
||||||
<div v-for="token in githubTokens" :key="token.sub">
|
|
||||||
<menu-entry @click.native="openGithub(token)">
|
|
||||||
<icon-provider slot="icon" provider-id="github"></icon-provider>
|
|
||||||
<div>Open from GitHub</div>
|
|
||||||
<span>{{token.name}}</span>
|
|
||||||
</menu-entry>
|
|
||||||
<menu-entry @click.native="saveGithub(token)">
|
|
||||||
<icon-provider slot="icon" provider-id="github"></icon-provider>
|
|
||||||
<div>Save on GitHub</div>
|
|
||||||
<span>{{token.name}}</span>
|
|
||||||
</menu-entry>
|
|
||||||
<menu-entry @click.native="saveGist(token)">
|
|
||||||
<icon-provider slot="icon" provider-id="gist"></icon-provider>
|
|
||||||
<div>Save on Gist</div>
|
|
||||||
<span>{{token.name}}</span>
|
|
||||||
</menu-entry>
|
|
||||||
</div>
|
|
||||||
<hr>
|
|
||||||
<menu-entry @click.native="addGoogleDriveAccount">
|
|
||||||
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
|
|
||||||
<span>Add Google Drive account</span>
|
|
||||||
</menu-entry>
|
|
||||||
<menu-entry @click.native="addDropboxAccount">
|
|
||||||
<icon-provider slot="icon" provider-id="dropbox"></icon-provider>
|
|
||||||
<span>Add Dropbox account</span>
|
|
||||||
</menu-entry>
|
|
||||||
<menu-entry @click.native="addGithubAccount">
|
|
||||||
<icon-provider slot="icon" provider-id="github"></icon-provider>
|
|
||||||
<span>Add GitHub account</span>
|
|
||||||
</menu-entry>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -107,6 +112,9 @@ export default {
|
|||||||
...mapGetters('workspace', [
|
...mapGetters('workspace', [
|
||||||
'syncToken',
|
'syncToken',
|
||||||
]),
|
]),
|
||||||
|
...mapGetters('file', [
|
||||||
|
'isCurrentTemp',
|
||||||
|
]),
|
||||||
...mapGetters('syncLocation', {
|
...mapGetters('syncLocation', {
|
||||||
syncLocations: 'current',
|
syncLocations: 'current',
|
||||||
}),
|
}),
|
||||||
|
@ -75,5 +75,6 @@ export default {
|
|||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
padding: 0.75em 1.5em;
|
padding: 0.75em 1.5em;
|
||||||
margin-bottom: 1.2em;
|
margin-bottom: 1.2em;
|
||||||
|
line-height: 1.55;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -341,16 +341,23 @@ function SelectionMgr(editor) {
|
|||||||
offsetInContainer = offset.offsetInContainer;
|
offsetInContainer = offset.offsetInContainer;
|
||||||
}
|
}
|
||||||
let containerElt = container;
|
let containerElt = container;
|
||||||
if (!containerElt.hasChildNodes()) {
|
if (!containerElt.hasChildNodes() && container.parentNode) {
|
||||||
containerElt = container.parentNode;
|
containerElt = container.parentNode;
|
||||||
}
|
}
|
||||||
let isInvisible = false;
|
let isInvisible = false;
|
||||||
while (containerElt.offsetHeight === 0) {
|
while (!containerElt.offsetHeight) {
|
||||||
isInvisible = true;
|
isInvisible = true;
|
||||||
if (containerElt.previousSibling) {
|
if (containerElt.previousSibling) {
|
||||||
containerElt = containerElt.previousSibling;
|
containerElt = containerElt.previousSibling;
|
||||||
} else {
|
} else {
|
||||||
containerElt = containerElt.parentNode;
|
containerElt = containerElt.parentNode;
|
||||||
|
if (!containerElt) {
|
||||||
|
return {
|
||||||
|
top: 0,
|
||||||
|
height: 0,
|
||||||
|
left: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let rect;
|
let rect;
|
||||||
|
@ -54,15 +54,15 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
|||||||
converter: null,
|
converter: null,
|
||||||
parsingCtx: null,
|
parsingCtx: null,
|
||||||
conversionCtx: null,
|
conversionCtx: null,
|
||||||
|
previewCtx: {
|
||||||
|
sectionDescList: [],
|
||||||
|
},
|
||||||
|
previewCtxMeasured: null,
|
||||||
|
previewCtxWithDiffs: null,
|
||||||
sectionList: null,
|
sectionList: null,
|
||||||
sectionDescList: [],
|
|
||||||
sectionDescMeasuredList: null,
|
|
||||||
sectionDescWithDiffsList: null,
|
|
||||||
selectionRange: null,
|
selectionRange: null,
|
||||||
previewSelectionRange: null,
|
previewSelectionRange: null,
|
||||||
previewSelectionStartOffset: null,
|
previewSelectionStartOffset: null,
|
||||||
previewHtml: null,
|
|
||||||
previewText: null,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the Prism grammar with the options
|
* Initialize the Prism grammar with the options
|
||||||
@ -86,8 +86,10 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
|||||||
* Initialize the cledit editor with markdown-it section parser and Prism highlighter
|
* Initialize the cledit editor with markdown-it section parser and Prism highlighter
|
||||||
*/
|
*/
|
||||||
initClEditor() {
|
initClEditor() {
|
||||||
this.sectionDescMeasuredList = null;
|
this.previewCtxMeasured = null;
|
||||||
this.sectionDescWithDiffsList = null;
|
editorSvc.$emit('previewCtxMeasured', null);
|
||||||
|
this.previewCtxWithDiffs = null;
|
||||||
|
editorSvc.$emit('previewCtxWithDiffs', null);
|
||||||
const options = {
|
const options = {
|
||||||
sectionHighlighter: section => Prism.highlight(
|
sectionHighlighter: section => Prism.highlight(
|
||||||
section.text, this.prismGrammars[section.data]),
|
section.text, this.prismGrammars[section.data]),
|
||||||
@ -119,7 +121,7 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
|||||||
* Refresh the preview with the result of `convert()`
|
* Refresh the preview with the result of `convert()`
|
||||||
*/
|
*/
|
||||||
refreshPreview() {
|
refreshPreview() {
|
||||||
const newSectionDescList = [];
|
const sectionDescList = [];
|
||||||
let sectionPreviewElt;
|
let sectionPreviewElt;
|
||||||
let sectionTocElt;
|
let sectionTocElt;
|
||||||
let sectionIdx = 0;
|
let sectionIdx = 0;
|
||||||
@ -132,14 +134,14 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
|||||||
for (let i = 0; i < item[1].length; i += 1) {
|
for (let i = 0; i < item[1].length; i += 1) {
|
||||||
const section = this.conversionCtx.sectionList[sectionIdx];
|
const section = this.conversionCtx.sectionList[sectionIdx];
|
||||||
if (item[0] === 0) {
|
if (item[0] === 0) {
|
||||||
let sectionDesc = this.sectionDescList[sectionDescIdx];
|
let sectionDesc = this.previewCtx.sectionDescList[sectionDescIdx];
|
||||||
sectionDescIdx += 1;
|
sectionDescIdx += 1;
|
||||||
if (sectionDesc.editorElt !== section.elt) {
|
if (sectionDesc.editorElt !== section.elt) {
|
||||||
// Force textToPreviewDiffs computation
|
// Force textToPreviewDiffs computation
|
||||||
sectionDesc = new SectionDesc(
|
sectionDesc = new SectionDesc(
|
||||||
section, sectionDesc.previewElt, sectionDesc.tocElt, sectionDesc.html);
|
section, sectionDesc.previewElt, sectionDesc.tocElt, sectionDesc.html);
|
||||||
}
|
}
|
||||||
newSectionDescList.push(sectionDesc);
|
sectionDescList.push(sectionDesc);
|
||||||
previewHtml += sectionDesc.html;
|
previewHtml += sectionDesc.html;
|
||||||
sectionIdx += 1;
|
sectionIdx += 1;
|
||||||
insertBeforePreviewElt = insertBeforePreviewElt.nextSibling;
|
insertBeforePreviewElt = insertBeforePreviewElt.nextSibling;
|
||||||
@ -187,20 +189,22 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
|||||||
}
|
}
|
||||||
|
|
||||||
previewHtml += html;
|
previewHtml += html;
|
||||||
newSectionDescList.push(new SectionDesc(section, sectionPreviewElt, sectionTocElt, html));
|
sectionDescList.push(new SectionDesc(section, sectionPreviewElt, sectionTocElt, html));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.sectionDescList = newSectionDescList;
|
|
||||||
this.previewHtml = previewHtml.replace(/^\s+|\s+$/g, '');
|
|
||||||
this.$emit('previewHtml', this.previewHtml);
|
|
||||||
this.tocElt.classList[
|
this.tocElt.classList[
|
||||||
this.tocElt.querySelector('.cl-toc-section *') ? 'remove' : 'add'
|
this.tocElt.querySelector('.cl-toc-section *') ? 'remove' : 'add'
|
||||||
]('toc-tab--empty');
|
]('toc-tab--empty');
|
||||||
|
|
||||||
this.previewText = this.previewElt.textContent;
|
this.previewCtx = {
|
||||||
this.$emit('previewText', this.previewText);
|
markdown: this.conversionCtx.text,
|
||||||
|
html: previewHtml.replace(/^\s+|\s+$/g, ''),
|
||||||
|
text: this.previewElt.textContent,
|
||||||
|
sectionDescList,
|
||||||
|
};
|
||||||
|
this.$emit('previewCtx', this.previewCtx);
|
||||||
this.makeTextToPreviewDiffs();
|
this.makeTextToPreviewDiffs();
|
||||||
|
|
||||||
// Wait for images to load
|
// Wait for images to load
|
||||||
@ -217,22 +221,20 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
|||||||
|
|
||||||
Promise.all(loadedPromises)
|
Promise.all(loadedPromises)
|
||||||
// Debounce if sections have already been measured
|
// Debounce if sections have already been measured
|
||||||
.then(() => this.measureSectionDimensions(!!this.sectionDescMeasuredList));
|
.then(() => this.measureSectionDimensions(!!this.previewCtxMeasured));
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Measure the height of each section in editor, preview and toc.
|
* Measure the height of each section in editor, preview and toc.
|
||||||
*/
|
*/
|
||||||
measureSectionDimensions: allowDebounce((restoreScrollPosition) => {
|
measureSectionDimensions: allowDebounce((restoreScrollPosition = false, force = false) => {
|
||||||
if (editorSvc.sectionDescList &&
|
if (force || editorSvc.previewCtx !== editorSvc.previewCtxMeasured) {
|
||||||
this.sectionDescList !== editorSvc.sectionDescMeasuredList
|
|
||||||
) {
|
|
||||||
sectionUtils.measureSectionDimensions(editorSvc);
|
sectionUtils.measureSectionDimensions(editorSvc);
|
||||||
editorSvc.sectionDescMeasuredList = editorSvc.sectionDescList;
|
editorSvc.previewCtxMeasured = editorSvc.previewCtx;
|
||||||
if (restoreScrollPosition) {
|
if (restoreScrollPosition) {
|
||||||
editorSvc.restoreScrollPosition();
|
editorSvc.restoreScrollPosition();
|
||||||
}
|
}
|
||||||
editorSvc.$emit('sectionDescMeasuredList', editorSvc.sectionDescMeasuredList);
|
editorSvc.$emit('previewCtxMeasured', editorSvc.previewCtxMeasured);
|
||||||
}
|
}
|
||||||
}, 500),
|
}, 500),
|
||||||
|
|
||||||
@ -241,12 +243,10 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
|||||||
* asynchronously unless there is only one section to compute.
|
* asynchronously unless there is only one section to compute.
|
||||||
*/
|
*/
|
||||||
makeTextToPreviewDiffs() {
|
makeTextToPreviewDiffs() {
|
||||||
if (editorSvc.sectionDescList &&
|
if (editorSvc.previewCtx !== editorSvc.previewCtxWithDiffs) {
|
||||||
editorSvc.sectionDescList !== editorSvc.sectionDescWithDiffsList
|
|
||||||
) {
|
|
||||||
const makeOne = () => {
|
const makeOne = () => {
|
||||||
let hasOne = false;
|
let hasOne = false;
|
||||||
const hasMore = editorSvc.sectionDescList
|
const hasMore = editorSvc.previewCtx.sectionDescList
|
||||||
.some((sectionDesc) => {
|
.some((sectionDesc) => {
|
||||||
if (!sectionDesc.textToPreviewDiffs) {
|
if (!sectionDesc.textToPreviewDiffs) {
|
||||||
if (hasOne) {
|
if (hasOne) {
|
||||||
@ -264,9 +264,8 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
|||||||
if (hasMore) {
|
if (hasMore) {
|
||||||
setTimeout(() => makeOne(), 10);
|
setTimeout(() => makeOne(), 10);
|
||||||
} else {
|
} else {
|
||||||
editorSvc.previewTextWithDiffsList = editorSvc.previewText;
|
editorSvc.previewCtxWithDiffs = editorSvc.previewCtx;
|
||||||
editorSvc.sectionDescWithDiffsList = editorSvc.sectionDescList;
|
editorSvc.$emit('previewCtxWithDiffs', editorSvc.previewCtxWithDiffs);
|
||||||
editorSvc.$emit('sectionDescWithDiffsList', editorSvc.sectionDescWithDiffsList);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
makeOne();
|
makeOne();
|
||||||
@ -517,7 +516,7 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
|||||||
let lastContentId = null;
|
let lastContentId = null;
|
||||||
let lastProperties;
|
let lastProperties;
|
||||||
store.watch(
|
store.watch(
|
||||||
() => store.getters['content/current'].hash,
|
() => store.getters['content/currentChangeTrigger'],
|
||||||
() => {
|
() => {
|
||||||
const content = store.getters['content/current'];
|
const content = store.getters['content/current'];
|
||||||
// Track ID changes
|
// Track ID changes
|
||||||
@ -541,7 +540,7 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
|||||||
if (initClEditor) {
|
if (initClEditor) {
|
||||||
this.initClEditor();
|
this.initClEditor();
|
||||||
}
|
}
|
||||||
// Apply possible text and discussion changes
|
// Apply potential text and discussion changes
|
||||||
this.applyContent();
|
this.applyContent();
|
||||||
}, {
|
}, {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
@ -555,7 +554,7 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
|||||||
});
|
});
|
||||||
|
|
||||||
store.watch(() => utils.serializeObject(store.getters['layout/styles']),
|
store.watch(() => utils.serializeObject(store.getters['layout/styles']),
|
||||||
() => this.measureSectionDimensions(false, true));
|
() => this.measureSectionDimensions(false, true, true));
|
||||||
|
|
||||||
this.initHighlighters();
|
this.initHighlighters();
|
||||||
this.$emit('inited');
|
this.$emit('inited');
|
||||||
|
@ -18,8 +18,8 @@ export default {
|
|||||||
: 'previewDimension';
|
: 'previewDimension';
|
||||||
const scrollTop = elt.parentNode.scrollTop;
|
const scrollTop = elt.parentNode.scrollTop;
|
||||||
let result;
|
let result;
|
||||||
if (this.sectionDescMeasuredList) {
|
if (this.previewCtxMeasured) {
|
||||||
this.sectionDescMeasuredList.some((sectionDesc, sectionIdx) => {
|
this.previewCtxMeasured.sectionDescList.some((sectionDesc, sectionIdx) => {
|
||||||
if (scrollTop >= sectionDesc[dimensionKey].endOffset) {
|
if (scrollTop >= sectionDesc[dimensionKey].endOffset) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -40,8 +40,8 @@ export default {
|
|||||||
*/
|
*/
|
||||||
restoreScrollPosition() {
|
restoreScrollPosition() {
|
||||||
const scrollPosition = store.getters['contentState/current'].scrollPosition;
|
const scrollPosition = store.getters['contentState/current'].scrollPosition;
|
||||||
if (scrollPosition && this.sectionDescMeasuredList) {
|
if (scrollPosition && this.previewCtxMeasured) {
|
||||||
const sectionDesc = this.sectionDescMeasuredList[scrollPosition.sectionIdx];
|
const sectionDesc = this.previewCtxMeasured.sectionDescList[scrollPosition.sectionIdx];
|
||||||
if (sectionDesc) {
|
if (sectionDesc) {
|
||||||
const editorScrollTop = sectionDesc.editorDimension.startOffset +
|
const editorScrollTop = sectionDesc.editorDimension.startOffset +
|
||||||
(sectionDesc.editorDimension.height * scrollPosition.posInSection);
|
(sectionDesc.editorDimension.height * scrollPosition.posInSection);
|
||||||
@ -56,7 +56,10 @@ export default {
|
|||||||
/**
|
/**
|
||||||
* Get the offset in the preview corresponding to the offset of the markdown in the editor
|
* Get the offset in the preview corresponding to the offset of the markdown in the editor
|
||||||
*/
|
*/
|
||||||
getPreviewOffset(editorOffset, sectionDescList = this.sectionDescWithDiffsList) {
|
getPreviewOffset(
|
||||||
|
editorOffset,
|
||||||
|
sectionDescList = (this.previewCtxWithDiffs || {}).sectionDescList,
|
||||||
|
) {
|
||||||
if (!sectionDescList) {
|
if (!sectionDescList) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -81,7 +84,10 @@ export default {
|
|||||||
/**
|
/**
|
||||||
* Get the offset of the markdown in the editor corresponding to the offset in the preview
|
* Get the offset of the markdown in the editor corresponding to the offset in the preview
|
||||||
*/
|
*/
|
||||||
getEditorOffset(previewOffset, sectionDescList = this.sectionDescWithDiffsList) {
|
getEditorOffset(
|
||||||
|
previewOffset,
|
||||||
|
sectionDescList = (this.previewCtxWithDiffs || {}).sectionDescList,
|
||||||
|
) {
|
||||||
if (!sectionDescList) {
|
if (!sectionDescList) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import welcomeFile from '../data/welcomeFile.md';
|
|||||||
const dbVersion = 1;
|
const dbVersion = 1;
|
||||||
const dbStoreName = 'objects';
|
const dbStoreName = 'objects';
|
||||||
const exportWorkspace = utils.queryParams.exportWorkspace;
|
const exportWorkspace = utils.queryParams.exportWorkspace;
|
||||||
|
const silent = utils.queryParams.silent;
|
||||||
const resetApp = utils.queryParams.reset;
|
const resetApp = utils.queryParams.reset;
|
||||||
const deleteMarkerMaxAge = 1000;
|
const deleteMarkerMaxAge = 1000;
|
||||||
const checkSponsorshipAfter = (5 * 60 * 1000) + (30 * 1000); // tokenExpirationMargin + 30 sec
|
const checkSponsorshipAfter = (5 * 60 * 1000) + (30 * 1000); // tokenExpirationMargin + 30 sec
|
||||||
@ -99,142 +100,6 @@ const localDbSvc = {
|
|||||||
hashMap,
|
hashMap,
|
||||||
connection: null,
|
connection: null,
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the connection and start syncing.
|
|
||||||
*/
|
|
||||||
init() {
|
|
||||||
return Promise.resolve()
|
|
||||||
.then(() => {
|
|
||||||
// Reset the app if reset flag was passed
|
|
||||||
if (resetApp) {
|
|
||||||
return Promise.all(
|
|
||||||
Object.keys(store.getters['data/workspaces'])
|
|
||||||
.map(workspaceId => localDbSvc.removeWorkspace(workspaceId)),
|
|
||||||
)
|
|
||||||
.then(() => utils.localStorageDataIds.forEach((id) => {
|
|
||||||
// Clean data stored in localStorage
|
|
||||||
localStorage.removeItem(`data/${id}`);
|
|
||||||
}))
|
|
||||||
.then(() => {
|
|
||||||
location.reload();
|
|
||||||
throw new Error('reload');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the connection
|
|
||||||
this.connection = new Connection();
|
|
||||||
|
|
||||||
// Load the DB
|
|
||||||
return localDbSvc.sync();
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
// If exportWorkspace parameter was provided
|
|
||||||
if (exportWorkspace) {
|
|
||||||
const backup = JSON.stringify(store.getters.allItemMap);
|
|
||||||
const blob = new Blob([backup], {
|
|
||||||
type: 'text/plain;charset=utf-8',
|
|
||||||
});
|
|
||||||
FileSaver.saveAs(blob, 'StackEdit workspace.json');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save welcome file content hash if not done already
|
|
||||||
const hash = utils.hash(welcomeFile);
|
|
||||||
const welcomeFileHashes = store.getters['data/localSettings'].welcomeFileHashes;
|
|
||||||
if (!welcomeFileHashes[hash]) {
|
|
||||||
store.dispatch('data/patchLocalSettings', {
|
|
||||||
welcomeFileHashes: {
|
|
||||||
...welcomeFileHashes,
|
|
||||||
[hash]: 1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// If app was last opened 7 days ago and synchronization is off
|
|
||||||
if (!store.getters['workspace/syncToken'] &&
|
|
||||||
(store.state.workspace.lastFocus + utils.cleanTrashAfter < Date.now())
|
|
||||||
) {
|
|
||||||
// Clean files
|
|
||||||
store.getters['file/items']
|
|
||||||
.filter(file => file.parentId === 'trash') // If file is in the trash
|
|
||||||
.forEach(file => store.dispatch('deleteFile', file.id));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable sponsorship
|
|
||||||
if (utils.queryParams.paymentSuccess) {
|
|
||||||
location.hash = ''; // PaymentSuccess param is always on its own
|
|
||||||
store.dispatch('modal/paymentSuccess');
|
|
||||||
const sponsorToken = store.getters['workspace/sponsorToken'];
|
|
||||||
// Force check sponsorship after a few seconds
|
|
||||||
const currentDate = Date.now();
|
|
||||||
if (sponsorToken && sponsorToken.expiresOn > currentDate - checkSponsorshipAfter) {
|
|
||||||
store.dispatch('data/setGoogleToken', {
|
|
||||||
...sponsorToken,
|
|
||||||
expiresOn: currentDate - checkSponsorshipAfter,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync local DB periodically
|
|
||||||
utils.setInterval(() => localDbSvc.sync(), 1000);
|
|
||||||
|
|
||||||
// watch current file changing
|
|
||||||
store.watch(
|
|
||||||
() => store.getters['file/current'].id,
|
|
||||||
() => {
|
|
||||||
// See if currentFile is real, ie it has an ID
|
|
||||||
const currentFile = store.getters['file/current'];
|
|
||||||
// If current file has no ID, get the most recent file
|
|
||||||
if (!currentFile.id) {
|
|
||||||
const recentFile = store.getters['file/lastOpened'];
|
|
||||||
// Set it as the current file
|
|
||||||
if (recentFile.id) {
|
|
||||||
store.commit('file/setCurrentId', recentFile.id);
|
|
||||||
} else {
|
|
||||||
// If still no ID, create a new file
|
|
||||||
store.dispatch('createFile', {
|
|
||||||
name: 'Welcome file',
|
|
||||||
text: welcomeFile,
|
|
||||||
})
|
|
||||||
// Set it as the current file
|
|
||||||
.then(newFile => store.commit('file/setCurrentId', newFile.id));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Promise.resolve()
|
|
||||||
// Load contentState from DB
|
|
||||||
.then(() => localDbSvc.loadContentState(currentFile.id))
|
|
||||||
// Load syncedContent from DB
|
|
||||||
.then(() => localDbSvc.loadSyncedContent(currentFile.id))
|
|
||||||
// Load content from DB
|
|
||||||
.then(() => localDbSvc.loadItem(`${currentFile.id}/content`))
|
|
||||||
.then(
|
|
||||||
() => {
|
|
||||||
// Set last opened file
|
|
||||||
store.dispatch('data/setLastOpenedId', currentFile.id);
|
|
||||||
// Cancel new discussion
|
|
||||||
store.commit('discussion/setCurrentDiscussionId');
|
|
||||||
// Open the gutter if file contains discussions
|
|
||||||
store.commit('discussion/setCurrentDiscussionId',
|
|
||||||
store.getters['discussion/nextDiscussionId']);
|
|
||||||
},
|
|
||||||
(err) => {
|
|
||||||
// Failure (content is not available), go back to previous file
|
|
||||||
const lastOpenedFile = store.getters['file/lastOpened'];
|
|
||||||
store.commit('file/setCurrentId', lastOpenedFile.id);
|
|
||||||
throw err;
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.catch((err) => {
|
|
||||||
console.error(err); // eslint-disable-line no-console
|
|
||||||
store.dispatch('notification/error', err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
immediate: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync data items stored in the localStorage.
|
* Sync data items stored in the localStorage.
|
||||||
*/
|
*/
|
||||||
@ -331,6 +196,10 @@ const localDbSvc = {
|
|||||||
* Write all changes from the store since previous transaction.
|
* Write all changes from the store since previous transaction.
|
||||||
*/
|
*/
|
||||||
writeAll(storeItemMap, tx) {
|
writeAll(storeItemMap, tx) {
|
||||||
|
if (silent) {
|
||||||
|
// Skip writing to DB in silent mode
|
||||||
|
return;
|
||||||
|
}
|
||||||
const dbStore = tx.objectStore(dbStoreName);
|
const dbStore = tx.objectStore(dbStoreName);
|
||||||
const incrementedTx = this.lastTx + 1;
|
const incrementedTx = this.lastTx + 1;
|
||||||
|
|
||||||
@ -475,6 +344,142 @@ const localDbSvc = {
|
|||||||
localStorage.removeItem(`${id}/lastWindowFocus`);
|
localStorage.removeItem(`${id}/lastWindowFocus`);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the connection and start syncing.
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(() => {
|
||||||
|
// Reset the app if reset flag was passed
|
||||||
|
if (resetApp) {
|
||||||
|
return Promise.all(
|
||||||
|
Object.keys(store.getters['data/workspaces'])
|
||||||
|
.map(workspaceId => localDbSvc.removeWorkspace(workspaceId)),
|
||||||
|
)
|
||||||
|
.then(() => utils.localStorageDataIds.forEach((id) => {
|
||||||
|
// Clean data stored in localStorage
|
||||||
|
localStorage.removeItem(`data/${id}`);
|
||||||
|
}))
|
||||||
|
.then(() => {
|
||||||
|
location.reload();
|
||||||
|
throw new Error('reload');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the connection
|
||||||
|
this.connection = new Connection();
|
||||||
|
|
||||||
|
// Load the DB
|
||||||
|
return localDbSvc.sync();
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
// If exportWorkspace parameter was provided
|
||||||
|
if (exportWorkspace) {
|
||||||
|
const backup = JSON.stringify(store.getters.allItemMap);
|
||||||
|
const blob = new Blob([backup], {
|
||||||
|
type: 'text/plain;charset=utf-8',
|
||||||
|
});
|
||||||
|
FileSaver.saveAs(blob, 'StackEdit workspace.json');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save welcome file content hash if not done already
|
||||||
|
const hash = utils.hash(welcomeFile);
|
||||||
|
const welcomeFileHashes = store.getters['data/localSettings'].welcomeFileHashes;
|
||||||
|
if (!welcomeFileHashes[hash]) {
|
||||||
|
store.dispatch('data/patchLocalSettings', {
|
||||||
|
welcomeFileHashes: {
|
||||||
|
...welcomeFileHashes,
|
||||||
|
[hash]: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If app was last opened 7 days ago and synchronization is off
|
||||||
|
if (!store.getters['workspace/syncToken'] &&
|
||||||
|
(store.state.workspace.lastFocus + utils.cleanTrashAfter < Date.now())
|
||||||
|
) {
|
||||||
|
// Clean files
|
||||||
|
store.getters['file/items']
|
||||||
|
.filter(file => file.parentId === 'trash') // If file is in the trash
|
||||||
|
.forEach(file => store.dispatch('deleteFile', file.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable sponsorship
|
||||||
|
if (utils.queryParams.paymentSuccess) {
|
||||||
|
location.hash = ''; // PaymentSuccess param is always on its own
|
||||||
|
store.dispatch('modal/paymentSuccess');
|
||||||
|
const sponsorToken = store.getters['workspace/sponsorToken'];
|
||||||
|
// Force check sponsorship after a few seconds
|
||||||
|
const currentDate = Date.now();
|
||||||
|
if (sponsorToken && sponsorToken.expiresOn > currentDate - checkSponsorshipAfter) {
|
||||||
|
store.dispatch('data/setGoogleToken', {
|
||||||
|
...sponsorToken,
|
||||||
|
expiresOn: currentDate - checkSponsorshipAfter,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync local DB periodically
|
||||||
|
utils.setInterval(() => localDbSvc.sync(), 1000);
|
||||||
|
|
||||||
|
// watch current file changing
|
||||||
|
store.watch(
|
||||||
|
() => store.getters['file/current'].id,
|
||||||
|
() => {
|
||||||
|
// See if currentFile is real, ie it has an ID
|
||||||
|
const currentFile = store.getters['file/current'];
|
||||||
|
// If current file has no ID, get the most recent file
|
||||||
|
if (!currentFile.id) {
|
||||||
|
const recentFile = store.getters['file/lastOpened'];
|
||||||
|
// Set it as the current file
|
||||||
|
if (recentFile.id) {
|
||||||
|
store.commit('file/setCurrentId', recentFile.id);
|
||||||
|
} else {
|
||||||
|
// If still no ID, create a new file
|
||||||
|
store.dispatch('createFile', {
|
||||||
|
name: 'Welcome file',
|
||||||
|
text: welcomeFile,
|
||||||
|
})
|
||||||
|
// Set it as the current file
|
||||||
|
.then(newFile => store.commit('file/setCurrentId', newFile.id));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Promise.resolve()
|
||||||
|
// Load contentState from DB
|
||||||
|
.then(() => localDbSvc.loadContentState(currentFile.id))
|
||||||
|
// Load syncedContent from DB
|
||||||
|
.then(() => localDbSvc.loadSyncedContent(currentFile.id))
|
||||||
|
// Load content from DB
|
||||||
|
.then(() => localDbSvc.loadItem(`${currentFile.id}/content`))
|
||||||
|
.then(
|
||||||
|
() => {
|
||||||
|
// Set last opened file
|
||||||
|
store.dispatch('data/setLastOpenedId', currentFile.id);
|
||||||
|
// Cancel new discussion
|
||||||
|
store.commit('discussion/setCurrentDiscussionId');
|
||||||
|
// Open the gutter if file contains discussions
|
||||||
|
store.commit('discussion/setCurrentDiscussionId',
|
||||||
|
store.getters['discussion/nextDiscussionId']);
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
// Failure (content is not available), go back to previous file
|
||||||
|
const lastOpenedFile = store.getters['file/lastOpened'];
|
||||||
|
store.commit('file/setCurrentId', lastOpenedFile.id);
|
||||||
|
throw err;
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err); // eslint-disable-line no-console
|
||||||
|
store.dispatch('notification/error', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
immediate: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const loader = type => fileId => localDbSvc.loadItem(`${fileId}/${type}`)
|
const loader = type => fileId => localDbSvc.loadItem(`${fileId}/${type}`)
|
||||||
|
@ -165,6 +165,7 @@ const markdownConversionSvc = {
|
|||||||
lines.pop();
|
lines.pop();
|
||||||
}
|
}
|
||||||
const parsingCtx = {
|
const parsingCtx = {
|
||||||
|
text,
|
||||||
sections: [],
|
sections: [],
|
||||||
converter,
|
converter,
|
||||||
markdownState,
|
markdownState,
|
||||||
@ -248,6 +249,7 @@ const markdownConversionSvc = {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
text: parsingCtx.text,
|
||||||
sectionList: parsingCtx.sectionList,
|
sectionList: parsingCtx.sectionList,
|
||||||
htmlSectionList,
|
htmlSectionList,
|
||||||
htmlSectionDiff,
|
htmlSectionDiff,
|
||||||
|
@ -11,7 +11,7 @@ let isScrollEditor;
|
|||||||
let isScrollPreview;
|
let isScrollPreview;
|
||||||
let isEditorMoving;
|
let isEditorMoving;
|
||||||
let isPreviewMoving;
|
let isPreviewMoving;
|
||||||
let sectionDescList;
|
let sectionDescList = [];
|
||||||
|
|
||||||
let throttleTimeoutId;
|
let throttleTimeoutId;
|
||||||
let throttleLastTime = 0;
|
let throttleLastTime = 0;
|
||||||
@ -34,7 +34,7 @@ function throttle(func, wait) {
|
|||||||
const doScrollSync = () => {
|
const doScrollSync = () => {
|
||||||
const localSkipAnimation = skipAnimation || !store.getters['layout/styles'].showSidePreview;
|
const localSkipAnimation = skipAnimation || !store.getters['layout/styles'].showSidePreview;
|
||||||
skipAnimation = false;
|
skipAnimation = false;
|
||||||
if (!store.getters['data/layoutSettings'].scrollSync || !sectionDescList || sectionDescList.length === 0) {
|
if (!store.getters['data/layoutSettings'].scrollSync || sectionDescList.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let editorScrollTop = editorScrollerElt.scrollTop;
|
let editorScrollTop = editorScrollerElt.scrollTop;
|
||||||
@ -144,10 +144,10 @@ editorSvc.$on('inited', () => {
|
|||||||
editorSvc.$on('sectionList', () => {
|
editorSvc.$on('sectionList', () => {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
isPreviewRefreshing = true;
|
isPreviewRefreshing = true;
|
||||||
sectionDescList = undefined;
|
sectionDescList = [];
|
||||||
});
|
});
|
||||||
|
|
||||||
editorSvc.$on('previewText', () => {
|
editorSvc.$on('previewCtx', () => {
|
||||||
// Assume the user is writing in the editor
|
// Assume the user is writing in the editor
|
||||||
isScrollEditor = store.getters['layout/styles'].showEditor;
|
isScrollEditor = store.getters['layout/styles'].showEditor;
|
||||||
// A preview scrolling event can occur if height is smaller
|
// A preview scrolling event can occur if height is smaller
|
||||||
@ -170,7 +170,9 @@ store.watch(
|
|||||||
skipAnimation = true;
|
skipAnimation = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
editorSvc.$on('sectionDescMeasuredList', (sectionDescMeasuredList) => {
|
editorSvc.$on('previewCtxMeasured', (previewCtxMeasured) => {
|
||||||
sectionDescList = sectionDescMeasuredList;
|
if (previewCtxMeasured) {
|
||||||
forceScrollSync();
|
sectionDescList = previewCtxMeasured.sectionDescList;
|
||||||
|
forceScrollSync();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
@ -111,6 +111,11 @@ function publishFile(fileId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function requestPublish() {
|
function requestPublish() {
|
||||||
|
// No publish in light mode
|
||||||
|
if (store.state.light) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
store.dispatch('queue/enqueuePublishRequest', () => new Promise((resolve, reject) => {
|
store.dispatch('queue/enqueuePublishRequest', () => new Promise((resolve, reject) => {
|
||||||
let intervalId;
|
let intervalId;
|
||||||
const attempt = () => {
|
const attempt = () => {
|
||||||
|
@ -6,7 +6,8 @@ function SectionDimension(startOffset, endOffset) {
|
|||||||
|
|
||||||
function dimensionNormalizer(dimensionName) {
|
function dimensionNormalizer(dimensionName) {
|
||||||
return (editorSvc) => {
|
return (editorSvc) => {
|
||||||
const dimensionList = editorSvc.sectionDescList.map(sectionDesc => sectionDesc[dimensionName]);
|
const dimensionList = editorSvc.previewCtx.sectionDescList.map(
|
||||||
|
sectionDesc => sectionDesc[dimensionName]);
|
||||||
let dimension;
|
let dimension;
|
||||||
let i;
|
let i;
|
||||||
let j;
|
let j;
|
||||||
@ -43,11 +44,11 @@ function measureSectionDimensions(editorSvc) {
|
|||||||
let editorSectionOffset = 0;
|
let editorSectionOffset = 0;
|
||||||
let previewSectionOffset = 0;
|
let previewSectionOffset = 0;
|
||||||
let tocSectionOffset = 0;
|
let tocSectionOffset = 0;
|
||||||
let sectionDesc = editorSvc.sectionDescList[0];
|
let sectionDesc = editorSvc.previewCtx.sectionDescList[0];
|
||||||
let nextSectionDesc;
|
let nextSectionDesc;
|
||||||
let i = 1;
|
let i = 1;
|
||||||
for (; i < editorSvc.sectionDescList.length; i += 1) {
|
for (; i < editorSvc.previewCtx.sectionDescList.length; i += 1) {
|
||||||
nextSectionDesc = editorSvc.sectionDescList[i];
|
nextSectionDesc = editorSvc.previewCtx.sectionDescList[i];
|
||||||
|
|
||||||
// Measure editor section
|
// Measure editor section
|
||||||
let newEditorSectionOffset = nextSectionDesc.editorElt
|
let newEditorSectionOffset = nextSectionDesc.editorElt
|
||||||
@ -84,7 +85,7 @@ function measureSectionDimensions(editorSvc) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Last section
|
// Last section
|
||||||
sectionDesc = editorSvc.sectionDescList[i - 1];
|
sectionDesc = editorSvc.previewCtx.sectionDescList[i - 1];
|
||||||
if (sectionDesc) {
|
if (sectionDesc) {
|
||||||
sectionDesc.editorDimension = new SectionDimension(
|
sectionDesc.editorDimension = new SectionDimension(
|
||||||
editorSectionOffset, editorSvc.editorElt.scrollHeight);
|
editorSectionOffset, editorSvc.editorElt.scrollHeight);
|
||||||
|
@ -7,6 +7,7 @@ import providerRegistry from './providers/providerRegistry';
|
|||||||
import googleDriveAppDataProvider from './providers/googleDriveAppDataProvider';
|
import googleDriveAppDataProvider from './providers/googleDriveAppDataProvider';
|
||||||
import './providers/googleDriveWorkspaceProvider';
|
import './providers/googleDriveWorkspaceProvider';
|
||||||
import './providers/couchdbWorkspaceProvider';
|
import './providers/couchdbWorkspaceProvider';
|
||||||
|
import tempFileSvc from './tempFileSvc';
|
||||||
|
|
||||||
const inactivityThreshold = 3 * 1000; // 3 sec
|
const inactivityThreshold = 3 * 1000; // 3 sec
|
||||||
const restartSyncAfter = 30 * 1000; // 30 sec
|
const restartSyncAfter = 30 * 1000; // 30 sec
|
||||||
@ -169,25 +170,16 @@ function createSyncLocation(syncLocation) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class SyncContext {
|
class SyncContext {
|
||||||
constructor() {
|
restart = false;
|
||||||
this.restart = false;
|
attempted = {};
|
||||||
this.synced = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FileSyncContext {
|
|
||||||
constructor() {
|
|
||||||
this.downloaded = {};
|
|
||||||
this.errors = {};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync one file with all its locations.
|
* Sync one file with all its locations.
|
||||||
*/
|
*/
|
||||||
function syncFile(fileId, syncContext = new SyncContext()) {
|
function syncFile(fileId, syncContext = new SyncContext()) {
|
||||||
const fileSyncContext = new FileSyncContext();
|
syncContext.attempted[`${fileId}/content`] = true;
|
||||||
syncContext.synced[`${fileId}/content`] = true;
|
|
||||||
return localDbSvc.loadSyncedContent(fileId)
|
return localDbSvc.loadSyncedContent(fileId)
|
||||||
.then(() => localDbSvc.loadItem(`${fileId}/content`)
|
.then(() => localDbSvc.loadItem(`${fileId}/content`)
|
||||||
.catch(() => {})) // Item may not exist if content has not been downloaded yet
|
.catch(() => {})) // Item may not exist if content has not been downloaded yet
|
||||||
@ -197,14 +189,9 @@ function syncFile(fileId, syncContext = new SyncContext()) {
|
|||||||
const getSyncedContent = () => store.state.syncedContent.itemMap[`${fileId}/syncedContent`];
|
const getSyncedContent = () => store.state.syncedContent.itemMap[`${fileId}/syncedContent`];
|
||||||
const getSyncHistoryItem = syncLocationId => getSyncedContent().syncHistory[syncLocationId];
|
const getSyncHistoryItem = syncLocationId => getSyncedContent().syncHistory[syncLocationId];
|
||||||
|
|
||||||
const isLocationSynced = (syncLocation) => {
|
const isTempFile = () => {
|
||||||
const syncHistoryItem = getSyncHistoryItem(syncLocation.id);
|
|
||||||
return syncHistoryItem && syncHistoryItem[LAST_SENT] === getContent().hash;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isWelcomeFile = () => {
|
|
||||||
if (store.getters['data/syncDataByItemId'][`${fileId}/content`]) {
|
if (store.getters['data/syncDataByItemId'][`${fileId}/content`]) {
|
||||||
// If file has already been synced, keep on syncing
|
// If file has already been synced, it's not a temp file
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const file = getFile();
|
const file = getFile();
|
||||||
@ -212,12 +199,29 @@ function syncFile(fileId, syncContext = new SyncContext()) {
|
|||||||
if (!file || !content) {
|
if (!file || !content) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (file.parentId === 'temp') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const locations = [
|
||||||
|
...store.getters['syncLocation/groupedByFileId'][fileId] || [],
|
||||||
|
...store.getters['publishLocation/groupedByFileId'][fileId] || [],
|
||||||
|
];
|
||||||
|
if (locations.length) {
|
||||||
|
// If file has explicit sync/publish locations, it's not a temp file
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Return true if it's a welcome file that has no discussion
|
||||||
const welcomeFileHashes = store.getters['data/localSettings'].welcomeFileHashes;
|
const welcomeFileHashes = store.getters['data/localSettings'].welcomeFileHashes;
|
||||||
const hash = utils.hash(content.text);
|
const hash = utils.hash(content.text);
|
||||||
const hasDiscussions = Object.keys(content.discussions).length;
|
const hasDiscussions = Object.keys(content.discussions).length;
|
||||||
return file.name === 'Welcome file' && welcomeFileHashes[hash] && !hasDiscussions;
|
return file.name === 'Welcome file' && welcomeFileHashes[hash] && !hasDiscussions;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (isTempFile()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attemptedLocations = {};
|
||||||
const syncOneContentLocation = () => {
|
const syncOneContentLocation = () => {
|
||||||
const syncLocations = [
|
const syncLocations = [
|
||||||
...store.getters['syncLocation/groupedByFileId'][fileId] || [],
|
...store.getters['syncLocation/groupedByFileId'][fileId] || [],
|
||||||
@ -229,20 +233,17 @@ function syncFile(fileId, syncContext = new SyncContext()) {
|
|||||||
syncLocations.some((syncLocation) => {
|
syncLocations.some((syncLocation) => {
|
||||||
const provider = providerRegistry.providers[syncLocation.providerId];
|
const provider = providerRegistry.providers[syncLocation.providerId];
|
||||||
if (
|
if (
|
||||||
// Skip if it previously threw an error
|
// Skip if it has been attempted already
|
||||||
!fileSyncContext.errors[syncLocation.id] &&
|
!attemptedLocations[syncLocation.id] &&
|
||||||
// Skip if it has previously been downloaded and has not changed since then
|
// Skip temp file
|
||||||
(!fileSyncContext.downloaded[syncLocation.id] || !isLocationSynced(syncLocation)) &&
|
!isTempFile()
|
||||||
// Skip welcome file if not synchronized explicitly
|
|
||||||
(syncLocations.length > 1 || !isWelcomeFile())
|
|
||||||
) {
|
) {
|
||||||
|
attemptedLocations[syncLocation.id] = true;
|
||||||
const token = provider && provider.getToken(syncLocation);
|
const token = provider && provider.getToken(syncLocation);
|
||||||
result = token && store.dispatch('queue/doWithLocation', {
|
result = token && store.dispatch('queue/doWithLocation', {
|
||||||
location: syncLocation,
|
location: syncLocation,
|
||||||
promise: provider.downloadContent(token, syncLocation)
|
promise: provider.downloadContent(token, syncLocation)
|
||||||
.then((serverContent = null) => {
|
.then((serverContent = null) => {
|
||||||
fileSyncContext.downloaded[syncLocation.id] = true;
|
|
||||||
|
|
||||||
const syncedContent = getSyncedContent();
|
const syncedContent = getSyncedContent();
|
||||||
const syncHistoryItem = getSyncHistoryItem(syncLocation.id);
|
const syncHistoryItem = getSyncHistoryItem(syncLocation.id);
|
||||||
let mergedContent = (() => {
|
let mergedContent = (() => {
|
||||||
@ -275,7 +276,6 @@ function syncFile(fileId, syncContext = new SyncContext()) {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
if (!mergedContent) {
|
if (!mergedContent) {
|
||||||
fileSyncContext.errors[syncLocation.id] = true;
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -370,7 +370,6 @@ function syncFile(fileId, syncContext = new SyncContext()) {
|
|||||||
}
|
}
|
||||||
console.error(err); // eslint-disable-line no-console
|
console.error(err); // eslint-disable-line no-console
|
||||||
store.dispatch('notification/error', err);
|
store.dispatch('notification/error', err);
|
||||||
fileSyncContext.errors[syncLocation.id] = true;
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.then(() => syncOneContentLocation());
|
.then(() => syncOneContentLocation());
|
||||||
@ -584,7 +583,7 @@ function syncWorkspace() {
|
|||||||
const syncData = store.getters['data/syncDataByItemId'][contentId];
|
const syncData = store.getters['data/syncDataByItemId'][contentId];
|
||||||
if (
|
if (
|
||||||
// Sync if content syncing was not attempted yet
|
// Sync if content syncing was not attempted yet
|
||||||
!syncContext.synced[contentId] &&
|
!syncContext.attempted[contentId] &&
|
||||||
// And if syncData does not exist or if content hash and syncData hash are inconsistent
|
// And if syncData does not exist or if content hash and syncData hash are inconsistent
|
||||||
(!syncData || syncData.hash !== hash)
|
(!syncData || syncData.hash !== hash)
|
||||||
) {
|
) {
|
||||||
@ -643,6 +642,11 @@ function syncWorkspace() {
|
|||||||
* Enqueue a sync task, if possible.
|
* Enqueue a sync task, if possible.
|
||||||
*/
|
*/
|
||||||
function requestSync() {
|
function requestSync() {
|
||||||
|
// No sync in light mode
|
||||||
|
if (store.state.light) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
store.dispatch('queue/enqueueSyncRequest', () => new Promise((resolve, reject) => {
|
store.dispatch('queue/enqueueSyncRequest', () => new Promise((resolve, reject) => {
|
||||||
let intervalId;
|
let intervalId;
|
||||||
const attempt = () => {
|
const attempt = () => {
|
||||||
@ -734,25 +738,28 @@ export default {
|
|||||||
return actionProvider && actionProvider.performAction && actionProvider.performAction()
|
return actionProvider && actionProvider.performAction && actionProvider.performAction()
|
||||||
.then(newSyncLocation => newSyncLocation && this.createSyncLocation(newSyncLocation));
|
.then(newSyncLocation => newSyncLocation && this.createSyncLocation(newSyncLocation));
|
||||||
})
|
})
|
||||||
|
.then(() => tempFileSvc.init())
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Sync periodically
|
if (!store.state.light) {
|
||||||
utils.setInterval(() => {
|
// Sync periodically
|
||||||
if (isSyncPossible()
|
utils.setInterval(() => {
|
||||||
&& networkSvc.isUserActive()
|
if (isSyncPossible()
|
||||||
&& isSyncWindow()
|
&& networkSvc.isUserActive()
|
||||||
&& isAutoSyncReady()
|
&& isSyncWindow()
|
||||||
) {
|
&& isAutoSyncReady()
|
||||||
requestSync();
|
) {
|
||||||
}
|
requestSync();
|
||||||
}, 1000);
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
// Unload contents from memory periodically
|
// Unload contents from memory periodically
|
||||||
utils.setInterval(() => {
|
utils.setInterval(() => {
|
||||||
// Wait for sync and publish to finish
|
// Wait for sync and publish to finish
|
||||||
if (store.state.queue.isEmpty) {
|
if (store.state.queue.isEmpty) {
|
||||||
localDbSvc.unloadContents();
|
localDbSvc.unloadContents();
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
isSyncPossible,
|
isSyncPossible,
|
||||||
|
95
src/services/tempFileSvc.js
Normal file
95
src/services/tempFileSvc.js
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import cledit from './cledit';
|
||||||
|
import store from '../store';
|
||||||
|
import utils from './utils';
|
||||||
|
import editorSvc from './editorSvc';
|
||||||
|
|
||||||
|
const origin = utils.queryParams.origin;
|
||||||
|
const fileName = utils.queryParams.fileName;
|
||||||
|
const contentText = utils.queryParams.contentText;
|
||||||
|
const contentProperties = utils.queryParams.contentProperties;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
setReady() {
|
||||||
|
if (origin && window.parent) {
|
||||||
|
window.parent.postMessage({ type: 'ready' }, origin);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
closed: false,
|
||||||
|
close() {
|
||||||
|
if (!this.closed && origin && window.parent) {
|
||||||
|
window.parent.postMessage({ type: 'close' }, origin);
|
||||||
|
}
|
||||||
|
this.closed = true;
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
if (!origin || !window.parent) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
store.commit('setLight', true);
|
||||||
|
|
||||||
|
return store.dispatch('createFile', {
|
||||||
|
name: fileName || utils.getHostname(origin),
|
||||||
|
text: contentText || '\n',
|
||||||
|
properties: contentProperties,
|
||||||
|
parentId: 'temp',
|
||||||
|
})
|
||||||
|
.then((file) => {
|
||||||
|
const fileItemMap = store.state.file.itemMap;
|
||||||
|
|
||||||
|
// Sanitize file creations
|
||||||
|
const lastCreated = {};
|
||||||
|
Object.entries(store.getters['data/lastCreated']).forEach(([id, createdOn]) => {
|
||||||
|
if (fileItemMap[id] && fileItemMap[id].parentId === 'temp') {
|
||||||
|
lastCreated[id] = createdOn;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Track file creation from other site
|
||||||
|
lastCreated[file.id] = {
|
||||||
|
created: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Keep only the last 10 temp files created by other sites
|
||||||
|
Object.entries(lastCreated)
|
||||||
|
.sort(([, createdOn1], [, createdOn2]) => createdOn2 - createdOn1)
|
||||||
|
.splice(10)
|
||||||
|
.forEach(([id]) => {
|
||||||
|
delete lastCreated[id];
|
||||||
|
store.dispatch('deleteFile', id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store file creations and open the file
|
||||||
|
store.dispatch('data/setLastCreated', lastCreated);
|
||||||
|
store.commit('file/setCurrentId', file.id);
|
||||||
|
|
||||||
|
const onChange = cledit.Utils.debounce(() => {
|
||||||
|
const currentFile = store.getters['file/current'];
|
||||||
|
if (currentFile.id !== file.id) {
|
||||||
|
// Close editor if file has changed for some reason
|
||||||
|
this.close();
|
||||||
|
} else if (!this.closed && editorSvc.previewCtx.html != null) {
|
||||||
|
const content = store.getters['content/current'];
|
||||||
|
const properties = utils.computeProperties(content.properties);
|
||||||
|
window.parent.postMessage({
|
||||||
|
type: 'fileChange',
|
||||||
|
payload: {
|
||||||
|
id: file.id,
|
||||||
|
name: currentFile.name,
|
||||||
|
content: {
|
||||||
|
text: content.text.slice(0, -1), // Remove trailing LF
|
||||||
|
properties,
|
||||||
|
yamlProperties: content.properties,
|
||||||
|
html: editorSvc.previewCtx.html,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, origin);
|
||||||
|
}
|
||||||
|
}, 25);
|
||||||
|
|
||||||
|
// Watch preview refresh and file name changes
|
||||||
|
editorSvc.$on('previewCtx', onChange);
|
||||||
|
store.watch(() => store.getters['file/current'].name, onChange);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
@ -35,9 +35,9 @@ export default {
|
|||||||
this.queryParams = params;
|
this.queryParams = params;
|
||||||
const serializedParams = Object.entries(this.queryParams).map(([key, value]) =>
|
const serializedParams = Object.entries(this.queryParams).map(([key, value]) =>
|
||||||
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join('&');
|
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join('&');
|
||||||
const hash = serializedParams && `#${serializedParams}`;
|
const hash = `#${serializedParams}`;
|
||||||
if (location.hash !== hash) {
|
if (location.hash !== hash) {
|
||||||
location.hash = hash;
|
location.replace(hash);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
types: [
|
types: [
|
||||||
@ -185,6 +185,10 @@ export default {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
getHostname(url) {
|
||||||
|
urlParser.href = url;
|
||||||
|
return urlParser.hostname;
|
||||||
|
},
|
||||||
createHiddenIframe(url) {
|
createHiddenIframe(url) {
|
||||||
const iframeElt = document.createElement('iframe');
|
const iframeElt = document.createElement('iframe');
|
||||||
iframeElt.style.position = 'absolute';
|
iframeElt.style.position = 'absolute';
|
||||||
|
@ -37,6 +37,14 @@ module.getters = {
|
|||||||
}
|
}
|
||||||
return state.itemMap[`${rootGetters['file/current'].id}/content`] || empty();
|
return state.itemMap[`${rootGetters['file/current'].id}/content`] || empty();
|
||||||
},
|
},
|
||||||
|
currentChangeTrigger: (state, getters) => {
|
||||||
|
const current = getters.current;
|
||||||
|
return utils.serializeObject([
|
||||||
|
current.id,
|
||||||
|
current.text,
|
||||||
|
current.hash,
|
||||||
|
]);
|
||||||
|
},
|
||||||
currentProperties: (state, getters) => utils.computeProperties(getters.current.properties),
|
currentProperties: (state, getters) => utils.computeProperties(getters.current.properties),
|
||||||
isCurrentEditable: (state, getters, rootState, rootGetters) =>
|
isCurrentEditable: (state, getters, rootState, rootGetters) =>
|
||||||
!state.revisionContent &&
|
!state.revisionContent &&
|
||||||
|
@ -170,6 +170,7 @@ export default {
|
|||||||
...getters.templates,
|
...getters.templates,
|
||||||
...additionalTemplates,
|
...additionalTemplates,
|
||||||
}),
|
}),
|
||||||
|
lastCreated: getter('lastCreated'),
|
||||||
lastOpened: getter('lastOpened'),
|
lastOpened: getter('lastOpened'),
|
||||||
lastOpenedIds: (state, getters, rootState) => {
|
lastOpenedIds: (state, getters, rootState) => {
|
||||||
const lastOpened = {
|
const lastOpened = {
|
||||||
@ -261,21 +262,18 @@ export default {
|
|||||||
});
|
});
|
||||||
commit('setItem', itemTemplate('templates', dataToCommit));
|
commit('setItem', itemTemplate('templates', dataToCommit));
|
||||||
},
|
},
|
||||||
|
setLastCreated: setter('lastCreated'),
|
||||||
setLastOpenedId: ({ getters, commit, dispatch, rootState }, fileId) => {
|
setLastOpenedId: ({ getters, commit, dispatch, rootState }, fileId) => {
|
||||||
const lastOpened = { ...getters.lastOpened };
|
const lastOpened = { ...getters.lastOpened };
|
||||||
lastOpened[fileId] = Date.now();
|
lastOpened[fileId] = Date.now();
|
||||||
commit('setItem', itemTemplate('lastOpened', lastOpened));
|
// Remove entries that don't exist anymore
|
||||||
dispatch('cleanLastOpenedId');
|
const cleanedLastOpened = {};
|
||||||
},
|
Object.entries(lastOpened).forEach(([id, value]) => {
|
||||||
cleanLastOpenedId: ({ getters, commit, rootState }) => {
|
if (rootState.file.itemMap[id]) {
|
||||||
const lastOpened = {};
|
cleanedLastOpened[id] = value;
|
||||||
const oldLastOpened = getters.lastOpened;
|
|
||||||
Object.entries(oldLastOpened).forEach(([fileId, date]) => {
|
|
||||||
if (rootState.file.itemMap[fileId]) {
|
|
||||||
lastOpened[fileId] = date;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
commit('setItem', itemTemplate('lastOpened', lastOpened));
|
commit('setItem', itemTemplate('lastOpened', cleanedLastOpened));
|
||||||
},
|
},
|
||||||
setSyncData: setter('syncData'),
|
setSyncData: setter('syncData'),
|
||||||
patchSyncData: patcher('syncData'),
|
patchSyncData: patcher('syncData'),
|
||||||
|
@ -69,13 +69,29 @@ export default {
|
|||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
nodeStructure: (state, getters, rootState, rootGetters) => {
|
nodeStructure: (state, getters, rootState, rootGetters) => {
|
||||||
|
const rootNode = new Node(emptyFolder(), [], true, true);
|
||||||
|
|
||||||
|
// Create Trash node
|
||||||
const trashFolderNode = new Node(emptyFolder(), [], true);
|
const trashFolderNode = new Node(emptyFolder(), [], true);
|
||||||
trashFolderNode.item.id = 'trash';
|
trashFolderNode.item.id = 'trash';
|
||||||
trashFolderNode.item.name = 'Trash';
|
trashFolderNode.item.name = 'Trash';
|
||||||
trashFolderNode.isTrash = true;
|
|
||||||
trashFolderNode.noDrag = true;
|
trashFolderNode.noDrag = true;
|
||||||
|
trashFolderNode.isTrash = true;
|
||||||
|
trashFolderNode.parentNode = rootNode;
|
||||||
|
|
||||||
|
// Create Temp node
|
||||||
|
const tempFolderNode = new Node(emptyFolder(), [], true);
|
||||||
|
tempFolderNode.item.id = 'temp';
|
||||||
|
tempFolderNode.item.name = 'Temp';
|
||||||
|
tempFolderNode.noDrag = true;
|
||||||
|
tempFolderNode.noDrop = true;
|
||||||
|
tempFolderNode.isTemp = true;
|
||||||
|
tempFolderNode.parentNode = rootNode;
|
||||||
|
|
||||||
|
// Fill nodeMap with all file and folder nodes
|
||||||
const nodeMap = {
|
const nodeMap = {
|
||||||
trash: trashFolderNode,
|
trash: trashFolderNode,
|
||||||
|
temp: tempFolderNode,
|
||||||
};
|
};
|
||||||
rootGetters['folder/items'].forEach((item) => {
|
rootGetters['folder/items'].forEach((item) => {
|
||||||
nodeMap[item.id] = new Node(item, [], true);
|
nodeMap[item.id] = new Node(item, [], true);
|
||||||
@ -89,11 +105,12 @@ export default {
|
|||||||
];
|
];
|
||||||
nodeMap[item.id] = new Node(item, locations);
|
nodeMap[item.id] = new Node(item, locations);
|
||||||
});
|
});
|
||||||
const rootNode = new Node(emptyFolder(), [], true, true);
|
|
||||||
|
// Build the tree
|
||||||
Object.entries(nodeMap).forEach(([, node]) => {
|
Object.entries(nodeMap).forEach(([, node]) => {
|
||||||
let parentNode = nodeMap[node.item.parentId];
|
let parentNode = nodeMap[node.item.parentId];
|
||||||
if (!parentNode || !parentNode.isFolder) {
|
if (!parentNode || !parentNode.isFolder) {
|
||||||
if (node.isTrash) {
|
if (node.isTrash || node.isTemp) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
parentNode = rootNode;
|
parentNode = rootNode;
|
||||||
@ -103,12 +120,20 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
parentNode.files.push(node);
|
parentNode.files.push(node);
|
||||||
}
|
}
|
||||||
|
node.parentNode = parentNode;
|
||||||
});
|
});
|
||||||
rootNode.sortChildren();
|
rootNode.sortChildren();
|
||||||
|
|
||||||
|
// Add Trash and Temp nodes
|
||||||
|
rootNode.folders.unshift(tempFolderNode);
|
||||||
|
tempFolderNode.files.forEach((node) => {
|
||||||
|
node.noDrop = true;
|
||||||
|
});
|
||||||
if (trashFolderNode.files.length) {
|
if (trashFolderNode.files.length) {
|
||||||
rootNode.folders.unshift(trashFolderNode);
|
rootNode.folders.unshift(trashFolderNode);
|
||||||
}
|
}
|
||||||
// Add a fake file at the end of the root folder to allow drag and drop into it.
|
|
||||||
|
// Add a fake file at the end of the root folder to allow drag and drop into it
|
||||||
rootNode.files.push(fakeFileNode);
|
rootNode.files.push(fakeFileNode);
|
||||||
return {
|
return {
|
||||||
nodeMap,
|
nodeMap,
|
||||||
@ -169,7 +194,9 @@ export default {
|
|||||||
},
|
},
|
||||||
newItem({ getters, commit, dispatch }, isFolder) {
|
newItem({ getters, commit, dispatch }, isFolder) {
|
||||||
let parentId = getters.selectedNodeFolder.item.id;
|
let parentId = getters.selectedNodeFolder.item.id;
|
||||||
if (parentId === 'trash') {
|
if (parentId === 'trash' // Not allowed to create new items in the trash
|
||||||
|
|| (isFolder && parentId === 'temp') // Not allowed to create new folders in the temp folder
|
||||||
|
) {
|
||||||
parentId = null;
|
parentId = null;
|
||||||
}
|
}
|
||||||
dispatch('openNode', parentId);
|
dispatch('openNode', parentId);
|
||||||
@ -186,34 +213,50 @@ export default {
|
|||||||
if (selectedNode.isTrash || selectedNode.item.parentId === 'trash') {
|
if (selectedNode.isTrash || selectedNode.item.parentId === 'trash') {
|
||||||
return dispatch('modal/trashDeletion', null, { root: true });
|
return dispatch('modal/trashDeletion', null, { root: true });
|
||||||
}
|
}
|
||||||
return dispatch(selectedNode.isFolder
|
|
||||||
? 'modal/folderDeletion'
|
// See if we have a dialog to show
|
||||||
: 'modal/fileDeletion',
|
let modalAction;
|
||||||
selectedNode.item,
|
let moveToTrash = true;
|
||||||
{ root: true },
|
if (selectedNode.isTemp) {
|
||||||
)
|
modalAction = 'modal/tempFolderDeletion';
|
||||||
|
moveToTrash = false;
|
||||||
|
} else if (selectedNode.item.parentId === 'temp') {
|
||||||
|
modalAction = 'modal/tempFileDeletion';
|
||||||
|
moveToTrash = false;
|
||||||
|
} else if (selectedNode.isFolder) {
|
||||||
|
modalAction = 'modal/folderDeletion';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (modalAction
|
||||||
|
? dispatch(modalAction, selectedNode.item, { root: true })
|
||||||
|
: Promise.resolve())
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
const deleteFile = (id) => {
|
||||||
|
if (moveToTrash) {
|
||||||
|
commit('file/patchItem', {
|
||||||
|
id,
|
||||||
|
parentId: 'trash',
|
||||||
|
}, { root: true });
|
||||||
|
} else {
|
||||||
|
dispatch('deleteFile', id, { root: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (selectedNode === getters.selectedNode) {
|
if (selectedNode === getters.selectedNode) {
|
||||||
const currentFileId = rootGetters['file/current'].id;
|
const currentFileId = rootGetters['file/current'].id;
|
||||||
let doClose = selectedNode.item.id === currentFileId;
|
let doClose = selectedNode.item.id === currentFileId;
|
||||||
if (selectedNode.isFolder) {
|
if (selectedNode.isFolder) {
|
||||||
const recursiveMoveToTrash = (folderNode) => {
|
const recursiveDelete = (folderNode) => {
|
||||||
folderNode.folders.forEach(recursiveMoveToTrash);
|
folderNode.folders.forEach(recursiveDelete);
|
||||||
folderNode.files.forEach((fileNode) => {
|
folderNode.files.forEach((fileNode) => {
|
||||||
commit('file/patchItem', {
|
|
||||||
id: fileNode.item.id,
|
|
||||||
parentId: 'trash',
|
|
||||||
}, { root: true });
|
|
||||||
doClose = doClose || fileNode.item.id === currentFileId;
|
doClose = doClose || fileNode.item.id === currentFileId;
|
||||||
|
deleteFile(fileNode.item.id);
|
||||||
});
|
});
|
||||||
commit('folder/deleteItem', folderNode.item.id, { root: true });
|
commit('folder/deleteItem', folderNode.item.id, { root: true });
|
||||||
};
|
};
|
||||||
recursiveMoveToTrash(selectedNode);
|
recursiveDelete(selectedNode);
|
||||||
} else {
|
} else {
|
||||||
commit('file/patchItem', {
|
deleteFile(selectedNode.item.id);
|
||||||
id: selectedNode.item.id,
|
|
||||||
parentId: 'trash',
|
|
||||||
}, { root: true });
|
|
||||||
}
|
}
|
||||||
if (doClose) {
|
if (doClose) {
|
||||||
// Close the current file by opening the last opened, not deleted one
|
// Close the current file by opening the last opened, not deleted one
|
||||||
|
@ -11,6 +11,7 @@ module.state = {
|
|||||||
module.getters = {
|
module.getters = {
|
||||||
...module.getters,
|
...module.getters,
|
||||||
current: state => state.itemMap[state.currentId] || empty(),
|
current: state => state.itemMap[state.currentId] || empty(),
|
||||||
|
isCurrentTemp: (state, getters) => getters.current.parentId === 'temp',
|
||||||
lastOpened: (state, getters, rootState, rootGetters) =>
|
lastOpened: (state, getters, rootState, rootGetters) =>
|
||||||
state.itemMap[rootGetters['data/lastOpenedIds'][0]] || getters.items[0] || empty(),
|
state.itemMap[rootGetters['data/lastOpenedIds'][0]] || getters.items[0] || empty(),
|
||||||
};
|
};
|
||||||
|
@ -47,6 +47,7 @@ const store = new Vuex.Store({
|
|||||||
workspace,
|
workspace,
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
|
light: false,
|
||||||
offline: false,
|
offline: false,
|
||||||
lastOfflineCheck: 0,
|
lastOfflineCheck: 0,
|
||||||
minuteCounter: 0,
|
minuteCounter: 0,
|
||||||
@ -64,6 +65,9 @@ const store = new Vuex.Store({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
|
setLight: (state, value) => {
|
||||||
|
state.light = value;
|
||||||
|
},
|
||||||
setOffline: (state, value) => {
|
setOffline: (state, value) => {
|
||||||
state.offline = value;
|
state.offline = value;
|
||||||
},
|
},
|
||||||
@ -109,16 +113,14 @@ const store = new Vuex.Store({
|
|||||||
return Promise.resolve(state.file.itemMap[id]);
|
return Promise.resolve(state.file.itemMap[id]);
|
||||||
},
|
},
|
||||||
deleteFile({ getters, commit }, fileId) {
|
deleteFile({ getters, commit }, fileId) {
|
||||||
|
(getters['syncLocation/groupedByFileId'][fileId] || [])
|
||||||
|
.forEach(item => commit('syncLocation/deleteItem', item.id));
|
||||||
|
(getters['publishLocation/groupedByFileId'][fileId] || [])
|
||||||
|
.forEach(item => commit('publishLocation/deleteItem', item.id));
|
||||||
commit('file/deleteItem', fileId);
|
commit('file/deleteItem', fileId);
|
||||||
commit('content/deleteItem', `${fileId}/content`);
|
commit('content/deleteItem', `${fileId}/content`);
|
||||||
commit('syncedContent/deleteItem', `${fileId}/syncedContent`);
|
commit('syncedContent/deleteItem', `${fileId}/syncedContent`);
|
||||||
commit('contentState/deleteItem', `${fileId}/contentState`);
|
commit('contentState/deleteItem', `${fileId}/contentState`);
|
||||||
getters['syncLocation/items']
|
|
||||||
.filter(item => item.fileId === fileId)
|
|
||||||
.forEach(item => commit('syncLocation/deleteItem', item.id));
|
|
||||||
getters['publishLocation/items']
|
|
||||||
.filter(item => item.fileId === fileId)
|
|
||||||
.forEach(item => commit('publishLocation/deleteItem', item.id));
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
strict: debug,
|
strict: debug,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
const minPadding = 20;
|
const minPadding = 20;
|
||||||
const editorTopPadding = 10;
|
const editorTopPadding = 10;
|
||||||
const navigationBarEditButtonsWidth = (36 * 14) + 8; // 14 buttons + 1 spacer
|
const navigationBarEditButtonsWidth = (36 * 14) + 8; // 14 buttons + 1 spacer
|
||||||
const navigationBarLeftButtonWidth = 38 + 4 + 15;
|
const navigationBarLeftButtonWidth = 38 + 4 + 12;
|
||||||
const navigationBarRightButtonWidth = 38 + 8;
|
const navigationBarRightButtonWidth = 38 + 8;
|
||||||
const navigationBarSpinnerWidth = 24 + 8 + 5; // 5 for left margin
|
const navigationBarSpinnerWidth = 24 + 8 + 5; // 5 for left margin
|
||||||
const navigationBarLocationWidth = 20;
|
const navigationBarLocationWidth = 20;
|
||||||
@ -21,14 +21,18 @@ const constants = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function computeStyles(state, getters, layoutSettings = getters['data/layoutSettings'], styles = {
|
function computeStyles(state, getters, layoutSettings = getters['data/layoutSettings'], styles = {
|
||||||
showNavigationBar: !layoutSettings.showEditor || layoutSettings.showNavigationBar,
|
showNavigationBar: layoutSettings.showNavigationBar
|
||||||
|
|| !layoutSettings.showEditor
|
||||||
|
|| state.content.revisionContent
|
||||||
|
|| state.light,
|
||||||
showStatusBar: layoutSettings.showStatusBar,
|
showStatusBar: layoutSettings.showStatusBar,
|
||||||
showEditor: layoutSettings.showEditor,
|
showEditor: layoutSettings.showEditor,
|
||||||
showSidePreview: layoutSettings.showSidePreview && layoutSettings.showEditor,
|
showSidePreview: layoutSettings.showSidePreview && layoutSettings.showEditor,
|
||||||
showPreview: layoutSettings.showSidePreview || !layoutSettings.showEditor,
|
showPreview: layoutSettings.showSidePreview || !layoutSettings.showEditor,
|
||||||
showSideBar: layoutSettings.showSideBar,
|
showSideBar: layoutSettings.showSideBar && !state.light,
|
||||||
showExplorer: layoutSettings.showExplorer,
|
showExplorer: layoutSettings.showExplorer && !state.light,
|
||||||
layoutOverflow: false,
|
layoutOverflow: false,
|
||||||
|
hideLocations: state.light,
|
||||||
}) {
|
}) {
|
||||||
styles.innerHeight = state.layout.bodyHeight;
|
styles.innerHeight = state.layout.bodyHeight;
|
||||||
if (styles.showNavigationBar) {
|
if (styles.showNavigationBar) {
|
||||||
@ -52,7 +56,8 @@ function computeStyles(state, getters, layoutSettings = getters['data/layoutSett
|
|||||||
}
|
}
|
||||||
|
|
||||||
let doublePanelWidth = styles.innerWidth - constants.buttonBarWidth;
|
let doublePanelWidth = styles.innerWidth - constants.buttonBarWidth;
|
||||||
const showGutter = !!getters['discussion/currentDiscussion'];
|
// No commenting for temp files
|
||||||
|
const showGutter = !getters['file/isCurrentTemp'] && !!getters['discussion/currentDiscussion'];
|
||||||
if (showGutter) {
|
if (showGutter) {
|
||||||
doublePanelWidth -= constants.gutterWidth;
|
doublePanelWidth -= constants.gutterWidth;
|
||||||
}
|
}
|
||||||
|
@ -47,16 +47,21 @@ export default {
|
|||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
fileDeletion: ({ dispatch }, item) => dispatch('open', {
|
folderDeletion: ({ dispatch }, item) => dispatch('open', {
|
||||||
content: `<p>You are about to delete the file <b>${item.name}</b>. Are you sure?</p>`,
|
content: `<p>You are about to delete the folder <b>${item.name}</b>. Its files will be moved to Trash. Are you sure?</p>`,
|
||||||
resolveText: 'Yes, delete',
|
resolveText: 'Yes, delete',
|
||||||
rejectText: 'No',
|
rejectText: 'No',
|
||||||
}),
|
}),
|
||||||
folderDeletion: ({ dispatch }, item) => dispatch('open', {
|
tempFileDeletion: ({ dispatch }, item) => dispatch('open', {
|
||||||
content: `<p>You are about to delete the folder <b>${item.name}</b> and all its files. Are you sure?</p>`,
|
content: `<p>You are about to permanently delete the temporary file <b>${item.name}</b>. Are you sure?</p>`,
|
||||||
resolveText: 'Yes, delete',
|
resolveText: 'Yes, delete',
|
||||||
rejectText: 'No',
|
rejectText: 'No',
|
||||||
}),
|
}),
|
||||||
|
tempFolderDeletion: ({ dispatch }) => dispatch('open', {
|
||||||
|
content: '<p>You are about to permanently delete all the temporary files. Are you sure?</p>',
|
||||||
|
resolveText: 'Yes, delete all',
|
||||||
|
rejectText: 'No',
|
||||||
|
}),
|
||||||
discussionDeletion: ({ dispatch }) => dispatch('open', {
|
discussionDeletion: ({ dispatch }) => dispatch('open', {
|
||||||
content: '<p>You are about to delete a discussion. Are you sure?</p>',
|
content: '<p>You are about to delete a discussion. Are you sure?</p>',
|
||||||
resolveText: 'Yes, delete',
|
resolveText: 'Yes, delete',
|
||||||
|
@ -2,31 +2,12 @@ import Vue from 'vue';
|
|||||||
import utils from '../services/utils';
|
import utils from '../services/utils';
|
||||||
|
|
||||||
export default (empty, simpleHash = false) => {
|
export default (empty, simpleHash = false) => {
|
||||||
// Use Date.now as a simple hash function, which is ok for not-synced types
|
// Use Date.now() as a simple hash function, which is ok for not-synced types
|
||||||
const hashFunc = simpleHash ? Date.now : item => utils.hash(utils.serializeObject({
|
const hashFunc = simpleHash ? Date.now : item => utils.hash(utils.serializeObject({
|
||||||
...item,
|
...item,
|
||||||
hash: undefined,
|
hash: undefined,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function setItem(state, value) {
|
|
||||||
const item = Object.assign(empty(value.id), value);
|
|
||||||
if (!item.hash) {
|
|
||||||
item.hash = hashFunc(item);
|
|
||||||
}
|
|
||||||
Vue.set(state.itemMap, item.id, item);
|
|
||||||
}
|
|
||||||
|
|
||||||
function patchItem(state, patch) {
|
|
||||||
const item = state.itemMap[patch.id];
|
|
||||||
if (item) {
|
|
||||||
Object.assign(item, patch);
|
|
||||||
item.hash = hashFunc(item);
|
|
||||||
Vue.set(state.itemMap, item.id, item);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: {
|
state: {
|
||||||
@ -36,8 +17,23 @@ export default (empty, simpleHash = false) => {
|
|||||||
items: state => Object.entries(state.itemMap).map(([, item]) => item),
|
items: state => Object.entries(state.itemMap).map(([, item]) => item),
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
setItem,
|
setItem(state, value) {
|
||||||
patchItem,
|
const item = Object.assign(empty(value.id), value);
|
||||||
|
if (!item.hash) {
|
||||||
|
item.hash = hashFunc(item);
|
||||||
|
}
|
||||||
|
Vue.set(state.itemMap, item.id, item);
|
||||||
|
},
|
||||||
|
patchItem(state, patch) {
|
||||||
|
const item = state.itemMap[patch.id];
|
||||||
|
if (item) {
|
||||||
|
Object.assign(item, patch);
|
||||||
|
item.hash = hashFunc(item);
|
||||||
|
Vue.set(state.itemMap, item.id, item);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
deleteItem(state, id) {
|
deleteItem(state, id) {
|
||||||
Vue.delete(state.itemMap, id);
|
Vue.delete(state.itemMap, id);
|
||||||
},
|
},
|
||||||
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
Loading…
Reference in New Issue
Block a user