@itfin/components 1.2.97 → 1.2.99
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -9
- package/src/assets/scss/_css_variables.scss +11 -0
- package/src/assets/scss/_dark-theme.scss +3 -0
- package/src/assets/scss/_variables.scss +1 -1
- package/src/components/datepicker/DatePicker.vue +11 -0
- package/src/components/datepicker/DateRangePicker.vue +2 -0
- package/src/components/dropdown/Dropdown.vue +12 -1
- package/src/components/editable/EditButton.vue +39 -0
- package/src/components/editable/EditableElement.vue +112 -0
- package/src/components/editable/index.stories.js +53 -0
- package/src/components/kanban/Board.vue +230 -0
- package/src/components/kanban/BoardCard.vue +41 -0
- package/src/components/kanban/BoardCardTimer.vue +40 -0
- package/src/components/kanban/BoardColumn.vue +116 -0
- package/src/components/kanban/index.stories.js +75 -0
- package/src/components/kanban/styles.scss +266 -0
- package/src/components/sortable/draggable.js +165 -0
- package/src/components/sortable/event.js +57 -0
- package/src/components/table/Sortable._js +288 -0
- package/src/components/table/Table2.vue +85 -20
- package/src/components/table/TableBody.vue +67 -25
- package/src/components/table/TableGroup.vue +171 -81
- package/src/components/table/TableHeader.vue +194 -58
- package/src/components/table/index.stories.js +80 -5
- package/src/locales/uk.js +36 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@itfin/components",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.99",
|
|
4
4
|
"author": "Vitalii Savchuk <esvit666@gmail.com>",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"serve": "vue-cli-service serve",
|
|
@@ -26,21 +26,22 @@
|
|
|
26
26
|
"@vue/cli-service": "^5.0.1",
|
|
27
27
|
"@vue/composition-api": "^1.7.1",
|
|
28
28
|
"air-datepicker": "^3.3.5",
|
|
29
|
-
"bootstrap": "
|
|
30
|
-
"core-js": "^3.
|
|
29
|
+
"bootstrap": "^5.2.3",
|
|
30
|
+
"core-js": "^3.7.0",
|
|
31
31
|
"debug": "^4.2.0",
|
|
32
32
|
"intersection-observer": "^0.12.2",
|
|
33
33
|
"lodash": "^4.17.20",
|
|
34
34
|
"luxon": "^3.3.0",
|
|
35
|
-
"pdfjs-dist": "^
|
|
35
|
+
"pdfjs-dist": "^2.10.377",
|
|
36
36
|
"tippy.js": "^6.3.2",
|
|
37
|
+
"vue": "^2.6.12",
|
|
37
38
|
"vue-imask": "^6.6.3",
|
|
38
39
|
"vue-property-decorator": "^9.1.2",
|
|
39
40
|
"vue-swatches": "^2.1.1",
|
|
40
41
|
"vue-virtual-scroller": "^1.1.2"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
43
|
-
"@babel/eslint-parser": "^7.
|
|
44
|
+
"@babel/eslint-parser": "^7.19.1",
|
|
44
45
|
"@babel/plugin-proposal-numeric-separator": "^7.18.6",
|
|
45
46
|
"@babel/plugin-syntax-numeric-separator": "^7.10.4",
|
|
46
47
|
"@storybook/addon-docs": "=6.3.8",
|
|
@@ -52,15 +53,14 @@
|
|
|
52
53
|
"@vue/eslint-config-airbnb": "^7.0.0",
|
|
53
54
|
"@vue/test-utils": "^1.1.1",
|
|
54
55
|
"babel-eslint": "^10.1.0",
|
|
55
|
-
"eslint": "^8.
|
|
56
|
-
"eslint-plugin-import": "^2.
|
|
56
|
+
"eslint": "^8.30.0",
|
|
57
|
+
"eslint-plugin-import": "^2.22.1",
|
|
57
58
|
"eslint-plugin-prettier": "^4.2.1",
|
|
58
|
-
"eslint-plugin-vue": "^9.
|
|
59
|
+
"eslint-plugin-vue": "^9.9.0",
|
|
59
60
|
"fibers": "^5.0.0",
|
|
60
61
|
"marked": "^4.2.5",
|
|
61
62
|
"sass": "^1.29.0",
|
|
62
63
|
"sass-loader": "^10.0.5",
|
|
63
|
-
"vue": "^2.6.12",
|
|
64
64
|
"vue-class-component": "^7.2.6",
|
|
65
65
|
"vue-eslint-parser": "^9.1.0",
|
|
66
66
|
"vue-template-compiler": "=2.6.14"
|
|
@@ -9,10 +9,21 @@ $color-outcome: #b91e1e;
|
|
|
9
9
|
--color-outcome: #{$color-outcome};
|
|
10
10
|
--color-primary: #{$primary};
|
|
11
11
|
--body-bg: #{$body-bg};
|
|
12
|
+
|
|
13
|
+
.modal-backdrop {
|
|
14
|
+
--bs-backdrop-bg: #{$body-bg};
|
|
15
|
+
--bs-backdrop-opacity: 0.75;
|
|
16
|
+
}
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
[data-theme="dark"] {
|
|
15
20
|
--color-primary: #{$dark-primary};
|
|
16
21
|
--body-bg: #{$dark-body-bg};
|
|
22
|
+
--bs-backdrop-bg: #{$dark-body-bg};
|
|
17
23
|
--color-primary-hover: #{darken($dark-primary, 10%)};
|
|
24
|
+
|
|
25
|
+
.modal-backdrop {
|
|
26
|
+
--bs-backdrop-opacity: 0.5;
|
|
27
|
+
--bs-backdrop-bg: #{$dark-body-bg};
|
|
28
|
+
}
|
|
18
29
|
}
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
:unmask="false"
|
|
22
22
|
:lazy="!focused"
|
|
23
23
|
:placeholder="placeholder"
|
|
24
|
+
:disabled="disabled"
|
|
24
25
|
/>
|
|
25
26
|
|
|
26
27
|
<div class="addon-end" v-if="clearable && value">
|
|
@@ -85,6 +86,7 @@ class itfDatePicker extends Vue {
|
|
|
85
86
|
@Prop({ type: String, default: 'bottom-start' }) placement;
|
|
86
87
|
@Prop({ type: [String, Date], default: '' }) minDate;
|
|
87
88
|
@Prop(Boolean) clearable;
|
|
89
|
+
@Prop(Boolean) disabled;
|
|
88
90
|
|
|
89
91
|
focused = false;
|
|
90
92
|
|
|
@@ -117,6 +119,9 @@ class itfDatePicker extends Vue {
|
|
|
117
119
|
}
|
|
118
120
|
|
|
119
121
|
mounted() {
|
|
122
|
+
if (this.disabled) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
120
125
|
// якщо в модалці, то контекст модалки, якщо ні, то аплікейшена
|
|
121
126
|
const context = this.$el.closest('.itf-append-context') || document.body;
|
|
122
127
|
this.tooltip = tippy(this.$refs.input.$el, {
|
|
@@ -183,10 +188,16 @@ class itfDatePicker extends Vue {
|
|
|
183
188
|
}
|
|
184
189
|
|
|
185
190
|
onFocus() {
|
|
191
|
+
if (this.disabled) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
186
194
|
this.focused = true;
|
|
187
195
|
}
|
|
188
196
|
|
|
189
197
|
onBlur(e) {
|
|
198
|
+
if (this.disabled) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
190
201
|
this.focused = false;
|
|
191
202
|
this.updateValue(e.target.value, !!e.target.value);
|
|
192
203
|
}
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
:mask="Date"
|
|
20
20
|
:pattern="dateFormat"
|
|
21
21
|
:blocks="blocks"
|
|
22
|
+
:disabled="disabled"
|
|
22
23
|
:format="format"
|
|
23
24
|
:parse="parse"
|
|
24
25
|
:unmask="false"
|
|
@@ -105,6 +106,7 @@ class itfDateRangePicker extends Vue {
|
|
|
105
106
|
@Prop({ type: String, default: 'bottom-start' }) placement;
|
|
106
107
|
@Prop({ type: [String, Date], default: '' }) minDate;
|
|
107
108
|
@Prop({ type: [String, Date], default: ''}) maxDate;
|
|
109
|
+
@Prop(Boolean) disabled;
|
|
108
110
|
|
|
109
111
|
focused = false;
|
|
110
112
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<div class="itf-dropdown" :class="`drop${placement}`">
|
|
4
4
|
<div v-if="disabled"><slot name="button">{{label}}</slot></div>
|
|
5
5
|
<itf-button
|
|
6
|
-
v-else
|
|
6
|
+
v-else-if="!text"
|
|
7
7
|
:class="{ 'dropdown-toggle': toggle }"
|
|
8
8
|
v-bind="buttonOptions"
|
|
9
9
|
ref="toggle"
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
>
|
|
14
14
|
<slot name="button">{{label}}</slot>
|
|
15
15
|
</itf-button>
|
|
16
|
+
<div v-else :class="{ 'dropdown-toggle': toggle }" ref="toggle" :id="modalId" data-bs-toggle="dropdown" aria-expanded="false">
|
|
17
|
+
<slot name="button">{{label}}</slot>
|
|
18
|
+
</div>
|
|
16
19
|
<div
|
|
17
20
|
class="itf-dropdown__menu dropdown-menu"
|
|
18
21
|
:class="{'dropdown-menu-end': right, 'shadow': shadow}"
|
|
@@ -44,6 +47,8 @@ class itfDropdown extends Vue {
|
|
|
44
47
|
@Prop({ type: Boolean }) toggle;
|
|
45
48
|
@Prop({ type: Boolean }) shadow;
|
|
46
49
|
@Prop({ type: Boolean }) disabled;
|
|
50
|
+
@Prop({ type: Boolean }) text;
|
|
51
|
+
@Prop({ type: Boolean }) appendToBody;
|
|
47
52
|
@Prop({ validator: (value) => [true, false, 'inside', 'outside'].includes(value), default: true }) autoclose;
|
|
48
53
|
@Prop({ type: Object, default: () => ({}) }) buttonOptions;
|
|
49
54
|
|
|
@@ -80,6 +85,12 @@ class itfDropdown extends Vue {
|
|
|
80
85
|
reference: 'toggle',
|
|
81
86
|
autoClose: this.autoclose
|
|
82
87
|
});
|
|
88
|
+
let context = document.body;
|
|
89
|
+
if (this.appendToBody && this.$refs.dropdown instanceof Node && this.$refs.dropdown.parentNode) {
|
|
90
|
+
this.$refs.dropdown.parentNode.removeChild(this.$refs.dropdown);
|
|
91
|
+
context.appendChild(this.$refs.dropdown); // should append only to body
|
|
92
|
+
}
|
|
93
|
+
|
|
83
94
|
this.$el.addEventListener('shown.bs.dropdown', () => {
|
|
84
95
|
setTimeout(() => {
|
|
85
96
|
this.$emit('open');
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="itf-editable-button">
|
|
3
|
+
<slot></slot>
|
|
4
|
+
<itf-button secondary icon small class="itf-editable-button__button" @click="$emit('click')">
|
|
5
|
+
<itf-icon name="pen" />
|
|
6
|
+
</itf-button>
|
|
7
|
+
</div>
|
|
8
|
+
</template>
|
|
9
|
+
<style lang="scss" scoped>
|
|
10
|
+
.itf-editable-button {
|
|
11
|
+
position: relative;
|
|
12
|
+
|
|
13
|
+
&__button {
|
|
14
|
+
position: absolute !important;
|
|
15
|
+
right: 0;
|
|
16
|
+
top: 0;
|
|
17
|
+
opacity: 0;
|
|
18
|
+
transition: opacity 0.2s ease-in-out;
|
|
19
|
+
}
|
|
20
|
+
&:hover &__button {
|
|
21
|
+
transition: opacity 0.2s ease-in-out;
|
|
22
|
+
opacity: 1;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
</style>
|
|
26
|
+
<script>
|
|
27
|
+
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
|
28
|
+
import itfIcon from '../icon/Icon';
|
|
29
|
+
import itfButton from '../button/Button';
|
|
30
|
+
|
|
31
|
+
export default @Component({
|
|
32
|
+
components: {
|
|
33
|
+
itfIcon,
|
|
34
|
+
itfButton,
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
class EditButton extends Vue {
|
|
38
|
+
}
|
|
39
|
+
</script>
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
|
|
3
|
+
<div>
|
|
4
|
+
<div ref="element">
|
|
5
|
+
<slot name="activator" :open="open">
|
|
6
|
+
<div @click.prevent.stop="open">
|
|
7
|
+
<slot :open="open" :hide="hide">
|
|
8
|
+
</slot>
|
|
9
|
+
</div>
|
|
10
|
+
</slot>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
</template>
|
|
15
|
+
<style lang="scss">
|
|
16
|
+
.itf-editable-element {
|
|
17
|
+
z-index: 1060;
|
|
18
|
+
position: fixed;
|
|
19
|
+
}
|
|
20
|
+
</style>
|
|
21
|
+
<script>
|
|
22
|
+
import { Vue, Component, Prop, PropSync } from 'vue-property-decorator';
|
|
23
|
+
import itfButton from '../button/Button.vue';
|
|
24
|
+
import itfIcon from '../icon/Icon.vue';
|
|
25
|
+
import FocusTrap from "bootstrap/js/src/util/focustrap";
|
|
26
|
+
|
|
27
|
+
export default @Component({
|
|
28
|
+
name: 'itfEditableElement',
|
|
29
|
+
components: {
|
|
30
|
+
itfButton,
|
|
31
|
+
itfIcon,
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
class itfEditableElement extends Vue {
|
|
35
|
+
_backdrop = null;
|
|
36
|
+
_context = null;
|
|
37
|
+
_focusTrap = null;
|
|
38
|
+
_elContainer = null;
|
|
39
|
+
_tempContainer = null;
|
|
40
|
+
|
|
41
|
+
isOpen = false;
|
|
42
|
+
|
|
43
|
+
async mounted() {
|
|
44
|
+
this._context = this.$el.closest('.itf-append-context:not(.modal-content)') || document.body;
|
|
45
|
+
|
|
46
|
+
const { default: Backdrop } = await import('../modal/backdrop');
|
|
47
|
+
this._backdrop = new Backdrop({
|
|
48
|
+
rootElement: this._context,
|
|
49
|
+
isVisible: true,
|
|
50
|
+
isAnimated: true,
|
|
51
|
+
clickCallback: () => {
|
|
52
|
+
this.hide();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
this._focusTrap = new FocusTrap({
|
|
56
|
+
trapElement: this.$el
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
open() {
|
|
61
|
+
if (this.isOpen) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.isOpen = true;
|
|
65
|
+
|
|
66
|
+
this.$emit('open');
|
|
67
|
+
this._backdrop.show();
|
|
68
|
+
this._focusTrap.activate();
|
|
69
|
+
|
|
70
|
+
this._tempContainer = document.createElement('div');
|
|
71
|
+
this.applyPosition(this._tempContainer, this.$refs.element)
|
|
72
|
+
this._tempContainer.classList.add('itf-editable-element');
|
|
73
|
+
this.$el.removeChild(this.$refs.element);
|
|
74
|
+
this._tempContainer.appendChild(this.$refs.element);
|
|
75
|
+
this._context.appendChild(this._tempContainer);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
applyPosition(container, el) {
|
|
79
|
+
const box = el.getBoundingClientRect();
|
|
80
|
+
const left = box.left + document.body.scrollLeft;
|
|
81
|
+
const top = box.top + document.body.scrollTop;
|
|
82
|
+
container.style.left = `${left}px`;
|
|
83
|
+
container.style.top = `${top}px`;
|
|
84
|
+
container.style.width = `${box.width}px`;
|
|
85
|
+
container.style.height = `${box.height}px`;
|
|
86
|
+
this.$el.style.width = `${box.width}px`;
|
|
87
|
+
this.$el.style.height = `${box.height}px`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
hide() {
|
|
91
|
+
if (!this.isOpen) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
this._focusTrap.deactivate();
|
|
95
|
+
this._backdrop.hide(() => {
|
|
96
|
+
this._tempContainer.removeChild(this.$refs.element);
|
|
97
|
+
this.$el.appendChild(this.$refs.element);
|
|
98
|
+
this._context.removeChild(this._tempContainer); // remove the temporary div
|
|
99
|
+
|
|
100
|
+
this.$el.style.width = null;
|
|
101
|
+
this.$el.style.height = null;
|
|
102
|
+
this.isOpen = false;
|
|
103
|
+
this.$emit('hide');
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
beforeDestroy() {
|
|
108
|
+
this._backdrop.dispose();
|
|
109
|
+
this._focusTrap.deactivate();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
</script>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { storiesOf } from '@storybook/vue';
|
|
2
|
+
import itfButton from '../button/Button.vue';
|
|
3
|
+
import itfEditableElement from './EditableElement.vue';
|
|
4
|
+
|
|
5
|
+
storiesOf('Common', module)
|
|
6
|
+
.add('Editable element', () => ({
|
|
7
|
+
components: {
|
|
8
|
+
itfButton,
|
|
9
|
+
itfEditableElement
|
|
10
|
+
},
|
|
11
|
+
data() {
|
|
12
|
+
return {}
|
|
13
|
+
},
|
|
14
|
+
template: `<div>
|
|
15
|
+
<p>You need wrap whole application with this tag</p>
|
|
16
|
+
|
|
17
|
+
<h2>Usage</h2>
|
|
18
|
+
|
|
19
|
+
<pre>
|
|
20
|
+
<itf-table
|
|
21
|
+
:columns="columns"
|
|
22
|
+
:rows="list"
|
|
23
|
+
>
|
|
24
|
+
<template #column.Employee="{ item }"></template>
|
|
25
|
+
</itf-table>
|
|
26
|
+
</pre>
|
|
27
|
+
|
|
28
|
+
<h3>Example</h3>
|
|
29
|
+
|
|
30
|
+
<itf-editable-element>
|
|
31
|
+
<template v-slot="{ hide }">
|
|
32
|
+
|
|
33
|
+
<div class="card" style="width: 300px">
|
|
34
|
+
<div class="card-body">
|
|
35
|
+
asdadas
|
|
36
|
+
|
|
37
|
+
<a href="#" @click="hide">Close</a>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</template>
|
|
41
|
+
</itf-editable-element>
|
|
42
|
+
|
|
43
|
+
<!--itf-table
|
|
44
|
+
:columns="columns"
|
|
45
|
+
:rows="list"
|
|
46
|
+
>
|
|
47
|
+
<template #column.Employee="{ item }">
|
|
48
|
+
{{item.Employee}}
|
|
49
|
+
</template>
|
|
50
|
+
</itf-table-->
|
|
51
|
+
|
|
52
|
+
</div>`,
|
|
53
|
+
}));
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="itf-board">
|
|
3
|
+
|
|
4
|
+
<div ref="container" class="itf-board-titles">
|
|
5
|
+
<div v-for="(column, index) of columns"
|
|
6
|
+
:data-column="index"
|
|
7
|
+
@mouseover="columnHighlight(index, 'enter')"
|
|
8
|
+
@mouseout="columnHighlight(index, 'leave')"
|
|
9
|
+
:key="index" class="itf-board-column itf-board-column__title p-2">
|
|
10
|
+
<div accept-group="board-columns"
|
|
11
|
+
class="itf-board-header-space"
|
|
12
|
+
@enter="columnHighlight(index, 'enter', 'drop')"
|
|
13
|
+
@leave="columnHighlight(index, 'leave', 'drop')"
|
|
14
|
+
v-dropzone="{ payload: { index, column } }">
|
|
15
|
+
<div class="itf-board-header-dropzone"></div>
|
|
16
|
+
</div>
|
|
17
|
+
<itf-edit-button
|
|
18
|
+
accept-group="board-columns"
|
|
19
|
+
@enter="columnHighlight(index, 'enter')"
|
|
20
|
+
@leave="columnHighlight(index, 'leave')"
|
|
21
|
+
v-dropzone="{ payload: { index, column } }"
|
|
22
|
+
class="d-flex align-items-center">
|
|
23
|
+
<div
|
|
24
|
+
group="board-columns"
|
|
25
|
+
@drop="reorderColumns"
|
|
26
|
+
v-draggable="{ handle: true, payload: { index, column }, mirror: { yAxis:false } }"
|
|
27
|
+
class="flex-grow-1 d-flex align-items-center itf-board-column__header justify-content-between">
|
|
28
|
+
<div><slot name="header" :column="column">{{ column[columnNameKey] }}</slot></div>
|
|
29
|
+
<div class="text-muted me-1"><slot name="header-addon" :column="column">{{ (groupedItems[column.Id] || []).length }}</slot></div>
|
|
30
|
+
</div>
|
|
31
|
+
</itf-edit-button>
|
|
32
|
+
<div v-if="index === columns.length - 1"
|
|
33
|
+
@enter="columnHighlightLast('enter', 'drop')"
|
|
34
|
+
@leave="columnHighlightLast('leave', 'drop')"
|
|
35
|
+
accept-group="board-columns"
|
|
36
|
+
class="itf-board-header-space right"
|
|
37
|
+
v-dropzone="{ payload:{ last: true } }">
|
|
38
|
+
<div class="itf-board-header-dropzone"></div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="align-items-end d-flex pb-1">
|
|
42
|
+
<slot name="add-column">
|
|
43
|
+
<itf-button icon small>
|
|
44
|
+
<itf-icon name="plus" />
|
|
45
|
+
</itf-button>
|
|
46
|
+
</slot>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div class="itf-board-columns flex-grow-1">
|
|
51
|
+
<div v-for="(column, index) of columns"
|
|
52
|
+
:key="`column-${index}`"
|
|
53
|
+
:data-column="index"
|
|
54
|
+
class="itf-board-column px-2 pb-2 pt-2" :class="{'empty': !groupedItems[column.Id]}">
|
|
55
|
+
<slot name="column" :column="column" :index="index" :items="groupedItems[column.Id] || []"></slot>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</template>
|
|
60
|
+
<script>
|
|
61
|
+
import './styles.scss';
|
|
62
|
+
import { Component, Prop, Vue } from 'vue-property-decorator';
|
|
63
|
+
import itfIcon from '../icon/Icon';
|
|
64
|
+
import itfButton from '../button/Button';
|
|
65
|
+
import loading from '../../directives/loading';
|
|
66
|
+
import itfForm from '../form/Form';
|
|
67
|
+
import itfEditButton from '../editable/EditButton.vue';
|
|
68
|
+
import createDraggable from '../sortable/draggable';
|
|
69
|
+
import { debounce } from '../../helpers/debounce';
|
|
70
|
+
|
|
71
|
+
const { Node, ...draggableDirectives } = createDraggable();
|
|
72
|
+
|
|
73
|
+
export default @Component({
|
|
74
|
+
components: {
|
|
75
|
+
itfIcon,
|
|
76
|
+
itfEditButton,
|
|
77
|
+
itfForm,
|
|
78
|
+
itfButton
|
|
79
|
+
},
|
|
80
|
+
directives: {
|
|
81
|
+
...draggableDirectives,
|
|
82
|
+
loading
|
|
83
|
+
},
|
|
84
|
+
provide() {
|
|
85
|
+
return { board: this }; // do not use Provide from vue-property-decorator
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
class Board extends Vue {
|
|
89
|
+
@Prop(Array) columns;
|
|
90
|
+
@Prop(Array) items;
|
|
91
|
+
@Prop(Array) columnOrders;
|
|
92
|
+
@Prop({ type: Function, default: (item) => item.Status?.Id }) groupFunc;
|
|
93
|
+
@Prop({ type: Function, default: (item, value) => { item.Status = value; } }) updateFunc;
|
|
94
|
+
@Prop({ type: String, default: 'Name' }) columnNameKey;
|
|
95
|
+
@Prop(Boolean) columnSorting;
|
|
96
|
+
|
|
97
|
+
beforeDestroy() {
|
|
98
|
+
if (this.columnSorting) {
|
|
99
|
+
Node.removeContainer(this.$refs.container);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
mounted() {
|
|
104
|
+
this.itemStatusChanged = debounce(this.emitItemStatusChanged, 10);
|
|
105
|
+
if (this.columnSorting) {
|
|
106
|
+
Node.addContainer(this.$refs.container);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
get groupedItems() {
|
|
111
|
+
const grouped = {};
|
|
112
|
+
for (const column of this.columns) {
|
|
113
|
+
grouped[column.Id] = this.orderItemsInColumn(column);
|
|
114
|
+
}
|
|
115
|
+
return grouped;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
orderItemsInColumn(column) {
|
|
119
|
+
const columnOrdering = this.columnOrders.find((c) => c.Id === column.Id) || { ItemIds: [] };
|
|
120
|
+
let items = this.items.filter((item) => column.Id === this.groupFunc(item));
|
|
121
|
+
items = items.sort((a, b) => {
|
|
122
|
+
const aIndex = columnOrdering.ItemIds.findIndex((c) => c === a.Id);
|
|
123
|
+
const bIndex = columnOrdering.ItemIds.findIndex((c) => c === b.Id);
|
|
124
|
+
if (aIndex === -1 && bIndex === -1) {
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
if (aIndex === -1) {
|
|
128
|
+
return 1;
|
|
129
|
+
}
|
|
130
|
+
if (bIndex === -1) {
|
|
131
|
+
return -1;
|
|
132
|
+
}
|
|
133
|
+
return aIndex - bIndex;
|
|
134
|
+
});
|
|
135
|
+
return items;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
columnHighlight(index, state, className = 'over') {
|
|
139
|
+
Array.from(this.$el.querySelectorAll(`[data-column="${index}"]`)).forEach(t=>{
|
|
140
|
+
state === 'enter' ? t.classList.add(className) : t.classList.remove(className)
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
columnHighlightLast(state, className = 'over') {
|
|
144
|
+
Array.from(this.$el.querySelectorAll(`[data-column="${this.columns.length - 1}"]`)).forEach(t=>{
|
|
145
|
+
if (state === "enter") {
|
|
146
|
+
t.classList.add(className);
|
|
147
|
+
t.classList.add('right');
|
|
148
|
+
} else {
|
|
149
|
+
t.classList.remove(className);
|
|
150
|
+
t.classList.remove('right');
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
reorderColumns({ detail }) {
|
|
156
|
+
const { index: fromIndex } = detail.draggablePayload;
|
|
157
|
+
const { index: toIndex, last } = detail.dropzonePayload;
|
|
158
|
+
const newValue = [...this.columns];
|
|
159
|
+
const [removed] = newValue.splice(fromIndex, 1);
|
|
160
|
+
|
|
161
|
+
if (last) {
|
|
162
|
+
newValue.push(removed);
|
|
163
|
+
} else {
|
|
164
|
+
newValue.splice((fromIndex < toIndex) ? toIndex - 1 : toIndex, 0, removed);
|
|
165
|
+
}
|
|
166
|
+
this.$emit('update:columns', newValue);
|
|
167
|
+
if (last) {
|
|
168
|
+
this.columnHighlightLast('leave');
|
|
169
|
+
this.columnHighlightLast('leave', 'drop');
|
|
170
|
+
} else {
|
|
171
|
+
this.columnHighlight(toIndex, 'leave');
|
|
172
|
+
this.columnHighlight(toIndex, 'leave', 'drop');
|
|
173
|
+
}
|
|
174
|
+
// for (const dropdown of this.$refs.dropdown) {
|
|
175
|
+
// dropdown.hide();
|
|
176
|
+
// }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
reorderCards({ detail }) {
|
|
180
|
+
const { item, column: fromColumn } = detail.draggablePayload;
|
|
181
|
+
const { index: toIndex, last, column } = detail.dropzonePayload;
|
|
182
|
+
|
|
183
|
+
const items = [...this.items];
|
|
184
|
+
const itemIndex = items.findIndex(t => t === item);
|
|
185
|
+
|
|
186
|
+
const newItem = { ...items[itemIndex] };
|
|
187
|
+
this.updateFunc(newItem, column);
|
|
188
|
+
|
|
189
|
+
items[itemIndex] = newItem;
|
|
190
|
+
|
|
191
|
+
this.lastUpdatedIndex = typeof this.lastUpdatedIndex === 'undefined' ? toIndex : this.lastUpdatedIndex; // запамятовуємо індекс, бо виклики може бути два, коли колонка і карточка обробляє
|
|
192
|
+
this.itemStatusChanged(items[itemIndex], column, items, () => {
|
|
193
|
+
const toIndex = this.lastUpdatedIndex; // беремо індекс з пам'яті, бо викликів може бути два, але індекс тільки в картці, якщо нема, то це колонка
|
|
194
|
+
this.lastUpdatedIndex = undefined; // обнуляємо, щоб при наступному перетягувані вже не брало його
|
|
195
|
+
const newOrders = [...this.columnOrders];
|
|
196
|
+
let sorting = newOrders.find((c) => c.Id === column.Id);
|
|
197
|
+
if (!sorting) {
|
|
198
|
+
sorting = { Id: column.Id, ItemIds: [] };
|
|
199
|
+
newOrders.push(sorting)
|
|
200
|
+
}
|
|
201
|
+
let fromSorting = newOrders.find((c) => c.Id === fromColumn.Id);
|
|
202
|
+
if (!fromSorting) {
|
|
203
|
+
fromSorting = { Id: fromColumn.Id, ItemIds: [] };
|
|
204
|
+
newOrders.push(fromSorting)
|
|
205
|
+
}
|
|
206
|
+
// update from
|
|
207
|
+
fromSorting.ItemIds = this.groupedItems[fromColumn.Id].map((i) => i.Id).filter((t) => t !== newItem.Id);
|
|
208
|
+
|
|
209
|
+
// update to
|
|
210
|
+
const items = [...this.groupedItems[column.Id]];
|
|
211
|
+
let placeIndex = last ? items.length : toIndex;
|
|
212
|
+
if (typeof placeIndex === 'undefined') { // ідекса може не бути, якщо на колонку переносять, тоді в кінець
|
|
213
|
+
placeIndex = items.length;
|
|
214
|
+
}
|
|
215
|
+
const itemsInColumn = items.map((t) => t.Id).filter((t) => t !== newItem.Id);
|
|
216
|
+
itemsInColumn.splice(placeIndex, 0, newItem.Id);
|
|
217
|
+
|
|
218
|
+
sorting.ItemIds = itemsInColumn;
|
|
219
|
+
this.$emit('update:columnOrders', newOrders);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
emitItemStatusChanged(item, column, items, updateOrder) {
|
|
224
|
+
updateOrder();
|
|
225
|
+
|
|
226
|
+
this.$emit('update:item', [item, column]);
|
|
227
|
+
this.$emit('update:items', items);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
</script>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="itf-board-card shadow-sm">
|
|
3
|
+
<slot name="header"></slot>
|
|
4
|
+
|
|
5
|
+
<div class="itf-board-card-inner d-flex">
|
|
6
|
+
<div class="flex-grow-1">
|
|
7
|
+
<slot>{{text}}</slot>
|
|
8
|
+
</div>
|
|
9
|
+
<div>
|
|
10
|
+
<itf-button icon small>
|
|
11
|
+
<itf-icon name="menu_horizontal" />
|
|
12
|
+
</itf-button>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<slot name="footer"></slot>
|
|
17
|
+
</div>
|
|
18
|
+
</template>
|
|
19
|
+
<script>
|
|
20
|
+
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
|
21
|
+
import itfIcon from '../icon/Icon';
|
|
22
|
+
import itfButton from '../button/Button';
|
|
23
|
+
import loading from '../../directives/loading';
|
|
24
|
+
import itfForm from '../form/Form';
|
|
25
|
+
import itfEditButton from '../editable/EditButton';
|
|
26
|
+
|
|
27
|
+
export default @Component({
|
|
28
|
+
components: {
|
|
29
|
+
itfIcon,
|
|
30
|
+
itfForm,
|
|
31
|
+
itfEditButton,
|
|
32
|
+
itfButton
|
|
33
|
+
},
|
|
34
|
+
directives: {
|
|
35
|
+
loading
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
class BoardColumn extends Vue {
|
|
39
|
+
@Prop() text;
|
|
40
|
+
}
|
|
41
|
+
</script>
|