@itfin/components 1.2.98 → 1.2.100

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itfin/components",
3
- "version": "1.2.98",
3
+ "version": "1.2.100",
4
4
  "author": "Vitalii Savchuk <esvit666@gmail.com>",
5
5
  "scripts": {
6
6
  "serve": "vue-cli-service serve",
@@ -44,6 +44,9 @@ body[data-theme="dark"] {
44
44
  &:hover, &:active, &:focus {
45
45
  color: $dark-body-color;
46
46
  }
47
+ &.btn-secondary {
48
+ //color: #000;
49
+ }
47
50
  }
48
51
  .modal-backdrop {
49
52
  background-color: #000;
@@ -57,7 +57,7 @@ $popover-border-width: 2px;
57
57
  // dropdown
58
58
 
59
59
  // Dark theme
60
- $dark-body-bg: #2e3136;
60
+ $dark-body-bg: #232529;// #2e3136;
61
61
  $dark-border-color: #4a4a4a;
62
62
  $dark-body-color: #ddd;
63
63
  $dark-input-bg: #383b41;
@@ -2,7 +2,7 @@
2
2
  <transition name="itf-toast-fade" mode="in-out">
3
3
  <div
4
4
  v-show="show"
5
- class="itf-toast-wrapper"
5
+ class="itf-toast-wrapper d-print-none"
6
6
  :style="{ width }"
7
7
  :class="wrapperClassName"
8
8
  >
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="itf-toast-message toast" :class="{'show': visible, [typeClass]: true}" role="alert" aria-live="assertive" aria-atomic="true">
2
+ <div class="itf-toast-message toast d-print-none" :class="{'show': visible, [typeClass]: true}" role="alert" aria-live="assertive" aria-atomic="true">
3
3
  <div class="toast-header" v-if="title || $slots.title" :class="{[typeClass]: true}">
4
4
  <template v-if="iconImg || $slots.icon">
5
5
  <img v-if="iconImg" :src="iconImg" class="rounded me-2" />
@@ -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,221 @@
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
+
70
+ const { Node, ...draggableDirectives } = createDraggable();
71
+
72
+ export default @Component({
73
+ components: {
74
+ itfIcon,
75
+ itfEditButton,
76
+ itfForm,
77
+ itfButton
78
+ },
79
+ directives: {
80
+ ...draggableDirectives,
81
+ loading
82
+ },
83
+ provide() {
84
+ return { board: this }; // do not use Provide from vue-property-decorator
85
+ }
86
+ })
87
+ class Board extends Vue {
88
+ @Prop(Array) columns;
89
+ @Prop(Array) items;
90
+ @Prop(Array) columnOrders;
91
+ @Prop({ type: Function, default: (item) => item.Status?.Id }) groupFunc;
92
+ @Prop({ type: Function, default: (item, value) => { item.Status = value; } }) updateFunc;
93
+ @Prop({ type: String, default: 'Name' }) columnNameKey;
94
+ @Prop(Boolean) columnSorting;
95
+
96
+ beforeDestroy() {
97
+ if (this.columnSorting) {
98
+ Node.removeContainer(this.$refs.container);
99
+ }
100
+ }
101
+
102
+ mounted() {
103
+ if (this.columnSorting) {
104
+ Node.addContainer(this.$refs.container);
105
+ }
106
+ }
107
+
108
+ get groupedItems() {
109
+ const grouped = {};
110
+ for (const column of this.columns) {
111
+ grouped[column.Id] = this.orderItemsInColumn(column);
112
+ }
113
+ return grouped;
114
+ }
115
+
116
+ orderItemsInColumn(column) {
117
+ const columnOrdering = this.columnOrders.find((c) => c.Id === column.Id) || { ItemIds: [] };
118
+ let items = this.items.filter((item) => column.Id === this.groupFunc(item));
119
+ items = items.sort((a, b) => {
120
+ const aIndex = columnOrdering.ItemIds.findIndex((c) => c === a.Id);
121
+ const bIndex = columnOrdering.ItemIds.findIndex((c) => c === b.Id);
122
+ if (aIndex === -1 && bIndex === -1) {
123
+ return 0;
124
+ }
125
+ if (aIndex === -1) {
126
+ return 1;
127
+ }
128
+ if (bIndex === -1) {
129
+ return -1;
130
+ }
131
+ return aIndex - bIndex;
132
+ });
133
+ return items;
134
+ }
135
+
136
+ columnHighlight(index, state, className = 'over') {
137
+ Array.from(this.$el.querySelectorAll(`[data-column="${index}"]`)).forEach(t=>{
138
+ state === 'enter' ? t.classList.add(className) : t.classList.remove(className)
139
+ });
140
+ }
141
+ columnHighlightLast(state, className = 'over') {
142
+ Array.from(this.$el.querySelectorAll(`[data-column="${this.columns.length - 1}"]`)).forEach(t=>{
143
+ if (state === "enter") {
144
+ t.classList.add(className);
145
+ t.classList.add('right');
146
+ } else {
147
+ t.classList.remove(className);
148
+ t.classList.remove('right');
149
+ }
150
+ });
151
+ }
152
+
153
+ reorderColumns({ detail }) {
154
+ const { index: fromIndex } = detail.draggablePayload;
155
+ const { index: toIndex, last } = detail.dropzonePayload;
156
+ const newValue = [...this.columns];
157
+ const [removed] = newValue.splice(fromIndex, 1);
158
+
159
+ if (last) {
160
+ newValue.push(removed);
161
+ } else {
162
+ newValue.splice((fromIndex < toIndex) ? toIndex - 1 : toIndex, 0, removed);
163
+ }
164
+ this.$emit('update:columns', newValue);
165
+ if (last) {
166
+ this.columnHighlightLast('leave');
167
+ this.columnHighlightLast('leave', 'drop');
168
+ } else {
169
+ this.columnHighlight(toIndex, 'leave');
170
+ this.columnHighlight(toIndex, 'leave', 'drop');
171
+ }
172
+ // for (const dropdown of this.$refs.dropdown) {
173
+ // dropdown.hide();
174
+ // }
175
+ }
176
+
177
+ reorderCards({ detail }) {
178
+ const { item, column: fromColumn } = detail.draggablePayload;
179
+ const { index: toIndex, last, column } = detail.dropzonePayload;
180
+
181
+ const items = [...this.items];
182
+ const itemIndex = items.findIndex(t => t === item);
183
+
184
+ const newItem = { ...items[itemIndex] };
185
+ this.updateFunc(newItem, column);
186
+
187
+ items[itemIndex] = newItem;
188
+
189
+ const newOrders = [...this.columnOrders];
190
+
191
+ let sorting = newOrders.find((c) => c.Id === column.Id);
192
+ if (!sorting) {
193
+ sorting = { Id: column.Id, ItemIds: [] };
194
+ newOrders.push(sorting)
195
+ }
196
+ let fromSorting = newOrders.find((c) => c.Id === fromColumn.Id);
197
+ if (!fromSorting) {
198
+ fromSorting = { Id: fromColumn.Id, ItemIds: [] };
199
+ newOrders.push(fromSorting)
200
+ }
201
+ // update from
202
+ fromSorting.ItemIds = this.groupedItems[fromColumn.Id].map((i) => i.Id).filter((t) => t !== newItem.Id);
203
+
204
+ // update to
205
+ const columnItems = [...this.groupedItems[column.Id]];
206
+ let placeIndex = last ? columnItems.length : toIndex;
207
+ if (typeof placeIndex === 'undefined') { // ідекса може не бути, якщо на колонку переносять, тоді в кінець
208
+ placeIndex = columnItems.length;
209
+ }
210
+ const itemsInColumn = columnItems.map((t) => t.Id).filter((t) => t !== newItem.Id);
211
+ itemsInColumn.splice(placeIndex, 0, newItem.Id);
212
+
213
+ sorting.ItemIds = itemsInColumn;
214
+
215
+ this.$emit('update:item', [item, column]);
216
+ this.$emit('update:items', items);
217
+
218
+ this.$emit('update:columnOrders', newOrders);
219
+ }
220
+ }
221
+ </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>
@@ -0,0 +1,40 @@
1
+ <template>
2
+ <div class="itf-board-card-timer px-1 py-1">
3
+ <div class="d-flex justify-content-between">
4
+ <div class="d-flex align-items-center ps-2">
5
+ <itf-icon name="clock" :size="16" class="me-1" />
6
+ <slot>{{text}}</slot>
7
+ </div>
8
+ <div>
9
+ <itf-button icon small>
10
+ <itf-icon name="pause" />
11
+ </itf-button>
12
+ </div>
13
+ </div>
14
+ </div>
15
+ </template>
16
+ <style>
17
+ .itf-board-card-timer {
18
+ background: var(--bs-primary);
19
+ color: #fff;
20
+
21
+ .btn {
22
+ color: #fff;
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.vue';
30
+
31
+ export default @Component({
32
+ components: {
33
+ itfIcon,
34
+ itfButton
35
+ }
36
+ })
37
+ class BoardCardTimer extends Vue {
38
+ @Prop() text;
39
+ }
40
+ </script>
@@ -0,0 +1,111 @@
1
+ <template>
2
+ <div class="itf-board-column-wrapper">
3
+ <itf-button v-if="showAddButton" block small class="text-muted">
4
+ <itf-icon name="plus" />
5
+
6
+ Add entry
7
+ </itf-button>
8
+
9
+ <div class="itf-board-cards-wrapper" ref="container">
10
+ <template v-if="!items.length">
11
+ <slot name="empty"></slot>
12
+ </template>
13
+ <template v-else>
14
+ <div v-for="(item, index) of items" :key="index" :data-card="index">
15
+ <div accept-group="board-cards"
16
+ class="itf-board-card-space"
17
+ @enter="cardHighlight(index, 'enter', 'drop')"
18
+ @leave="cardHighlight(index, 'leave', 'drop')"
19
+ v-dropzone="{ payload: { index, item, column } }">
20
+ <div class="itf-board-header-dropzone"></div>
21
+ </div>
22
+ <div group="board-cards"
23
+ @drop="board.reorderCards"
24
+ v-draggable="{ handle: true, payload: { index, item, column }, mirror: { xAxis: true, yAxis: true } }">
25
+ <slot name="item" :item="item" :index="index"></slot>
26
+ </div>
27
+ </div>
28
+ </template>
29
+ <div @enter="cardHighlightLast('enter', 'drop')"
30
+ @leave="cardHighlightLast('leave', 'drop')"
31
+ accept-group="board-cards"
32
+ class="itf-board-card-space bottom"
33
+ v-dropzone="{ payload:{ last: true, column } }">
34
+ <div class="itf-board-header-dropzone"></div>
35
+ </div>
36
+ </div>
37
+ </div>
38
+ </template>
39
+ <script>
40
+ import { Component, Prop, Vue, Inject } from 'vue-property-decorator';
41
+ import itfIcon from '../icon/Icon';
42
+ import itfButton from '../button/Button';
43
+ import loading from '../../directives/loading';
44
+ import itfForm from '../form/Form';
45
+ import itfEditButton from '../editable/EditButton';
46
+ import createDraggable from '../sortable/draggable';
47
+
48
+ const { Node, ...draggableDirectives } = createDraggable({
49
+ mirror: {
50
+ yAxis: true,
51
+ xAxis: true,
52
+ constrainDimensions: true
53
+ },
54
+ scrollable: {
55
+ speed: 20,
56
+ sensitivity: 80
57
+ }
58
+ });
59
+
60
+ export default @Component({
61
+ components: {
62
+ itfIcon,
63
+ itfForm,
64
+ itfEditButton,
65
+ itfButton
66
+ },
67
+ directives: {
68
+ ...draggableDirectives,
69
+ loading
70
+ }
71
+ })
72
+ class BoardColumn extends Vue {
73
+ @Inject({ default: null }) board;
74
+
75
+ @Prop() title;
76
+ @Prop() column;
77
+ @Prop(Array) items;
78
+ @Prop(Boolean) showAddButton;
79
+ @Prop(Boolean) cardSorting;
80
+
81
+ beforeDestroy() {
82
+ if (this.cardSorting) {
83
+ Node.removeContainer(this.$refs.container);
84
+ }
85
+ }
86
+
87
+ mounted() {
88
+ if (this.cardSorting) {
89
+ Node.addContainer(this.$refs.container);
90
+ }
91
+ }
92
+
93
+ cardHighlight(index, state, className = 'over') {
94
+ this.isHaveHighlight = state === 'enter';
95
+ Array.from(this.$el.querySelectorAll(`[data-card="${index}"]`)).forEach(t=>{
96
+ state === 'enter' ? t.classList.add(className) : t.classList.remove(className)
97
+ });
98
+ }
99
+ cardHighlightLast(state, className = 'over') {
100
+ Array.from(this.$el.querySelectorAll(`[data-card="${this.items.length - 1}"]`)).forEach(t=>{
101
+ if (state === "enter") {
102
+ t.classList.add(className);
103
+ t.classList.add('bottom');
104
+ } else {
105
+ t.classList.remove(className);
106
+ t.classList.remove('bottom');
107
+ }
108
+ });
109
+ }
110
+ }
111
+ </script>
@@ -0,0 +1,75 @@
1
+ import { storiesOf } from '@storybook/vue';
2
+ import itfApp from '../app/App.vue';
3
+ import itfIcon from '../icon/Icon.vue';
4
+ import itfButton from '../button/Button.vue';
5
+ import itfBoard from './Board.vue';
6
+ import itfBoardColumn from './BoardColumn.vue';
7
+ import itfBoardCard from './BoardCard.vue';
8
+ import itfBoardCardTimer from './BoardCardTimer.vue';
9
+
10
+ storiesOf('Common', module)
11
+ .add('Kanban board', () => ({
12
+ components: {
13
+ itfIcon,
14
+ itfBoard,
15
+ itfBoardColumn,
16
+ itfBoardCard,
17
+ itfBoardCardTimer
18
+ },
19
+ data() {
20
+ // document.body.setAttribute('data-theme', 'dark')
21
+ return {
22
+ columnOrders: [],
23
+ columns: [
24
+ { Id: 1, Name: '➡️ Todo' },
25
+ { Id: 2, Name: '⚙️ In progress' },
26
+ { Id: 3, Name: '✅ Done' },
27
+ ],
28
+ tasks: [
29
+ { Id: 1, Name: 'Test', Status: { Id: 1 } },
30
+ { Id: 2, Name: 'Test 2', Status: { Id: 1 } },
31
+ { Id: 3, Name: 'Test 3', Status: { Id: 2 } },
32
+ { Id: 4, Name: 'Test 4', Status: { Id: 3 } },
33
+ ]
34
+ }
35
+ },
36
+ methods: {
37
+ onItemUpdate([item, status]) {
38
+ console.info('onItemUpdate', item, status);
39
+ }
40
+ },
41
+ template: `<div>
42
+ <p></p>
43
+
44
+ <h2>Usage</h2>
45
+
46
+ <pre></pre>
47
+
48
+ <h3>Example</h3>
49
+
50
+ <div style="height: 90vh; display: flex; width: 100%">
51
+ <itf-board class="flex-grow-1" :columns.sync="columns" :column-orders.sync="columnOrders" :items.sync="tasks" column-sorting @update:item="onItemUpdate">
52
+ <template #column="{ column, items }">
53
+ <itf-board-column :column="column" :items="items" card-sorting @update:columns="columns = $event">
54
+ <template #item="{ item }">
55
+ <itf-board-card :text="item.Name">
56
+ <template #header>
57
+ <!-- <itf-board-card-timer text="01:20:00"></itf-board-card-timer>-->
58
+ </template>
59
+ <template #footer>
60
+ <!--div class="d-flex p-2">
61
+ <div>
62
+ <itf-icon name="clock" />
63
+ 1h 20m
64
+ </div>
65
+ </div-->
66
+ <!-- <itf-board-card-timer text="01:20:00"></itf-board-card-timer>-->
67
+ </template>
68
+ </itf-board-card>
69
+ </template>
70
+ </itf-board-column>
71
+ </template>
72
+ </itf-board>
73
+ </div>
74
+ </div>`,
75
+ }))
@@ -0,0 +1,269 @@
1
+ :root {
2
+ --itf-board-column-hover-color: rgba(0, 0, 0, .05);
3
+ --itf-board-column-width: 300px;
4
+ --itf-board-placeholder-color: #47BEFF;
5
+ --itf-board-placeholder-border-color: #0567eb;
6
+ --itf-board-placeholder-bg-color: #ebeffe;
7
+ --itf-board-card-bg-color: #fff;
8
+ }
9
+
10
+ [data-theme="dark"] {
11
+ --itf-board-column-hover-color: #494646;
12
+ --itf-board-placeholder-color: #FFCC00;
13
+ --itf-board-placeholder-border-color: #FFCC00;
14
+ --itf-board-placeholder-bg-color: #383b41;
15
+ --itf-board-card-bg-color: #313337;
16
+
17
+ .itf-board {
18
+ --bs-border-color: #424448;
19
+ }
20
+ }
21
+
22
+ .itf-board {
23
+ display: flex;
24
+ flex-wrap: nowrap;
25
+ flex-direction: column;
26
+
27
+ .draggable-mirror {
28
+ z-index: 100000001;
29
+ min-width: var(--itf-board-column-width);
30
+ padding: 1rem;
31
+ background-color: var(--itf-board-column-hover-color);
32
+ border-radius: var(--bs-border-radius);
33
+ }
34
+
35
+ .draggable-mirror {
36
+ opacity: .5;
37
+ }
38
+
39
+ .draggable-source--is-dragging {
40
+ z-index: 100;
41
+ position: absolute;
42
+ }
43
+
44
+ .draggable-container--over .itf-board-header-space,
45
+ .draggable-container--over .itf-board-card-space {
46
+ display: block;
47
+ }
48
+
49
+ .draggable-container--over .draggable--over .itf-board-header-space,
50
+ .draggable-container--over .draggable--over .itf-board-card-space, {
51
+ background: var(--itf-board-placeholder-color);
52
+ }
53
+
54
+ .draggable-container--is-dragging .itf-editable-button .itf-editable-button__button {
55
+ opacity: 0 !important;
56
+ }
57
+
58
+ .itf-board-header-space {
59
+ height: 100%;
60
+ position: absolute;
61
+ left: 0;
62
+ display: none;
63
+ background: transparent;
64
+ z-index: 100;
65
+ width: 4px;
66
+ top: 0;
67
+ border-radius: 10px;
68
+ margin-left: -2px;
69
+
70
+ &.right {
71
+ left: auto;
72
+ right: -2px;
73
+ top: 0;
74
+ }
75
+
76
+ &.active {
77
+ display: block;
78
+
79
+ &.over {
80
+ background: var(--itf-board-placeholder-color);
81
+ }
82
+ }
83
+
84
+ .draggable-container--over & {
85
+ display: block;
86
+ }
87
+
88
+ .draggable-container--over .draggable--over & {
89
+ background: var(--itf-board-placeholder-color);
90
+ }
91
+
92
+ .itf-board-header-dropzone {
93
+ position: absolute;
94
+ height: 100px;
95
+ top: 0;
96
+ right: -50px;
97
+ bottom: 0;
98
+ left: -50px;
99
+ }
100
+ }
101
+ }
102
+
103
+ .itf-board-titles,
104
+ .itf-board-columns {
105
+ display: flex;
106
+ flex-wrap: nowrap;
107
+
108
+ .itf-board-column {
109
+ position: relative;
110
+ width: var(--itf-board-column-width);
111
+ min-width: var(--itf-board-column-width);
112
+ border-radius: 0 0 var(--bs-border-radius) var(--bs-border-radius);
113
+
114
+ &__title {
115
+ cursor: move;
116
+ border-radius: var(--bs-border-radius) var(--bs-border-radius) 0 0;
117
+
118
+ .itf-board-column__header {
119
+ min-height: 1.5rem;
120
+ }
121
+
122
+ .itf-editable-button__button {
123
+ top: -3px;
124
+ right: -3px;
125
+ }
126
+ }
127
+
128
+ &.over {
129
+ background-color: var(--itf-board-column-hover-color);
130
+ }
131
+
132
+ &.drop:not(.itf-board-column__title) {
133
+ &:after {
134
+ content: '';
135
+ position: absolute;
136
+ left: 0;
137
+ background: var(--itf-board-placeholder-color);
138
+ z-index: 100;
139
+ width: 4px;
140
+ top: -10px;
141
+ bottom: 0;
142
+ border-radius: 10px;
143
+ margin-left: -2px;
144
+ }
145
+
146
+ &.right:after {
147
+ left: auto;
148
+ right: -2px;
149
+ }
150
+ }
151
+ }
152
+ }
153
+
154
+ .itf-board-titles {
155
+ border-bottom: 1px solid var(--bs-border-color);
156
+ }
157
+
158
+ .itf-board-columns {
159
+ max-height: 100%;
160
+
161
+ .itf-board-column {
162
+ display: flex;
163
+ }
164
+ .itf-board-card-space {
165
+ display: none;
166
+ background: transparent;
167
+ height: 0px;
168
+ position: relative;
169
+
170
+ &:after {
171
+ content: '';
172
+ position: absolute;
173
+ height: 5px;
174
+ left: 5px;
175
+ right: 5px;
176
+ z-index: 100;
177
+ border-radius: 10px;
178
+ top: -8px;
179
+ }
180
+
181
+ .itf-board-header-dropzone {
182
+ position: relative;
183
+ right: 0;
184
+ top: -50px;
185
+ left: 0;
186
+ bottom: -50px;
187
+ height: 100px;
188
+ }
189
+ &.bottom {
190
+ top: auto;
191
+ position: relative;
192
+ }
193
+
194
+ &.active {
195
+ display: block;
196
+
197
+ &.over:after {
198
+ background: var(--itf-board-placeholder-color);
199
+ }
200
+ }
201
+
202
+ .draggable-container--over & {
203
+ display: block;
204
+ }
205
+
206
+ .draggable-container--over .draggable--over & {
207
+ background: var(--itf-board-placeholder-color);
208
+ }
209
+ }
210
+ }
211
+
212
+ .itf-board-card {
213
+ background-color: var(--itf-board-card-bg-color);
214
+ border-radius: var(--bs-border-radius);
215
+ border: 1px solid var(--bs-border-color);
216
+ margin-bottom: .75rem;
217
+ overflow: hidden;
218
+ user-select: none;
219
+
220
+ .itf-board-card-inner {
221
+ padding: .5rem;
222
+ font-size: .875rem;
223
+ word-wrap: break-word;
224
+ -webkit-line-clamp: unset;
225
+ overflow-x: unset;
226
+ word-break: break-word;
227
+ }
228
+ }
229
+
230
+ .itf-board-column-wrapper {
231
+ display: flex;
232
+ flex-direction: column;
233
+ max-height: 100%;
234
+ flex-grow: 1;
235
+ }
236
+
237
+ .itf-board-cards-wrapper {
238
+ overflow: auto;
239
+ flex-grow: 1;
240
+ padding-right: .25rem;
241
+ padding-top: .5rem;
242
+
243
+ .draggable-source--is-dragging {
244
+ position: relative !important;
245
+ .itf-board-card {
246
+ background-color: var(--bs-border-color);
247
+
248
+ & > div {
249
+ opacity: 0;
250
+ }
251
+ }
252
+ }
253
+
254
+ .draggable-mirror {
255
+ padding: 0;
256
+ opacity: .8;
257
+ background: transparent;
258
+ min-width: auto;
259
+ .itf-board-card {
260
+ transform: rotate(-2deg);
261
+ border: 2px solid var(--itf-board-placeholder-border-color);
262
+ background: var(--itf-board-placeholder-bg-color);
263
+ }
264
+ }
265
+
266
+ & > div {
267
+ position: relative;
268
+ }
269
+ }
@@ -0,0 +1,165 @@
1
+ import { Draggable } from '@shopify/draggable';
2
+ import DraggableEvent from './event';
3
+
4
+ const DRAGGABLE_CLASS = 'draggable-item'
5
+ const DRAG_HANDLE_CLASS = 'drag-handle'
6
+ const SORTABLE_ATTRIBUTES = ['drag-ignore-handle', 'scrollable'];
7
+
8
+ const DEFAULT_OPTIONS = {
9
+ draggableClass: DRAGGABLE_CLASS,
10
+ dragHandleClass: DRAG_HANDLE_CLASS,
11
+ delay: 200,
12
+ tresholdDistance: 2,
13
+ draggable: `.${DRAGGABLE_CLASS}`,
14
+ handle: `.${DRAG_HANDLE_CLASS}`,
15
+ ignoreHandleClassList: SORTABLE_ATTRIBUTES,
16
+ mirror: {
17
+ yAxis: false,
18
+ constrainDimensions: true
19
+ },
20
+ scrollable: {
21
+ speed: 20,
22
+ sensitivity: 80
23
+ }
24
+ };
25
+
26
+ export default
27
+ function createDraggable(options = {}) {
28
+ const draggableNode = new Draggable([], Object.assign({}, DEFAULT_OPTIONS, options));
29
+
30
+ return {
31
+ Node: draggableNode,
32
+ dragHandle: {
33
+ inserted(el) {
34
+ if (el.getAttribute('drag-disabled') !== "true") {
35
+ el.classList.add(DRAG_HANDLE_CLASS);
36
+ } else {
37
+ el.classList.remove(DRAG_HANDLE_CLASS);
38
+ }
39
+ },
40
+ update(el) {
41
+ if (el.getAttribute('drag-disabled') !== "true") {
42
+ el.classList.add(DRAG_HANDLE_CLASS);
43
+ } else {
44
+ el.classList.remove(DRAG_HANDLE_CLASS);
45
+ }
46
+ }
47
+ },
48
+ dropzone: {
49
+ inserted(el, {value}) {
50
+ el.over = false;
51
+ el.dropzonePayload = value.payload;
52
+ el.dropCondition = value.condition;
53
+
54
+ el.dropzoneDragMove = event => {
55
+ if (el.initialized) {
56
+ return;
57
+ }
58
+ el.initialized = true;
59
+ if (el.getAttribute("accept-group").split(",").includes(event.group)) {
60
+ el.classList.add('active');
61
+ draggableNode.on('drag:move', el.onDragMoveReal);
62
+ el.event = event
63
+ }
64
+ };
65
+
66
+ el.onDragMoveReal = event => {
67
+ const { target } = event.sensorEvent;
68
+ if (target === el || el.contains(target)) {
69
+ el.over === false && (!el.dropCondition || el.dropCondition({
70
+ dropzonePayload: el.dropzonePayload,
71
+ draggablePayload: el.event.data.draggablePayload
72
+ })) && (el.dispatchEvent(new Event("enter")), el.classList.add("over"), el.over = !0)
73
+ } else {
74
+ el.over === !0 && (el.dispatchEvent(new Event("leave")), el.classList.remove("over"), el.over = !1);
75
+ }
76
+ };
77
+
78
+ el.onDragStop = (event) => {
79
+ if (draggableNode.off('drag:move', el.onDragMoveReal),
80
+ el.classList.remove("active"),
81
+ el.initialized = false,
82
+ !el.over) {
83
+ return;
84
+ }
85
+ el.classList.remove("over");
86
+ el.over = false;
87
+ const detail = {
88
+ ...el.event.data,
89
+ dropzonePayload: el.dropzonePayload,
90
+ newComponent: el
91
+ };
92
+ el.dispatchEvent(new CustomEvent('receive', { detail }));
93
+ el.event.sourceComponent.dispatchEvent(new CustomEvent('drop', { detail }));
94
+ };
95
+
96
+ draggableNode.on("vue:drag:move", el.dropzoneDragMove).on("drag:stop", el.onDragStop);
97
+ if (draggableNode.dragging) {
98
+ el.dropzoneDragMove(draggableNode.lastEvent)
99
+ }
100
+ },
101
+ update(el, {value}) {
102
+ el.dropzonePayload = value.payload
103
+ },
104
+ unbind(el) {
105
+ draggableNode.off("vue:drag:move", el.dropzoneDragMove).off("drag:stop", el.onDragStop).off("drag:move", el.onDragMoveReal)
106
+ }
107
+ },
108
+ draggable: {
109
+ inserted(el, {value}) {
110
+ el.currentDraggablePayload = value?.payload;
111
+ el.classList.add(draggableNode.options.draggableClass);
112
+ value?.handle && el.classList.add(draggableNode.options.dragHandleClass);
113
+ value?.mirror && (el.dataset.draggableMirror = JSON.stringify(value.mirror));
114
+
115
+ el.onDragStart = event => {
116
+ if (draggableNode.options.ignoreHandleClassList.some(M => event.sensorEvent.target.classList.contains(M))) {
117
+ event.cancel();
118
+ return
119
+ }
120
+ if (el === event.originalEvent.target.closest("." + draggableNode.options.draggableClass)) {
121
+ if (event.originalEvent.target.tagName === "INPUT") {
122
+ event.cancel();
123
+ return
124
+ }
125
+ el.draggablePayload = el.currentDraggablePayload;
126
+ draggableNode.on("drag:move", el.dragMove);
127
+ draggableNode.on("drag:stop", el.dragStop);
128
+ el.dispatchEvent(new Event("drag-start"));
129
+ }
130
+ };
131
+
132
+ el.dragMove = () => {
133
+ const event = new DraggableEvent({
134
+ draggablePayload: el.draggablePayload,
135
+ sourceComponent: el,
136
+ group: el.getAttribute("group")
137
+ });
138
+ draggableNode.trigger(event);
139
+ draggableNode.lastEvent = event;
140
+ };
141
+
142
+ el.dragStop = () => {
143
+ draggableNode.off("drag:move", el.dragMove);
144
+ draggableNode.off("drag:stop", el.dragStop);
145
+ }
146
+ draggableNode.on("drag:start", el.onDragStart);
147
+ },
148
+ update(el, { value }) {
149
+ el.classList.add(draggableNode.options.draggableClass);
150
+ el.currentDraggablePayload = value?.payload;
151
+ value?.mirror && (el.dataset.draggableMirror = JSON.stringify(value.mirror));
152
+ if (value?.handle) {
153
+ el.classList.add(draggableNode.options.dragHandleClass);
154
+ } else {
155
+ el.classList.remove(draggableNode.options.dragHandleClass);
156
+ }
157
+ },
158
+ unbind(el) {
159
+ draggableNode.off("drag:start", el.onDragStart);
160
+ draggableNode.off("drag:move", el.dragMove);
161
+ draggableNode.off("drag:stop", el.dragStop);
162
+ }
163
+ }
164
+ };
165
+ }
@@ -27,7 +27,7 @@
27
27
  <div class="table-row-template d-flex align-items-stretch"
28
28
  style="height: var(--group-title-height)">
29
29
  <div class="shadow-area"></div>
30
- <div class="header-wrapper" v-drag-handle>
30
+ <div class="header-wrapper">
31
31
  <div class="header-content position-sticky d-flex align-items-center">
32
32
  <a href="" class="collapse-arrow" @click.prevent="toggleGroup">
33
33
  <itf-icon :name="isShowTable ? 'chevron_down' : 'chevron_right'"/>
@@ -233,15 +233,20 @@
233
233
  import { Vue, Component, Prop, PropSync } from 'vue-property-decorator';
234
234
  import itfButton from '../button/Button.vue';
235
235
  import itfIcon from '../icon/Icon.vue';
236
- import { Draggable } from './draggable';
236
+ import createDraggable from '../sortable/draggable';
237
237
  import itfDropdown from '../dropdown/Dropdown.vue';
238
238
 
239
+ const { Node, ...draggableDirectives } = createDraggable();
240
+
239
241
  export default @Component({
240
242
  name: 'itfTableHeader',
241
243
  components: {
242
244
  itfDropdown,
243
245
  itfButton,
244
246
  itfIcon
247
+ },
248
+ directives: {
249
+ ...draggableDirectives
245
250
  }
246
251
  })
247
252
  class itfTable extends Vue {
@@ -336,13 +341,13 @@ class itfTable extends Vue {
336
341
 
337
342
  beforeDestroy() {
338
343
  if (this.columnSorting) {
339
- Draggable.removeContainer(this.$refs.container);
344
+ Node.removeContainer(this.$refs.container);
340
345
  }
341
346
  }
342
347
 
343
348
  mounted() {
344
349
  if (this.columnSorting) {
345
- Draggable.addContainer(this.$refs.container);
350
+ Node.addContainer(this.$refs.container);
346
351
  }
347
352
  if (this.columnResizing) {
348
353
  this.initResizing();
@@ -1,161 +0,0 @@
1
- import Vue from 'vue';
2
- import { Draggable } from '@shopify/draggable';
3
- import DraggableEvent from './event';
4
-
5
- const DRAGGABLE_CLASS = 'draggable-item'
6
- const DRAG_HANDLE_CLASS = 'drag-handle'
7
- const SORTABLE_ATTRIBUTES = ['drag-ignore-handle', 'scrollable'];
8
-
9
- const draggableNode = new Draggable([], {
10
- draggableClass: DRAGGABLE_CLASS,
11
- dragHandleClass: DRAG_HANDLE_CLASS,
12
- delay: 200,
13
- tresholdDistance: 2,
14
- draggable: `.${DRAGGABLE_CLASS}`,
15
- handle: `.${DRAG_HANDLE_CLASS}`,
16
- ignoreHandleClassList: SORTABLE_ATTRIBUTES,
17
- mirror: {
18
- yAxis: false,
19
- constrainDimensions: !0
20
- },
21
- scrollable: {
22
- speed: 20,
23
- sensitivity: 80
24
- }
25
- });
26
-
27
- export {draggableNode as Draggable};
28
-
29
- Vue.directive('DragHandle', {
30
- inserted(el) {
31
- if (el.getAttribute("drag-disabled") !== "true") {
32
- el.classList.add(DRAG_HANDLE_CLASS);
33
- } else {
34
- el.classList.remove(DRAG_HANDLE_CLASS);
35
- }
36
- },
37
- update(el) {
38
- if (el.getAttribute("drag-disabled") !== "true") {
39
- el.classList.add(DRAG_HANDLE_CLASS);
40
- } else {
41
- el.classList.remove(DRAG_HANDLE_CLASS);
42
- }
43
- }
44
- });
45
-
46
- Vue.directive("dropzone", {
47
- inserted(el, {value}) {
48
- // console.log('dropzone');
49
- el.over = false;
50
- el.dropzonePayload = value.payload;
51
- el.dropCondition = value.condition;
52
-
53
- el.dropzoneDragMove = event => {
54
- // console.log('dropzone event', event);
55
- el.initialized || (el.initialized = !0,
56
- el.getAttribute("accept-group").split(",").includes(event.group) && (el.classList.add("active"),
57
- draggableNode.on("drag:move", el.onDragMoveReal),
58
- el.event = event))
59
- };
60
-
61
- el.onDragMoveReal = event => {
62
- const { target } = event.sensorEvent;
63
- if (target === el || el.contains(target)) {
64
- el.over === false && (!el.dropCondition || el.dropCondition({
65
- dropzonePayload: el.dropzonePayload,
66
- draggablePayload: el.event.data.draggablePayload
67
- })) && (el.dispatchEvent(new Event("enter")), el.classList.add("over"), el.over = !0)
68
- } else {
69
- el.over === !0 && (el.dispatchEvent(new Event("leave")), el.classList.remove("over"), el.over = !1);
70
- }
71
- };
72
-
73
- el.onDragStop = (event) => {
74
- if (draggableNode.off("drag:move", el.onDragMoveReal),
75
- el.classList.remove("active"),
76
- el.initialized = false,
77
- !el.over) {
78
- return;
79
- }
80
- el.classList.remove("over");
81
- el.over = false;
82
- const detail = {
83
- ...el.event.data,
84
- // dropzonePayload: p()(el.dropzonePayload),
85
- dropzonePayload: el.dropzonePayload,
86
- newComponent: el
87
- };
88
- el.dispatchEvent(new CustomEvent("receive", {detail}));
89
- el.event.sourceComponent.dispatchEvent(new CustomEvent("drop", {detail}));
90
- };
91
-
92
- draggableNode.on("vue:drag:move", el.dropzoneDragMove).on("drag:stop", el.onDragStop),
93
- draggableNode.dragging && el.dropzoneDragMove(draggableNode.lastEvent)
94
- },
95
- update(el, {value}) {
96
- el.dropzonePayload = value.payload
97
- },
98
- unbind(el) {
99
- draggableNode.off("vue:drag:move", el.dropzoneDragMove).off("drag:stop", el.onDragStop).off("drag:move", el.onDragMoveReal)
100
- }
101
- });
102
-
103
- Vue.directive("draggable", {
104
- inserted(el, {value}) {
105
- // console.log('directive - draggable');
106
- el.currentDraggablePayload = value?.payload;
107
- el.classList.add(draggableNode.options.draggableClass);
108
- value?.handle && el.classList.add(draggableNode.options.dragHandleClass);
109
- value?.mirror && (el.dataset.draggableMirror = JSON.stringify(value.mirror));
110
-
111
- el.onDragStart = event => {
112
- // console.log('drag start', el.currentDraggablePayload)
113
- if (draggableNode.options.ignoreHandleClassList.some(M => event.sensorEvent.target.classList.contains(M))) {
114
- event.cancel();
115
- return
116
- }
117
- if (el === event.originalEvent.target.closest("." + draggableNode.options.draggableClass)) {
118
- if (event.originalEvent.target.tagName === "INPUT") {
119
- event.cancel();
120
- return
121
- }
122
- el.draggablePayload = el.currentDraggablePayload;
123
- draggableNode.on("drag:move", el.dragMove);
124
- draggableNode.on("drag:stop", el.dragStop);
125
- el.dispatchEvent(new Event("drag-start"));
126
- }
127
- };
128
-
129
- el.dragMove = () => {
130
- // console.log('onDragMove')
131
- const D = new DraggableEvent({
132
- draggablePayload: el.draggablePayload,
133
- sourceComponent: el,
134
- group: el.getAttribute("group")
135
- });
136
- draggableNode.trigger(D);
137
- draggableNode.lastEvent = D;
138
- };
139
-
140
- el.dragStop = () => {
141
- draggableNode.off("drag:move", el.dragMove);
142
- draggableNode.off("drag:stop", el.dragStop);
143
- }
144
- draggableNode.on("drag:start", el.onDragStart);
145
- },
146
- update(el, { value }) {
147
- el.classList.add(draggableNode.options.draggableClass);
148
- el.currentDraggablePayload = value?.payload;
149
- value?.mirror && (el.dataset.draggableMirror = JSON.stringify(value.mirror));
150
- if (value?.handle) {
151
- el.classList.add(draggableNode.options.dragHandleClass);
152
- } else {
153
- el.classList.remove(draggableNode.options.dragHandleClass);
154
- }
155
- },
156
- unbind(el) {
157
- draggableNode.off("drag:start", el.onDragStart);
158
- draggableNode.off("drag:move", el.dragMove);
159
- draggableNode.off("drag:stop", el.dragStop);
160
- }
161
- })
File without changes