Skip to content

Commit

Permalink
improvement: ability to navigate directory horizontally from keyboard
Browse files Browse the repository at this point in the history
  • Loading branch information
aleksey-hoffman committed May 27, 2021
1 parent c4d4df6 commit cdde373
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 11 deletions.
1 change: 1 addition & 0 deletions src/components/DirItemRow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Copyright © 2021 - present Aleksey Hoffman. All rights reserved.
@addToThumbLoadSchedule="$emit('addToThumbLoadSchedule', $event)"
@removeFromThumbLoadSchedule="$emit('removeFromThumbLoadSchedule', $event)"
:ref="'dirItem' + item.positionIndex"
:row-type="row.type"
></dir-item>
</template>

Expand Down
1 change: 1 addition & 0 deletions src/components/VirtualWorkspaceAreaContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ Copyright © 2021 - present Aleksey Hoffman. All rights reserved.
class="
root
custom-scrollbar
main-content-container
drag-drop-container
unselectable
fade-mask--bottom
Expand Down
34 changes: 26 additions & 8 deletions src/components/WorkspaceAreaContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ export default {
},
formattedDirItemsRows () {
let results = []
let data = {
gridColumnAmount: null,
directoryRowsFormatted: [],
fileRowsFormatted: [],
}
const dividerMarginBottom = 8
const dirItemMarginBottom = 24
const navMenuWidth = 64
Expand All @@ -111,9 +116,12 @@ export default {
// to avoid item card clipping when it's hovered (scaled).
containerWidth = this.$utils.getNodeContentWidth(container.firstChild)
}
const chunkItemsAmount = Math.floor(containerWidth / itemMinWidth)
const gapAmount = chunkItemsAmount - 1
const gap = gapSize / chunkItemsAmount
// TODO: Refactor: move all properties to data object
data.gridColumnAmount = this.getGridColumnAmount({containerWidth, itemMinWidth})
const gapAmount = data.gridColumnAmount - 1
const gap = gapSize / data.gridColumnAmount
const chunkItemsAmountWithIncludedGap = Math.floor(containerWidth / (itemMinWidth + (gap * gapAmount)))
const imageFilesDirItems = this.imageFilesDirItems
const videoFilesDirItems = this.videoFilesDirItems
Expand Down Expand Up @@ -145,15 +153,15 @@ export default {
height: 8,
marginBottom: 0
}]
const directoryDirItemsAsRowsFormatted = directoryDirItemsAsRows.map(row => {
data.directoryRowsFormatted = directoryDirItemsAsRows.map(row => {
return {
type: 'directory-row',
height: 64 + dirItemMarginBottom,
marginBottom: dirItemMarginBottom,
items: row
}
})
const fileDirItemsAsRowsFormatted = fileDirItemsAsRows.map(row => {
data.fileRowsFormatted = fileDirItemsAsRows.map(row => {
return {
type: 'file-row',
height: 158 + dirItemMarginBottom,
Expand Down Expand Up @@ -193,7 +201,7 @@ export default {
results = [
...topSpacer,
...directoryDivider,
...directoryDirItemsAsRowsFormatted,
...data.directoryRowsFormatted,
...fileDivider,
...fileDirItemsAsRowsFormattedGrouped,
...bottomSpacer
Expand All @@ -203,9 +211,9 @@ export default {
results = [
...topSpacer,
...directoryDivider,
...directoryDirItemsAsRowsFormatted,
...data.directoryRowsFormatted,
...fileDivider,
...fileDirItemsAsRowsFormatted,
...data.fileRowsFormatted,
...bottomSpacer
]
}
Expand All @@ -222,6 +230,7 @@ export default {
item.dirItemPositionIndex = index
return item
})
this.setNavigatorViewInfo(data)
return results
},
formattedDirItems () {
Expand Down Expand Up @@ -330,6 +339,15 @@ export default {
}
},
methods: {
setNavigatorViewInfo (data) {
this.$store.dispatch('SET', {
key: 'navigatorView.info',
value: data
})
},
getGridColumnAmount (params) {
return Math.floor(params.containerWidth / params.itemMinWidth)
},
getItemsMatchingFilter (items) {
return itemFilter({
filterQuery: this.filterQuery,
Expand Down
197 changes: 196 additions & 1 deletion src/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ export default new Vuex.Store({
]
},
navigatorView: {
info: {},
visibleDirItems: [],
dirItemsInfoIsFetched: false,
timeSinceLoadDirItems: 0,
Expand Down Expand Up @@ -1022,6 +1023,107 @@ export default new Vuex.Store({
isLastDirItemSelected: (state, getters) => {
return getters.selectedDirItems.some(item => item.path === getters.lastDirItem.path)
},
navigatorGridData: (state, getters) => {
let rowData = getRowContainingSelectedDirItem()
let itemData = getItemData(rowData)
let dirItemNodes = document.querySelectorAll('.dir-item-card')
let selectedDirItemNode
dirItemNodes.forEach(node => {
if (node.dataset.itemPath === state.navigatorView.selectedDirItems.getLast().path) {
selectedDirItemNode = node
}
})

let data = {
rowIndex: rowData.rowIndex,
rowPositionIndex: rowData.row.positionIndex,
type: rowData.row.type,
inFirstRow: rowData.rowIndex === 0,
inLastRow: rowData.rowIndex === getters.dirItemRows.length - 1,
gridUpIndex: itemData.gridUpIndex,
gridDownIndex: itemData.gridDownIndex,
row: itemData.row,
isDirItemNodeInViewport: isInViewport(selectedDirItemNode),
selectedDirItemNode
}

function isInViewport (node) {
if (!node) {return false}
const nodeRect = node.getBoundingClientRect()
return (
nodeRect.top >= 0 &&
nodeRect.left >= 0 &&
nodeRect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
nodeRect.right <= (window.innerWidth || document.documentElement.clientWidth)
)
}

function getItemData (rowData) {
let data = {
gridUpIndex: 0,
gridDownIndex: 0,
row: getters.dirItemRows[rowData.rowIndex],
previousRow: getters.dirItemRows[rowData.rowIndex - 1],
nextRow: getters.dirItemRows[rowData.rowIndex + 1],
previousPotentialItemSelectionIndex: 0,
nextPotentialItemSelectionIndex: 0,
}
if (data.previousRow) {
data.previousPotentialItemSelectionIndex = data.previousRow.items[rowData.columnIndex]?.dirItemPositionIndex
if (data.previousPotentialItemSelectionIndex) {
data.gridUpIndex = data.previousPotentialItemSelectionIndex
}
else if (!data.previousPotentialItemSelectionIndex && getters.dirItemRows[rowData.rowIndex - 2]) {
data.gridUpIndex = getters.dirItemRows[rowData.rowIndex - 2].items[rowData.columnIndex].dirItemPositionIndex
}
}
if (data.nextRow) {
data.nextPotentialItemSelectionIndex = data.nextRow.items[rowData.columnIndex]?.dirItemPositionIndex
if (data.nextPotentialItemSelectionIndex) {
data.gridDownIndex = data.nextPotentialItemSelectionIndex
}
else if (!data.nextPotentialItemSelectionIndex && getters.dirItemRows[rowData.rowIndex + 2]) {
data.gridDownIndex = getters.dirItemRows[rowData.rowIndex + 2].items[rowData.columnIndex].dirItemPositionIndex
}
}
return data
}

function getRowContainingSelectedDirItem () {
let data = {
rowIndex: null,
columnIndex: null,
row: null
}
getters.dirItemRows.forEach((row, rowIndex) => {
row.items.forEach((item, columnIndex) => {
const rowContainsItem = item.dirItemPositionIndex === state.navigatorView.selectedDirItems.getLast().dirItemPositionIndex
if (rowContainsItem) {
data.rowIndex = rowIndex
data.row = row
data.columnIndex = columnIndex
}
})
})
if (data.columnIndex !== null) {
return data
}
}
return data
},
dirItemRows: (state, getters) => {
const directoryRows = state.navigatorView.info.directoryRowsFormatted
const fileRows = state.navigatorView.info.fileRowsFormatted
if (directoryRows && fileRows) {
return [
...state.navigatorView.info.directoryRowsFormatted,
...state.navigatorView.info.fileRowsFormatted
]
}
else {
return []
}
},
isOnlyCurrentDirItemSelected: (state, getters) => {
return getters.selectedDirItems.length === 1 &&
getters.selectedDirItems[0].path === state.navigatorView.currentDir.path
Expand Down Expand Up @@ -2303,6 +2405,76 @@ export default new Vuex.Store({
router.push(item.to).catch((error) => {})
},
NAVIGATE_DIR_UP (store) {
// Layout: grid
if (store.state.storageData.settings.navigatorLayout === 'grid') {
let navigatorGridData = store.getters.navigatorGridData
if (store.getters.isOnlyCurrentDirItemSelected) {
store.dispatch('SELECT_DIR_ITEM', {index: store.getters.firstDirItemIndex})
}
else if (store.state.navigatorView.selectedDirItems.length > 1) {
store.dispatch('SELECT_DIR_ITEM', {
index: store.state.navigatorView.selectedDirItems[0].dirItemPositionIndex
})
}
else if (store.state.navigatorView.selectedDirItems.length === 1) {
if (!navigatorGridData.inFirstRow) {
store.dispatch('SELECT_DIR_ITEM', {
index: navigatorGridData.gridUpIndex
})
}
}
}
// Layout: list
else {
if (store.getters.isOnlyCurrentDirItemSelected) {
store.dispatch('SELECT_DIR_ITEM', {index: store.getters.firstDirItemIndex})
}
else {
if (!store.getters.isFirstDirItemSelected) {
store.dispatch('SELECT_DIR_ITEM', {
index: store.getters.lastSelectedDirItem.dirItemPositionIndex - 1
})
}
}
}
store.dispatch('HANDLE_NAVIGATOR_ITEM_MOVE_SCROLL', {direction: 'up'})
},
NAVIGATE_DIR_DOWN (store) {
// Layout: grid
if (store.state.storageData.settings.navigatorLayout === 'grid') {
let navigatorGridData = store.getters.navigatorGridData
if (store.getters.isOnlyCurrentDirItemSelected) {
store.dispatch('SELECT_DIR_ITEM', {index: store.getters.firstDirItemIndex})
}
else if (store.state.navigatorView.selectedDirItems.length > 1) {
store.dispatch('SELECT_DIR_ITEM', {
index: store.state.navigatorView.selectedDirItems.getLast().dirItemPositionIndex
})
}
else if (store.state.navigatorView.selectedDirItems.length === 1) {
if (!navigatorGridData.inLastRow) {
store.dispatch('SELECT_DIR_ITEM', {
index: navigatorGridData.gridDownIndex
})
}
}
}
// Layout: list
else {
if (store.getters.isOnlyCurrentDirItemSelected) {
store.dispatch('SELECT_DIR_ITEM', {index: store.getters.firstDirItemIndex})
}
else {
if (!store.getters.isLastDirItemSelected) {
store.dispatch('SELECT_DIR_ITEM', {
index: store.getters.lastSelectedDirItem.dirItemPositionIndex + 1
})
}
}
}
store.dispatch('HANDLE_NAVIGATOR_ITEM_MOVE_SCROLL', {direction: 'down'})
},
NAVIGATE_DIR_LEFT (store) {
if (store.getters.isOnlyCurrentDirItemSelected) {
store.dispatch('SELECT_DIR_ITEM', {index: store.getters.firstDirItemIndex})
}
Expand All @@ -2314,7 +2486,7 @@ export default new Vuex.Store({
}
}
},
NAVIGATE_DIR_DOWN (store) {
NAVIGATE_DIR_RIGHT (store) {
if (store.getters.isOnlyCurrentDirItemSelected) {
store.dispatch('SELECT_DIR_ITEM', {index: store.getters.firstDirItemIndex})
}
Expand All @@ -2326,6 +2498,29 @@ export default new Vuex.Store({
}
}
},
HANDLE_NAVIGATOR_ITEM_MOVE_SCROLL (store, params) {
// Layout: grid
if (store.state.storageData.settings.navigatorLayout === 'grid') {
// TODO: finish
// updatedNavigatorGridData.selectedDirItemNode is undefined when scrolling up
// Is the virtual container causing the problem?
let updatedNavigatorGridData = store.getters.navigatorGridData
if (!updatedNavigatorGridData.isDirItemNodeInViewport && updatedNavigatorGridData.selectedDirItemNode) {
let scrollContentNode = utils.getContentAreaNode(router.currentRoute.name)
// scrollContentNode.scroll({
// top: params.direction === 'up'
// ? scrollContentNode.scrollTop - updatedNavigatorGridData.row.height
// : scrollContentNode.scrollTop + updatedNavigatorGridData.row.height,
// left: 0,
// behavior: 'smooth'
// })
updatedNavigatorGridData.selectedDirItemNode.scrollIntoView({
behavior: 'smooth',
block: 'nearest'
})
}
}
},
OPEN_LAST_SELECTED_DIRITEM (store) {
let item = store.state.navigatorView.selectedDirItems.getLast()
store.dispatch('OPEN_DIR_ITEM', item)
Expand Down
4 changes: 2 additions & 2 deletions src/utils/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,10 @@ export default {
getContentAreaNode (routeName) {
try {
if (routeName === 'navigator') {
return document.querySelector('.main-content-container')
return document.querySelector('#navigator-route .main-content-container')
}
else if (routeName === 'home') {
return document.querySelector('.main-content-container .os-viewport')
return document.querySelector('#home-route.main-content-container .os-viewport')
}
else {
return document.querySelector('.content-area')
Expand Down

0 comments on commit cdde373

Please sign in to comment.