Stackedit/src/components/ExplorerNode.vue

259 lines
8.1 KiB
Vue
Raw Normal View History

2017-07-31 09:04:01 +00:00
<template>
2018-03-12 00:45:54 +00:00
<div class="explorer-node" :class="{'explorer-node--selected': isSelected, 'explorer-node--open': isOpen, 'explorer-node--drag-target': isDragTargetFolder}" @dragover.prevent @dragenter.stop="node.noDrop || setDragTarget(node.item.id)" @dragleave.stop="isDragTarget && setDragTargetId()" @drop.prevent.stop="onDrop" @contextmenu="onContextMenu">
2017-12-23 18:25:14 +00:00
<div class="explorer-node__item-editor" v-if="isEditing" :class="['explorer-node__item-editor--' + node.item.type]" :style="{paddingLeft: leftPadding}" draggable="true" @dragstart.stop.prevent>
2018-03-18 15:48:29 +00:00
<input type="text" class="text-input" v-focus @blur="submitEdit()" @keydown.stop @keydown.enter="submitEdit()" @keydown.esc="submitEdit(true)" v-model="editingNodeName">
2017-07-31 09:04:01 +00:00
</div>
2018-03-09 21:18:20 +00:00
<div class="explorer-node__item" v-else :class="['explorer-node__item--' + node.item.type]" :style="{paddingLeft: leftPadding}" @click="select()" draggable="true" @dragstart.stop="setDragSourceId" @dragend.stop="setDragTargetId()">
2017-07-31 09:04:01 +00:00
{{node.item.name}}
2017-09-23 19:01:50 +00:00
<icon-provider class="explorer-node__location" v-for="location in node.locations" :key="location.id" :provider-id="location.providerId"></icon-provider>
2017-07-31 09:04:01 +00:00
</div>
<div class="explorer-node__children" v-if="node.isFolder && isOpen">
2017-07-31 09:04:01 +00:00
<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="{paddingLeft: childLeftPadding}">
2018-03-18 15:48:29 +00:00
<input type="text" class="text-input" v-focus @blur="submitNewChild()" @keydown.stop @keydown.enter="submitNewChild()" @keydown.esc="submitNewChild(true)" v-model.trim="newChildName">
2017-07-31 09:04:01 +00:00
</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 { mapMutations, mapActions } from 'vuex';
2017-07-31 09:04:01 +00:00
import utils from '../services/utils';
export default {
name: 'explorer-node', // Required for recursivity
2017-07-31 09:04:01 +00:00
props: ['node', 'depth'],
data: () => ({
editingValue: '',
}),
computed: {
leftPadding() {
return `${this.depth * 15}px`;
},
childLeftPadding() {
return `${(this.depth + 1) * 15}px`;
},
isSelected() {
2017-07-31 09:04:01 +00:00
return this.$store.getters['explorer/selectedNode'] === this.node;
},
isEditing() {
2017-07-31 09:04:01 +00:00
return this.$store.getters['explorer/editingNode'] === this.node;
},
isDragTarget() {
return this.$store.getters['explorer/dragTargetNode'] === this.node;
},
isDragTargetFolder() {
return this.$store.getters['explorer/dragTargetNodeFolder'] === this.node;
},
isOpen() {
2017-07-31 09:04:01 +00:00
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);
2017-07-31 09:04:01 +00:00
},
},
editingNodeName: {
get() {
return this.$store.getters['explorer/editingNode'].item.name;
},
set(value) {
this.editingValue = value.trim();
},
},
},
methods: {
...mapMutations('explorer', [
'setDragTargetId',
2018-03-09 21:18:20 +00:00
'setEditingId',
]),
...mapActions('explorer', [
'setDragTarget',
2018-03-09 21:18:20 +00:00
'newItem',
'deleteItem',
]),
2018-03-09 21:18:20 +00:00
select(id = this.node.item.id, doOpen = true) {
const node = this.$store.getters['explorer/nodeMap'][id];
2018-03-09 21:18:20 +00:00
if (!node) {
return false;
}
this.$store.commit('explorer/setSelectedId', id);
if (doOpen) {
// Prevent from freezing the UI while loading the file
setTimeout(() => {
if (node.isFolder) {
this.$store.commit('explorer/toggleOpenNode', id);
} else {
2017-12-10 23:49:20 +00:00
this.$store.commit('file/setCurrentId', id);
2018-03-09 21:18:20 +00:00
}
}, 10);
2017-07-31 09:04:01 +00:00
}
2018-03-09 21:18:20 +00:00
return true;
2017-07-31 09:04:01 +00:00
},
submitNewChild(cancel) {
const newChildNode = this.$store.state.explorer.newChildNode;
if (!cancel && !newChildNode.isNil && newChildNode.item.name) {
2017-07-31 09:04:01 +00:00
if (newChildNode.isFolder) {
const id = utils.uid();
2017-08-17 23:10:35 +00:00
this.$store.commit('folder/setItem', {
2017-07-31 09:04:01 +00:00
...newChildNode.item,
id,
name: utils.sanitizeName(newChildNode.item.name),
2017-07-31 09:04:01 +00:00
});
this.select(id);
2017-07-31 09:04:01 +00:00
} else {
this.$store.dispatch('createFile', newChildNode.item)
.then(file => this.select(file.id));
2017-07-31 09:04:01 +00:00
}
}
this.$store.commit('explorer/setNewItem', null);
},
submitEdit(cancel) {
2017-09-23 19:01:50 +00:00
const editingNode = this.$store.getters['explorer/editingNode'];
const id = editingNode.item.id;
2017-07-31 09:04:01 +00:00
const value = this.editingValue;
if (!cancel && id && value) {
2017-09-23 19:01:50 +00:00
this.$store.commit(editingNode.isFolder ? 'folder/patchItem' : 'file/patchItem', {
2017-07-31 09:04:01 +00:00
id,
name: utils.sanitizeName(value),
2017-07-31 09:04:01 +00:00
});
}
2018-03-09 21:18:20 +00:00
this.setEditingId(null);
2017-07-31 09:04:01 +00:00
},
setDragSourceId(evt) {
2017-10-05 07:16:35 +00:00
if (this.node.noDrag) {
evt.preventDefault();
return;
}
2017-10-05 07:16:35 +00:00
this.$store.commit('explorer/setDragSourceId', this.node.item.id);
// Fix for Firefox
// See https://stackoverflow.com/a/3977637/1333165
evt.dataTransfer.setData('Text', this.node.item.id);
},
onDrop() {
const sourceNode = this.$store.getters['explorer/dragSourceNode'];
const targetNode = this.$store.getters['explorer/dragTargetNodeFolder'];
this.setDragTargetId();
if (!sourceNode.isNil
&& !targetNode.isNil
&& sourceNode.item.id !== targetNode.item.id
) {
const patch = {
id: sourceNode.item.id,
parentId: targetNode.item.id,
};
if (sourceNode.isFolder) {
2017-08-17 23:10:35 +00:00
this.$store.commit('folder/patchItem', patch);
} else {
2017-08-17 23:10:35 +00:00
this.$store.commit('file/patchItem', patch);
}
}
},
2018-03-09 21:18:20 +00:00
onContextMenu(evt) {
if (this.select(undefined, false)) {
evt.preventDefault();
evt.stopPropagation();
this.$store.dispatch('contextMenu/open', {
coordinates: {
left: evt.clientX,
top: evt.clientY,
},
items: [{
name: 'New file',
disabled: !this.node.isFolder || this.node.isTrash,
perform: () => this.newItem(false),
}, {
name: 'New folder',
2018-03-14 00:42:26 +00:00
disabled: !this.node.isFolder || this.node.isTrash || this.node.isTemp,
2018-03-09 21:18:20 +00:00
perform: () => this.newItem(true),
}, {
type: 'separator',
}, {
name: 'Rename',
disabled: this.node.isTrash || this.node.isTemp,
2018-03-09 21:18:20 +00:00
perform: () => this.setEditingId(this.node.item.id),
}, {
name: 'Delete',
perform: () => this.deleteItem(),
}],
})
.then(item => item.perform());
}
},
2017-07-31 09:04:01 +00:00
},
};
</script>
<style lang="scss">
$item-font-size: 14px;
.explorer-node--drag-target {
background-color: rgba(0, 128, 255, 0.2);
}
2017-07-31 09:04:01 +00:00
.explorer-node__item {
2018-03-09 21:18:20 +00:00
position: relative;
2017-07-31 09:04:01 +00:00
cursor: pointer;
font-size: $item-font-size;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
2017-09-23 19:01:50 +00:00
padding-right: 5px;
2017-07-31 09:04:01 +00:00
.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;
}
2017-09-23 19:01:50 +00:00
.explorer-node__location {
float: right;
width: 18px;
height: 18px;
margin: 2px 1px;
}
2017-07-31 09:04:01 +00:00
}
.explorer-node__item--folder,
.explorer-node__item-editor--folder,
.explorer-node__new-child--folder {
&::before {
2018-03-18 15:52:33 +00:00
content: '▹';
2017-07-31 09:04:01 +00:00
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>