Added explorer

This commit is contained in:
benweet 2017-07-31 10:04:01 +01:00
parent c07fc7135e
commit 0c27a8337a
37 changed files with 719 additions and 290 deletions

1
.gitignore vendored
View File

@ -5,3 +5,4 @@ dist/
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
.vscode

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -20,17 +20,15 @@
</template> </template>
<script> <script>
import { mapState, mapActions } from 'vuex'; import { mapState, mapMutations } from 'vuex';
export default { export default {
computed: mapState('layout', [ computed: mapState('layout', [
'showNavigationBar', 'showNavigationBar',
'showEditor',
'showSidePreview', 'showSidePreview',
'showSideBar',
'showStatusBar', 'showStatusBar',
]), ]),
methods: mapActions('layout', [ methods: mapMutations('layout', [
'toggleNavigationBar', 'toggleNavigationBar',
'toggleEditor', 'toggleEditor',
'toggleSidePreview', 'toggleSidePreview',
@ -61,7 +59,6 @@ export default {
width: 26px; width: 26px;
height: 26px; height: 26px;
padding: 2px; padding: 2px;
border-radius: 3px;
margin: 3px 0; margin: 3px 0;
&:hover { &:hover {

View File

@ -1,16 +1,16 @@
<template> <template>
<div class="editor"> <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> </div>
</template> </template>
<script> <script>
import { mapState } from 'vuex'; import { mapGetters } from 'vuex';
export default { export default {
computed: mapState('layout', [ computed: mapGetters('layout', [
'editorPadding', 'styles',
]), ]),
}; };
</script> </script>

View File

@ -1,18 +1,122 @@
<template> <template>
<div class="explorer"> <div class="explorer flex flex--column">
<div class="side-title"> <div class="side-title">
<div class="side-title__text"> <button class="side-title__button side-title__button--right button" @click="toggleExplorer(false)">
Explorer <icon-close></icon-close>
</div> </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>
</div> </div>
</template> </template>
<script> <script>
import { mapState, mapGetters, mapMutations } from 'vuex';
import ExplorerNode from './ExplorerNode';
import emptyFile from '../data/emptyFile';
import emptyFolder from '../data/emptyFolder';
export default { 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> </script>
<style lang="scss"> <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> </style>

View File

@ -1,13 +0,0 @@
<template>
<div class="explorer-item">
</div>
</template>
<script>
export default {
};
</script>
<style lang="scss">
@import 'common/variables.scss';
</style>

View 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>

View File

@ -1,29 +1,29 @@
<template> <template>
<div class="layout"> <div class="layout">
<div class="layout__panel flex flex--row"> <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> <explorer></explorer>
</div> </div>
<div class="layout__panel flex flex--column" :style="{ width: innerWidth + 'px' }"> <div class="layout__panel flex flex--column" :style="{ width: styles.innerWidth + 'px' }">
<div class="layout__panel layout__panel--navigation-bar" v-show="showNavigationBar || !showEditor" :style="{ height: navigationBarHeight + 'px' }"> <div class="layout__panel layout__panel--navigation-bar" v-show="styles.showNavigationBar" :style="{ height: constants.navigationBarHeight + 'px' }">
<navigation-bar></navigation-bar> <navigation-bar></navigation-bar>
</div> </div>
<div class="layout__panel flex flex--row" :style="{ height: innerHeight + 'px' }"> <div class="layout__panel flex flex--row" :style="{ height: styles.innerHeight + 'px' }">
<div class="layout__panel layout__panel--editor" v-show="showEditor" :style="{ width: editorWidth + 'px', 'font-size': fontSize + 'px' }"> <div class="layout__panel layout__panel--editor" v-show="styles.showEditor" :style="{ width: styles.editorWidth + 'px', 'font-size': styles.fontSize + 'px' }">
<editor></editor> <editor></editor>
</div> </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> <button-bar></button-bar>
</div> </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> <preview></preview>
</div> </div>
</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> <status-bar></status-bar>
</div> </div>
</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> <side-bar></side-bar>
</div> </div>
</div> </div>
@ -31,7 +31,7 @@
</template> </template>
<script> <script>
import { mapState, mapActions } from 'vuex'; import { mapState, mapGetters, mapMutations } from 'vuex';
import NavigationBar from './NavigationBar'; import NavigationBar from './NavigationBar';
import ButtonBar from './ButtonBar'; import ButtonBar from './ButtonBar';
import StatusBar from './StatusBar'; import StatusBar from './StatusBar';
@ -40,7 +40,6 @@ import SideBar from './SideBar';
import Editor from './Editor'; import Editor from './Editor';
import Preview from './Preview'; import Preview from './Preview';
import editorSvc from '../services/editorSvc'; import editorSvc from '../services/editorSvc';
import constants from '../services/constants';
export default { export default {
components: { components: {
@ -52,33 +51,23 @@ export default {
Editor, Editor,
Preview, Preview,
}, },
computed: mapState('layout', { computed: {
explorerWidth: 'explorerWidth', ...mapState('layout', [
sideBarWidth: 'sideBarWidth', 'constants',
navigationBarHeight: 'navigationBarHeight', ]),
buttonBarWidth: 'buttonBarWidth', ...mapGetters('layout', [
statusBarHeight: 'statusBarHeight', 'styles',
showEditor: 'showEditor', ]),
showSidePreview: 'showSidePreview', },
showNavigationBar: 'showNavigationBar',
showStatusBar: 'showStatusBar',
showSideBar: 'showSideBar',
showExplorer: 'showExplorer',
fontSize: 'fontSize',
innerWidth: 'innerWidth',
innerHeight: 'innerHeight',
previewWidth: 'previewWidth',
editorWidth: 'editorWidth',
}),
methods: { methods: {
...mapActions('layout', [ ...mapMutations('layout', [
'updateStyle', 'updateBodySize',
]), ]),
saveSelection: () => editorSvc.saveSelection(true), saveSelection: () => editorSvc.saveSelection(true),
}, },
created() { created() {
this.updateStyle(); this.updateBodySize();
window.addEventListener('resize', this.updateStyle); window.addEventListener('resize', this.updateBodySize);
window.addEventListener('keyup', this.saveSelection); window.addEventListener('keyup', this.saveSelection);
window.addEventListener('mouseup', this.saveSelection); window.addEventListener('mouseup', this.saveSelection);
window.addEventListener('contextmenu', this.saveSelection); window.addEventListener('contextmenu', this.saveSelection);
@ -106,10 +95,10 @@ export default {
/ (sectionDesc.tocDimension.height || 1); / (sectionDesc.tocDimension.height || 1);
const editorScrollTop = sectionDesc.editorDimension.startOffset const editorScrollTop = sectionDesc.editorDimension.startOffset
+ (sectionDesc.editorDimension.height * posInSection); + (sectionDesc.editorDimension.height * posInSection);
editorElt.parentNode.scrollTop = editorScrollTop - constants.scrollOffset; editorElt.parentNode.scrollTop = editorScrollTop;
const previewScrollTop = sectionDesc.previewDimension.startOffset const previewScrollTop = sectionDesc.previewDimension.startOffset
+ (sectionDesc.previewDimension.height * posInSection); + (sectionDesc.previewDimension.height * posInSection);
previewElt.parentNode.scrollTop = previewScrollTop - constants.scrollOffset; previewElt.parentNode.scrollTop = previewScrollTop;
return true; return true;
}); });
} }
@ -162,7 +151,7 @@ export default {
} }
.layout__panel--explorer { .layout__panel--explorer {
background-color: #ddd; background-color: #dadada;
} }
.layout__panel--button-bar, .layout__panel--button-bar,

View File

@ -1,13 +1,13 @@
<template> <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"> <div class="navigation-bar__inner navigation-bar__inner--left navigation-bar__inner--button">
<button class="navigation-bar__button button" @click="toggleExplorer()"> <button class="navigation-bar__button button" @click="toggleExplorer()">
<icon-menu></icon-menu> <icon-folder-multiple></icon-folder-multiple>
</button> </button>
</div> </div>
<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 button" @click="toggleExplorer()"> <button class="navigation-bar__button button" @click="toggleExplorer()">
<icon-settings></icon-settings> <icon-menu></icon-menu>
</button> </button>
</div> </div>
<div class="navigation-bar__inner navigation-bar__inner--right flex flex--row"> <div class="navigation-bar__inner navigation-bar__inner--right flex flex--row">
@ -15,7 +15,7 @@
<div class="spinner"></div> <div class="spinner"></div>
</div> </div>
<div class="navigation-bar__title navigation-bar__title--fake text-input"></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"> <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>
<div class="navigation-bar__inner navigation-bar__inner--edit-buttons"> <div class="navigation-bar__inner navigation-bar__inner--edit-buttons">
@ -60,7 +60,7 @@
</template> </template>
<script> <script>
import { mapState, mapActions } from 'vuex'; import { mapGetters, mapMutations } from 'vuex';
import editorSvc from '../services/editorSvc'; import editorSvc from '../services/editorSvc';
import animationSvc from '../services/animationSvc'; import animationSvc from '../services/animationSvc';
@ -72,19 +72,18 @@ export default {
titleHover: false, titleHover: false,
}), }),
computed: { computed: {
...mapState('layout', { ...mapGetters('layout', [
showEditor: 'showEditor', 'styles',
titleMaxWidth: 'titleMaxWidth', ]),
}),
titleWidth() { titleWidth() {
if (!this.mounted) { if (!this.mounted) {
return 0; return 0;
} }
this.titleFakeElt.textContent = this.title; this.titleFakeElt.textContent = this.title;
const width = this.titleFakeElt.getBoundingClientRect().width + 1; // 1px for the caret const width = this.titleFakeElt.getBoundingClientRect().width + 2; // 2px for the caret
return width < this.titleMaxWidth return width < this.styles.titleMaxWidth
? width ? width
: this.titleMaxWidth; : this.styles.titleMaxWidth;
}, },
titleScrolling() { titleScrolling() {
const result = this.titleHover && !this.titleFocus; const result = this.titleHover && !this.titleFocus;
@ -106,7 +105,7 @@ export default {
}, },
}, },
methods: { methods: {
...mapActions('layout', [ ...mapMutations('layout', [
'toggleExplorer', 'toggleExplorer',
'toggleSideBar', 'toggleSideBar',
]), ]),
@ -120,7 +119,7 @@ export default {
} else { } else {
const title = this.title.trim(); const title = this.title.trim();
if (title) { if (title) {
this.$store.dispatch('files/patchCurrent', { name: title }); this.$store.dispatch('files/patchCurrent', { name: title.slice(0, 250) });
} else { } else {
this.title = this.$store.getters['files/current'].name; this.title = this.$store.getters['files/current'].name;
} }
@ -161,6 +160,10 @@ export default {
.navigation-bar__inner--left { .navigation-bar__inner--left {
float: left; float: left;
&.navigation-bar__inner--button {
margin-right: 15px;
}
} }
.navigation-bar__inner--right { .navigation-bar__inner--right {
@ -176,9 +179,16 @@ export default {
} }
.navigation-bar__button { .navigation-bar__button {
display: inline-block;
width: 34px; width: 34px;
padding: 6px; padding: 6px;
/* prevent from seeing wrapped buttons */
margin-bottom: 20px;
.navigation-bar__inner--button & {
padding: 7px;
width: 38px;
}
} }
.navigation-bar__title, .navigation-bar__title,
@ -186,7 +196,7 @@ export default {
display: inline-block; display: inline-block;
color: $navbar-color; color: $navbar-color;
background-color: transparent; background-color: transparent;
font-weight: 400; font-size: 22px;
} }
.navigation-bar__title--input, .navigation-bar__title--input,
@ -235,7 +245,7 @@ export default {
} }
.navigation-bar__spinner { .navigation-bar__spinner {
margin: 10px 5px 0; margin: 10px 5px 0 15px;
color: rgba(255, 255, 255, 0.33); color: rgba(255, 255, 255, 0.33);
} }

View File

@ -1,19 +1,19 @@
<template> <template>
<div class="preview"> <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>
</div> </div>
</template> </template>
<script> <script>
import { mapState } from 'vuex'; import { mapGetters } from 'vuex';
const appUri = `${window.location.protocol}//${window.location.host}`; const appUri = `${window.location.protocol}//${window.location.host}`;
export default { export default {
computed: mapState('layout', [ computed: mapGetters('layout', [
'previewPadding', 'styles',
]), ]),
mounted() { mounted() {
this.$el.addEventListener('click', (evt) => { this.$el.addEventListener('click', (evt) => {
@ -45,4 +45,17 @@ export default {
margin: 0; margin: 0;
padding: 0 1035px 360px; padding: 0 1035px 360px;
} }
.preview__inner > :first-child {
& > h1,
& > h2,
& > h3,
& > h4,
& > h5,
& > h6 {
&:first-child {
margin-top: 0;
}
}
}
</style> </style>

View File

@ -1,8 +1,8 @@
<template> <template>
<div class="stat-panel panel no-overflow"> <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"> <span class="stat-panel__block-name">
Text Markdown
<small v-show="textSelection">(selection)</small> <small v-show="textSelection">(selection)</small>
</span> </span>
<span v-for="stat in textStats" :key="stat.id"> <span v-for="stat in textStats" :key="stat.id">
@ -23,7 +23,7 @@
</template> </template>
<script> <script>
import { mapState } from 'vuex'; import { mapGetters } from 'vuex';
import editorSvc from '../services/editorSvc'; import editorSvc from '../services/editorSvc';
import editorEngineSvc from '../services/editorEngineSvc'; import editorEngineSvc from '../services/editorEngineSvc';
import utils from '../services/utils'; import utils from '../services/utils';
@ -54,9 +54,9 @@ export default {
new Stat('paragraphs', '\\S.*'), new Stat('paragraphs', '\\S.*'),
], ],
}), }),
computed: mapState('layout', { computed: mapGetters('layout', [
showEditor: 'showEditor', 'styles',
}), ]),
created() { created() {
editorSvc.$on('sectionList', () => this.computeText()); editorSvc.$on('sectionList', () => this.computeText());
editorSvc.$on('selectionRange', () => this.computeText()); editorSvc.$on('selectionRange', () => this.computeText());

View File

@ -38,10 +38,11 @@ textarea {
.text-input { .text-input {
display: block; display: block;
font-variant-ligatures: no-common-ligatures;
width: 100%; width: 100%;
height: 36px; height: 36px;
padding: 3px 12px; padding: 3px 12px;
font-size: 22px; font-size: inherit;
line-height: 1.5; line-height: 1.5;
color: inherit; color: inherit;
background-color: #fff; background-color: #fff;
@ -57,7 +58,7 @@ textarea {
height: 36px; height: 36px;
padding: 3px 12px; padding: 3px 12px;
margin-bottom: 0; margin-bottom: 0;
font-size: 22px; font-size: inherit;
font-weight: 400; font-weight: 400;
line-height: 1.4; line-height: 1.4;
overflow: hidden; overflow: hidden;
@ -106,13 +107,3 @@ textarea {
flex-direction: column; flex-direction: column;
} }
.side-title {
height: 44px;
line-height: 44px;
padding: 0 10px;
background-color: #ccc;
}
.side-title__text {
text-transform: uppercase;
}

View File

@ -27,15 +27,15 @@
} }
@font-face { @font-face {
font-family: 'Inconsolata'; font-family: 'Roboto Mono';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: url('../assets/fonts/inconsolata.woff') format('woff'); src: url('../assets/fonts/RobotoMono-Regular.woff') format('woff');
} }
@font-face { @font-face {
font-family: 'Inconsolata'; font-family: 'Roboto Mono';
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
src: url('../assets/fonts/inconsolata-bold.woff') format('woff'); src: url('../assets/fonts/RobotoMono-Bold.woff') format('woff');
} }

View File

@ -1,8 +1,8 @@
$font-family-main: Lato, 'Helvetica Neue', Helvetica, sans-serif; $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-base: 1.67;
$line-height-title: 1.33; $line-height-title: 1.33;
$font-size-monospace: 0.95em; $font-size-monospace: 0.85em;
$code-bg: rgba(0, 0, 0, 0.05); $code-bg: rgba(0, 0, 0, 0.05);
$code-border-radius: 2px; $code-border-radius: 2px;
$link-color: #4a80cf; $link-color: #4a80cf;
@ -10,7 +10,7 @@ $border-radius-base: 2px;
$hr-color: rgba(128, 128, 128, 0.2); $hr-color: rgba(128, 128, 128, 0.2);
$navbar-color: rgba(255, 255, 255, 0.67); $navbar-color: rgba(255, 255, 255, 0.67);
$navbar-hover-color: #fff; $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: rgba(0, 0, 0, 0.8);
$editor-color-light: rgba(0, 0, 0, 0.28); $editor-color-light: rgba(0, 0, 0, 0.28);

View File

@ -0,0 +1,4 @@
> Written with [StackEdit](https://stackedit.io/).

View File

@ -1,4 +1,5 @@
export default () => ({ export default () => ({
id: null,
type: 'content', type: 'content',
state: {}, state: {},
text: '\n', text: '\n',

View File

@ -1,6 +1,7 @@
export default () => ({ export default () => ({
id: null,
type: 'file', type: 'file',
name: '', name: '',
folderId: null, parentId: null,
contentId: null, contentId: null,
}); });

6
src/data/emptyFolder.js Normal file
View File

@ -0,0 +1,6 @@
export default () => ({
id: null,
type: 'folder',
name: '',
parentId: null,
});

5
src/icons/Close.vue Normal file
View 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
View 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
View 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>

View 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
View 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
View 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>

View File

@ -17,6 +17,12 @@ import SidePreview from './SidePreview';
import Eye from './Eye'; import Eye from './Eye';
import Menu from './Menu'; import Menu from './Menu';
import Settings from './Settings'; 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('iconFormatBold', FormatBold);
Vue.component('iconFormatItalic', FormatItalic); Vue.component('iconFormatItalic', FormatItalic);
@ -36,3 +42,9 @@ Vue.component('iconSidePreview', SidePreview);
Vue.component('iconEye', Eye); Vue.component('iconEye', Eye);
Vue.component('iconMenu', Menu); Vue.component('iconMenu', Menu);
Vue.component('iconSettings', Settings); 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);

View File

@ -1,3 +0,0 @@
export default {
scrollOffset: 0,
};

View File

@ -9,7 +9,6 @@ import markdownConversionSvc from './markdownConversionSvc';
import markdownGrammarSvc from './markdownGrammarSvc'; import markdownGrammarSvc from './markdownGrammarSvc';
import sectionUtils from './sectionUtils'; import sectionUtils from './sectionUtils';
import extensionSvc from './extensionSvc'; import extensionSvc from './extensionSvc';
import constants from './constants';
import animationSvc from './animationSvc'; import animationSvc from './animationSvc';
import editorEngineSvc from './editorEngineSvc'; import editorEngineSvc from './editorEngineSvc';
import store from '../store'; import store from '../store';
@ -63,7 +62,7 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
getObjectToScroll() { getObjectToScroll() {
let elt = this.editorElt.parentNode; let elt = this.editorElt.parentNode;
let dimensionKey = 'editorDimension'; let dimensionKey = 'editorDimension';
if (!store.state.layout.showEditor) { if (!store.getters['layout/styles'].showEditor) {
elt = this.previewElt.parentNode; elt = this.previewElt.parentNode;
dimensionKey = 'previewDimension'; 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; let scrollerElt = this.previewElt.parentNode;
const sectionDesc = anchorHash[anchor]; const sectionDesc = anchorHash[anchor];
if (sectionDesc) { if (sectionDesc) {
if (store.state.layout.showSidePreview || !store.state.layout.showEditor) { if (store.getters['layout/styles'].showPreview) {
scrollTop = sectionDesc.previewDimension.startOffset; scrollTop = sectionDesc.previewDimension.startOffset;
} else { } else {
scrollTop = sectionDesc.editorDimension.startOffset - constants.scrollOffset; scrollTop = sectionDesc.editorDimension.startOffset;
scrollerElt = this.editorElt.parentNode; scrollerElt = this.editorElt.parentNode;
} }
} else { } else {
@ -575,7 +574,7 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
let newSectionList; let newSectionList;
let newSelectionRange; let newSelectionRange;
const debouncedEditorChanged = debounce(() => { const onEditorChanged = () => {
if (this.sectionList !== newSectionList) { if (this.sectionList !== newSectionList) {
this.sectionList = newSectionList; this.sectionList = newSectionList;
this.$emit('sectionList', this.sectionList); 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.$emit('selectionRange', this.selectionRange);
} }
this.saveContentState(); this.saveContentState();
}, 10); };
const debouncedEditorChanged = debounce(onEditorChanged, 10);
editorEngineSvc.clEditor.selectionMgr.on('selectionChanged', (start, end, selectionRange) => { editorEngineSvc.clEditor.selectionMgr.on('selectionChanged', (start, end, selectionRange) => {
newSelectionRange = 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) => { editorEngineSvc.clEditor.on('contentChanged', (content, diffs, sectionList) => {
newSectionList = sectionList; newSectionList = sectionList;
debouncedEditorChanged(); if (instantPreview) {
onEditorChanged();
} else {
debouncedEditorChanged();
}
}); });
this.$emit('inited');
// scope.$watch('editorLayoutSvc.isEditorOpen', function (isOpen) { // scope.$watch('editorLayoutSvc.isEditorOpen', function (isOpen) {
// clEditorSvc.cledit.toggleEditable(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, 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)); () => editorSvc.measureSectionDimensions(true));
store.watch(state => state.layout.showSidePreview, store.watch(() => store.getters['layout/styles'].showSidePreview,
showSidePreview => showSidePreview && editorSvc.measureSectionDimensions()); showSidePreview => showSidePreview && editorSvc.measureSectionDimensions());
this.$emit('inited');
}, },
}); });

View File

@ -118,6 +118,7 @@ export default {
[ [
store.state.contents, store.state.contents,
store.state.files, store.state.files,
store.state.folders,
].forEach(moduleState => Object.assign(storeItemMap, moduleState.itemMap)); ].forEach(moduleState => Object.assign(storeItemMap, moduleState.itemMap));
this.connection.createTx((tx) => { this.connection.createTx((tx) => {
this.readAll(storeItemMap, tx, () => { this.readAll(storeItemMap, tx, () => {

View File

@ -1,5 +1,4 @@
import store from '../../store'; import store from '../../store';
import constants from '../constants';
import animationSvc from '../animationSvc'; import animationSvc from '../animationSvc';
import editorSvc from '../editorSvc'; import editorSvc from '../editorSvc';
@ -34,9 +33,7 @@ function throttle(func, wait) {
} }
const doScrollSync = () => { const doScrollSync = () => {
const localSkipAnimation = skipAnimation const localSkipAnimation = skipAnimation || !store.getters['layout/styles'].showSidePreview;
|| !store.state.layout.showSidePreview
|| !store.state.layout.showEditor;
skipAnimation = false; skipAnimation = false;
if (!store.state.editor.scrollSync || !sectionDescList || sectionDescList.length === 0) { if (!store.state.editor.scrollSync || !sectionDescList || sectionDescList.length === 0) {
return; return;
@ -45,12 +42,11 @@ const doScrollSync = () => {
if (editorScrollTop < 0) { if (editorScrollTop < 0) {
editorScrollTop = 0; editorScrollTop = 0;
} }
let previewScrollTop = previewScrollerElt.scrollTop; const previewScrollTop = previewScrollerElt.scrollTop;
let scrollTo; let scrollTo;
if (isScrollEditor) { if (isScrollEditor) {
// Scroll the preview // Scroll the preview
isScrollEditor = false; isScrollEditor = false;
editorScrollTop += constants.scrollOffset;
sectionDescList.some((sectionDesc) => { sectionDescList.some((sectionDesc) => {
if (editorScrollTop > sectionDesc.editorDimension.endOffset) { if (editorScrollTop > sectionDesc.editorDimension.endOffset) {
return false; return false;
@ -58,7 +54,7 @@ const doScrollSync = () => {
const posInSection = (editorScrollTop - sectionDesc.editorDimension.startOffset) const posInSection = (editorScrollTop - sectionDesc.editorDimension.startOffset)
/ (sectionDesc.editorDimension.height || 1); / (sectionDesc.editorDimension.height || 1);
scrollTo = (sectionDesc.previewDimension.startOffset scrollTo = (sectionDesc.previewDimension.startOffset
+ (sectionDesc.previewDimension.height * posInSection)) - constants.scrollOffset; + (sectionDesc.previewDimension.height * posInSection));
return true; return true;
}); });
scrollTo = Math.min( scrollTo = Math.min(
@ -79,10 +75,9 @@ const doScrollSync = () => {
isPreviewMoving = true; isPreviewMoving = true;
}); });
}, localSkipAnimation ? 500 : 50); }, localSkipAnimation ? 500 : 50);
} else if (!store.state.layout.showEditor || isScrollPreview) { } else if (!store.getters['layout/styles'].showEditor || isScrollPreview) {
// Scroll the editor // Scroll the editor
isScrollPreview = false; isScrollPreview = false;
previewScrollTop += constants.scrollOffset;
sectionDescList.some((sectionDesc) => { sectionDescList.some((sectionDesc) => {
if (previewScrollTop > sectionDesc.previewDimension.endOffset) { if (previewScrollTop > sectionDesc.previewDimension.endOffset) {
return false; return false;
@ -90,7 +85,7 @@ const doScrollSync = () => {
const posInSection = (previewScrollTop - sectionDesc.previewDimension.startOffset) const posInSection = (previewScrollTop - sectionDesc.previewDimension.startOffset)
/ (sectionDesc.previewDimension.height || 1); / (sectionDesc.previewDimension.height || 1);
scrollTo = (sectionDesc.editorDimension.startOffset scrollTo = (sectionDesc.editorDimension.startOffset
+ (sectionDesc.editorDimension.height * posInSection)) - constants.scrollOffset; + (sectionDesc.editorDimension.height * posInSection));
return true; return true;
}); });
scrollTo = Math.min( scrollTo = Math.min(
@ -119,7 +114,7 @@ let timeoutId;
const forceScrollSync = () => { const forceScrollSync = () => {
if (!isPreviewRefreshing) { if (!isPreviewRefreshing) {
doScrollSync(!store.state.layout.showSidePreview); doScrollSync();
} }
}; };
store.watch(state => state.editor.scrollSync, forceScrollSync); store.watch(state => state.editor.scrollSync, forceScrollSync);
@ -135,7 +130,7 @@ editorSvc.$on('inited', () => {
} }
isScrollEditor = true; isScrollEditor = true;
isScrollPreview = false; isScrollPreview = false;
doScrollSync(!store.state.layout.showSidePreview); doScrollSync();
}); });
previewScrollerElt.addEventListener('scroll', () => { previewScrollerElt.addEventListener('scroll', () => {
@ -144,7 +139,7 @@ editorSvc.$on('inited', () => {
} }
isScrollPreview = true; isScrollPreview = true;
isScrollEditor = false; isScrollEditor = false;
doScrollSync(!store.state.layout.showSidePreview); doScrollSync();
}); });
}); });
@ -163,14 +158,15 @@ editorSvc.$on('previewText', () => {
// Remove height property once the preview as been refreshed // Remove height property once the preview as been refreshed
previewElt.style.removeProperty('height'); previewElt.style.removeProperty('height');
// Assume the user is writing in the editor // 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 // A preview scrolling event can occur if height is smaller
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
isPreviewRefreshing = false; isPreviewRefreshing = false;
}, 100); }, 100);
}); });
store.watch(state => state.layout.showSidePreview, store.watch(
() => store.getters['layout/styles'].showSidePreview,
(showSidePreview) => { (showSidePreview) => {
if (showSidePreview) { if (showSidePreview) {
isScrollEditor = true; 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) => { editorSvc.$on('sectionDescMeasuredList', (sectionDescMeasuredList) => {
sectionDescList = sectionDescMeasuredList; sectionDescList = sectionDescMeasuredList;
forceScrollSync(); forceScrollSync();

View File

@ -10,39 +10,35 @@ const ifNoId = cb => (obj) => {
return cb(); return cb();
}; };
// Watch file changing // Load the DB on boot
store.watch( localDbSvc.sync()
() => store.getters['files/current'].id, // Watch file changing
() => Promise.resolve(store.getters['files/current']) .then(() => store.watch(
// If current file has no ID, get the most recent file () => store.getters['files/current'].id,
.then(ifNoId(() => store.getters['files/mostRecent'])) () => Promise.resolve(store.getters['files/current'])
// If no ID, load the DB (we're booting) // If current file has no ID, get the most recent file
.then(ifNoId(() => localDbSvc.sync() .then(ifNoId(() => store.getters['files/mostRecent']))
// Retry // If still no ID, create a new file
.then(() => store.getters['files/current']) .then(ifNoId(() => {
.then(ifNoId(() => store.getters['files/mostRecent'])), const contentId = utils.uid();
)) store.commit('contents/setItem', {
// Finally create a new file id: contentId,
.then(ifNoId(() => { text: welcomeFile,
const contentId = utils.uid(); });
store.commit('contents/setItem', { const fileId = utils.uid();
id: contentId, store.commit('files/setItem', {
text: welcomeFile, id: fileId,
}); name: 'Welcome file',
const fileId = utils.uid(); contentId,
store.commit('files/setItem', { });
id: fileId, return store.state.files.itemMap[fileId];
name: 'Welcome file', }))
contentId, .then((currentFile) => {
}); store.commit('files/setCurrentId', currentFile.id);
return store.state.files.itemMap[fileId]; store.dispatch('files/patchCurrent', {}); // Update `updated` field to make it the mostRecent
})) }),
.then((currentFile) => { {
store.commit('files/setCurrentId', currentFile.id); immediate: true,
store.dispatch('files/patchCurrent', {}); // Update `updated` field to make it the mostRecent }));
}),
{
immediate: true,
});
utils.setInterval(() => localDbSvc.sync(), 1200); utils.setInterval(() => localDbSvc.sync(), 1200);

View File

@ -3,8 +3,10 @@ import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import contents from './modules/contents'; import contents from './modules/contents';
import files from './modules/files'; import files from './modules/files';
import folders from './modules/folders';
import layout from './modules/layout'; import layout from './modules/layout';
import editor from './modules/editor'; import editor from './modules/editor';
import explorer from './modules/explorer';
Vue.use(Vuex); Vue.use(Vuex);
@ -14,8 +16,10 @@ const store = new Vuex.Store({
modules: { modules: {
contents, contents,
files, files,
folders,
layout, layout,
editor, editor,
explorer,
}, },
strict: debug, strict: debug,
plugins: debug ? [createLogger()] : [], plugins: debug ? [createLogger()] : [],

View 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);
}
},
},
};

View File

@ -0,0 +1,6 @@
import moduleTemplate from './moduleTemplate';
import empty from '../../data/emptyFolder';
const module = moduleTemplate(empty);
export default module;

View File

@ -9,148 +9,135 @@ const setter = propertyName => (state, value) => {
state[propertyName] = value; state[propertyName] = value;
}; };
const toggler = (propertyName, setterName) => ({ state, commit, dispatch }, show) => { const toggler = propertyName => (state, value) => {
commit(setterName, show === undefined ? !state[propertyName] : show); state[propertyName] = value === undefined ? !state[propertyName] : value;
dispatch('updateStyle');
}; };
export default { export default {
namespaced: true, namespaced: true,
state: { state: {
// Constants constants: {
explorerWidth: 280, explorerWidth: 250,
sideBarWidth: 280, sideBarWidth: 280,
navigationBarHeight: 44, navigationBarHeight: 44,
buttonBarWidth: 30, buttonBarWidth: 30,
statusBarHeight: 20, statusBarHeight: 20,
},
// Configuration // Configuration
showNavigationBar: true, showNavigationBar: true,
showEditor: true, showEditor: true,
showSidePreview: true, showSidePreview: true,
showStatusBar: true, showStatusBar: true,
showSideBar: false, showSideBar: false,
showExplorer: false, showExplorer: true,
editorWidthFactor: 1, editorWidthFactor: 1,
fontSizeFactor: 1, fontSizeFactor: 1,
// Style // Styles
fontSize: 0, bodyWidth: 0,
innerWidth: 0, bodyHeight: 0,
innerHeight: 0,
editorWidth: 0,
editorPadding: 0,
previewWidth: 0,
previewPadding: 0,
titleMaxWidth: 0,
}, },
mutations: { mutations: {
setShowNavigationBar: setter('showNavigationBar'), toggleNavigationBar: toggler('showNavigationBar'),
setShowEditor: setter('showEditor'), toggleEditor: toggler('showEditor'),
setShowSidePreview: setter('showSidePreview'), toggleSidePreview: toggler('showSidePreview'),
setShowStatusBar: setter('showStatusBar'), toggleStatusBar: toggler('showStatusBar'),
setShowSideBar: setter('showSideBar'), toggleSideBar: toggler('showSideBar'),
setShowExplorer: setter('showExplorer'), toggleExplorer: toggler('showExplorer'),
setEditorWidthFactor: setter('editorWidthFactor'), setEditorWidthFactor: setter('editorWidthFactor'),
setFontSizeFactor: setter('fontSizeFactor'), setFontSizeFactor: setter('fontSizeFactor'),
setFontSize: setter('fontSize'), updateBodySize: (state) => {
setInnerWidth: setter('innerWidth'), state.bodyWidth = document.body.clientWidth;
setInnerHeight: setter('innerHeight'), state.bodyHeight = document.body.clientHeight;
setEditorWidth: setter('editorWidth'), },
setEditorPadding: setter('editorPadding'),
setPreviewWidth: setter('previewWidth'),
setPreviewPadding: setter('previewPadding'),
setTitleMaxWidth: setter('titleMaxWidth'),
}, },
actions: { getters: {
toggleNavigationBar: toggler('showNavigationBar', 'setShowNavigationBar'), styles: (state) => {
toggleEditor: toggler('showEditor', 'setShowEditor'), const styles = {
toggleSidePreview: toggler('showSidePreview', 'setShowSidePreview'), showNavigationBar: !state.showEditor || state.showNavigationBar,
toggleStatusBar: toggler('showStatusBar', 'setShowStatusBar'), showStatusBar: state.showStatusBar,
toggleSideBar: toggler('showSideBar', 'setShowSideBar'), showEditor: state.showEditor,
toggleExplorer: toggler('showExplorer', 'setShowExplorer'), showSidePreview: state.showSidePreview && state.showEditor,
updateStyle({ state, commit, dispatch }) { showPreview: state.showSidePreview || !state.showEditor,
const bodyWidth = document.body.clientWidth; showSideBar: state.showSideBar,
const bodyHeight = document.body.clientHeight; showExplorer: state.showExplorer,
};
const showNavigationBar = !state.showEditor || state.showNavigationBar; function computeStyles() {
let innerHeight = bodyHeight; styles.innerHeight = state.bodyHeight;
if (showNavigationBar) { if (styles.showNavigationBar) {
innerHeight -= state.navigationBarHeight; styles.innerHeight -= state.constants.navigationBarHeight;
} }
if (state.showStatusBar) { if (styles.showStatusBar) {
innerHeight -= state.statusBarHeight; styles.innerHeight -= state.constants.statusBarHeight;
} }
let innerWidth = bodyWidth; styles.innerWidth = state.bodyWidth;
if (state.showSideBar) { if (styles.showSideBar) {
innerWidth -= state.sideBarWidth; styles.innerWidth -= state.constants.sideBarWidth;
} }
if (state.showExplorer) { if (styles.showExplorer) {
innerWidth -= state.explorerWidth; styles.innerWidth -= state.constants.explorerWidth;
} }
let doublePanelWidth = innerWidth - state.buttonBarWidth;
if (doublePanelWidth < editorMinWidth) { let doublePanelWidth = styles.innerWidth - state.constants.buttonBarWidth;
if (state.showSideBar) { if (doublePanelWidth < editorMinWidth) {
dispatch('toggleSideBar', false); 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; return;
} }
if (state.showExplorer) {
dispatch('toggleExplorer', false); styles.fontSize = 18;
return; styles.textWidth = 990;
if (doublePanelWidth < 1120) {
styles.fontSize -= 1;
styles.textWidth = 910;
} }
doublePanelWidth = editorMinWidth; if (doublePanelWidth < 1040) {
} styles.textWidth = 830;
const splitPanel = state.showEditor && state.showSidePreview; }
if (splitPanel && doublePanelWidth / 2 < editorMinWidth) { styles.textWidth *= state.editorWidthFactor;
dispatch('toggleSidePreview', false); if (doublePanelWidth < styles.textWidth) {
return; 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; computeStyles();
let textWidth = 990; return styles;
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);
}, },
}, },
}; };