Added explorer
This commit is contained in:
parent
c07fc7135e
commit
0c27a8337a
1
.gitignore
vendored
1
.gitignore
vendored
@ -5,3 +5,4 @@ dist/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.vscode
|
||||
|
BIN
src/assets/fonts/RobotoMono-Bold.woff
Normal file
BIN
src/assets/fonts/RobotoMono-Bold.woff
Normal file
Binary file not shown.
BIN
src/assets/fonts/RobotoMono-Regular.woff
Normal file
BIN
src/assets/fonts/RobotoMono-Regular.woff
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -20,17 +20,15 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import { mapState, mapMutations } from 'vuex';
|
||||
|
||||
export default {
|
||||
computed: mapState('layout', [
|
||||
'showNavigationBar',
|
||||
'showEditor',
|
||||
'showSidePreview',
|
||||
'showSideBar',
|
||||
'showStatusBar',
|
||||
]),
|
||||
methods: mapActions('layout', [
|
||||
methods: mapMutations('layout', [
|
||||
'toggleNavigationBar',
|
||||
'toggleEditor',
|
||||
'toggleSidePreview',
|
||||
@ -61,7 +59,6 @@ export default {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
padding: 2px;
|
||||
border-radius: 3px;
|
||||
margin: 3px 0;
|
||||
|
||||
&:hover {
|
||||
|
@ -1,16 +1,16 @@
|
||||
<template>
|
||||
<div class="editor">
|
||||
<pre class="editor__inner markdown-highlighting" :style="{ 'padding-left': editorPadding + 'px', 'padding-right': editorPadding + 'px' }"></pre>
|
||||
<pre class="editor__inner markdown-highlighting" :style="{'padding-left': styles.editorPadding + 'px', 'padding-right': styles.editorPadding + 'px'}"></pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
computed: mapState('layout', [
|
||||
'editorPadding',
|
||||
computed: mapGetters('layout', [
|
||||
'styles',
|
||||
]),
|
||||
};
|
||||
</script>
|
||||
|
@ -1,18 +1,122 @@
|
||||
<template>
|
||||
<div class="explorer">
|
||||
<div class="explorer flex flex--column">
|
||||
<div class="side-title">
|
||||
<div class="side-title__text">
|
||||
Explorer
|
||||
</div>
|
||||
<button class="side-title__button side-title__button--right button" @click="toggleExplorer(false)">
|
||||
<icon-close></icon-close>
|
||||
</button>
|
||||
<button class="side-title__button button" @click="newItem()">
|
||||
<icon-file-plus></icon-file-plus>
|
||||
</button>
|
||||
<button class="side-title__button button" @click="newItem(true)">
|
||||
<icon-folder-plus></icon-folder-plus>
|
||||
</button>
|
||||
<button class="side-title__button button" @click="editItem()">
|
||||
<icon-pen></icon-pen>
|
||||
</button>
|
||||
<button class="side-title__button button" @click="deleteItem()">
|
||||
<icon-delete></icon-delete>
|
||||
</button>
|
||||
</div>
|
||||
<div class="explorer__tree" :class="{'explorer__tree--new-item': !newChildNode.isNil}" tabindex="0">
|
||||
<explorer-node :node="rootNode" :depth="0"></explorer-node>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters, mapMutations } from 'vuex';
|
||||
import ExplorerNode from './ExplorerNode';
|
||||
import emptyFile from '../data/emptyFile';
|
||||
import emptyFolder from '../data/emptyFolder';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ExplorerNode,
|
||||
},
|
||||
computed: {
|
||||
...mapState('explorer', [
|
||||
'newChildNode',
|
||||
]),
|
||||
...mapGetters('explorer', [
|
||||
'rootNode',
|
||||
]),
|
||||
},
|
||||
methods: {
|
||||
...mapMutations('explorer', [
|
||||
'setSelectedId',
|
||||
]),
|
||||
...mapMutations('layout', [
|
||||
'toggleExplorer',
|
||||
]),
|
||||
newItem(isFolder) {
|
||||
const parentId = this.$store.getters['explorer/selectedNodeFolder'].item.id;
|
||||
this.$store.dispatch('explorer/openNode', parentId);
|
||||
this.$store.commit('explorer/setNewItem', {
|
||||
...isFolder ? emptyFolder() : emptyFile(),
|
||||
parentId,
|
||||
});
|
||||
},
|
||||
editItem() {
|
||||
const selectNode = this.$store.getters['explorer/selectedNode'];
|
||||
this.$store.commit('explorer/setEditingId', selectNode.item.id);
|
||||
},
|
||||
deleteItem() {
|
||||
// const selectNode = this.$store.getters['explorer/selectedNode'];
|
||||
// switch (this.node.item.type) {
|
||||
// case 'file':
|
||||
// default:
|
||||
// this.$store.commit('files/setCurrentId', id);
|
||||
// break;
|
||||
// case 'folder':
|
||||
// this.$store.commit('explorer/toggleOpenNode', id);
|
||||
// break;
|
||||
// }
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.$store.watch(
|
||||
() => this.$store.getters['files/current'].id,
|
||||
(currentFileId) => {
|
||||
this.setSelectedId(currentFileId);
|
||||
}, {
|
||||
immediate: true,
|
||||
});
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import 'common/variables.scss';
|
||||
.explorer,
|
||||
.explorer__tree {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.side-title {
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
padding: 4px 8px 0;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.side-title__button {
|
||||
width: 36px;
|
||||
padding: 6px;
|
||||
display: inline-block;
|
||||
background-color: transparent;
|
||||
opacity: 0.75;
|
||||
|
||||
/* prevent from seeing wrapped buttons */
|
||||
margin-bottom: 20px;
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.side-title__button--right {
|
||||
float: right;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,13 +0,0 @@
|
||||
<template>
|
||||
<div class="explorer-item">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import 'common/variables.scss';
|
||||
</style>
|
172
src/components/ExplorerNode.vue
Normal file
172
src/components/ExplorerNode.vue
Normal file
@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<div class="explorer-node" :class="{'explorer-node--selected': selected, 'explorer-node--open': open}">
|
||||
<div v-if="editing" class="explorer-node__item-editor" :class="['explorer-node__item-editor--' + node.item.type]" :style="{'padding-left': leftPadding}">
|
||||
<input type="text" class="text-input" v-focus @blur="submitEdit()" @keyup.enter="submitEdit()" @keyup.esc="submitEdit(true)" v-model="editingNodeName">
|
||||
</div>
|
||||
<div v-else-if="!node.isRoot" class="explorer-node__item" :class="['explorer-node__item--' + node.item.type]" :style="{'padding-left': leftPadding}" @click="select(node.item.id)">
|
||||
{{node.item.name}}
|
||||
</div>
|
||||
<div v-if="node.isFolder && open">
|
||||
<explorer-node v-for="node in node.folders" :key="node.item.id" :node="node" :depth="depth + 1"></explorer-node>
|
||||
<div v-if="newChild" class="explorer-node__new-child" :class="['explorer-node__new-child--' + newChild.item.type]" :style="{'padding-left': childLeftPadding}">
|
||||
<input type="text" class="text-input" v-focus @blur="submitNewChild()" @keyup.enter="submitNewChild()" @keyup.esc="submitNewChild(true)" v-model.trim="newChildName">
|
||||
</div>
|
||||
<explorer-node v-for="node in node.files" :key="node.item.id" :node="node" :depth="depth + 1"></explorer-node>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import utils from '../services/utils';
|
||||
import defaultContent from '../data/defaultContent.md';
|
||||
|
||||
export default {
|
||||
name: 'explorer-node',
|
||||
props: ['node', 'depth'],
|
||||
directives: {
|
||||
focus: {
|
||||
inserted(el) {
|
||||
el.focus();
|
||||
},
|
||||
},
|
||||
},
|
||||
data: () => ({
|
||||
editingValue: '',
|
||||
}),
|
||||
computed: {
|
||||
leftPadding() {
|
||||
return `${this.depth * 15}px`;
|
||||
},
|
||||
childLeftPadding() {
|
||||
return `${(this.depth + 1) * 15}px`;
|
||||
},
|
||||
selected() {
|
||||
return this.$store.getters['explorer/selectedNode'] === this.node;
|
||||
},
|
||||
editing() {
|
||||
return this.$store.getters['explorer/editingNode'] === this.node;
|
||||
},
|
||||
open() {
|
||||
return this.$store.state.explorer.openNodes[this.node.item.id] || this.node.isRoot;
|
||||
},
|
||||
newChild() {
|
||||
return this.$store.getters['explorer/newChildNodeParent'] === this.node
|
||||
&& this.$store.state.explorer.newChildNode;
|
||||
},
|
||||
newChildName: {
|
||||
get() {
|
||||
return this.$store.state.explorer.newChildNode.item.name;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit('explorer/setNewItemName', value && value.slice(0, 250));
|
||||
},
|
||||
},
|
||||
editingNodeName: {
|
||||
get() {
|
||||
return this.$store.getters['explorer/editingNode'].item.name;
|
||||
},
|
||||
set(value) {
|
||||
this.editingValue = value.trim();
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
select(id) {
|
||||
this.$store.commit('explorer/setSelectedId', id);
|
||||
if (this.node.isFolder) {
|
||||
this.$store.commit('explorer/toggleOpenNode', id);
|
||||
} else {
|
||||
this.$store.commit('files/setCurrentId', id);
|
||||
}
|
||||
},
|
||||
submitNewChild(cancel) {
|
||||
const newChildNode = this.$store.state.explorer.newChildNode;
|
||||
if (!cancel && newChildNode.item.name) {
|
||||
const id = utils.uid();
|
||||
if (newChildNode.isFolder) {
|
||||
this.$store.commit('folders/setItem', {
|
||||
...newChildNode.item,
|
||||
id,
|
||||
});
|
||||
} else {
|
||||
const contentId = utils.uid();
|
||||
this.$store.commit('contents/setItem', {
|
||||
id: contentId,
|
||||
text: defaultContent,
|
||||
});
|
||||
this.$store.commit('files/setItem', {
|
||||
...newChildNode.item,
|
||||
id,
|
||||
contentId,
|
||||
});
|
||||
}
|
||||
this.select(id);
|
||||
}
|
||||
this.$store.commit('explorer/setNewItem', null);
|
||||
},
|
||||
submitEdit(cancel) {
|
||||
const id = this.$store.getters['explorer/editingNode'].item.id;
|
||||
const value = this.editingValue;
|
||||
if (!cancel && id && value) {
|
||||
this.$store.commit('files/patchItem', {
|
||||
id,
|
||||
name: value.slice(0, 250),
|
||||
});
|
||||
}
|
||||
this.$store.commit('explorer/setEditingId', null);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
$item-font-size: 14px;
|
||||
|
||||
.explorer-node__item {
|
||||
cursor: pointer;
|
||||
font-size: $item-font-size;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
.explorer-node--selected > & {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
|
||||
.explorer__tree:focus & {
|
||||
background-color: #39f;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.explorer__tree--new-item & {
|
||||
opacity: 0.33;
|
||||
}
|
||||
}
|
||||
|
||||
.explorer-node__item--folder,
|
||||
.explorer-node__item-editor--folder,
|
||||
.explorer-node__new-child--folder {
|
||||
&::before {
|
||||
content: '▸';
|
||||
position: absolute;
|
||||
margin-left: -13px;
|
||||
|
||||
.explorer-node--open > & {
|
||||
content: '▾';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$new-child-height: 25px;
|
||||
|
||||
.explorer-node__item-editor,
|
||||
.explorer-node__new-child {
|
||||
padding: 1px 10px;
|
||||
|
||||
.text-input {
|
||||
font-size: $item-font-size;
|
||||
padding: 2px;
|
||||
height: $new-child-height;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,29 +1,29 @@
|
||||
<template>
|
||||
<div class="layout">
|
||||
<div class="layout__panel flex flex--row">
|
||||
<div class="layout__panel layout__panel--explorer" v-show="showExplorer" :style="{ width: explorerWidth + 'px' }">
|
||||
<div class="layout__panel layout__panel--explorer" v-show="styles.showExplorer" :style="{ width: constants.explorerWidth + 'px' }">
|
||||
<explorer></explorer>
|
||||
</div>
|
||||
<div class="layout__panel flex flex--column" :style="{ width: innerWidth + 'px' }">
|
||||
<div class="layout__panel layout__panel--navigation-bar" v-show="showNavigationBar || !showEditor" :style="{ height: navigationBarHeight + 'px' }">
|
||||
<div class="layout__panel flex flex--column" :style="{ width: styles.innerWidth + 'px' }">
|
||||
<div class="layout__panel layout__panel--navigation-bar" v-show="styles.showNavigationBar" :style="{ height: constants.navigationBarHeight + 'px' }">
|
||||
<navigation-bar></navigation-bar>
|
||||
</div>
|
||||
<div class="layout__panel flex flex--row" :style="{ height: innerHeight + 'px' }">
|
||||
<div class="layout__panel layout__panel--editor" v-show="showEditor" :style="{ width: editorWidth + 'px', 'font-size': fontSize + 'px' }">
|
||||
<div class="layout__panel flex flex--row" :style="{ height: styles.innerHeight + 'px' }">
|
||||
<div class="layout__panel layout__panel--editor" v-show="styles.showEditor" :style="{ width: styles.editorWidth + 'px', 'font-size': styles.fontSize + 'px' }">
|
||||
<editor></editor>
|
||||
</div>
|
||||
<div class="layout__panel layout__panel--button-bar" v-show="showEditor" :style="{ width: buttonBarWidth + 'px' }">
|
||||
<div class="layout__panel layout__panel--button-bar" v-show="styles.showEditor" :style="{ width: constants.buttonBarWidth + 'px' }">
|
||||
<button-bar></button-bar>
|
||||
</div>
|
||||
<div class="layout__panel layout__panel--preview" v-show="showSidePreview || !showEditor" :style="{ width: previewWidth + 'px', 'font-size': fontSize + 'px' }">
|
||||
<div class="layout__panel layout__panel--preview" v-show="styles.showPreview" :style="{ width: styles.previewWidth + 'px', 'font-size': styles.fontSize + 'px' }">
|
||||
<preview></preview>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout__panel layout__panel--status-bar" v-show="showStatusBar" :style="{ height: statusBarHeight + 'px' }">
|
||||
<div class="layout__panel layout__panel--status-bar" v-show="styles.showStatusBar" :style="{ height: constants.statusBarHeight + 'px' }">
|
||||
<status-bar></status-bar>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout__panel layout__panel--side-bar" v-show="showSideBar" :style="{ width: sideBarWidth + 'px' }">
|
||||
<div class="layout__panel layout__panel--side-bar" v-show="styles.showSideBar" :style="{ width: constants.sideBarWidth + 'px' }">
|
||||
<side-bar></side-bar>
|
||||
</div>
|
||||
</div>
|
||||
@ -31,7 +31,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import { mapState, mapGetters, mapMutations } from 'vuex';
|
||||
import NavigationBar from './NavigationBar';
|
||||
import ButtonBar from './ButtonBar';
|
||||
import StatusBar from './StatusBar';
|
||||
@ -40,7 +40,6 @@ import SideBar from './SideBar';
|
||||
import Editor from './Editor';
|
||||
import Preview from './Preview';
|
||||
import editorSvc from '../services/editorSvc';
|
||||
import constants from '../services/constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -52,33 +51,23 @@ export default {
|
||||
Editor,
|
||||
Preview,
|
||||
},
|
||||
computed: mapState('layout', {
|
||||
explorerWidth: 'explorerWidth',
|
||||
sideBarWidth: 'sideBarWidth',
|
||||
navigationBarHeight: 'navigationBarHeight',
|
||||
buttonBarWidth: 'buttonBarWidth',
|
||||
statusBarHeight: 'statusBarHeight',
|
||||
showEditor: 'showEditor',
|
||||
showSidePreview: 'showSidePreview',
|
||||
showNavigationBar: 'showNavigationBar',
|
||||
showStatusBar: 'showStatusBar',
|
||||
showSideBar: 'showSideBar',
|
||||
showExplorer: 'showExplorer',
|
||||
fontSize: 'fontSize',
|
||||
innerWidth: 'innerWidth',
|
||||
innerHeight: 'innerHeight',
|
||||
previewWidth: 'previewWidth',
|
||||
editorWidth: 'editorWidth',
|
||||
}),
|
||||
computed: {
|
||||
...mapState('layout', [
|
||||
'constants',
|
||||
]),
|
||||
...mapGetters('layout', [
|
||||
'styles',
|
||||
]),
|
||||
},
|
||||
methods: {
|
||||
...mapActions('layout', [
|
||||
'updateStyle',
|
||||
...mapMutations('layout', [
|
||||
'updateBodySize',
|
||||
]),
|
||||
saveSelection: () => editorSvc.saveSelection(true),
|
||||
},
|
||||
created() {
|
||||
this.updateStyle();
|
||||
window.addEventListener('resize', this.updateStyle);
|
||||
this.updateBodySize();
|
||||
window.addEventListener('resize', this.updateBodySize);
|
||||
window.addEventListener('keyup', this.saveSelection);
|
||||
window.addEventListener('mouseup', this.saveSelection);
|
||||
window.addEventListener('contextmenu', this.saveSelection);
|
||||
@ -106,10 +95,10 @@ export default {
|
||||
/ (sectionDesc.tocDimension.height || 1);
|
||||
const editorScrollTop = sectionDesc.editorDimension.startOffset
|
||||
+ (sectionDesc.editorDimension.height * posInSection);
|
||||
editorElt.parentNode.scrollTop = editorScrollTop - constants.scrollOffset;
|
||||
editorElt.parentNode.scrollTop = editorScrollTop;
|
||||
const previewScrollTop = sectionDesc.previewDimension.startOffset
|
||||
+ (sectionDesc.previewDimension.height * posInSection);
|
||||
previewElt.parentNode.scrollTop = previewScrollTop - constants.scrollOffset;
|
||||
previewElt.parentNode.scrollTop = previewScrollTop;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@ -162,7 +151,7 @@ export default {
|
||||
}
|
||||
|
||||
.layout__panel--explorer {
|
||||
background-color: #ddd;
|
||||
background-color: #dadada;
|
||||
}
|
||||
|
||||
.layout__panel--button-bar,
|
||||
|
@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div class="navigation-bar" v-bind:class="{'navigation-bar--editor': showEditor}">
|
||||
<div class="navigation-bar" v-bind:class="{'navigation-bar--editor': styles.showEditor}">
|
||||
<div class="navigation-bar__inner navigation-bar__inner--left navigation-bar__inner--button">
|
||||
<button class="navigation-bar__button button" @click="toggleExplorer()">
|
||||
<icon-menu></icon-menu>
|
||||
<icon-folder-multiple></icon-folder-multiple>
|
||||
</button>
|
||||
</div>
|
||||
<div class="navigation-bar__inner navigation-bar__inner--right navigation-bar__inner--button">
|
||||
<button class="navigation-bar__button button" @click="toggleExplorer()">
|
||||
<icon-settings></icon-settings>
|
||||
<icon-menu></icon-menu>
|
||||
</button>
|
||||
</div>
|
||||
<div class="navigation-bar__inner navigation-bar__inner--right flex flex--row">
|
||||
@ -15,7 +15,7 @@
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
<div class="navigation-bar__title navigation-bar__title--fake text-input"></div>
|
||||
<div class="navigation-bar__title navigation-bar__title--text text-input" v-bind:style="{maxWidth: titleMaxWidth + 'px'}">{{title}}</div>
|
||||
<div class="navigation-bar__title navigation-bar__title--text text-input" v-bind:style="{maxWidth: styles.titleMaxWidth + 'px'}">{{title}}</div>
|
||||
<input class="navigation-bar__title navigation-bar__title--input text-input" v-bind:class="{'navigation-bar__title--focus': titleFocus, 'navigation-bar__title--scrolling': titleScrolling}" v-bind:style="{width: titleWidth + 'px'}" @focus="editTitle(true)" @blur="editTitle(false)" @keyup.enter="submitTitle()" @keyup.esc="submitTitle(true)" v-on:mouseenter="titleHover = true" v-on:mouseleave="titleHover = false" v-model="title">
|
||||
</div>
|
||||
<div class="navigation-bar__inner navigation-bar__inner--edit-buttons">
|
||||
@ -60,7 +60,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import { mapGetters, mapMutations } from 'vuex';
|
||||
import editorSvc from '../services/editorSvc';
|
||||
import animationSvc from '../services/animationSvc';
|
||||
|
||||
@ -72,19 +72,18 @@ export default {
|
||||
titleHover: false,
|
||||
}),
|
||||
computed: {
|
||||
...mapState('layout', {
|
||||
showEditor: 'showEditor',
|
||||
titleMaxWidth: 'titleMaxWidth',
|
||||
}),
|
||||
...mapGetters('layout', [
|
||||
'styles',
|
||||
]),
|
||||
titleWidth() {
|
||||
if (!this.mounted) {
|
||||
return 0;
|
||||
}
|
||||
this.titleFakeElt.textContent = this.title;
|
||||
const width = this.titleFakeElt.getBoundingClientRect().width + 1; // 1px for the caret
|
||||
return width < this.titleMaxWidth
|
||||
const width = this.titleFakeElt.getBoundingClientRect().width + 2; // 2px for the caret
|
||||
return width < this.styles.titleMaxWidth
|
||||
? width
|
||||
: this.titleMaxWidth;
|
||||
: this.styles.titleMaxWidth;
|
||||
},
|
||||
titleScrolling() {
|
||||
const result = this.titleHover && !this.titleFocus;
|
||||
@ -106,7 +105,7 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions('layout', [
|
||||
...mapMutations('layout', [
|
||||
'toggleExplorer',
|
||||
'toggleSideBar',
|
||||
]),
|
||||
@ -120,7 +119,7 @@ export default {
|
||||
} else {
|
||||
const title = this.title.trim();
|
||||
if (title) {
|
||||
this.$store.dispatch('files/patchCurrent', { name: title });
|
||||
this.$store.dispatch('files/patchCurrent', { name: title.slice(0, 250) });
|
||||
} else {
|
||||
this.title = this.$store.getters['files/current'].name;
|
||||
}
|
||||
@ -161,6 +160,10 @@ export default {
|
||||
|
||||
.navigation-bar__inner--left {
|
||||
float: left;
|
||||
|
||||
&.navigation-bar__inner--button {
|
||||
margin-right: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation-bar__inner--right {
|
||||
@ -176,9 +179,16 @@ export default {
|
||||
}
|
||||
|
||||
.navigation-bar__button {
|
||||
display: inline-block;
|
||||
width: 34px;
|
||||
padding: 6px;
|
||||
|
||||
/* prevent from seeing wrapped buttons */
|
||||
margin-bottom: 20px;
|
||||
|
||||
.navigation-bar__inner--button & {
|
||||
padding: 7px;
|
||||
width: 38px;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation-bar__title,
|
||||
@ -186,7 +196,7 @@ export default {
|
||||
display: inline-block;
|
||||
color: $navbar-color;
|
||||
background-color: transparent;
|
||||
font-weight: 400;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.navigation-bar__title--input,
|
||||
@ -235,7 +245,7 @@ export default {
|
||||
}
|
||||
|
||||
.navigation-bar__spinner {
|
||||
margin: 10px 5px 0;
|
||||
margin: 10px 5px 0 15px;
|
||||
color: rgba(255, 255, 255, 0.33);
|
||||
}
|
||||
|
||||
|
@ -1,19 +1,19 @@
|
||||
<template>
|
||||
<div class="preview">
|
||||
<div class="preview__inner" :style="{ 'padding-left': previewPadding + 'px', 'padding-right': previewPadding + 'px' }">
|
||||
<div class="preview__inner" :style="{ 'padding-left': styles.previewPadding + 'px', 'padding-right': styles.previewPadding + 'px' }">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
const appUri = `${window.location.protocol}//${window.location.host}`;
|
||||
|
||||
export default {
|
||||
computed: mapState('layout', [
|
||||
'previewPadding',
|
||||
computed: mapGetters('layout', [
|
||||
'styles',
|
||||
]),
|
||||
mounted() {
|
||||
this.$el.addEventListener('click', (evt) => {
|
||||
@ -45,4 +45,17 @@ export default {
|
||||
margin: 0;
|
||||
padding: 0 1035px 360px;
|
||||
}
|
||||
|
||||
.preview__inner > :first-child {
|
||||
& > h1,
|
||||
& > h2,
|
||||
& > h3,
|
||||
& > h4,
|
||||
& > h5,
|
||||
& > h6 {
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="stat-panel panel no-overflow">
|
||||
<div class="stat-panel__block stat-panel__block--left" v-if="showEditor">
|
||||
<div class="stat-panel__block stat-panel__block--left" v-if="styles.showEditor">
|
||||
<span class="stat-panel__block-name">
|
||||
Text
|
||||
Markdown
|
||||
<small v-show="textSelection">(selection)</small>
|
||||
</span>
|
||||
<span v-for="stat in textStats" :key="stat.id">
|
||||
@ -23,7 +23,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import { mapGetters } from 'vuex';
|
||||
import editorSvc from '../services/editorSvc';
|
||||
import editorEngineSvc from '../services/editorEngineSvc';
|
||||
import utils from '../services/utils';
|
||||
@ -54,9 +54,9 @@ export default {
|
||||
new Stat('paragraphs', '\\S.*'),
|
||||
],
|
||||
}),
|
||||
computed: mapState('layout', {
|
||||
showEditor: 'showEditor',
|
||||
}),
|
||||
computed: mapGetters('layout', [
|
||||
'styles',
|
||||
]),
|
||||
created() {
|
||||
editorSvc.$on('sectionList', () => this.computeText());
|
||||
editorSvc.$on('selectionRange', () => this.computeText());
|
||||
|
@ -38,10 +38,11 @@ textarea {
|
||||
|
||||
.text-input {
|
||||
display: block;
|
||||
font-variant-ligatures: no-common-ligatures;
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
padding: 3px 12px;
|
||||
font-size: 22px;
|
||||
font-size: inherit;
|
||||
line-height: 1.5;
|
||||
color: inherit;
|
||||
background-color: #fff;
|
||||
@ -57,7 +58,7 @@ textarea {
|
||||
height: 36px;
|
||||
padding: 3px 12px;
|
||||
margin-bottom: 0;
|
||||
font-size: 22px;
|
||||
font-size: inherit;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
@ -106,13 +107,3 @@ textarea {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.side-title {
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
padding: 0 10px;
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
.side-title__text {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
@ -27,15 +27,15 @@
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inconsolata';
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('../assets/fonts/inconsolata.woff') format('woff');
|
||||
src: url('../assets/fonts/RobotoMono-Regular.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inconsolata';
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url('../assets/fonts/inconsolata-bold.woff') format('woff');
|
||||
src: url('../assets/fonts/RobotoMono-Bold.woff') format('woff');
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
$font-family-main: Lato, 'Helvetica Neue', Helvetica, sans-serif;
|
||||
$font-family-monospace: Inconsolata, 'Lucida Sans Typewriter', 'Lucida Console', monaco, Courrier, monospace;
|
||||
$font-family-monospace: 'Roboto Mono', 'Lucida Sans Typewriter', 'Lucida Console', monaco, Courrier, monospace;
|
||||
$line-height-base: 1.67;
|
||||
$line-height-title: 1.33;
|
||||
$font-size-monospace: 0.95em;
|
||||
$font-size-monospace: 0.85em;
|
||||
$code-bg: rgba(0, 0, 0, 0.05);
|
||||
$code-border-radius: 2px;
|
||||
$link-color: #4a80cf;
|
||||
@ -10,7 +10,7 @@ $border-radius-base: 2px;
|
||||
$hr-color: rgba(128, 128, 128, 0.2);
|
||||
$navbar-color: rgba(255, 255, 255, 0.67);
|
||||
$navbar-hover-color: #fff;
|
||||
$navbar-hover-background: #484848;
|
||||
$navbar-hover-background: rgba(255, 255, 255, 0.1);
|
||||
|
||||
$editor-color: rgba(0, 0, 0, 0.8);
|
||||
$editor-color-light: rgba(0, 0, 0, 0.28);
|
||||
|
4
src/data/defaultContent.md
Normal file
4
src/data/defaultContent.md
Normal file
@ -0,0 +1,4 @@
|
||||
|
||||
|
||||
|
||||
> Written with [StackEdit](https://stackedit.io/).
|
@ -1,4 +1,5 @@
|
||||
export default () => ({
|
||||
id: null,
|
||||
type: 'content',
|
||||
state: {},
|
||||
text: '\n',
|
||||
|
@ -1,6 +1,7 @@
|
||||
export default () => ({
|
||||
id: null,
|
||||
type: 'file',
|
||||
name: '',
|
||||
folderId: null,
|
||||
parentId: null,
|
||||
contentId: null,
|
||||
});
|
||||
|
6
src/data/emptyFolder.js
Normal file
6
src/data/emptyFolder.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default () => ({
|
||||
id: null,
|
||||
type: 'folder',
|
||||
name: '',
|
||||
parentId: null,
|
||||
});
|
5
src/icons/Close.vue
Normal file
5
src/icons/Close.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00" enable-background="new 0 0 24.00 24.00" xml:space="preserve">
|
||||
<path d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" />
|
||||
</svg>
|
||||
</template>
|
5
src/icons/Delete.vue
Normal file
5
src/icons/Delete.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00" enable-background="new 0 0 24.00 24.00" xml:space="preserve">
|
||||
<path d="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19C6,20.1 6.9,21 8,21H16C17.1,21 18,20.1 18,19V7H6V19Z" />
|
||||
</svg>
|
||||
</template>
|
5
src/icons/FilePlus.vue
Normal file
5
src/icons/FilePlus.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00" enable-background="new 0 0 24.00 24.00" xml:space="preserve">
|
||||
<path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20C20,21.1 19.1,22 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M11,15V12H9V15H6V17H9V20H11V17H14V15H11Z" />
|
||||
</svg>
|
||||
</template>
|
5
src/icons/FolderMultiple.vue
Normal file
5
src/icons/FolderMultiple.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00" enable-background="new 0 0 24.00 24.00" xml:space="preserve">
|
||||
<path d="M22,4H14L12,2H6C4.9,2 4,2.9 4,4V16C4,17.1 4.9,18 6,18H22C23.1,18 24,17.1 24,16V6C24,4.9 23.1,4 22,4M2,6H0V11H0V20C0,21.1 0.9,22 2,22H20V20H2V6Z" />
|
||||
</svg>
|
||||
</template>
|
5
src/icons/FolderPlus.vue
Normal file
5
src/icons/FolderPlus.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00" enable-background="new 0 0 24.00 24.00" xml:space="preserve">
|
||||
<path d="M10,4L12,6H20C21.1,6 22,6.9 22,8V18C22,19.1 21.1,20 20,20H4C2.89,20 2,19.1 2,18V6C2,4.89 2.89,4 4,4H10M15,9V12H12V14H15V17H17V14H20V12H17V9H15Z" />
|
||||
</svg>
|
||||
</template>
|
5
src/icons/Pen.vue
Normal file
5
src/icons/Pen.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00" enable-background="new 0 0 24.00 24.00" xml:space="preserve">
|
||||
<path d="M20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18,2.9 17.35,2.9 16.96,3.29L15.12,5.12L18.87,8.87M3,17.25V21H6.75L17.81,9.93L14.06,6.18L3,17.25Z" />
|
||||
</svg>
|
||||
</template>
|
@ -17,6 +17,12 @@ import SidePreview from './SidePreview';
|
||||
import Eye from './Eye';
|
||||
import Menu from './Menu';
|
||||
import Settings from './Settings';
|
||||
import FilePlus from './FilePlus';
|
||||
import FolderPlus from './FolderPlus';
|
||||
import Delete from './Delete';
|
||||
import Close from './Close';
|
||||
import FolderMultiple from './FolderMultiple';
|
||||
import Pen from './Pen';
|
||||
|
||||
Vue.component('iconFormatBold', FormatBold);
|
||||
Vue.component('iconFormatItalic', FormatItalic);
|
||||
@ -36,3 +42,9 @@ Vue.component('iconSidePreview', SidePreview);
|
||||
Vue.component('iconEye', Eye);
|
||||
Vue.component('iconMenu', Menu);
|
||||
Vue.component('iconSettings', Settings);
|
||||
Vue.component('iconFilePlus', FilePlus);
|
||||
Vue.component('iconFolderPlus', FolderPlus);
|
||||
Vue.component('iconDelete', Delete);
|
||||
Vue.component('iconClose', Close);
|
||||
Vue.component('iconFolderMultiple', FolderMultiple);
|
||||
Vue.component('iconPen', Pen);
|
||||
|
@ -1,3 +0,0 @@
|
||||
export default {
|
||||
scrollOffset: 0,
|
||||
};
|
@ -9,7 +9,6 @@ import markdownConversionSvc from './markdownConversionSvc';
|
||||
import markdownGrammarSvc from './markdownGrammarSvc';
|
||||
import sectionUtils from './sectionUtils';
|
||||
import extensionSvc from './extensionSvc';
|
||||
import constants from './constants';
|
||||
import animationSvc from './animationSvc';
|
||||
import editorEngineSvc from './editorEngineSvc';
|
||||
import store from '../store';
|
||||
@ -63,7 +62,7 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||
getObjectToScroll() {
|
||||
let elt = this.editorElt.parentNode;
|
||||
let dimensionKey = 'editorDimension';
|
||||
if (!store.state.layout.showEditor) {
|
||||
if (!store.getters['layout/styles'].showEditor) {
|
||||
elt = this.previewElt.parentNode;
|
||||
dimensionKey = 'previewDimension';
|
||||
}
|
||||
@ -426,10 +425,10 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||
let scrollerElt = this.previewElt.parentNode;
|
||||
const sectionDesc = anchorHash[anchor];
|
||||
if (sectionDesc) {
|
||||
if (store.state.layout.showSidePreview || !store.state.layout.showEditor) {
|
||||
if (store.getters['layout/styles'].showPreview) {
|
||||
scrollTop = sectionDesc.previewDimension.startOffset;
|
||||
} else {
|
||||
scrollTop = sectionDesc.editorDimension.startOffset - constants.scrollOffset;
|
||||
scrollTop = sectionDesc.editorDimension.startOffset;
|
||||
scrollerElt = this.editorElt.parentNode;
|
||||
}
|
||||
} else {
|
||||
@ -575,7 +574,7 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||
|
||||
let newSectionList;
|
||||
let newSelectionRange;
|
||||
const debouncedEditorChanged = debounce(() => {
|
||||
const onEditorChanged = () => {
|
||||
if (this.sectionList !== newSectionList) {
|
||||
this.sectionList = newSectionList;
|
||||
this.$emit('sectionList', this.sectionList);
|
||||
@ -590,7 +589,8 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||
this.$emit('selectionRange', this.selectionRange);
|
||||
}
|
||||
this.saveContentState();
|
||||
}, 10);
|
||||
};
|
||||
const debouncedEditorChanged = debounce(onEditorChanged, 10);
|
||||
|
||||
editorEngineSvc.clEditor.selectionMgr.on('selectionChanged', (start, end, selectionRange) => {
|
||||
newSelectionRange = selectionRange;
|
||||
@ -685,9 +685,15 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||
|
||||
editorEngineSvc.clEditor.on('contentChanged', (content, diffs, sectionList) => {
|
||||
newSectionList = sectionList;
|
||||
debouncedEditorChanged();
|
||||
if (instantPreview) {
|
||||
onEditorChanged();
|
||||
} else {
|
||||
debouncedEditorChanged();
|
||||
}
|
||||
});
|
||||
|
||||
this.$emit('inited');
|
||||
|
||||
// scope.$watch('editorLayoutSvc.isEditorOpen', function (isOpen) {
|
||||
// clEditorSvc.cledit.toggleEditable(isOpen)
|
||||
// })
|
||||
@ -736,12 +742,10 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
store.watch(state => `${state.layout.editorWidth},${state.layout.previewWidth}`,
|
||||
store.watch(() => `${store.getters['layout/styles'].editorWidth},${store.getters['layout/styles'].previewWidth}`,
|
||||
() => editorSvc.measureSectionDimensions(true));
|
||||
store.watch(state => state.layout.showSidePreview,
|
||||
store.watch(() => store.getters['layout/styles'].showSidePreview,
|
||||
showSidePreview => showSidePreview && editorSvc.measureSectionDimensions());
|
||||
|
||||
this.$emit('inited');
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -118,6 +118,7 @@ export default {
|
||||
[
|
||||
store.state.contents,
|
||||
store.state.files,
|
||||
store.state.folders,
|
||||
].forEach(moduleState => Object.assign(storeItemMap, moduleState.itemMap));
|
||||
this.connection.createTx((tx) => {
|
||||
this.readAll(storeItemMap, tx, () => {
|
||||
|
@ -1,5 +1,4 @@
|
||||
import store from '../../store';
|
||||
import constants from '../constants';
|
||||
import animationSvc from '../animationSvc';
|
||||
import editorSvc from '../editorSvc';
|
||||
|
||||
@ -34,9 +33,7 @@ function throttle(func, wait) {
|
||||
}
|
||||
|
||||
const doScrollSync = () => {
|
||||
const localSkipAnimation = skipAnimation
|
||||
|| !store.state.layout.showSidePreview
|
||||
|| !store.state.layout.showEditor;
|
||||
const localSkipAnimation = skipAnimation || !store.getters['layout/styles'].showSidePreview;
|
||||
skipAnimation = false;
|
||||
if (!store.state.editor.scrollSync || !sectionDescList || sectionDescList.length === 0) {
|
||||
return;
|
||||
@ -45,12 +42,11 @@ const doScrollSync = () => {
|
||||
if (editorScrollTop < 0) {
|
||||
editorScrollTop = 0;
|
||||
}
|
||||
let previewScrollTop = previewScrollerElt.scrollTop;
|
||||
const previewScrollTop = previewScrollerElt.scrollTop;
|
||||
let scrollTo;
|
||||
if (isScrollEditor) {
|
||||
// Scroll the preview
|
||||
isScrollEditor = false;
|
||||
editorScrollTop += constants.scrollOffset;
|
||||
sectionDescList.some((sectionDesc) => {
|
||||
if (editorScrollTop > sectionDesc.editorDimension.endOffset) {
|
||||
return false;
|
||||
@ -58,7 +54,7 @@ const doScrollSync = () => {
|
||||
const posInSection = (editorScrollTop - sectionDesc.editorDimension.startOffset)
|
||||
/ (sectionDesc.editorDimension.height || 1);
|
||||
scrollTo = (sectionDesc.previewDimension.startOffset
|
||||
+ (sectionDesc.previewDimension.height * posInSection)) - constants.scrollOffset;
|
||||
+ (sectionDesc.previewDimension.height * posInSection));
|
||||
return true;
|
||||
});
|
||||
scrollTo = Math.min(
|
||||
@ -79,10 +75,9 @@ const doScrollSync = () => {
|
||||
isPreviewMoving = true;
|
||||
});
|
||||
}, localSkipAnimation ? 500 : 50);
|
||||
} else if (!store.state.layout.showEditor || isScrollPreview) {
|
||||
} else if (!store.getters['layout/styles'].showEditor || isScrollPreview) {
|
||||
// Scroll the editor
|
||||
isScrollPreview = false;
|
||||
previewScrollTop += constants.scrollOffset;
|
||||
sectionDescList.some((sectionDesc) => {
|
||||
if (previewScrollTop > sectionDesc.previewDimension.endOffset) {
|
||||
return false;
|
||||
@ -90,7 +85,7 @@ const doScrollSync = () => {
|
||||
const posInSection = (previewScrollTop - sectionDesc.previewDimension.startOffset)
|
||||
/ (sectionDesc.previewDimension.height || 1);
|
||||
scrollTo = (sectionDesc.editorDimension.startOffset
|
||||
+ (sectionDesc.editorDimension.height * posInSection)) - constants.scrollOffset;
|
||||
+ (sectionDesc.editorDimension.height * posInSection));
|
||||
return true;
|
||||
});
|
||||
scrollTo = Math.min(
|
||||
@ -119,7 +114,7 @@ let timeoutId;
|
||||
|
||||
const forceScrollSync = () => {
|
||||
if (!isPreviewRefreshing) {
|
||||
doScrollSync(!store.state.layout.showSidePreview);
|
||||
doScrollSync();
|
||||
}
|
||||
};
|
||||
store.watch(state => state.editor.scrollSync, forceScrollSync);
|
||||
@ -135,7 +130,7 @@ editorSvc.$on('inited', () => {
|
||||
}
|
||||
isScrollEditor = true;
|
||||
isScrollPreview = false;
|
||||
doScrollSync(!store.state.layout.showSidePreview);
|
||||
doScrollSync();
|
||||
});
|
||||
|
||||
previewScrollerElt.addEventListener('scroll', () => {
|
||||
@ -144,7 +139,7 @@ editorSvc.$on('inited', () => {
|
||||
}
|
||||
isScrollPreview = true;
|
||||
isScrollEditor = false;
|
||||
doScrollSync(!store.state.layout.showSidePreview);
|
||||
doScrollSync();
|
||||
});
|
||||
});
|
||||
|
||||
@ -163,14 +158,15 @@ editorSvc.$on('previewText', () => {
|
||||
// Remove height property once the preview as been refreshed
|
||||
previewElt.style.removeProperty('height');
|
||||
// Assume the user is writing in the editor
|
||||
isScrollEditor = store.state.layout.showEditor;
|
||||
isScrollEditor = store.getters['layout/styles'].showEditor;
|
||||
// A preview scrolling event can occur if height is smaller
|
||||
timeoutId = setTimeout(() => {
|
||||
isPreviewRefreshing = false;
|
||||
}, 100);
|
||||
});
|
||||
|
||||
store.watch(state => state.layout.showSidePreview,
|
||||
store.watch(
|
||||
() => store.getters['layout/styles'].showSidePreview,
|
||||
(showSidePreview) => {
|
||||
if (showSidePreview) {
|
||||
isScrollEditor = true;
|
||||
@ -179,6 +175,12 @@ store.watch(state => state.layout.showSidePreview,
|
||||
}
|
||||
});
|
||||
|
||||
store.watch(
|
||||
() => store.getters['files/current'].id,
|
||||
() => {
|
||||
skipAnimation = true;
|
||||
});
|
||||
|
||||
editorSvc.$on('sectionDescMeasuredList', (sectionDescMeasuredList) => {
|
||||
sectionDescList = sectionDescMeasuredList;
|
||||
forceScrollSync();
|
||||
|
@ -10,39 +10,35 @@ const ifNoId = cb => (obj) => {
|
||||
return cb();
|
||||
};
|
||||
|
||||
// Watch file changing
|
||||
store.watch(
|
||||
() => store.getters['files/current'].id,
|
||||
() => Promise.resolve(store.getters['files/current'])
|
||||
// If current file has no ID, get the most recent file
|
||||
.then(ifNoId(() => store.getters['files/mostRecent']))
|
||||
// If no ID, load the DB (we're booting)
|
||||
.then(ifNoId(() => localDbSvc.sync()
|
||||
// Retry
|
||||
.then(() => store.getters['files/current'])
|
||||
.then(ifNoId(() => store.getters['files/mostRecent'])),
|
||||
))
|
||||
// Finally create a new file
|
||||
.then(ifNoId(() => {
|
||||
const contentId = utils.uid();
|
||||
store.commit('contents/setItem', {
|
||||
id: contentId,
|
||||
text: welcomeFile,
|
||||
});
|
||||
const fileId = utils.uid();
|
||||
store.commit('files/setItem', {
|
||||
id: fileId,
|
||||
name: 'Welcome file',
|
||||
contentId,
|
||||
});
|
||||
return store.state.files.itemMap[fileId];
|
||||
}))
|
||||
.then((currentFile) => {
|
||||
store.commit('files/setCurrentId', currentFile.id);
|
||||
store.dispatch('files/patchCurrent', {}); // Update `updated` field to make it the mostRecent
|
||||
}),
|
||||
{
|
||||
immediate: true,
|
||||
});
|
||||
// Load the DB on boot
|
||||
localDbSvc.sync()
|
||||
// Watch file changing
|
||||
.then(() => store.watch(
|
||||
() => store.getters['files/current'].id,
|
||||
() => Promise.resolve(store.getters['files/current'])
|
||||
// If current file has no ID, get the most recent file
|
||||
.then(ifNoId(() => store.getters['files/mostRecent']))
|
||||
// If still no ID, create a new file
|
||||
.then(ifNoId(() => {
|
||||
const contentId = utils.uid();
|
||||
store.commit('contents/setItem', {
|
||||
id: contentId,
|
||||
text: welcomeFile,
|
||||
});
|
||||
const fileId = utils.uid();
|
||||
store.commit('files/setItem', {
|
||||
id: fileId,
|
||||
name: 'Welcome file',
|
||||
contentId,
|
||||
});
|
||||
return store.state.files.itemMap[fileId];
|
||||
}))
|
||||
.then((currentFile) => {
|
||||
store.commit('files/setCurrentId', currentFile.id);
|
||||
store.dispatch('files/patchCurrent', {}); // Update `updated` field to make it the mostRecent
|
||||
}),
|
||||
{
|
||||
immediate: true,
|
||||
}));
|
||||
|
||||
utils.setInterval(() => localDbSvc.sync(), 1200);
|
||||
|
@ -3,8 +3,10 @@ import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import contents from './modules/contents';
|
||||
import files from './modules/files';
|
||||
import folders from './modules/folders';
|
||||
import layout from './modules/layout';
|
||||
import editor from './modules/editor';
|
||||
import explorer from './modules/explorer';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
@ -14,8 +16,10 @@ const store = new Vuex.Store({
|
||||
modules: {
|
||||
contents,
|
||||
files,
|
||||
folders,
|
||||
layout,
|
||||
editor,
|
||||
explorer,
|
||||
},
|
||||
strict: debug,
|
||||
plugins: debug ? [createLogger()] : [],
|
||||
|
114
src/store/modules/explorer.js
Normal file
114
src/store/modules/explorer.js
Normal file
@ -0,0 +1,114 @@
|
||||
import Vue from 'vue';
|
||||
import emptyFile from '../../data/emptyFile';
|
||||
import emptyFolder from '../../data/emptyFolder';
|
||||
|
||||
const setter = propertyName => (state, value) => {
|
||||
state[propertyName] = value;
|
||||
};
|
||||
|
||||
const collator = new Intl.Collator(undefined, { sensitivity: 'base' });
|
||||
const compare = (node1, node2) => collator.compare(node1.item.name, node2.item.name);
|
||||
|
||||
class Node {
|
||||
constructor(item, isFolder, isRoot) {
|
||||
this.item = item;
|
||||
this.isFolder = isFolder;
|
||||
this.isRoot = isRoot;
|
||||
if (isFolder) {
|
||||
this.folders = [];
|
||||
this.files = [];
|
||||
}
|
||||
}
|
||||
|
||||
sortChildren() {
|
||||
if (this.isFolder) {
|
||||
this.folders.sort(compare);
|
||||
this.files.sort(compare);
|
||||
this.folders.forEach(child => child.sortChildren());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const nilFileNode = new Node(emptyFile());
|
||||
nilFileNode.isNil = true;
|
||||
|
||||
function getParent(node, getters) {
|
||||
if (node === nilFileNode) {
|
||||
return nilFileNode;
|
||||
}
|
||||
return getters.nodeMap[node.item.parentId] || getters.rootNode;
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: {
|
||||
selectedId: null,
|
||||
editingId: null,
|
||||
newChildNode: nilFileNode,
|
||||
openNodes: {},
|
||||
},
|
||||
getters: {
|
||||
nodeStructure: (state, getters, rootState, rootGetters) => {
|
||||
const nodeMap = {};
|
||||
rootGetters['folders/items'].forEach((item) => {
|
||||
nodeMap[item.id] = new Node(item, true);
|
||||
});
|
||||
rootGetters['files/items'].forEach((item) => {
|
||||
nodeMap[item.id] = new Node(item);
|
||||
});
|
||||
const rootNode = new Node(emptyFolder(), true, true);
|
||||
Object.keys(nodeMap).forEach((id) => {
|
||||
const node = nodeMap[id];
|
||||
let parentNode = nodeMap[node.item.parentId];
|
||||
if (!parentNode || !parentNode.isFolder) {
|
||||
parentNode = rootNode;
|
||||
}
|
||||
if (node.isFolder) {
|
||||
parentNode.folders.push(node);
|
||||
} else {
|
||||
parentNode.files.push(node);
|
||||
}
|
||||
});
|
||||
rootNode.sortChildren();
|
||||
return {
|
||||
nodeMap,
|
||||
rootNode,
|
||||
};
|
||||
},
|
||||
nodeMap: (state, getters) => getters.nodeStructure.nodeMap,
|
||||
rootNode: (state, getters) => getters.nodeStructure.rootNode,
|
||||
newChildNodeParent: (state, getters) => getParent(state.newChildNode, getters),
|
||||
selectedNode: (state, getters) => getters.nodeMap[state.selectedId] || nilFileNode,
|
||||
selectedNodeFolder: (state, getters) => {
|
||||
const selectedNode = getters.selectedNode;
|
||||
return selectedNode.item.type === 'folder'
|
||||
? selectedNode
|
||||
: getParent(selectedNode, getters);
|
||||
},
|
||||
editingNode: (state, getters) => getters.nodeMap[state.editingId] || nilFileNode,
|
||||
},
|
||||
mutations: {
|
||||
setSelectedId: setter('selectedId'),
|
||||
setEditingId: setter('editingId'),
|
||||
setNewItem(state, item) {
|
||||
state.newChildNode = item ? new Node(item) : nilFileNode;
|
||||
},
|
||||
setNewItemName(state, name) {
|
||||
state.newChildNode.item.name = name;
|
||||
},
|
||||
toggleOpenNode(state, id) {
|
||||
Vue.set(state.openNodes, id, !state.openNodes[id]);
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
openNode({ state, getters, commit, dispatch }, id) {
|
||||
const node = getters.nodeMap[id];
|
||||
if (node) {
|
||||
if (node.isFolder && !state.openNodes[id]) {
|
||||
commit('toggleOpenNode', id);
|
||||
}
|
||||
dispatch('openNode', node.item.parentId);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
6
src/store/modules/folders.js
Normal file
6
src/store/modules/folders.js
Normal file
@ -0,0 +1,6 @@
|
||||
import moduleTemplate from './moduleTemplate';
|
||||
import empty from '../../data/emptyFolder';
|
||||
|
||||
const module = moduleTemplate(empty);
|
||||
|
||||
export default module;
|
@ -9,148 +9,135 @@ const setter = propertyName => (state, value) => {
|
||||
state[propertyName] = value;
|
||||
};
|
||||
|
||||
const toggler = (propertyName, setterName) => ({ state, commit, dispatch }, show) => {
|
||||
commit(setterName, show === undefined ? !state[propertyName] : show);
|
||||
dispatch('updateStyle');
|
||||
const toggler = propertyName => (state, value) => {
|
||||
state[propertyName] = value === undefined ? !state[propertyName] : value;
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: {
|
||||
// Constants
|
||||
explorerWidth: 280,
|
||||
sideBarWidth: 280,
|
||||
navigationBarHeight: 44,
|
||||
buttonBarWidth: 30,
|
||||
statusBarHeight: 20,
|
||||
constants: {
|
||||
explorerWidth: 250,
|
||||
sideBarWidth: 280,
|
||||
navigationBarHeight: 44,
|
||||
buttonBarWidth: 30,
|
||||
statusBarHeight: 20,
|
||||
},
|
||||
// Configuration
|
||||
showNavigationBar: true,
|
||||
showEditor: true,
|
||||
showSidePreview: true,
|
||||
showStatusBar: true,
|
||||
showSideBar: false,
|
||||
showExplorer: false,
|
||||
showExplorer: true,
|
||||
editorWidthFactor: 1,
|
||||
fontSizeFactor: 1,
|
||||
// Style
|
||||
fontSize: 0,
|
||||
innerWidth: 0,
|
||||
innerHeight: 0,
|
||||
editorWidth: 0,
|
||||
editorPadding: 0,
|
||||
previewWidth: 0,
|
||||
previewPadding: 0,
|
||||
titleMaxWidth: 0,
|
||||
// Styles
|
||||
bodyWidth: 0,
|
||||
bodyHeight: 0,
|
||||
},
|
||||
mutations: {
|
||||
setShowNavigationBar: setter('showNavigationBar'),
|
||||
setShowEditor: setter('showEditor'),
|
||||
setShowSidePreview: setter('showSidePreview'),
|
||||
setShowStatusBar: setter('showStatusBar'),
|
||||
setShowSideBar: setter('showSideBar'),
|
||||
setShowExplorer: setter('showExplorer'),
|
||||
toggleNavigationBar: toggler('showNavigationBar'),
|
||||
toggleEditor: toggler('showEditor'),
|
||||
toggleSidePreview: toggler('showSidePreview'),
|
||||
toggleStatusBar: toggler('showStatusBar'),
|
||||
toggleSideBar: toggler('showSideBar'),
|
||||
toggleExplorer: toggler('showExplorer'),
|
||||
setEditorWidthFactor: setter('editorWidthFactor'),
|
||||
setFontSizeFactor: setter('fontSizeFactor'),
|
||||
setFontSize: setter('fontSize'),
|
||||
setInnerWidth: setter('innerWidth'),
|
||||
setInnerHeight: setter('innerHeight'),
|
||||
setEditorWidth: setter('editorWidth'),
|
||||
setEditorPadding: setter('editorPadding'),
|
||||
setPreviewWidth: setter('previewWidth'),
|
||||
setPreviewPadding: setter('previewPadding'),
|
||||
setTitleMaxWidth: setter('titleMaxWidth'),
|
||||
updateBodySize: (state) => {
|
||||
state.bodyWidth = document.body.clientWidth;
|
||||
state.bodyHeight = document.body.clientHeight;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
toggleNavigationBar: toggler('showNavigationBar', 'setShowNavigationBar'),
|
||||
toggleEditor: toggler('showEditor', 'setShowEditor'),
|
||||
toggleSidePreview: toggler('showSidePreview', 'setShowSidePreview'),
|
||||
toggleStatusBar: toggler('showStatusBar', 'setShowStatusBar'),
|
||||
toggleSideBar: toggler('showSideBar', 'setShowSideBar'),
|
||||
toggleExplorer: toggler('showExplorer', 'setShowExplorer'),
|
||||
updateStyle({ state, commit, dispatch }) {
|
||||
const bodyWidth = document.body.clientWidth;
|
||||
const bodyHeight = document.body.clientHeight;
|
||||
getters: {
|
||||
styles: (state) => {
|
||||
const styles = {
|
||||
showNavigationBar: !state.showEditor || state.showNavigationBar,
|
||||
showStatusBar: state.showStatusBar,
|
||||
showEditor: state.showEditor,
|
||||
showSidePreview: state.showSidePreview && state.showEditor,
|
||||
showPreview: state.showSidePreview || !state.showEditor,
|
||||
showSideBar: state.showSideBar,
|
||||
showExplorer: state.showExplorer,
|
||||
};
|
||||
|
||||
const showNavigationBar = !state.showEditor || state.showNavigationBar;
|
||||
let innerHeight = bodyHeight;
|
||||
if (showNavigationBar) {
|
||||
innerHeight -= state.navigationBarHeight;
|
||||
}
|
||||
if (state.showStatusBar) {
|
||||
innerHeight -= state.statusBarHeight;
|
||||
}
|
||||
function computeStyles() {
|
||||
styles.innerHeight = state.bodyHeight;
|
||||
if (styles.showNavigationBar) {
|
||||
styles.innerHeight -= state.constants.navigationBarHeight;
|
||||
}
|
||||
if (styles.showStatusBar) {
|
||||
styles.innerHeight -= state.constants.statusBarHeight;
|
||||
}
|
||||
|
||||
let innerWidth = bodyWidth;
|
||||
if (state.showSideBar) {
|
||||
innerWidth -= state.sideBarWidth;
|
||||
}
|
||||
if (state.showExplorer) {
|
||||
innerWidth -= state.explorerWidth;
|
||||
}
|
||||
let doublePanelWidth = innerWidth - state.buttonBarWidth;
|
||||
if (doublePanelWidth < editorMinWidth) {
|
||||
if (state.showSideBar) {
|
||||
dispatch('toggleSideBar', false);
|
||||
styles.innerWidth = state.bodyWidth;
|
||||
if (styles.showSideBar) {
|
||||
styles.innerWidth -= state.constants.sideBarWidth;
|
||||
}
|
||||
if (styles.showExplorer) {
|
||||
styles.innerWidth -= state.constants.explorerWidth;
|
||||
}
|
||||
|
||||
let doublePanelWidth = styles.innerWidth - state.constants.buttonBarWidth;
|
||||
if (doublePanelWidth < editorMinWidth) {
|
||||
if (styles.showSideBar) {
|
||||
styles.showSideBar = false;
|
||||
computeStyles();
|
||||
return;
|
||||
}
|
||||
if (styles.showExplorer) {
|
||||
styles.showExplorer = false;
|
||||
computeStyles();
|
||||
return;
|
||||
}
|
||||
doublePanelWidth = editorMinWidth;
|
||||
}
|
||||
|
||||
if (styles.showSidePreview && doublePanelWidth / 2 < editorMinWidth) {
|
||||
styles.showSidePreview = false;
|
||||
computeStyles();
|
||||
return;
|
||||
}
|
||||
if (state.showExplorer) {
|
||||
dispatch('toggleExplorer', false);
|
||||
return;
|
||||
|
||||
styles.fontSize = 18;
|
||||
styles.textWidth = 990;
|
||||
if (doublePanelWidth < 1120) {
|
||||
styles.fontSize -= 1;
|
||||
styles.textWidth = 910;
|
||||
}
|
||||
doublePanelWidth = editorMinWidth;
|
||||
}
|
||||
const splitPanel = state.showEditor && state.showSidePreview;
|
||||
if (splitPanel && doublePanelWidth / 2 < editorMinWidth) {
|
||||
dispatch('toggleSidePreview', false);
|
||||
return;
|
||||
if (doublePanelWidth < 1040) {
|
||||
styles.textWidth = 830;
|
||||
}
|
||||
styles.textWidth *= state.editorWidthFactor;
|
||||
if (doublePanelWidth < styles.textWidth) {
|
||||
styles.textWidth = doublePanelWidth;
|
||||
}
|
||||
if (styles.textWidth < 640) {
|
||||
styles.fontSize -= 1;
|
||||
}
|
||||
styles.fontSize *= state.fontSizeFactor;
|
||||
|
||||
const panelWidth = doublePanelWidth / 2;
|
||||
styles.previewWidth = styles.showSidePreview ?
|
||||
panelWidth :
|
||||
styles.innerWidth;
|
||||
styles.previewPadding = Math.max((styles.previewWidth - styles.textWidth) / 2, minPadding);
|
||||
styles.editorWidth = styles.showSidePreview ?
|
||||
panelWidth :
|
||||
doublePanelWidth;
|
||||
styles.editorPadding = Math.max((styles.editorWidth - styles.textWidth) / 2, minPadding);
|
||||
|
||||
styles.titleMaxWidth = styles.innerWidth - navigationBarSpaceWidth;
|
||||
if (styles.showEditor) {
|
||||
styles.titleMaxWidth -= navigationBarLeftWidth;
|
||||
}
|
||||
styles.titleMaxWidth = Math.min(styles.titleMaxWidth, maxTitleMaxWidth);
|
||||
styles.titleMaxWidth = Math.max(styles.titleMaxWidth, minTitleMaxWidth);
|
||||
}
|
||||
|
||||
let fontSize = 18;
|
||||
let textWidth = 990;
|
||||
if (doublePanelWidth < 1120) {
|
||||
fontSize -= 1;
|
||||
textWidth = 910;
|
||||
}
|
||||
if (doublePanelWidth < 1040) {
|
||||
textWidth = 830;
|
||||
}
|
||||
if (textWidth < 640) {
|
||||
fontSize -= 1;
|
||||
}
|
||||
textWidth *= state.editorWidthFactor;
|
||||
fontSize *= state.fontSizeFactor;
|
||||
|
||||
const panelWidth = doublePanelWidth / 2;
|
||||
const previewWidth = splitPanel ?
|
||||
panelWidth :
|
||||
innerWidth;
|
||||
let previewPadding = (previewWidth - textWidth) / 2;
|
||||
if (previewPadding < minPadding) {
|
||||
previewPadding = minPadding;
|
||||
}
|
||||
const editorWidth = splitPanel ?
|
||||
panelWidth :
|
||||
doublePanelWidth;
|
||||
let editorPadding = (editorWidth - textWidth) / 2;
|
||||
if (editorPadding < minPadding) {
|
||||
editorPadding = minPadding;
|
||||
}
|
||||
|
||||
let titleMaxWidth = innerWidth - navigationBarSpaceWidth;
|
||||
if (state.showEditor) {
|
||||
titleMaxWidth -= navigationBarLeftWidth;
|
||||
}
|
||||
titleMaxWidth = Math.min(titleMaxWidth, maxTitleMaxWidth);
|
||||
titleMaxWidth = Math.max(titleMaxWidth, minTitleMaxWidth);
|
||||
|
||||
commit('setFontSize', fontSize);
|
||||
commit('setInnerWidth', innerWidth);
|
||||
commit('setInnerHeight', innerHeight);
|
||||
commit('setPreviewWidth', previewWidth);
|
||||
commit('setPreviewPadding', previewPadding);
|
||||
commit('setEditorWidth', editorWidth);
|
||||
commit('setEditorPadding', editorPadding);
|
||||
commit('setTitleMaxWidth', titleMaxWidth);
|
||||
computeStyles();
|
||||
return styles;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user