Temporary folder
This commit is contained in:
parent
906e672e49
commit
623c265adc
@ -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">
|
||||||
<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>
|
||||||
|
@ -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',
|
||||||
]),
|
]),
|
||||||
|
@ -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', [
|
||||||
@ -178,9 +182,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;
|
||||||
@ -255,6 +262,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 +298,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: {
|
||||||
|
...mapGetters('file', [
|
||||||
|
'isCurrentTemp',
|
||||||
|
]),
|
||||||
|
...mapGetters('layout', [
|
||||||
'styles',
|
'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,5 +1,9 @@
|
|||||||
<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="isCurrentTemp">
|
||||||
|
<p><b>{{currentFileName}}</b> can not be published as it's a temporary file.</p>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
<div class="side-bar__info" v-if="noToken">
|
<div class="side-bar__info" v-if="noToken">
|
||||||
<p>You have to <b>link an account</b> to start publishing files.</p>
|
<p>You have to <b>link an account</b> to start publishing files.</p>
|
||||||
</div>
|
</div>
|
||||||
@ -95,6 +99,7 @@
|
|||||||
<span>Add Zendesk account</span>
|
<span>Add Zendesk account</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -126,6 +131,9 @@ export default {
|
|||||||
...mapState('queue', [
|
...mapState('queue', [
|
||||||
'isPublishRequested',
|
'isPublishRequested',
|
||||||
]),
|
]),
|
||||||
|
...mapGetters('file', [
|
||||||
|
'isCurrentTemp',
|
||||||
|
]),
|
||||||
...mapGetters('publishLocation', {
|
...mapGetters('publishLocation', {
|
||||||
publishLocations: 'current',
|
publishLocations: 'current',
|
||||||
}),
|
}),
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
<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="isCurrentTemp">
|
||||||
|
<p><b>{{currentFileName}}</b> can not be synchronized as it's a temporary file.</p>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
<div class="side-bar__info" v-if="noToken">
|
<div class="side-bar__info" v-if="noToken">
|
||||||
<p>You have to <b>link an account</b> to start syncing files.</p>
|
<p>You have to <b>link an account</b> to start syncing files.</p>
|
||||||
</div>
|
</div>
|
||||||
@ -72,6 +76,7 @@
|
|||||||
<span>Add GitHub account</span>
|
<span>Add GitHub account</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -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,
|
||||||
sectionList: null,
|
previewCtx: {
|
||||||
sectionDescList: [],
|
sectionDescList: [],
|
||||||
sectionDescMeasuredList: null,
|
},
|
||||||
sectionDescWithDiffsList: null,
|
previewCtxMeasured: null,
|
||||||
|
previewCtxWithDiffs: null,
|
||||||
|
sectionList: 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;
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import welcomeFile from '../data/welcomeFile.md';
|
|||||||
|
|
||||||
const dbVersion = 1;
|
const dbVersion = 1;
|
||||||
const dbStoreName = 'objects';
|
const dbStoreName = 'objects';
|
||||||
|
const fileIdToOpen = utils.queryParams.fileId;
|
||||||
const exportWorkspace = utils.queryParams.exportWorkspace;
|
const exportWorkspace = utils.queryParams.exportWorkspace;
|
||||||
const resetApp = utils.queryParams.reset;
|
const resetApp = utils.queryParams.reset;
|
||||||
const deleteMarkerMaxAge = 1000;
|
const deleteMarkerMaxAge = 1000;
|
||||||
@ -178,6 +179,10 @@ const localDbSvc = {
|
|||||||
// Sync local DB periodically
|
// Sync local DB periodically
|
||||||
utils.setInterval(() => localDbSvc.sync(), 1000);
|
utils.setInterval(() => localDbSvc.sync(), 1000);
|
||||||
|
|
||||||
|
if (fileIdToOpen) {
|
||||||
|
store.commit('file/setCurrentId', fileIdToOpen);
|
||||||
|
}
|
||||||
|
|
||||||
// watch current file changing
|
// watch current file changing
|
||||||
store.watch(
|
store.watch(
|
||||||
() => store.getters['file/current'].id,
|
() => store.getters['file/current'].id,
|
||||||
@ -216,6 +221,11 @@ const localDbSvc = {
|
|||||||
// Open the gutter if file contains discussions
|
// Open the gutter if file contains discussions
|
||||||
store.commit('discussion/setCurrentDiscussionId',
|
store.commit('discussion/setCurrentDiscussionId',
|
||||||
store.getters['discussion/nextDiscussionId']);
|
store.getters['discussion/nextDiscussionId']);
|
||||||
|
// Add the fileId to the queryParams
|
||||||
|
utils.setQueryParams({
|
||||||
|
...utils.queryParams,
|
||||||
|
fileId: currentFile.id,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
// Failure (content is not available), go back to previous file
|
// Failure (content is not available), go back to previous file
|
||||||
|
@ -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) {
|
||||||
|
sectionDescList = previewCtxMeasured.sectionDescList;
|
||||||
forceScrollSync();
|
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,7 +738,9 @@ 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(() => {
|
||||||
|
if (!store.state.light) {
|
||||||
// Sync periodically
|
// Sync periodically
|
||||||
utils.setInterval(() => {
|
utils.setInterval(() => {
|
||||||
if (isSyncPossible()
|
if (isSyncPossible()
|
||||||
@ -753,6 +759,7 @@ export default {
|
|||||||
localDbSvc.unloadContents();
|
localDbSvc.unloadContents();
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
isSyncPossible,
|
isSyncPossible,
|
||||||
|
113
src/services/tempFileSvc.js
Normal file
113
src/services/tempFileSvc.js
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import cledit from './cledit';
|
||||||
|
import store from '../store';
|
||||||
|
import utils from './utils';
|
||||||
|
import editorSvc from './editorSvc';
|
||||||
|
|
||||||
|
const origin = utils.queryParams.origin;
|
||||||
|
const existingFileId = utils.queryParams.fileId;
|
||||||
|
const fileName = utils.queryParams.fileName;
|
||||||
|
const contentText = utils.queryParams.contentText;
|
||||||
|
const contentProperties = utils.queryParams.contentProperties;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
close() {
|
||||||
|
if (origin && window.parent) {
|
||||||
|
window.parent.postMessage({ type: 'close' }, origin);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
if (!origin || !window.parent) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
store.commit('setLight', true);
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(() => {
|
||||||
|
const file = store.state.file.itemMap[existingFileId];
|
||||||
|
if (file) {
|
||||||
|
// If file exists, check that the origin site has created it
|
||||||
|
const fileCreation = store.getters['data/fileCreations'][file.id];
|
||||||
|
if (fileCreation && fileCreation.origin === origin) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new temp file
|
||||||
|
return store.dispatch('createFile', {
|
||||||
|
name: fileName,
|
||||||
|
text: contentText,
|
||||||
|
properties: contentProperties,
|
||||||
|
parentId: 'temp',
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then((file) => {
|
||||||
|
const fileItemMap = store.state.file.itemMap;
|
||||||
|
|
||||||
|
// Sanitize file creations
|
||||||
|
const fileCreations = {};
|
||||||
|
Object.entries(store.getters['data/fileCreations']).forEach(([id, fileCreation]) => {
|
||||||
|
if (fileItemMap[id]) {
|
||||||
|
fileCreations[id] = fileCreation;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Track file creation from the origin site
|
||||||
|
fileCreations[file.id] = {
|
||||||
|
created: Date.now(),
|
||||||
|
origin,
|
||||||
|
};
|
||||||
|
|
||||||
|
// List temp files
|
||||||
|
const tempFileCreations = [];
|
||||||
|
Object.entries(fileCreations).forEach(([id, fileCreation]) => {
|
||||||
|
if (fileItemMap[id].parentId === 'temp') {
|
||||||
|
tempFileCreations.push({
|
||||||
|
id,
|
||||||
|
created: fileCreation.created,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep only the last 10 temp files
|
||||||
|
tempFileCreations
|
||||||
|
.sort((fileCreation1, fileCreation2) => fileCreation2.created - fileCreation1.created)
|
||||||
|
.splice(10)
|
||||||
|
.forEach((fileCreation) => {
|
||||||
|
delete fileCreations[fileCreation.id];
|
||||||
|
store.dispatch('deleteFile', fileCreation.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store file creations and open the file
|
||||||
|
store.dispatch('data/setFileCreations', fileCreations);
|
||||||
|
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 (editorSvc.previewCtx.html != null) {
|
||||||
|
const content = store.getters['content/current'];
|
||||||
|
const properties = utils.computeProperties(content.properties);
|
||||||
|
window.parent.postMessage({
|
||||||
|
type: 'fileChange',
|
||||||
|
file: {
|
||||||
|
id: file.id,
|
||||||
|
name: currentFile.name,
|
||||||
|
content: {
|
||||||
|
text: content.text,
|
||||||
|
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,
|
||||||
}),
|
}),
|
||||||
|
fileCreations: getter('fileCreations'),
|
||||||
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));
|
||||||
},
|
},
|
||||||
|
setFileCreations: setter('fileCreations'),
|
||||||
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'),
|
||||||
|
@ -72,10 +72,17 @@ export default {
|
|||||||
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;
|
||||||
|
const tempFolderNode = new Node(emptyFolder(), [], true);
|
||||||
|
tempFolderNode.item.id = 'temp';
|
||||||
|
tempFolderNode.item.name = 'Temp';
|
||||||
|
tempFolderNode.noDrag = true;
|
||||||
|
tempFolderNode.noDrop = true;
|
||||||
|
tempFolderNode.isTemp = true;
|
||||||
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);
|
||||||
@ -93,7 +100,7 @@ export default {
|
|||||||
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;
|
||||||
@ -105,6 +112,10 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
rootNode.sortChildren();
|
rootNode.sortChildren();
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
@ -169,7 +180,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 +199,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,
|
||||||
|
@ -21,14 +21,17 @@ 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,
|
||||||
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 +55,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);
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user