The draggable component defines a source area from which elements can be dragged.
<draggable
v-if="sourcePanelActive"
:list="availableItems"
:group="{ name: 'dragGroup', pull: 'clone', put: false }"
:sort="false"
:move="validateDragOperation"
@end="handleDragEnd"
class="source-panel"
>
<div v-for="item in availableItems" :key="item.id" class="item-card">
<img :src="resolveItemImage(item)" :alt="item.title" />
<p>{{ item.label }}</p>
</div>
<div v-if="availableItems.length === 0" class="empty-state">
<img src="@/assets/no-data.png" alt="No data" />
<p>No items available</p>
</div>
</draggable>
Validation and event handling functions for the drag oeprations.
validateDragOperation(event) {
const draggedElement = event.draggedContext.element;
const isAlreadySelected = this.selectedItems.some(item => item.id === draggedElement.id);
if (isAlreadySelected) {
return false;
}
},
handleDragEnd(event) {
const insertionIndex = event.newIndex;
if (this.selectedItems.length > this.maxItems) {
if (this.selectedItems[insertionIndex + 1]?.id) {
this.selectedItems.splice(insertionIndex, 1);
} else {
this.selectedItems.splice(insertionIndex + 1, 1);
}
}
this.layoutArray = [...this.selectedItems];
}
The drop target area, including temlpate management UI.
<div class="template-editor">
<div class="editor-header">Template Builder</div>
<el-scrollbar>
<div class="editor-content">
<div class="layout-slot" v-for="(slot, idx) in layoutArray" :key="idx">
<span class="slot-index">{{ idx + 1 }}</span>
<div v-if="!slot.id" class="empty-slot">
<i class="el-icon-plus"></i>
</div>
<div v-else class="filled-slot" @click="selectSlot(slot)">
<img :src="resolveItemImage(slot)" :alt="slot.label" />
<p>{{ slot.label }}</p>
</div>
<span
v-if="slot.id"
class="remove-btn"
@click="removeItem(slot, idx)"
>
<i class="el-icon-minus"></i>
</span>
</div>
<div class="drop-layer">
<draggable :list="selectedItems" :group="{ name: 'dragGroup' }">
<div
class="drop-slot"
v-for="(item, idx) in selectedItems"
:key="idx"
>
<div v-if="!item.id" class="drop-empty"></div>
<div v-else class="drop-filled"></div>
<span
v-if="item.id"
class="invisible-remove"
@click="removeItem(item, idx)"
></span>
</div>
</draggable>
</div>
</div>
</el-scrollbar>
<div class="editor-actions">
<div class="actions-container">
<el-popover
placement="top-start"
width="280"
trigger="click"
popper-class="template-popover"
>
<el-scrollbar>
<div class="template-list">
<div
class="template-option"
:class="{ 'active': activeTemplateId === template.id }"
v-for="template in savedTemplates"
:key="template.id"
>
<div
class="template-name"
@click="loadTemplate(template)"
>
{{ template.name }}
</div>
<i
class="el-icon-delete"
@click.stop="deleteTemplate(template.id)"
></i>
</div>
<div
v-if="savedTemplates.length === 0"
class="no-templates"
>
No templates saved
</div>
</div>
</el-scrollbar>
<div class="action-btn" slot="reference">Select</div>
</el-popover>
<div class="action-btn primary" @click="openSaveDialog">Save</div>
<div class="action-btn primary" @click="applyTemplate">Apply</div>
</div>
</div>
<el-dialog
title="Save Template"
:visible.sync="saveDialogVisible"
width="30%"
>
<el-form
:model="templateForm"
:rules="validationRules"
ref="templateFormRef"
>
<el-form-item label="Template Name" prop="name">
<el-input
v-model="templateForm.name"
placeholder="Enter 3-10 characters"
></el-input>
</el-form-item>
</el-form>
<span slot="footer">
<el-button @click="saveDialogVisible = false">Cancel</el-button>
<el-button type="primary" @click="saveCurrentTemplate">Confirm</el-button>
</span>
</el-dialog>
</div>
The drop layer is positioned absolutely over the main layout area. This techinque prevents visual shifting of adjacent elements during drag operations, creating a consistent grid.
Import and register the draggable component.
import draggable from 'vuedraggable';
export default {
components: {
draggable
},
// ... other component options
}
Styling for the template editor interface.
.template-editor {
width: 400px;
background: #2d2d2d;
height: 100%;
.editor-header {
height: 60px;
border-bottom: 1px solid #fff;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 16px;
}
::v-deep .el-scrollbar {
.el-scrollbar__wrap {
overflow-x: hidden;
}
}
.editor-content {
width: 100%;
height: calc(100vh - 277px);
padding-top: 30px;
box-sizing: border-box;
position: relative;
.layout-slot {
width: 100%;
height: 50px;
margin-bottom: 30px;
display: flex;
align-items: center;
position: relative;
.slot-index {
color: #fff;
width: 10px;
margin-left: 10px;
display: block;
}
.empty-slot {
background: #2d2d2d;
border: 1px solid #999;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
color: #999;
width: 300px;
height: 100%;
margin-left: 20px;
box-sizing: border-box;
}
.filled-slot {
width: 300px;
background: #424242;
border-radius: 4px;
display: flex;
align-items: center;
padding: 10px;
color: #fff;
margin-left: 20px;
height: 100%;
box-sizing: border-box;
cursor: pointer;
img {
width: 30px;
height: 30px;
}
p {
width: 260px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.remove-btn {
display: block;
width: 20px;
height: 20px;
padding: 5px;
box-sizing: border-box;
border-radius: 50%;
background-color: #f00;
display: flex;
align-items: center;
justify-content: center;
margin-left: 14px;
cursor: pointer;
i {
color: #fff;
}
}
}
}
.drop-layer {
position: absolute;
background-color: transparent;
top: 0;
left: 0;
padding-top: 30px;
width: 340px;
padding-left: 20px;
box-sizing: border-box;
.drop-slot {
width: 360px;
background-color: transparent;
.drop-filled, .drop-empty {
width: 303px;
background-color: transparent;
border: none;
}
}
}
.editor-actions {
width: 340px;
height: 80px;
background: #383838;
position: fixed;
bottom: 0;
left: 0;
.actions-container {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: space-evenly;
.action-btn {
width: 72px;
height: 30px;
background: #383838;
border: 1px solid #006eff;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
color: #006eff;
cursor: pointer;
&.primary {
background: #006eff;
color: #fff;
}
}
}
}
}