@processmaker/screen-builder 2.83.11 → 2.84.1
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/dist/vue-form-builder.css +1 -1
- package/dist/vue-form-builder.es.js +4504 -4042
- package/dist/vue-form-builder.es.js.map +1 -1
- package/dist/vue-form-builder.umd.js +52 -52
- package/dist/vue-form-builder.umd.js.map +1 -1
- package/package.json +18 -2
- package/src/App.vue +34 -56
- package/src/assets/css/custom.css +11 -0
- package/src/assets/css/tabs.css +118 -0
- package/src/bootstrap.js +111 -0
- package/src/components/ScreenToolbar.vue +100 -0
- package/src/components/TabsBar.vue +233 -0
- package/src/components/editor/pagesDropdown.vue +125 -0
- package/src/components/index.js +1 -0
- package/src/components/inspector/color-select.vue +18 -1
- package/src/components/sortable/Sortable.vue +80 -0
- package/src/components/sortable/sortable.scss +25 -0
- package/src/components/sortable/sortableList/SortableList.vue +141 -0
- package/src/components/sortable/sortableList/sortableList.scss +73 -0
- package/src/components/vue-form-builder.vue +342 -253
- package/src/main.js +1 -2
- package/src/mixins/canOpenJsonFile.js +1 -1
- package/src/stories/ColorSelect.stories.js +79 -0
- package/src/stories/Configure.mdx +78 -0
- package/src/stories/DropdownAndPages.stories.js +113 -0
- package/src/stories/PageTabs.stories.js +338 -0
- package/src/stories/PagesDropdown.stories.js +132 -0
- package/src/stories/ScreenToolbar.stories.js +188 -0
- package/src/stories/Sortable.stories.js +236 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<b-tabs
|
|
3
|
+
ref="tabs"
|
|
4
|
+
v-model="activeTab"
|
|
5
|
+
class="h-100 w-100 flat-tabs"
|
|
6
|
+
content-class="h-tab"
|
|
7
|
+
lazy
|
|
8
|
+
@changed="tabsUpdated"
|
|
9
|
+
@input="tabOpened"
|
|
10
|
+
>
|
|
11
|
+
<template #tabs-start>
|
|
12
|
+
<div class="tabs-sticky d-flex flex-row tabs-start">
|
|
13
|
+
<div
|
|
14
|
+
v-show="tabsListOverflow"
|
|
15
|
+
class="position-relative overflow-visible"
|
|
16
|
+
>
|
|
17
|
+
<div
|
|
18
|
+
role="link"
|
|
19
|
+
class="nav-scroll nav-scroll-left"
|
|
20
|
+
data-test="scroll-left"
|
|
21
|
+
@click="scrollTabsLeft"
|
|
22
|
+
>
|
|
23
|
+
<i class="fas fa-chevron-left" />
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div :class="{'dd-ml': tabsListOverflow}">
|
|
29
|
+
<slot name="tabs-start" />
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
|
32
|
+
<b-tab
|
|
33
|
+
v-for="(index, n) in validLocalOpenedPages"
|
|
34
|
+
:key="`tab-${n}`"
|
|
35
|
+
class="h-100 w-100"
|
|
36
|
+
>
|
|
37
|
+
<template #title>
|
|
38
|
+
<b-badge variant="primary" class="mr-1">
|
|
39
|
+
{{ pageNumber(index) }}
|
|
40
|
+
</b-badge>
|
|
41
|
+
<span :data-test="`tab-${n}`">
|
|
42
|
+
{{ pages[index]?.name }}
|
|
43
|
+
</span>
|
|
44
|
+
<span
|
|
45
|
+
:data-test="`close-tab-${n}`"
|
|
46
|
+
class="close-tab"
|
|
47
|
+
role="link"
|
|
48
|
+
@click.stop="closeTab(n)"
|
|
49
|
+
>
|
|
50
|
+
<i class="fas fa-times" />
|
|
51
|
+
</span>
|
|
52
|
+
</template>
|
|
53
|
+
<template #default>
|
|
54
|
+
<div class="h-100 w-100" data-test="tab-content">
|
|
55
|
+
<slot :current-page="index" />
|
|
56
|
+
</div>
|
|
57
|
+
</template>
|
|
58
|
+
</b-tab>
|
|
59
|
+
<template #tabs-end>
|
|
60
|
+
<div
|
|
61
|
+
v-if="tabsListOverflow"
|
|
62
|
+
class="tabs-sticky overflow-visible"
|
|
63
|
+
>
|
|
64
|
+
<div
|
|
65
|
+
role="link"
|
|
66
|
+
class="nav-scroll nav-scroll-right"
|
|
67
|
+
data-test="scroll-right"
|
|
68
|
+
@click="scrollTabsRight"
|
|
69
|
+
>
|
|
70
|
+
<i class="fas fa-chevron-right" />
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</template>
|
|
74
|
+
<template #empty>
|
|
75
|
+
<p class="text-center m-5 text-secondary" data-test="tab-content">
|
|
76
|
+
{{ $t("There are no open pages.") }}<br />
|
|
77
|
+
{{ $t("Open a new page above using the button") }}
|
|
78
|
+
<i :class="buttonIcon" />
|
|
79
|
+
</p>
|
|
80
|
+
</template>
|
|
81
|
+
</b-tabs>
|
|
82
|
+
</template>
|
|
83
|
+
|
|
84
|
+
<script>
|
|
85
|
+
const SCROLL_STEP = 200;
|
|
86
|
+
|
|
87
|
+
export default {
|
|
88
|
+
props: {
|
|
89
|
+
/**
|
|
90
|
+
* The configuration of all the pages
|
|
91
|
+
*/
|
|
92
|
+
pages: {
|
|
93
|
+
type: Array,
|
|
94
|
+
required: true
|
|
95
|
+
},
|
|
96
|
+
/**
|
|
97
|
+
* The array of initial opened pages indexes
|
|
98
|
+
*/
|
|
99
|
+
initialOpenedPages: {
|
|
100
|
+
type: Array,
|
|
101
|
+
default: () => [0]
|
|
102
|
+
},
|
|
103
|
+
/**
|
|
104
|
+
* Icon to open a new tab, displayed when there are no pages opened.
|
|
105
|
+
*/
|
|
106
|
+
buttonIcon: {
|
|
107
|
+
type: String,
|
|
108
|
+
default: () => "fa fa-file"
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
data() {
|
|
112
|
+
return {
|
|
113
|
+
tabsListOverflow: false,
|
|
114
|
+
showLeftScroll: true,
|
|
115
|
+
showRightScroll: true,
|
|
116
|
+
updates: 0,
|
|
117
|
+
activeTab: 0,
|
|
118
|
+
localOpenedPages: this.initialOpenedPages
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
computed: {
|
|
122
|
+
validLocalOpenedPages() {
|
|
123
|
+
return this.localOpenedPages.filter((page) => this.pages[page]);
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
watch: {
|
|
127
|
+
openedPages: {
|
|
128
|
+
handler(newVal) {
|
|
129
|
+
this.localOpenedPages = newVal;
|
|
130
|
+
},
|
|
131
|
+
deep: true
|
|
132
|
+
},
|
|
133
|
+
pages: {
|
|
134
|
+
handler() {
|
|
135
|
+
this.localOpenedPages = this.localOpenedPages.filter(
|
|
136
|
+
(page) => this.pages[page]
|
|
137
|
+
);
|
|
138
|
+
},
|
|
139
|
+
deep: true
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
mounted() {
|
|
143
|
+
this.$nextTick(() => {
|
|
144
|
+
// check resize of tabs list
|
|
145
|
+
window.addEventListener("resize", this.checkTabsOverflow);
|
|
146
|
+
// listen to scroll event
|
|
147
|
+
const tablist = this.$refs.tabs.$el.querySelector(".nav-tabs");
|
|
148
|
+
tablist.addEventListener("scroll", this.checkScrollPosition);
|
|
149
|
+
|
|
150
|
+
Promise.resolve().then(() => {
|
|
151
|
+
this.checkTabsOverflow();
|
|
152
|
+
this.checkScrollPosition();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
},
|
|
157
|
+
beforeDestroy() {
|
|
158
|
+
window.removeEventListener("resize", this.checkTabsOverflow);
|
|
159
|
+
},
|
|
160
|
+
updated() {
|
|
161
|
+
this.checkTabsOverflow();
|
|
162
|
+
},
|
|
163
|
+
methods: {
|
|
164
|
+
tabOpened() {
|
|
165
|
+
const pageIndex = this.localOpenedPages[this.activeTab];
|
|
166
|
+
this.$emit("tab-opened", pageIndex);
|
|
167
|
+
},
|
|
168
|
+
pageNumber(index) {
|
|
169
|
+
return index + 1;
|
|
170
|
+
},
|
|
171
|
+
checkScrollPosition() {
|
|
172
|
+
const tablist = this.$refs.tabs.$el.querySelector(".nav-tabs");
|
|
173
|
+
this.showLeftScroll = tablist.scrollLeft > 0;
|
|
174
|
+
this.showRightScroll =
|
|
175
|
+
tablist.scrollWidth - tablist.clientWidth > tablist.scrollLeft;
|
|
176
|
+
},
|
|
177
|
+
scrollTabsLeft() {
|
|
178
|
+
const tablist = this.$refs.tabs.$el.querySelector(".nav-tabs");
|
|
179
|
+
tablist.scrollLeft -= SCROLL_STEP;
|
|
180
|
+
},
|
|
181
|
+
scrollTabsRight() {
|
|
182
|
+
const tablist = this.$refs.tabs.$el.querySelector(".nav-tabs");
|
|
183
|
+
tablist.scrollLeft += SCROLL_STEP;
|
|
184
|
+
},
|
|
185
|
+
tabsUpdated() {
|
|
186
|
+
this.updates++;
|
|
187
|
+
},
|
|
188
|
+
waitUpdates(n, timeout, visualThreshold = 80) {
|
|
189
|
+
return new Promise((resolve) => {
|
|
190
|
+
const start = Date.now();
|
|
191
|
+
const interval = setInterval(() => {
|
|
192
|
+
if (this.updates >= n || Date.now() - start > timeout) {
|
|
193
|
+
clearInterval(interval);
|
|
194
|
+
resolve();
|
|
195
|
+
}
|
|
196
|
+
}, visualThreshold);
|
|
197
|
+
});
|
|
198
|
+
},
|
|
199
|
+
closeTab(pageId) {
|
|
200
|
+
this.localOpenedPages.splice(this.localOpenedPages.indexOf(pageId), 1);
|
|
201
|
+
this.$emit("tab-closed", this.pages[pageId], this.localOpenedPages);
|
|
202
|
+
},
|
|
203
|
+
updateTabsReferences(pageDelete) {
|
|
204
|
+
this.localOpenedPages = this.localOpenedPages.map((page) =>
|
|
205
|
+
page > pageDelete ? page - 1 : page
|
|
206
|
+
);
|
|
207
|
+
},
|
|
208
|
+
async openPageByIndex(index) {
|
|
209
|
+
if (index === -1) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const n = this.localOpenedPages.indexOf(index * 1);
|
|
213
|
+
if (n === -1) {
|
|
214
|
+
this.localOpenedPages.push(index);
|
|
215
|
+
await this.waitUpdates(this.updates + 2, 1000);
|
|
216
|
+
this.activeTab = this.localOpenedPages.length - 1;
|
|
217
|
+
} else {
|
|
218
|
+
this.activeTab = n;
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
closePageByIndex(index) {
|
|
222
|
+
const n = this.localOpenedPages.indexOf(index);
|
|
223
|
+
if (n !== -1) {
|
|
224
|
+
this.localOpenedPages.splice(n, 1);
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
checkTabsOverflow() {
|
|
228
|
+
const tablist = this.$refs.tabs.$el.querySelector(".nav-tabs");
|
|
229
|
+
this.tabsListOverflow = tablist.scrollWidth > tablist.clientWidth;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
</script>
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!--
|
|
3
|
+
Dropdown component to display options related to pages.
|
|
4
|
+
Provides options to add a new page, see all pages, and select individual pages.
|
|
5
|
+
-->
|
|
6
|
+
<b-dropdown
|
|
7
|
+
ref="pageDropdown"
|
|
8
|
+
data-test="page-dropdown"
|
|
9
|
+
variant="platform"
|
|
10
|
+
:boundary="boundary"
|
|
11
|
+
menu-class="page-dropdown-menu"
|
|
12
|
+
>
|
|
13
|
+
<!-- Dropdown button content -->
|
|
14
|
+
<template #button-content>
|
|
15
|
+
<!-- Icon representing a file -->
|
|
16
|
+
<i class="fa fa-file"></i>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<!-- Option to add a new page -->
|
|
20
|
+
<b-dropdown-item data-test="add-page" @click="onAddPage">
|
|
21
|
+
<!-- Icon for adding a new page -->
|
|
22
|
+
<i class="fa fa-plus platform-dropdown-item-icon"></i>
|
|
23
|
+
<!-- Text for adding a new page -->
|
|
24
|
+
{{ $t("Create Page") }}
|
|
25
|
+
</b-dropdown-item>
|
|
26
|
+
|
|
27
|
+
<!-- Option to see all pages -->
|
|
28
|
+
<b-dropdown-item data-test="see-all-pages" @click="onSeeAllPages">
|
|
29
|
+
<!-- Icon for seeing all pages -->
|
|
30
|
+
<i class="fa fa-eye platform-dropdown-item-icon"></i>
|
|
31
|
+
<!-- Text for seeing all pages -->
|
|
32
|
+
{{ $t("See all pages") }}
|
|
33
|
+
</b-dropdown-item>
|
|
34
|
+
|
|
35
|
+
<!-- Divider between adding and viewing options -->
|
|
36
|
+
<b-dropdown-divider></b-dropdown-divider>
|
|
37
|
+
|
|
38
|
+
<!-- Dropdown items for selecting individual pages -->
|
|
39
|
+
<b-dropdown-item
|
|
40
|
+
v-for="(item, page) in data"
|
|
41
|
+
:key="page"
|
|
42
|
+
:data-test="'page-' + item.name"
|
|
43
|
+
:data-cy="'page-' + page"
|
|
44
|
+
@click="onClickPage(page)"
|
|
45
|
+
>
|
|
46
|
+
<!-- Display the name of the page -->
|
|
47
|
+
{{ item.name }}
|
|
48
|
+
</b-dropdown-item>
|
|
49
|
+
</b-dropdown>
|
|
50
|
+
</template>
|
|
51
|
+
|
|
52
|
+
<script>
|
|
53
|
+
/**
|
|
54
|
+
* Vue component for managing pages through a dropdown menu.
|
|
55
|
+
* @component
|
|
56
|
+
* @prop {Props} props - The component's props object.
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
export default {
|
|
60
|
+
/**
|
|
61
|
+
* The name of the component.
|
|
62
|
+
* @type {string}
|
|
63
|
+
*/
|
|
64
|
+
name: "PagesDropdown",
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The props that the component accepts.
|
|
68
|
+
* @type {Object}
|
|
69
|
+
* @property {PageItem[]} data - The array of page items to be displayed in the dropdown.
|
|
70
|
+
* Defaults to null.
|
|
71
|
+
*/
|
|
72
|
+
props: {
|
|
73
|
+
data: {
|
|
74
|
+
type: Array,
|
|
75
|
+
default: null
|
|
76
|
+
},
|
|
77
|
+
boundary: {
|
|
78
|
+
type: String,
|
|
79
|
+
default: "viewport"
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The methods available within the component.
|
|
85
|
+
*/
|
|
86
|
+
methods: {
|
|
87
|
+
/**
|
|
88
|
+
* Handler for when the "Add Page" option is clicked.
|
|
89
|
+
* Emits the "addPage" event.
|
|
90
|
+
*/
|
|
91
|
+
onAddPage() {
|
|
92
|
+
this.$emit("addPage");
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Handler for when the "See All Pages" option is clicked.
|
|
97
|
+
* Emits the "seeAllPages" event.
|
|
98
|
+
*/
|
|
99
|
+
onSeeAllPages() {
|
|
100
|
+
this.$emit("seeAllPages");
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Handler for when a specific page is clicked.
|
|
105
|
+
* Emits the "clickPage" event with the selected page.
|
|
106
|
+
* @param {PageItem} page - The selected page item.
|
|
107
|
+
*/
|
|
108
|
+
onClickPage(page) {
|
|
109
|
+
this.$emit("clickPage", this.data[page]);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
</script>
|
|
114
|
+
|
|
115
|
+
<style lang="scss" scoped>
|
|
116
|
+
// Platform btn style
|
|
117
|
+
.btn-platform {
|
|
118
|
+
background-color: #ffff;
|
|
119
|
+
color: #6a7888;
|
|
120
|
+
}
|
|
121
|
+
.platform-dropdown-item-icon {
|
|
122
|
+
// Style for the icons in dropdown items.
|
|
123
|
+
color: #1572c2;
|
|
124
|
+
}
|
|
125
|
+
</style>
|
package/src/components/index.js
CHANGED
|
@@ -47,6 +47,7 @@ import FormListTable from "./renderer/form-list-table.vue";
|
|
|
47
47
|
import FormAnalyticsChart from "./renderer/form-analytics-chart.vue";
|
|
48
48
|
import accordions from "@/components/accordions";
|
|
49
49
|
import VariableNameGenerator from "@/components/VariableNameGenerator";
|
|
50
|
+
import "../assets/css/tabs.css";
|
|
50
51
|
|
|
51
52
|
const rendererComponents = {
|
|
52
53
|
...renderer,
|
|
@@ -36,7 +36,24 @@
|
|
|
36
36
|
<script>
|
|
37
37
|
export default {
|
|
38
38
|
components: {},
|
|
39
|
-
props:
|
|
39
|
+
props: {
|
|
40
|
+
/**
|
|
41
|
+
* The label for the color select
|
|
42
|
+
*/
|
|
43
|
+
label: {},
|
|
44
|
+
/**
|
|
45
|
+
* The value of the color select. eg. `alert alert-success`
|
|
46
|
+
*/
|
|
47
|
+
value: {},
|
|
48
|
+
/**
|
|
49
|
+
* The helper text for the color select (not visible yet)
|
|
50
|
+
*/
|
|
51
|
+
helper: {},
|
|
52
|
+
/**
|
|
53
|
+
* The options for the color select
|
|
54
|
+
*/
|
|
55
|
+
options: {},
|
|
56
|
+
},
|
|
40
57
|
data() {
|
|
41
58
|
return {
|
|
42
59
|
newColor: ""
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="container sortable-box">
|
|
3
|
+
<div class="row">
|
|
4
|
+
<div class="col-sm border rounded-lg p-0 mr-3 sortable-search-box">
|
|
5
|
+
<i class="fa fa-search sortable-search-icon"></i>
|
|
6
|
+
<input
|
|
7
|
+
id="search"
|
|
8
|
+
v-model="search"
|
|
9
|
+
class="form-control border-0 shadow-none px-0"
|
|
10
|
+
:placeholder="$t('Search here')"
|
|
11
|
+
data-test="search"
|
|
12
|
+
/>
|
|
13
|
+
</div>
|
|
14
|
+
<div>
|
|
15
|
+
<button
|
|
16
|
+
type="button"
|
|
17
|
+
class="btn sortable-btn-new"
|
|
18
|
+
@click="$emit('add-page', $event)"
|
|
19
|
+
>
|
|
20
|
+
<i class="fa fa-plus"></i>
|
|
21
|
+
</button>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<SortableList
|
|
26
|
+
:items="items"
|
|
27
|
+
:filtered-items="filteredItems"
|
|
28
|
+
@ordered="$emit('ordered', $event)"
|
|
29
|
+
@item-edit="$emit('item-edit', $event)"
|
|
30
|
+
@item-delete="$emit('item-delete', $event)"
|
|
31
|
+
/>
|
|
32
|
+
</div>
|
|
33
|
+
</template>
|
|
34
|
+
|
|
35
|
+
<script>
|
|
36
|
+
import SortableList from './sortableList/SortableList.vue'
|
|
37
|
+
|
|
38
|
+
export default {
|
|
39
|
+
name: 'Sortable',
|
|
40
|
+
components: {
|
|
41
|
+
SortableList
|
|
42
|
+
},
|
|
43
|
+
props: {
|
|
44
|
+
items: { type: Array, required: true },
|
|
45
|
+
filterKey: { type: String, required: true },
|
|
46
|
+
},
|
|
47
|
+
data() {
|
|
48
|
+
return {
|
|
49
|
+
search: "",
|
|
50
|
+
filteredItems: [...this.items],
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
watch: {
|
|
54
|
+
search(value) {
|
|
55
|
+
this.filteredItems = this.filterItems(value, this.items);
|
|
56
|
+
},
|
|
57
|
+
items: {
|
|
58
|
+
handler(newItems) {
|
|
59
|
+
this.filteredItems = [...newItems];
|
|
60
|
+
|
|
61
|
+
if (this.search.length > 0) {
|
|
62
|
+
this.filteredItems = this.filterItems(this.search, newItems);
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
deep: true,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
methods: {
|
|
69
|
+
clearSearch(value) {
|
|
70
|
+
return value.trim().toLowerCase();
|
|
71
|
+
},
|
|
72
|
+
filterItems(searchValue, items) {
|
|
73
|
+
const cleanSearch = this.clearSearch(searchValue);
|
|
74
|
+
return items.filter((item) => item[this.filterKey].toLowerCase().includes(cleanSearch));
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<style lang="scss" scoped src="./sortable.scss"></style>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
.sortable {
|
|
2
|
+
&-box {
|
|
3
|
+
font-family: "Open Sans", sans-serif !important;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
&-search-box {
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
border-color: #cdddee !important;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
&-search-icon {
|
|
13
|
+
margin: {
|
|
14
|
+
left: 16px;
|
|
15
|
+
right: 8px;
|
|
16
|
+
}
|
|
17
|
+
color: #6A7888;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
&-btn-new {
|
|
21
|
+
background: #1572C2;
|
|
22
|
+
color: #ffffff;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="row mt-3">
|
|
3
|
+
<div class="col p-0 border rounded-lg sortable-list">
|
|
4
|
+
<div class="sortable-list-header">
|
|
5
|
+
<div class="sortable-item-icon"></div>
|
|
6
|
+
<div class="sortable-list-title">PAGE NAME</div>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="sortable-container" @dragover="dragOver">
|
|
9
|
+
<div
|
|
10
|
+
v-for="(item, index) in sortedItems"
|
|
11
|
+
:key="index"
|
|
12
|
+
:data-order="item.order"
|
|
13
|
+
:data-test="`item-${item.order}`"
|
|
14
|
+
:title="item.name"
|
|
15
|
+
draggable="true"
|
|
16
|
+
@dragstart="(event) => dragStart(event, item.order)"
|
|
17
|
+
@dragenter="(event) => dragEnter(event, item.order)"
|
|
18
|
+
@dragend="dragEnd"
|
|
19
|
+
class="sortable-item sortable-draggable"
|
|
20
|
+
>
|
|
21
|
+
<div class="sortable-item-icon">
|
|
22
|
+
<i class="fas fa-bars"></i>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="rounded sortable-item-name">
|
|
25
|
+
<b-form-input
|
|
26
|
+
v-if="editRowIndex === index"
|
|
27
|
+
v-model="item.name"
|
|
28
|
+
type="text"
|
|
29
|
+
autofocus
|
|
30
|
+
@blur.stop="onBlur()"
|
|
31
|
+
/>
|
|
32
|
+
<span v-else>{{ item.name }}</span>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="border rounded-lg sortable-item-action">
|
|
35
|
+
<button class="btn" @click.stop="onClick(item, index)">
|
|
36
|
+
<i class="fas fa-edit"></i>
|
|
37
|
+
</button>
|
|
38
|
+
<div class="sortable-item-vr"></div>
|
|
39
|
+
<button class="btn" @click="$emit('item-delete', item)">
|
|
40
|
+
<i class="fas fa-trash-alt"></i>
|
|
41
|
+
</button>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</template>
|
|
48
|
+
|
|
49
|
+
<script>
|
|
50
|
+
export default {
|
|
51
|
+
name: 'SortableList',
|
|
52
|
+
props: {
|
|
53
|
+
items: { type: Array, required: true },
|
|
54
|
+
filteredItems: { type: Array, required: true },
|
|
55
|
+
},
|
|
56
|
+
data() {
|
|
57
|
+
return {
|
|
58
|
+
draggedItem: 0,
|
|
59
|
+
draggedOverItem: 0,
|
|
60
|
+
editRowIndex: null,
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
computed: {
|
|
64
|
+
sortedItems() {
|
|
65
|
+
const sortedItems = [...this.filteredItems].sort(
|
|
66
|
+
(a, b) => a.order - b.order
|
|
67
|
+
);
|
|
68
|
+
return sortedItems;
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
methods: {
|
|
72
|
+
onBlur() {
|
|
73
|
+
this.editRowIndex = -1;
|
|
74
|
+
},
|
|
75
|
+
onClick(item, index) {
|
|
76
|
+
if (this.editRowIndex === -1 || this.editRowIndex === index) {
|
|
77
|
+
this.editRowIndex = null;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
this.editRowIndex = index;
|
|
81
|
+
this.$emit("item-edit", item);
|
|
82
|
+
},
|
|
83
|
+
dragStart(event, order) {
|
|
84
|
+
// disable edit mode
|
|
85
|
+
this.editRowIndex = null;
|
|
86
|
+
this.draggedItem = order;
|
|
87
|
+
// add dragging class to the element
|
|
88
|
+
event.target.classList.add('dragging');
|
|
89
|
+
},
|
|
90
|
+
dragEnter(event, order) {
|
|
91
|
+
this.draggedOverItem = order;
|
|
92
|
+
},
|
|
93
|
+
dragEnd(event) {
|
|
94
|
+
// remove dragging class from the element
|
|
95
|
+
event.target.classList.remove('dragging');
|
|
96
|
+
|
|
97
|
+
// get the index of the dragged item and the dragged over item
|
|
98
|
+
const itemsSortedClone = [...this.items].sort(
|
|
99
|
+
(a, b) => a.order - b.order
|
|
100
|
+
);
|
|
101
|
+
const draggedItemIndex = itemsSortedClone.findIndex(
|
|
102
|
+
(item) => item.order === this.draggedItem
|
|
103
|
+
);
|
|
104
|
+
const draggedOverItemIndex = itemsSortedClone.findIndex(
|
|
105
|
+
(item) => item.order === this.draggedOverItem
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
if (draggedItemIndex !== draggedOverItemIndex) {
|
|
109
|
+
// get the order of the dragged over item
|
|
110
|
+
const tempOrder = itemsSortedClone[draggedOverItemIndex].order;
|
|
111
|
+
// set the increment
|
|
112
|
+
const increment = this.draggedItem > this.draggedOverItem ? 1 : -1;
|
|
113
|
+
|
|
114
|
+
// update the order of the items between the dragged item and the dragged over item
|
|
115
|
+
if (draggedItemIndex < draggedOverItemIndex) {
|
|
116
|
+
for (let i = draggedItemIndex + 1; i <= draggedOverItemIndex; i++) {
|
|
117
|
+
const orderAux = itemsSortedClone[i].order;
|
|
118
|
+
itemsSortedClone[i].order = orderAux + increment;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
itemsSortedClone[draggedItemIndex].order = tempOrder;
|
|
122
|
+
} else if (draggedItemIndex > draggedOverItemIndex) {
|
|
123
|
+
for (let i = draggedOverItemIndex; i <= draggedItemIndex - 1; i++) {
|
|
124
|
+
const orderAux = itemsSortedClone[i].order;
|
|
125
|
+
itemsSortedClone[i].order = orderAux + increment;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
itemsSortedClone[draggedItemIndex].order = tempOrder;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
this.$emit('ordered', itemsSortedClone);
|
|
133
|
+
},
|
|
134
|
+
dragOver(event) {
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
</script>
|
|
140
|
+
|
|
141
|
+
<style lang="scss" scoped src="./sortableList.scss"></style>
|