Merge pull request #5358 from nextcloud/backport/3758/stable28
This commit is contained in:
@@ -40,12 +40,14 @@
|
|||||||
|
|
||||||
<router-view name="sidebar" :visible="!cardDetailsInModal || !$route.params.cardId" />
|
<router-view name="sidebar" :visible="!cardDetailsInModal || !$route.params.cardId" />
|
||||||
</div>
|
</div>
|
||||||
|
<KeyboardShortcuts />
|
||||||
</NcContent>
|
</NcContent>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import AppNavigation from './components/navigation/AppNavigation.vue'
|
import AppNavigation from './components/navigation/AppNavigation.vue'
|
||||||
|
import KeyboardShortcuts from './components/KeyboardShortcuts.vue'
|
||||||
import { NcModal, NcContent, NcAppContent } from '@nextcloud/vue'
|
import { NcModal, NcContent, NcAppContent } from '@nextcloud/vue'
|
||||||
import { BoardApi } from './services/BoardApi.js'
|
import { BoardApi } from './services/BoardApi.js'
|
||||||
import { emit, subscribe } from '@nextcloud/event-bus'
|
import { emit, subscribe } from '@nextcloud/event-bus'
|
||||||
@@ -60,6 +62,7 @@ export default {
|
|||||||
NcModal,
|
NcModal,
|
||||||
NcContent,
|
NcContent,
|
||||||
NcAppContent,
|
NcAppContent,
|
||||||
|
KeyboardShortcuts,
|
||||||
},
|
},
|
||||||
provide() {
|
provide() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -22,6 +22,9 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
|
<NcModal v-if="showAddCardModal" class="card-selector" @close="clickHideAddCardModel">
|
||||||
|
<CreateNewCardCustomPicker show-created-notice @cancel="clickHideAddCardModel" />
|
||||||
|
</NcModal>
|
||||||
<div v-if="overviewName" class="board-title">
|
<div v-if="overviewName" class="board-title">
|
||||||
<div class="board-bullet icon-calendar-dark" />
|
<div class="board-bullet icon-calendar-dark" />
|
||||||
<h2 dir="auto">
|
<h2 dir="auto">
|
||||||
@@ -32,9 +35,6 @@
|
|||||||
{{ t('deck', 'Add card') }}
|
{{ t('deck', 'Add card') }}
|
||||||
</NcActionButton>
|
</NcActionButton>
|
||||||
</NcActions>
|
</NcActions>
|
||||||
<NcModal v-if="showAddCardModal" class="card-selector" @close="clickHideAddCardModel">
|
|
||||||
<CreateNewCardCustomPicker show-created-notice @cancel="clickHideAddCardModel" />
|
|
||||||
</NcModal>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="board" class="board-title">
|
<div v-else-if="board" class="board-title">
|
||||||
<div :style="{backgroundColor: '#' + board.color}" class="board-bullet" />
|
<div :style="{backgroundColor: '#' + board.color}" class="board-bullet" />
|
||||||
@@ -49,9 +49,14 @@
|
|||||||
<SessionList v-if="isNotifyPushEnabled && presentUsers.length"
|
<SessionList v-if="isNotifyPushEnabled && presentUsers.length"
|
||||||
:sessions="presentUsers" />
|
:sessions="presentUsers" />
|
||||||
<div v-if="searchQuery || true" class="deck-search">
|
<div v-if="searchQuery || true" class="deck-search">
|
||||||
<input type="search"
|
<input id="deck-search-input"
|
||||||
|
ref="search"
|
||||||
|
:tabindex="0"
|
||||||
|
type="search"
|
||||||
class="icon-search"
|
class="icon-search"
|
||||||
:value="searchQuery"
|
:value="searchQuery"
|
||||||
|
@focus="$store.dispatch('toggleShortcutLock', true)"
|
||||||
|
@blur="$store.dispatch('toggleShortcutLock', false)"
|
||||||
@input="$store.commit('setSearchQuery', $event.target.value)">
|
@input="$store.commit('setSearchQuery', $event.target.value)">
|
||||||
</div>
|
</div>
|
||||||
<div v-if="board && canManage && !showArchived && !board.archived"
|
<div v-if="board && canManage && !showArchived && !board.archived"
|
||||||
@@ -70,7 +75,9 @@
|
|||||||
type="text"
|
type="text"
|
||||||
class="no-close"
|
class="no-close"
|
||||||
:placeholder="t('deck', 'List name')"
|
:placeholder="t('deck', 'List name')"
|
||||||
required>
|
required
|
||||||
|
@focus="$store.dispatch('toggleShortcutLock', true)"
|
||||||
|
@blur="$store.dispatch('toggleShortcutLock', false)">
|
||||||
<input v-tooltip="t('deck', 'Add list')"
|
<input v-tooltip="t('deck', 'Add list')"
|
||||||
class="icon-confirm"
|
class="icon-confirm"
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -88,6 +95,7 @@
|
|||||||
@hide="filterVisible=false">
|
@hide="filterVisible=false">
|
||||||
<!-- We cannot use NcActions here are the popover trigger does not update on reactive icons -->
|
<!-- We cannot use NcActions here are the popover trigger does not update on reactive icons -->
|
||||||
<NcButton slot="trigger"
|
<NcButton slot="trigger"
|
||||||
|
ref="filterPopover"
|
||||||
:title="t('deck', 'Apply filter')"
|
:title="t('deck', 'Apply filter')"
|
||||||
class="filter-button"
|
class="filter-button"
|
||||||
:type="isFilterActive ? 'primary' : 'tertiary'">
|
:type="isFilterActive ? 'primary' : 'tertiary'">
|
||||||
@@ -233,6 +241,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState, mapGetters } from 'vuex'
|
import { mapState, mapGetters } from 'vuex'
|
||||||
|
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
|
||||||
import { NcActions, NcActionButton, NcAvatar, NcButton, NcPopover, NcModal } from '@nextcloud/vue'
|
import { NcActions, NcActionButton, NcAvatar, NcButton, NcPopover, NcModal } from '@nextcloud/vue'
|
||||||
import labelStyle from '../mixins/labelStyle.js'
|
import labelStyle from '../mixins/labelStyle.js'
|
||||||
import ArchiveIcon from 'vue-material-design-icons/Archive.vue'
|
import ArchiveIcon from 'vue-material-design-icons/Archive.vue'
|
||||||
@@ -244,6 +253,7 @@ import ArrowExpandVerticalIcon from 'vue-material-design-icons/ArrowExpandVertic
|
|||||||
import SessionList from './SessionList.vue'
|
import SessionList from './SessionList.vue'
|
||||||
import { isNotifyPushEnabled } from '../sessions.js'
|
import { isNotifyPushEnabled } from '../sessions.js'
|
||||||
import CreateNewCardCustomPicker from '../views/CreateNewCardCustomPicker.vue'
|
import CreateNewCardCustomPicker from '../views/CreateNewCardCustomPicker.vue'
|
||||||
|
import { getCurrentUser } from '@nextcloud/auth'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Controls',
|
name: 'Controls',
|
||||||
@@ -327,7 +337,18 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
beforeMount() {
|
||||||
|
subscribe('deck:board:show-new-card', this.clickShowAddCardModel)
|
||||||
|
subscribe('deck:board:toggle-filter-popover', this.triggerOpenFilters)
|
||||||
|
subscribe('deck:board:clear-filter', this.triggerClearFilter)
|
||||||
|
subscribe('deck:board:toggle-filter-by-me', this.triggerFilterByMe)
|
||||||
|
|
||||||
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
|
unsubscribe('deck:board:show-new-card', this.clickShowAddCardModel)
|
||||||
|
unsubscribe('deck:board:toggle-filter-popover', this.triggerOpenFilters)
|
||||||
|
unsubscribe('deck:board:clear-filter', this.triggerClearFilter)
|
||||||
|
unsubscribe('deck:board:toggle-filter-by-me', this.triggerFilterByMe)
|
||||||
this.setPageTitle('')
|
this.setPageTitle('')
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -406,6 +427,23 @@ export default {
|
|||||||
}
|
}
|
||||||
window.document.title = newTitle
|
window.document.title = newTitle
|
||||||
},
|
},
|
||||||
|
triggerOpenFilters() {
|
||||||
|
this.$refs.filterPopover.$el.click()
|
||||||
|
},
|
||||||
|
triggerOpenSearch() {
|
||||||
|
this.$refs.search.focus()
|
||||||
|
},
|
||||||
|
triggerClearFilter() {
|
||||||
|
this.clearFilter()
|
||||||
|
},
|
||||||
|
triggerFilterByMe() {
|
||||||
|
if (this.isFilterActive) {
|
||||||
|
this.clearFilter()
|
||||||
|
} else {
|
||||||
|
this.filter.users = [getCurrentUser().uid]
|
||||||
|
this.setFilter()
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
279
src/components/KeyboardShortcuts.vue
Normal file
279
src/components/KeyboardShortcuts.vue
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
<template>
|
||||||
|
<!-- :style="{top:cardTop, left:cardLeft}" -->
|
||||||
|
<div v-if="card && selector"
|
||||||
|
ref="shortcutModal"
|
||||||
|
v-click-outside="close"
|
||||||
|
class="keyboard-shortcuts__modal"
|
||||||
|
tabindex="0"
|
||||||
|
@keydown.esc="close">
|
||||||
|
<CardItem :card="card" />
|
||||||
|
<DueDateSelector v-if="selector === 'due-date'" :card="card" :can-edit="true" />
|
||||||
|
<TagSelector v-if="selector === 'tag'" :card="card" :can-edit="true" />
|
||||||
|
<AssignmentSelector v-if="selector === 'assignment'" :card="card" :can-edit="true" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import DueDateSelector from './card/DueDateSelector.vue'
|
||||||
|
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
import TagSelector from './card/TagSelector.vue'
|
||||||
|
import AssignmentSelector from './card/AssignmentSelector.vue'
|
||||||
|
import CardItem from './cards/CardItem.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'KeyboardShortcuts',
|
||||||
|
components: {
|
||||||
|
DueDateSelector,
|
||||||
|
TagSelector,
|
||||||
|
AssignmentSelector,
|
||||||
|
CardItem,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
card: null,
|
||||||
|
cardTop: null,
|
||||||
|
cardLeft: null,
|
||||||
|
selector: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
board: state => state.currentBoard,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
document.addEventListener('keydown', this.onKeydown)
|
||||||
|
subscribe('deck:card:show-assignment-selector', this.handleShowAssignemnt)
|
||||||
|
subscribe('deck:card:show-due-date-selector', this.handleShowDueDate)
|
||||||
|
subscribe('deck:card:show-label-selector', this.handleShowLabel)
|
||||||
|
},
|
||||||
|
destroyed() {
|
||||||
|
document.removeEventListener('keydown', this.onKeydown)
|
||||||
|
unsubscribe('deck:card:show-assignment-selector', this.handleShowAssignemnt)
|
||||||
|
unsubscribe('deck:card:show-due-date-selector', this.handleShowDueDate)
|
||||||
|
unsubscribe('deck:card:show-label-selector', this.handleShowLabel)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onKeydown(key) {
|
||||||
|
if (OCP.Accessibility.disableKeyboardShortcuts()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON'].includes(key.target.tagName) || key.target.isContentEditable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global shortcuts (not board specific)
|
||||||
|
if ((key.metaKey || key.ctrlKey) && key.code === 'KeyF') {
|
||||||
|
const searchInput = document.getElementById('deck-search-input')
|
||||||
|
if (searchInput === document.activeElement) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('deck-search-input').focus()
|
||||||
|
key.preventDefault()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (key.code === 'Minus') {
|
||||||
|
emit('deck:global:toggle-help-dialog')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.$store.state.shortcutLock || key.shiftKey || key.ctrlKey || key.altKey || key.metaKey) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.$route.name === 'card' && key.code === 'Escape') {
|
||||||
|
this.$router.push({ name: 'board' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Board specific shortcuts
|
||||||
|
if (!this.board) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (key.code) {
|
||||||
|
case 'KeyN':
|
||||||
|
emit('deck:board:show-new-card', this.board.id)
|
||||||
|
break
|
||||||
|
case 'KeyF':
|
||||||
|
emit('deck:board:toggle-filter-popover', this.board.id)
|
||||||
|
break
|
||||||
|
case 'KeyX':
|
||||||
|
emit('deck:board:clear-filter', this.board.id)
|
||||||
|
break
|
||||||
|
case 'KeyQ':
|
||||||
|
emit('deck:board:toggle-filter-by-me', this.board.id)
|
||||||
|
break
|
||||||
|
case 'ArrowDown':
|
||||||
|
this.keyboardFocusDown()
|
||||||
|
break
|
||||||
|
case 'ArrowUp':
|
||||||
|
this.keyboardFocusUp()
|
||||||
|
break
|
||||||
|
case 'ArrowLeft':
|
||||||
|
this.keyboardFocusLeft()
|
||||||
|
break
|
||||||
|
case 'ArrowRight':
|
||||||
|
this.keyboardFocusRight()
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
key.preventDefault()
|
||||||
|
},
|
||||||
|
keyboardFocusDown() {
|
||||||
|
const activeCard = document.activeElement.closest('.card')
|
||||||
|
const cards = document.querySelectorAll('.card')
|
||||||
|
const stacks = document.querySelectorAll('.stack')
|
||||||
|
const index = Array.from(cards).findIndex(card => card === activeCard)
|
||||||
|
if (index === -1) {
|
||||||
|
cards[0]?.focus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentStack = Array.from(stacks).find(stack => stack.contains(document.activeElement))
|
||||||
|
const currentStackCards = currentStack.querySelectorAll('.card')
|
||||||
|
const currentStackIndex = Array.from(currentStackCards).findIndex(card => card === document.activeElement)
|
||||||
|
|
||||||
|
if (currentStackIndex === currentStackCards.length - 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cards[index + 1]?.focus()
|
||||||
|
cards[index + 1]?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
||||||
|
},
|
||||||
|
|
||||||
|
keyboardFocusUp() {
|
||||||
|
const activeCard = document.activeElement.closest('.card')
|
||||||
|
const cards = document.querySelectorAll('.card')
|
||||||
|
const stacks = document.querySelectorAll('.stack')
|
||||||
|
const index = Array.from(cards).findIndex(card => card === activeCard)
|
||||||
|
if (index === -1) {
|
||||||
|
cards[0]?.focus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentStack = Array.from(stacks).find(stack => stack.contains(document.activeElement))
|
||||||
|
const currentStackCards = currentStack.querySelectorAll('.card')
|
||||||
|
const currentStackIndex = Array.from(currentStackCards).findIndex(card => card === document.activeElement)
|
||||||
|
|
||||||
|
if (currentStackIndex === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cards[index - 1]?.focus()
|
||||||
|
cards[index - 1]?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
||||||
|
},
|
||||||
|
keyboardFocusLeft() {
|
||||||
|
const activeCard = document.activeElement.closest('.card')
|
||||||
|
const stacks = document.querySelectorAll('.stack')
|
||||||
|
const currentStackIndex = Array.from(stacks).findIndex(stack => stack.contains(activeCard))
|
||||||
|
|
||||||
|
if (!currentStackIndex === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextStack = stacks[currentStackIndex - 1] ?? stacks[0]
|
||||||
|
|
||||||
|
const currentCardTopOffset = document.activeElement.getBoundingClientRect().top
|
||||||
|
|
||||||
|
// iterate over all next stack cards and remember the one with the smallest offset
|
||||||
|
const nextStackCards = nextStack.querySelectorAll('.card')
|
||||||
|
let nextCard = null
|
||||||
|
for (const card of nextStackCards) {
|
||||||
|
const cardTopOffset = card.getBoundingClientRect().bottom
|
||||||
|
if (cardTopOffset < currentCardTopOffset) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
nextCard = card
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nextCard) {
|
||||||
|
nextCard = nextStackCards[nextStackCards.length - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
nextCard?.focus()
|
||||||
|
nextCard?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
||||||
|
},
|
||||||
|
keyboardFocusRight() {
|
||||||
|
const activeCard = document.activeElement.closest('.card')
|
||||||
|
const stacks = document.querySelectorAll('.stack')
|
||||||
|
const currentStackIndex = Array.from(stacks).findIndex(stack => stack.contains(activeCard))
|
||||||
|
|
||||||
|
if (currentStackIndex === stacks.length - 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextStack = stacks[currentStackIndex + 1]
|
||||||
|
|
||||||
|
const currentCardTopOffset = document.activeElement.getBoundingClientRect().top
|
||||||
|
|
||||||
|
// iterate over all next stack cards and remember the one with the smallest offset
|
||||||
|
const nextStackCards = nextStack.querySelectorAll('.card')
|
||||||
|
let nextCard = null
|
||||||
|
for (const card of nextStackCards) {
|
||||||
|
const cardTopOffset = card.getBoundingClientRect().bottom
|
||||||
|
if (cardTopOffset < currentCardTopOffset) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
nextCard = card
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nextCard) {
|
||||||
|
nextCard = nextStackCards[nextStackCards.length - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
nextCard?.focus()
|
||||||
|
nextCard?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
||||||
|
},
|
||||||
|
handleShowDueDate({ card, element }) {
|
||||||
|
// this.cardTop = element.getBoundingClientRect().top + 'px'
|
||||||
|
// this.cardLeft = element.getBoundingClientRect().left + 'px'
|
||||||
|
this.card = card
|
||||||
|
this.selector = 'due-date'
|
||||||
|
this.$refs.shortcutModal?.focus()
|
||||||
|
},
|
||||||
|
handleShowAssignemnt({ card, element }) {
|
||||||
|
// this.cardTop = element.getBoundingClientRect().top + 'px'
|
||||||
|
// this.cardLeft = element.getBoundingClientRect().left + 'px'
|
||||||
|
this.card = card
|
||||||
|
this.selector = 'assignment'
|
||||||
|
this.$refs.shortcutModal?.focus()
|
||||||
|
},
|
||||||
|
handleShowLabel({ card, element }) {
|
||||||
|
// this.cardTop = element.getBoundingClientRect().top + 'px'
|
||||||
|
// this.cardLeft = element.getBoundingClientRect().left + 'px'
|
||||||
|
this.card = card
|
||||||
|
this.selector = 'tag'
|
||||||
|
this.$refs.shortcutModal?.focus()
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
this.card = null
|
||||||
|
this.selector = null
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.keyboard-shortcuts__modal {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 9999;
|
||||||
|
box-shadow: 0 0 100px 30px rgba(0, 0, 0, 0.5);
|
||||||
|
max-width: 500px;
|
||||||
|
bottom: 32px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background-color: var(--color-background-dark);
|
||||||
|
border-radius: var(--border-radius-rounded);
|
||||||
|
padding: 24px 32px;
|
||||||
|
width: 100%;
|
||||||
|
border: 2px solid var(--color-border-maxcontrast);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="board-wrapper">
|
<div class="board-wrapper" :tabindex="-1">
|
||||||
<Controls :board="board" />
|
<Controls :board="board" />
|
||||||
|
|
||||||
<transition name="fade" mode="out-in">
|
<transition name="fade" mode="out-in">
|
||||||
@@ -86,7 +86,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
import { Container, Draggable } from 'vue-smooth-dnd'
|
import { Container, Draggable } from 'vue-smooth-dnd'
|
||||||
import { mapState, mapGetters } from 'vuex'
|
import { mapState, mapGetters } from 'vuex'
|
||||||
import Controls from '../Controls.vue'
|
import Controls from '../Controls.vue'
|
||||||
|
|||||||
@@ -26,10 +26,14 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AttachmentDragAndDrop v-if="card" :card-id="card.id" class="drop-upload--card">
|
<AttachmentDragAndDrop v-if="card" :card-id="card.id" class="drop-upload--card">
|
||||||
<div :class="{'compact': compactMode, 'current-card': currentCard, 'has-labels': card.labels && card.labels.length > 0, 'card__editable': canEdit, 'card__archived': card.archived }"
|
<div :ref="`card${card.id}`"
|
||||||
|
:class="{'compact': compactMode, 'current-card': currentCard, 'has-labels': card.labels && card.labels.length > 0, 'card__editable': canEdit, 'card__archived': card.archived }"
|
||||||
tag="div"
|
tag="div"
|
||||||
|
:tabindex="0"
|
||||||
class="card"
|
class="card"
|
||||||
@click="openCard">
|
@click="openCard"
|
||||||
|
@keyup.self="handleCardKeyboardShortcut"
|
||||||
|
@mouseenter="focus(card.id)">
|
||||||
<div v-if="standalone" class="card-related">
|
<div v-if="standalone" class="card-related">
|
||||||
<div :style="{backgroundColor: '#' + board.color}" class="board-bullet" dir="auto" />
|
<div :style="{backgroundColor: '#' + board.color}" class="board-bullet" dir="auto" />
|
||||||
{{ board.title }} » {{ stack.title }}
|
{{ board.title }} » {{ stack.title }}
|
||||||
@@ -47,7 +51,8 @@
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
contenteditable="true"
|
contenteditable="true"
|
||||||
role="textbox"
|
role="textbox"
|
||||||
@blur="blurTitle"
|
@focus="onTitleFocus"
|
||||||
|
@blur="onTitleBlur"
|
||||||
@click.stop
|
@click.stop
|
||||||
@keyup.esc="cancelEdit"
|
@keyup.esc="cancelEdit"
|
||||||
@keyup.stop>{{ card.title }}</span>
|
@keyup.stop>{{ card.title }}</span>
|
||||||
@@ -92,6 +97,7 @@ import AttachmentDragAndDrop from '../AttachmentDragAndDrop.vue'
|
|||||||
import CardMenu from './CardMenu.vue'
|
import CardMenu from './CardMenu.vue'
|
||||||
import CardCover from './CardCover.vue'
|
import CardCover from './CardCover.vue'
|
||||||
import DueDate from './badges/DueDate.vue'
|
import DueDate from './badges/DueDate.vue'
|
||||||
|
import { getCurrentUser } from '@nextcloud/auth'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CardItem',
|
name: 'CardItem',
|
||||||
@@ -198,6 +204,10 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
focus(card) {
|
||||||
|
card = this.$refs[`card${card}`]
|
||||||
|
card.focus()
|
||||||
|
},
|
||||||
openCard() {
|
openCard() {
|
||||||
if (this.dragging) {
|
if (this.dragging) {
|
||||||
return
|
return
|
||||||
@@ -205,7 +215,7 @@ export default {
|
|||||||
const boardId = this.card && this.card.boardId ? this.card.boardId : this.$route.params.id
|
const boardId = this.card && this.card.boardId ? this.card.boardId : this.$route.params.id
|
||||||
this.$router.push({ name: 'card', params: { id: boardId, cardId: this.card.id } }).catch(() => {})
|
this.$router.push({ name: 'card', params: { id: boardId, cardId: this.card.id } }).catch(() => {})
|
||||||
},
|
},
|
||||||
blurTitle(e) {
|
onTitleBlur(e) {
|
||||||
// TODO Handle empty title
|
// TODO Handle empty title
|
||||||
if (e.target.innerText !== this.card.title) {
|
if (e.target.innerText !== this.card.title) {
|
||||||
this.$store.dispatch('updateCardTitle', {
|
this.$store.dispatch('updateCardTitle', {
|
||||||
@@ -213,9 +223,45 @@ export default {
|
|||||||
title: e.target.innerText,
|
title: e.target.innerText,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
this.$store.dispatch('toggleShortcutLock', false)
|
||||||
|
},
|
||||||
|
onTitleFocus() {
|
||||||
|
this.$store.dispatch('toggleShortcutLock', true)
|
||||||
},
|
},
|
||||||
cancelEdit() {
|
cancelEdit() {
|
||||||
this.$refs.titleContentEditable.textContent = this.card.title
|
this.$refs.titleContentEditable.textContent = this.card.title
|
||||||
|
this.$store.dispatch('toggleShortcutLock', false)
|
||||||
|
},
|
||||||
|
handleCardKeyboardShortcut(key) {
|
||||||
|
if (OCP.Accessibility.disableKeyboardShortcuts()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.canEdit || this.$store.state.shortcutLock || key.shiftKey || key.ctrlKey || key.altKey || key.metaKey) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (key.code) {
|
||||||
|
case 'KeyE':
|
||||||
|
this.$refs.titleContentEditable?.focus()
|
||||||
|
break
|
||||||
|
case 'KeyA':
|
||||||
|
this.$store.dispatch('archiveUnarchiveCard', { ...this.card, archived: !this.card.archived })
|
||||||
|
break
|
||||||
|
case 'KeyO':
|
||||||
|
this.$store.dispatch('changeCardDoneStatus', { ...this.card, done: !this.card.done })
|
||||||
|
break
|
||||||
|
case 'KeyM':
|
||||||
|
this.$el.querySelector('button.action-item__menutoggle')?.click()
|
||||||
|
break
|
||||||
|
case 'Enter':
|
||||||
|
case 'Space':
|
||||||
|
this.openCard().then(() => document.getElementById('app-sidebar-vue')?.focus())
|
||||||
|
break
|
||||||
|
case 'KeyS':
|
||||||
|
this.toggleSelfAsignment()
|
||||||
|
break
|
||||||
|
}
|
||||||
},
|
},
|
||||||
applyLabelFilter(label) {
|
applyLabelFilter(label) {
|
||||||
if (this.dragging) {
|
if (this.dragging) {
|
||||||
@@ -223,6 +269,18 @@ export default {
|
|||||||
}
|
}
|
||||||
this.$nextTick(() => this.$store.dispatch('toggleFilter', { tags: [label.id] }))
|
this.$nextTick(() => this.$store.dispatch('toggleFilter', { tags: [label.id] }))
|
||||||
},
|
},
|
||||||
|
toggleSelfAsignment() {
|
||||||
|
const isAssigned = this.card.assignedUsers.find(
|
||||||
|
(item) => item.type === 0 && item.participant.uid === getCurrentUser()?.uid,
|
||||||
|
)
|
||||||
|
this.$store.dispatch(isAssigned ? 'removeUserFromCard' : 'assignCardToUser', {
|
||||||
|
card: this.card,
|
||||||
|
assignee: {
|
||||||
|
userId: getCurrentUser()?.uid,
|
||||||
|
type: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -253,17 +311,17 @@ export default {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border: 2px solid var(--color-border-dark);
|
|
||||||
}
|
|
||||||
&.current-card {
|
&.current-card {
|
||||||
border: 2px solid var(--color-primary-element);
|
border: 2px solid var(--color-primary-element);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus, &:focus-visible, &:focus-within {
|
&:focus, &:focus-visible, &:focus-within {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
border: 2px solid var(--color-border-maxcontrast);
|
||||||
|
&.current-card {
|
||||||
border: 2px solid var(--color-primary-element);
|
border: 2px solid var(--color-primary-element);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.card-upper {
|
.card-upper {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
228
src/components/modals/HelpModal.vue
Normal file
228
src/components/modals/HelpModal.vue
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
<template>
|
||||||
|
<NcModal size="normal"
|
||||||
|
data-text-el="formatting-help"
|
||||||
|
:name="t('deck', 'Keyboard shortcuts')"
|
||||||
|
@close="$emit('close')">
|
||||||
|
<div class="help-modal">
|
||||||
|
<h2>{{ t('deck', 'Keyboard shortcuts') }}</h2>
|
||||||
|
<p>{{ t('deck', 'Speed up using Deck with simple shortcuts.') }}</p>
|
||||||
|
|
||||||
|
<h3>{{ t('deck', 'Board actions') }}</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ t('deck', 'Keyboard shortcut') }}</th>
|
||||||
|
<th>{{ t('deck', 'Action') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<kbd>{{ t('deck', 'Shift') }}</kbd>
|
||||||
|
+
|
||||||
|
<kbd>{{ t('deck', 'Scroll') }}</kbd>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ t('deck', 'Scroll sideways') }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<kbd>←</kbd>
|
||||||
|
<kbd>→</kbd>
|
||||||
|
<kbd>↑</kbd>
|
||||||
|
<kbd>↓</kbd>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ t('deck', 'Navigate between cards') }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<kbd>{{ t('deck', 'Esc') }}</kbd>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ t('deck', 'Close card details') }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<kbd>{{ t('deck', 'Ctrl') }}</kbd>
|
||||||
|
<kbd>F</kbd>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ t('deck', 'Search') }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<kbd>F</kbd>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ t('deck', 'Show card filters') }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<kbd>X</kbd>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ t('deck', 'Clear card filters') }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<kbd>?</kbd>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ t('deck', 'Show help dialog') }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>{{ t('deck', 'Card actions') }}</h3>
|
||||||
|
<p>{{ t('deck', 'The following actions can be triggered on the currently highlighted card') }}</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ t('deck', 'Keyboard shortcut') }}</th>
|
||||||
|
<th>{{ t('deck', 'Action') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<kbd>{{ t('deck', 'Enter') }}</kbd>
|
||||||
|
<kbd>{{ t('deck', 'Space') }}</kbd>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ t('deck', 'Open card details') }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<kbd>E</kbd>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ t('deck', 'Edit the card title') }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<kbd>S</kbd>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ t('deck', 'Assign yorself to the current card') }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<kbd>A</kbd>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ t('deck', 'Archive/unarchive the current card') }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<kbd>O</kbd>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ t('deck', 'Mark card as completed/not completed') }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<kbd>M</kbd>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ t('deck', 'Open card menu') }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</NcModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { NcModal, Tooltip } from '@nextcloud/vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HelpModal',
|
||||||
|
components: {
|
||||||
|
NcModal,
|
||||||
|
},
|
||||||
|
directives: {
|
||||||
|
Tooltip,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.help-modal {
|
||||||
|
width: max-content;
|
||||||
|
padding: 30px 40px 20px;
|
||||||
|
user-select: text;
|
||||||
|
|
||||||
|
h2, h3 {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
margin-top: 24px;
|
||||||
|
border-collapse: collapse;
|
||||||
|
|
||||||
|
tbody tr {
|
||||||
|
&:hover, &:focus, &:active {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
thead tr {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
font-weight: bold;
|
||||||
|
padding: .75rem 1rem .75rem 0;
|
||||||
|
border-bottom: 2px solid var(--color-background-darker);
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: .75rem 1rem .75rem 0;
|
||||||
|
border-top: 1px solid var(--color-background-dark);
|
||||||
|
border-bottom: unset;
|
||||||
|
|
||||||
|
&.noborder {
|
||||||
|
border-top: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ellipsis_top {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ellipsis {
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ellipsis_bottom {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kbd {
|
||||||
|
font-size: smaller;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
padding: .2em .4em;
|
||||||
|
font-size: 90%;
|
||||||
|
background-color: var(--color-background-dark);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -62,6 +62,10 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<NcAppNavigationSettings :title="t('deck', 'Deck settings')">
|
<NcAppNavigationSettings :title="t('deck', 'Deck settings')">
|
||||||
|
<NcButton @click="showHelp = true">
|
||||||
|
{{ t('deck', 'Keyboard shortcuts') }}
|
||||||
|
</NcButton>
|
||||||
|
<HelpModal v-if="showHelp" @close="showHelp=false" />
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<input id="toggle-modal"
|
<input id="toggle-modal"
|
||||||
@@ -117,7 +121,7 @@
|
|||||||
import axios from '@nextcloud/axios'
|
import axios from '@nextcloud/axios'
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
import ClickOutside from 'vue-click-outside'
|
import ClickOutside from 'vue-click-outside'
|
||||||
import { NcAppNavigation, NcAppNavigationItem, NcAppNavigationSettings, NcMultiselect } from '@nextcloud/vue'
|
import { NcAppNavigation, NcAppNavigationItem, NcAppNavigationSettings, NcMultiselect, NcButton } from '@nextcloud/vue'
|
||||||
import AppNavigationAddBoard from './AppNavigationAddBoard.vue'
|
import AppNavigationAddBoard from './AppNavigationAddBoard.vue'
|
||||||
import AppNavigationBoardCategory from './AppNavigationBoardCategory.vue'
|
import AppNavigationBoardCategory from './AppNavigationBoardCategory.vue'
|
||||||
import { loadState } from '@nextcloud/initial-state'
|
import { loadState } from '@nextcloud/initial-state'
|
||||||
@@ -127,6 +131,8 @@ import ArchiveIcon from 'vue-material-design-icons/Archive.vue'
|
|||||||
import CalendarIcon from 'vue-material-design-icons/Calendar.vue'
|
import CalendarIcon from 'vue-material-design-icons/Calendar.vue'
|
||||||
import DeckIcon from './../icons/DeckIcon.vue'
|
import DeckIcon from './../icons/DeckIcon.vue'
|
||||||
import ShareVariantIcon from 'vue-material-design-icons/Share.vue'
|
import ShareVariantIcon from 'vue-material-design-icons/Share.vue'
|
||||||
|
import HelpModal from './../modals/HelpModal.vue'
|
||||||
|
import { subscribe } from '@nextcloud/event-bus'
|
||||||
|
|
||||||
const canCreateState = loadState('deck', 'canCreate')
|
const canCreateState = loadState('deck', 'canCreate')
|
||||||
|
|
||||||
@@ -135,6 +141,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
NcAppNavigation,
|
NcAppNavigation,
|
||||||
NcAppNavigationSettings,
|
NcAppNavigationSettings,
|
||||||
|
NcButton,
|
||||||
AppNavigationAddBoard,
|
AppNavigationAddBoard,
|
||||||
AppNavigationBoardCategory,
|
AppNavigationBoardCategory,
|
||||||
NcMultiselect,
|
NcMultiselect,
|
||||||
@@ -143,6 +150,7 @@ export default {
|
|||||||
CalendarIcon,
|
CalendarIcon,
|
||||||
DeckIcon,
|
DeckIcon,
|
||||||
ShareVariantIcon,
|
ShareVariantIcon,
|
||||||
|
HelpModal,
|
||||||
},
|
},
|
||||||
directives: {
|
directives: {
|
||||||
ClickOutside,
|
ClickOutside,
|
||||||
@@ -160,6 +168,7 @@ export default {
|
|||||||
groupLimit: [],
|
groupLimit: [],
|
||||||
groupLimitDisabled: true,
|
groupLimitDisabled: true,
|
||||||
canCreate: canCreateState,
|
canCreate: canCreateState,
|
||||||
|
showHelp: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -196,6 +205,11 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
subscribe('deck:global:toggle-help-dialog', () => {
|
||||||
|
this.showHelp = !this.showHelp
|
||||||
|
})
|
||||||
|
},
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
if (this.isAdmin) {
|
if (this.isAdmin) {
|
||||||
this.groupLimit = this.$store.getters.config('groupLimit')
|
this.groupLimit = this.$store.getters.config('groupLimit')
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export default {
|
|||||||
|
|
||||||
if (users.length > 0) {
|
if (users.length > 0) {
|
||||||
users.forEach((user) => {
|
users.forEach((user) => {
|
||||||
if (card.assignedUsers.findIndex((u) => u.participant.uid === user) === -1) {
|
if (!card?.assignedUsers || card.assignedUsers.findIndex((u) => u.participant.uid === user) === -1) {
|
||||||
allUsersMatch = false
|
allUsersMatch = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export default new Vuex.Store({
|
|||||||
activity: [],
|
activity: [],
|
||||||
activityLoadMore: true,
|
activityLoadMore: true,
|
||||||
filter: { tags: [], users: [], due: '' },
|
filter: { tags: [], users: [], due: '' },
|
||||||
|
shortcutLock: false,
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
config: state => (key) => {
|
config: state => (key) => {
|
||||||
@@ -307,7 +308,9 @@ export default new Vuex.Store({
|
|||||||
Vue.delete(state.currentBoard.acl, removeIndex)
|
Vue.delete(state.currentBoard.acl, removeIndex)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
TOGGLE_SHORTCUT_LOCK(state, lock) {
|
||||||
|
state.shortcutLock = lock
|
||||||
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
async setConfig({ commit }, config) {
|
async setConfig({ commit }, config) {
|
||||||
@@ -515,5 +518,8 @@ export default new Vuex.Store({
|
|||||||
newOwner,
|
newOwner,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
toggleShortcutLock({ commit }, lock) {
|
||||||
|
commit('TOGGLE_SHORTCUT_LOCK', lock)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -371,7 +371,7 @@ h2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.multiselect {
|
.multiselect {
|
||||||
min-width: auto;
|
min-width: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-content {
|
.empty-content {
|
||||||
|
|||||||
Reference in New Issue
Block a user