282 lines
5.5 KiB
Vue
282 lines
5.5 KiB
Vue
<template>
|
|
<div class="drag-container">
|
|
<ul class="drag-list">
|
|
<li
|
|
v-for="stage in stages"
|
|
:key="stage.text"
|
|
class="drag-column"
|
|
:class="{ ['drag-column-' + stage.text]: true }"
|
|
>
|
|
<span class="drag-column-header">
|
|
<slot :name="stage.text">
|
|
<h2>{{ stage.text }}</h2>
|
|
</slot>
|
|
</span>
|
|
<img v-if="stage.image" class="personImage" :src="stage.image">
|
|
<div class="drag-options" />
|
|
<ul ref="list" class="drag-inner-list" :data-status="stage.text">
|
|
<li
|
|
v-for="block in getBlocks(stage.text)"
|
|
:key="block.id"
|
|
class="drag-item"
|
|
:data-block-id="block.id"
|
|
>
|
|
<slot :name="block.id">
|
|
<strong>{{ block.title }}</strong>
|
|
</slot>
|
|
</li>
|
|
</ul>
|
|
<div class="drag-column-footer">
|
|
<slot :name="`footer-${stage.text}`" />
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import dragula from 'dragula'
|
|
import { Machine } from 'xstate'
|
|
|
|
export default {
|
|
name: 'KanbanBoard',
|
|
|
|
props: {
|
|
stages: {
|
|
type: Array,
|
|
required: true
|
|
},
|
|
blocks: {
|
|
type: Array,
|
|
required: true
|
|
},
|
|
config: {
|
|
type: Object,
|
|
default: () => ({})
|
|
},
|
|
stateMachineConfig: {
|
|
type: Object,
|
|
default: null
|
|
}
|
|
},
|
|
|
|
data() {
|
|
return {
|
|
machine: null
|
|
}
|
|
},
|
|
|
|
computed: {
|
|
localBlocks() {
|
|
return this.blocks
|
|
}
|
|
},
|
|
|
|
updated() {
|
|
this.drake.containers = this.$refs.list
|
|
},
|
|
|
|
mounted() {
|
|
this.config.accepts = this.config.accepts || this.accepts
|
|
this.drake = dragula(this.$refs.list, this.config)
|
|
.on('drag', el => {
|
|
el.classList.add('is-moving')
|
|
})
|
|
.on('drop', (block, list, source) => {
|
|
let index = 0
|
|
for (index = 0; index < list.children.length; index += 1) {
|
|
if (list.children[index].classList.contains('is-moving')) break
|
|
}
|
|
|
|
let newState = list.dataset.status
|
|
|
|
if (this.machine) {
|
|
const transition = this.findTransition(list, source)
|
|
if (!transition) return
|
|
newState = this.machine.transition(source.dataset.status, transition)
|
|
.value
|
|
}
|
|
|
|
this.$emit('update-block', block.dataset.blockId, newState, index)
|
|
})
|
|
.on('dragend', el => {
|
|
el.classList.remove('is-moving')
|
|
|
|
window.setTimeout(() => {
|
|
el.classList.add('is-moved')
|
|
window.setTimeout(() => {
|
|
el.classList.remove('is-moved')
|
|
}, 600)
|
|
}, 100)
|
|
})
|
|
},
|
|
|
|
created() {
|
|
if (!this.stateMachineConfig) return
|
|
this.machine = Machine(this.stateMachineConfig)
|
|
},
|
|
|
|
methods: {
|
|
getBlocks(status) {
|
|
return this.localBlocks.filter(block => block.status === status)
|
|
},
|
|
|
|
findPossibleTransitions(sourceState) {
|
|
return this.machine.config.states[sourceState].on || {}
|
|
},
|
|
|
|
findTransition(target, source) {
|
|
const targetState = target.dataset.status
|
|
const sourceState = source.dataset.status
|
|
const possibleTransitions = this.findPossibleTransitions(sourceState)
|
|
return Object.keys(possibleTransitions).find(
|
|
transition => possibleTransitions[transition] === targetState
|
|
)
|
|
},
|
|
|
|
accepts(block, target, source) {
|
|
if (!this.machine) return true
|
|
const targetState = target.dataset.status
|
|
const sourceState = source.dataset.status
|
|
return Object.values(this.findPossibleTransitions(sourceState)).includes(
|
|
targetState
|
|
)
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
.personImage {
|
|
height: 100px;
|
|
}
|
|
$ease-out: all 0.3s cubic-bezier(0.23, 1, 0.32, 1);
|
|
|
|
ul.drag-list,
|
|
ul.drag-inner-list {
|
|
list-style-type: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
.drag-container {
|
|
max-width: 1000px;
|
|
margin: 20px auto;
|
|
}
|
|
|
|
.drag-list {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
|
|
@media (max-width: 690px) {
|
|
display: block;
|
|
}
|
|
}
|
|
|
|
.drag-column {
|
|
flex: 1;
|
|
margin: 0 10px;
|
|
position: relative;
|
|
background: rgba(black, 0.2);
|
|
overflow: hidden;
|
|
|
|
@media (max-width: 690px) {
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
h2 {
|
|
font-size: 0.8rem;
|
|
margin: 0;
|
|
text-transform: uppercase;
|
|
font-weight: 600;
|
|
}
|
|
}
|
|
|
|
.drag-column-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 10px;
|
|
}
|
|
|
|
.drag-inner-list {
|
|
min-height: 50px;
|
|
color: white;
|
|
}
|
|
|
|
.drag-item {
|
|
padding: 10px;
|
|
margin: 10px;
|
|
//height: 100px;
|
|
background: rgba(black, 0.4);
|
|
transition: $ease-out;
|
|
|
|
&.is-moving {
|
|
transform: scale(1.5);
|
|
background: rgba(black, 0.8);
|
|
}
|
|
}
|
|
|
|
.drag-header-more {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.drag-options {
|
|
position: absolute;
|
|
top: 44px;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
padding: 10px;
|
|
transform: translateX(100%);
|
|
opacity: 0;
|
|
transition: $ease-out;
|
|
|
|
&.active {
|
|
transform: translateX(0);
|
|
opacity: 1;
|
|
}
|
|
|
|
&-label {
|
|
display: block;
|
|
margin: 0 0 5px 0;
|
|
|
|
input {
|
|
opacity: 0.6;
|
|
}
|
|
|
|
span {
|
|
display: inline-block;
|
|
font-size: 0.9rem;
|
|
font-weight: 400;
|
|
margin-left: 5px;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Dragula CSS */
|
|
|
|
.gu-mirror {
|
|
position: fixed !important;
|
|
margin: 0 !important;
|
|
z-index: 9999 !important;
|
|
opacity: 0.8;
|
|
list-style-type: none;
|
|
}
|
|
|
|
.gu-hide {
|
|
display: none !important;
|
|
}
|
|
|
|
.gu-unselectable {
|
|
-webkit-user-select: none !important;
|
|
-moz-user-select: none !important;
|
|
-ms-user-select: none !important;
|
|
user-select: none !important;
|
|
}
|
|
|
|
.gu-transit {
|
|
opacity: 0.2;
|
|
}
|
|
</style>
|