+
{{node.item.name}}
@@ -73,23 +73,30 @@ export default {
methods: {
...mapMutations('explorer', [
'setDragTargetId',
+ 'setEditingId',
]),
...mapActions('explorer', [
'setDragTarget',
+ 'newItem',
+ 'deleteItem',
]),
- select(id) {
+ select(id = this.node.item.id, doOpen = true) {
const node = this.$store.getters['explorer/nodeMap'][id];
- if (node) {
- this.$store.commit('explorer/setSelectedId', id);
- if (node.isFolder) {
- this.$store.commit('explorer/toggleOpenNode', id);
- } else {
- // Prevent from freezing the UI while loading the file
- setTimeout(() => {
- this.$store.commit('file/setCurrentId', id);
- }, 10);
- }
+ 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 {
+ this.$store.commit('file/setCurrentId', id);
+ }
+ }, 10);
+ }
+ return true;
},
submitNewChild(cancel) {
const newChildNode = this.$store.state.explorer.newChildNode;
@@ -119,7 +126,7 @@ export default {
name: utils.sanitizeName(value),
});
}
- this.$store.commit('explorer/setEditingId', null);
+ this.setEditingId(null);
},
setDragSourceId(evt) {
if (this.node.noDrag) {
@@ -147,6 +154,38 @@ export default {
}
}
},
+ 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',
+ disabled: !this.node.isFolder || this.node.isTrash,
+ perform: () => this.newItem(true),
+ }, {
+ type: 'separator',
+ }, {
+ name: 'Rename',
+ disabled: this.node.isTrash,
+ perform: () => this.setEditingId(this.node.item.id),
+ }, {
+ name: 'Delete',
+ disabled: this.node.isTrash || this.node.item.parentId === 'trash',
+ perform: () => this.deleteItem(),
+ }],
+ })
+ .then(item => item.perform());
+ }
+ },
},
};
@@ -159,6 +198,7 @@ $item-font-size: 14px;
}
.explorer-node__item {
+ position: relative;
cursor: pointer;
font-size: $item-font-size;
overflow: hidden;
diff --git a/src/components/common/base.scss b/src/components/common/base.scss
index b322ab50..c442eded 100644
--- a/src/components/common/base.scss
+++ b/src/components/common/base.scss
@@ -226,6 +226,7 @@ img {
position: fixed;
display: none;
width: 250px;
+ height: 100%;
top: 0;
left: 0;
overflow-x: hidden;
diff --git a/src/icons/Magnify.vue b/src/icons/Magnify.vue
new file mode 100644
index 00000000..fe128425
--- /dev/null
+++ b/src/icons/Magnify.vue
@@ -0,0 +1,5 @@
+
+
+
diff --git a/src/icons/index.js b/src/icons/index.js
index 9e48c2e4..1badf4d5 100644
--- a/src/icons/index.js
+++ b/src/icons/index.js
@@ -48,6 +48,7 @@ import ContentSave from './ContentSave';
import Message from './Message';
import History from './History';
import Database from './Database';
+import Magnify from './Magnify';
Vue.component('iconProvider', Provider);
Vue.component('iconFormatBold', FormatBold);
@@ -98,3 +99,4 @@ Vue.component('iconContentSave', ContentSave);
Vue.component('iconMessage', Message);
Vue.component('iconHistory', History);
Vue.component('iconDatabase', Database);
+Vue.component('iconMagnify', Magnify);
diff --git a/src/store/contextMenu.js b/src/store/contextMenu.js
new file mode 100644
index 00000000..ce1fbe7a
--- /dev/null
+++ b/src/store/contextMenu.js
@@ -0,0 +1,53 @@
+const setter = propertyName => (state, value) => {
+ state[propertyName] = value;
+};
+
+export default {
+ namespaced: true,
+ state: {
+ coordinates: {
+ left: 0,
+ top: 0,
+ },
+ items: [],
+ resolve: () => {},
+ },
+ mutations: {
+ setCoordinates: setter('coordinates'),
+ setItems: setter('items'),
+ setResolve: setter('resolve'),
+ },
+ actions: {
+ open({ commit, rootState }, { coordinates, items }) {
+ commit('setItems', items);
+ // Place the context menu outside the screen
+ commit('setCoordinates', { top: 0, left: -9999 });
+ // Let the UI refresh itself
+ setTimeout(() => {
+ // Take the size of the context menu and place it
+ const elt = document.querySelector('.context-menu__inner');
+ const height = elt.offsetHeight;
+ if (coordinates.top + height > rootState.layout.bodyHeight) {
+ coordinates.top -= height;
+ }
+ if (coordinates.top < 0) {
+ coordinates.top = 0;
+ }
+ const width = elt.offsetWidth;
+ if (coordinates.left + width > rootState.layout.bodyWidth) {
+ coordinates.left -= width;
+ }
+ if (coordinates.left < 0) {
+ coordinates.left = 0;
+ }
+ commit('setCoordinates', coordinates);
+ }, 1);
+
+ return new Promise(resolve => commit('setResolve', resolve));
+ },
+ close({ commit }) {
+ commit('setItems', []);
+ commit('setResolve', () => {});
+ },
+ },
+};
diff --git a/src/store/explorer.js b/src/store/explorer.js
index 9e55dee2..32546e39 100644
--- a/src/store/explorer.js
+++ b/src/store/explorer.js
@@ -72,6 +72,7 @@ export default {
const trashFolderNode = new Node(emptyFolder(), [], true);
trashFolderNode.item.id = 'trash';
trashFolderNode.item.name = 'Trash';
+ trashFolderNode.isTrash = true;
trashFolderNode.noDrag = true;
const nodeMap = {
trash: trashFolderNode,
@@ -79,18 +80,20 @@ export default {
rootGetters['folder/items'].forEach((item) => {
nodeMap[item.id] = new Node(item, [], true);
});
+ const syncLocationsByFileId = rootGetters['syncLocation/groupedByFileId'];
+ const publishLocationsByFileId = rootGetters['publishLocation/groupedByFileId'];
rootGetters['file/items'].forEach((item) => {
const locations = [
- ...rootGetters['syncLocation/groupedByFileId'][item.id] || [],
- ...rootGetters['publishLocation/groupedByFileId'][item.id] || [],
+ ...syncLocationsByFileId[item.id] || [],
+ ...publishLocationsByFileId[item.id] || [],
];
nodeMap[item.id] = new Node(item, locations);
});
const rootNode = new Node(emptyFolder(), [], true, true);
- Object.entries(nodeMap).forEach(([id, node]) => {
+ Object.entries(nodeMap).forEach(([, node]) => {
let parentNode = nodeMap[node.item.parentId];
if (!parentNode || !parentNode.isFolder) {
- if (id === 'trash') {
+ if (node.isTrash) {
return;
}
parentNode = rootNode;
@@ -105,7 +108,7 @@ export default {
if (trashFolderNode.files.length) {
rootNode.folders.unshift(trashFolderNode);
}
- // Add a fake file at the end of the root folder to always allow drag and drop into it.
+ // Add a fake file at the end of the root folder to allow drag and drop into it.
rootNode.files.push(fakeFileNode);
return {
nodeMap,
@@ -164,5 +167,67 @@ export default {
commit('setDragTargetId', id);
dispatch('openDragTarget');
},
+ newItem({ getters, commit, dispatch }, isFolder) {
+ let parentId = getters.selectedNodeFolder.item.id;
+ if (parentId === 'trash') {
+ parentId = null;
+ }
+ dispatch('openNode', parentId);
+ commit('setNewItem', {
+ type: isFolder ? 'folder' : 'file',
+ parentId,
+ });
+ },
+ deleteItem({ rootState, getters, rootGetters, commit, dispatch }) {
+ const selectedNode = getters.selectedNode;
+ if (selectedNode.isNil) {
+ return Promise.resolve();
+ }
+ if (selectedNode.isTrash || selectedNode.item.parentId === 'trash') {
+ return dispatch('modal/trashDeletion', null, { root: true });
+ }
+ return dispatch(selectedNode.isFolder
+ ? 'modal/folderDeletion'
+ : 'modal/fileDeletion',
+ selectedNode.item,
+ { root: true },
+ )
+ .then(() => {
+ if (selectedNode === getters.selectedNode) {
+ const currentFileId = rootGetters['file/current'].id;
+ let doClose = selectedNode.item.id === currentFileId;
+ if (selectedNode.isFolder) {
+ const recursiveMoveToTrash = (folderNode) => {
+ folderNode.folders.forEach(recursiveMoveToTrash);
+ folderNode.files.forEach((fileNode) => {
+ commit('file/patchItem', {
+ id: fileNode.item.id,
+ parentId: 'trash',
+ }, { root: true });
+ doClose = doClose || fileNode.item.id === currentFileId;
+ });
+ commit('folder/deleteItem', folderNode.item.id, { root: true });
+ };
+ recursiveMoveToTrash(selectedNode);
+ } else {
+ commit('file/patchItem', {
+ id: selectedNode.item.id,
+ parentId: 'trash',
+ }, { root: true });
+ }
+ if (doClose) {
+ // Close the current file by opening the last opened, not deleted one
+ rootGetters['data/lastOpenedIds'].some((id) => {
+ const file = rootState.file.itemMap[id];
+ if (file.parentId === 'trash') {
+ return false;
+ }
+ commit('file/setCurrentId', id, { root: true });
+ return true;
+ });
+ }
+ }
+ }, () => {}); // Cancel
+ },
},
};
diff --git a/src/store/index.js b/src/store/index.js
index 1eb209a6..330079c7 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -2,21 +2,22 @@
import Vue from 'vue';
import Vuex from 'vuex';
import utils from '../services/utils';
-import contentState from './contentState';
-import syncedContent from './syncedContent';
import content from './content';
+import contentState from './contentState';
+import contextMenu from './contextMenu';
+import data from './data';
+import discussion from './discussion';
+import explorer from './explorer';
import file from './file';
import findReplace from './findReplace';
import folder from './folder';
-import publishLocation from './publishLocation';
-import syncLocation from './syncLocation';
-import data from './data';
-import discussion from './discussion';
import layout from './layout';
-import explorer from './explorer';
import modal from './modal';
import notification from './notification';
+import publishLocation from './publishLocation';
import queue from './queue';
+import syncedContent from './syncedContent';
+import syncLocation from './syncLocation';
import userInfo from './userInfo';
import workspace from './workspace';
@@ -26,21 +27,22 @@ const debug = NODE_ENV !== 'production';
const store = new Vuex.Store({
modules: {
- contentState,
- syncedContent,
content,
+ contentState,
+ contextMenu,
+ data,
discussion,
+ explorer,
file,
findReplace,
folder,
- publishLocation,
- syncLocation,
- data,
layout,
- explorer,
modal,
notification,
+ publishLocation,
queue,
+ syncedContent,
+ syncLocation,
userInfo,
workspace,
},