@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.
@@ -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,116 @@
1
+ <template>
2
+ <div class="itf-board-column-wrapper">
3
+ <itf-button 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"
10
+ ref="container"
11
+ accept-group="board-cards"
12
+ @enter="cardHighlight(0, 'enter', 'drop')"
13
+ @leave="cardHighlight(0, 'leave', 'drop')"
14
+ v-dropzone="{ payload: { column } }"
15
+ >
16
+ <template v-if="!items.length">
17
+ <slot name="empty"></slot>
18
+ </template>
19
+ <template v-else>
20
+ <div v-for="(item, index) of items" :key="index" :data-card="index">
21
+ <div accept-group="board-cards"
22
+ class="itf-board-card-space"
23
+ @enter="cardHighlight(index, 'enter', 'drop')"
24
+ @leave="cardHighlight(index, 'leave', 'drop')"
25
+ v-dropzone="{ payload: { index, item, column } }">
26
+ <div class="itf-board-header-dropzone"></div>
27
+ </div>
28
+ <div group="board-cards"
29
+ @drop="board.reorderCards"
30
+ v-draggable="{ handle: true, payload: { index, item, column }, mirror: { xAxis: true, yAxis: true } }">
31
+ <slot name="item" :item="item" :index="index"></slot>
32
+ </div>
33
+ </div>
34
+ </template>
35
+ <div @enter="cardHighlightLast('enter', 'drop')"
36
+ @leave="cardHighlightLast('leave', 'drop')"
37
+ accept-group="board-cards"
38
+ class="itf-board-card-space bottom"
39
+ v-dropzone="{ payload:{ last: true, column } }">
40
+ <div class="itf-board-header-dropzone"></div>
41
+ </div>
42
+ </div>
43
+ </div>
44
+ </template>
45
+ <script>
46
+ import { Component, Prop, Vue, Inject } from 'vue-property-decorator';
47
+ import itfIcon from '../icon/Icon';
48
+ import itfButton from '../button/Button';
49
+ import loading from '../../directives/loading';
50
+ import itfForm from '../form/Form';
51
+ import itfEditButton from '../editable/EditButton';
52
+ import createDraggable from '../sortable/draggable';
53
+
54
+ const { Node, ...draggableDirectives } = createDraggable({
55
+ mirror: {
56
+ yAxis: true,
57
+ xAxis: true,
58
+ constrainDimensions: true
59
+ },
60
+ scrollable: {
61
+ speed: 20,
62
+ sensitivity: 80
63
+ }
64
+ });
65
+
66
+ export default @Component({
67
+ components: {
68
+ itfIcon,
69
+ itfForm,
70
+ itfEditButton,
71
+ itfButton
72
+ },
73
+ directives: {
74
+ ...draggableDirectives,
75
+ loading
76
+ }
77
+ })
78
+ class BoardColumn extends Vue {
79
+ @Inject({ default: null }) board;
80
+
81
+ @Prop() title;
82
+ @Prop() column;
83
+ @Prop(Array) items;
84
+ @Prop(Boolean) cardSorting;
85
+
86
+ beforeDestroy() {
87
+ if (this.cardSorting) {
88
+ Node.removeContainer(this.$refs.container);
89
+ }
90
+ }
91
+
92
+ mounted() {
93
+ if (this.cardSorting) {
94
+ Node.addContainer(this.$refs.container);
95
+ }
96
+ }
97
+
98
+ cardHighlight(index, state, className = 'over') {
99
+ this.isHaveHighlight = state === 'enter';
100
+ Array.from(this.$el.querySelectorAll(`[data-card="${index}"]`)).forEach(t=>{
101
+ state === 'enter' ? t.classList.add(className) : t.classList.remove(className)
102
+ });
103
+ }
104
+ cardHighlightLast(state, className = 'over') {
105
+ Array.from(this.$el.querySelectorAll(`[data-card="${this.items.length - 1}"]`)).forEach(t=>{
106
+ if (state === "enter") {
107
+ t.classList.add(className);
108
+ t.classList.add('bottom');
109
+ } else {
110
+ t.classList.remove(className);
111
+ t.classList.remove('bottom');
112
+ }
113
+ });
114
+ }
115
+ }
116
+ </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,266 @@
1
+ :root {
2
+ --itf-board-column-hover-color: #f5f9fe;
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
+
18
+ .itf-board {
19
+ --bs-border-color: #424448;
20
+
21
+ display: flex;
22
+ flex-wrap: nowrap;
23
+ flex-direction: column;
24
+
25
+ .draggable-mirror {
26
+ z-index: 100000001;
27
+ min-width: var(--itf-board-column-width);
28
+ padding: 1rem;
29
+ background-color: var(--itf-board-column-hover-color);
30
+ border-radius: var(--bs-border-radius);
31
+ }
32
+
33
+ .draggable-mirror {
34
+ opacity: .5;
35
+ }
36
+
37
+ .draggable-source--is-dragging {
38
+ z-index: 100;
39
+ position: absolute;
40
+ }
41
+
42
+ .draggable-container--over .itf-board-header-space,
43
+ .draggable-container--over .itf-board-card-space {
44
+ display: block;
45
+ }
46
+
47
+ .draggable-container--over .draggable--over .itf-board-header-space,
48
+ .draggable-container--over .draggable--over .itf-board-card-space, {
49
+ background: var(--itf-board-placeholder-color);
50
+ }
51
+
52
+ .draggable-container--is-dragging .itf-editable-button .itf-editable-button__button {
53
+ opacity: 0 !important;
54
+ }
55
+
56
+ .itf-board-header-space {
57
+ height: 100%;
58
+ position: absolute;
59
+ left: 0;
60
+ display: none;
61
+ background: transparent;
62
+ z-index: 100;
63
+ width: 4px;
64
+ top: 0;
65
+ border-radius: 10px;
66
+ margin-left: -2px;
67
+
68
+ &.right {
69
+ left: auto;
70
+ right: -2px;
71
+ top: 0;
72
+ }
73
+
74
+ &.active {
75
+ display: block;
76
+
77
+ &.over {
78
+ background: var(--itf-board-placeholder-color);
79
+ }
80
+ }
81
+
82
+ .draggable-container--over & {
83
+ display: block;
84
+ }
85
+
86
+ .draggable-container--over .draggable--over & {
87
+ background: var(--itf-board-placeholder-color);
88
+ }
89
+
90
+ .itf-board-header-dropzone {
91
+ position: absolute;
92
+ height: 100px;
93
+ top: 0;
94
+ right: -50px;
95
+ bottom: 0;
96
+ left: -50px;
97
+ }
98
+ }
99
+ }
100
+
101
+ .itf-board-titles,
102
+ .itf-board-columns {
103
+ display: flex;
104
+ flex-wrap: nowrap;
105
+
106
+ .itf-board-column {
107
+ position: relative;
108
+ width: var(--itf-board-column-width);
109
+ border-radius: 0 0 var(--bs-border-radius) var(--bs-border-radius);
110
+
111
+ &__title {
112
+ cursor: move;
113
+ border-radius: var(--bs-border-radius) var(--bs-border-radius) 0 0;
114
+
115
+ .itf-board-column__header {
116
+ min-height: 1.5rem;
117
+ }
118
+
119
+ .itf-editable-button__button {
120
+ top: -3px;
121
+ right: -3px;
122
+ }
123
+ }
124
+
125
+ &.over {
126
+ background-color: var(--itf-board-column-hover-color);
127
+ }
128
+
129
+ &.drop:not(.itf-board-column__title) {
130
+ &:after {
131
+ content: '';
132
+ position: absolute;
133
+ left: 0;
134
+ background: var(--itf-board-placeholder-color);
135
+ z-index: 100;
136
+ width: 4px;
137
+ top: -10px;
138
+ bottom: 0;
139
+ border-radius: 10px;
140
+ margin-left: -2px;
141
+ }
142
+
143
+ &.right:after {
144
+ left: auto;
145
+ right: -2px;
146
+ }
147
+ }
148
+ }
149
+ }
150
+
151
+ .itf-board-titles {
152
+ border-bottom: 1px solid var(--bs-border-color);
153
+ }
154
+
155
+ .itf-board-columns {
156
+ max-height: 100%;
157
+
158
+ .itf-board-column {
159
+ display: flex;
160
+ }
161
+ .itf-board-card-space {
162
+ display: none;
163
+ background: transparent;
164
+ height: 0px;
165
+ position: relative;
166
+
167
+ &:after {
168
+ content: '';
169
+ position: absolute;
170
+ height: 5px;
171
+ left: 5px;
172
+ right: 5px;
173
+ z-index: 100;
174
+ border-radius: 10px;
175
+ top: -8px;
176
+ }
177
+
178
+ .itf-board-header-dropzone {
179
+ position: relative;
180
+ right: 0;
181
+ top: -50px;
182
+ left: 0;
183
+ bottom: -50px;
184
+ height: 100px;
185
+ }
186
+ &.bottom {
187
+ top: auto;
188
+ position: relative;
189
+ }
190
+
191
+ &.active {
192
+ display: block;
193
+
194
+ &.over:after {
195
+ background: var(--itf-board-placeholder-color);
196
+ }
197
+ }
198
+
199
+ .draggable-container--over & {
200
+ display: block;
201
+ }
202
+
203
+ .draggable-container--over .draggable--over & {
204
+ background: var(--itf-board-placeholder-color);
205
+ }
206
+ }
207
+ }
208
+
209
+ .itf-board-card {
210
+ background-color: var(--itf-board-card-bg-color);
211
+ border-radius: var(--bs-border-radius);
212
+ border: 1px solid var(--bs-border-color);
213
+ margin-bottom: .75rem;
214
+ overflow: hidden;
215
+ user-select: none;
216
+
217
+ .itf-board-card-inner {
218
+ padding: .5rem;
219
+ font-size: .875rem;
220
+ word-wrap: break-word;
221
+ -webkit-line-clamp: unset;
222
+ overflow-x: unset;
223
+ word-break: break-word;
224
+ }
225
+ }
226
+
227
+ .itf-board-column-wrapper {
228
+ display: flex;
229
+ flex-direction: column;
230
+ max-height: 100%;
231
+ flex-grow: 1;
232
+ }
233
+
234
+ .itf-board-cards-wrapper {
235
+ overflow: auto;
236
+ flex-grow: 1;
237
+ padding-right: .25rem;
238
+ padding-top: .5rem;
239
+
240
+ .draggable-source--is-dragging {
241
+ position: relative !important;
242
+ .itf-board-card {
243
+ background-color: var(--bs-border-color);
244
+
245
+ & > div {
246
+ opacity: 0;
247
+ }
248
+ }
249
+ }
250
+
251
+ .draggable-mirror {
252
+ padding: 0;
253
+ opacity: .8;
254
+ background: transparent;
255
+ min-width: auto;
256
+ .itf-board-card {
257
+ transform: rotate(-2deg);
258
+ border: 2px solid var(--itf-board-placeholder-border-color);
259
+ background: var(--itf-board-placeholder-bg-color);
260
+ }
261
+ }
262
+
263
+ & > div {
264
+ position: relative;
265
+ }
266
+ }
@@ -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
+ }