Vue Destroy Sortable Element

42 views Asked by At

Summary

Create Panel, and add Layers to panel. When trying to delete a layer the last layer in the panel is deleted, instead of the layer item selected for destruction.

Full Issue:

Understanding control.html in the Vue.js Lower Third Graphics Controller

The control.html file is a crucial component of a web application designed to create and manage layers. This interface, built using Vue.js, allows users to configure and design layers with a high degree of customization.

Overview of control.html

The control.html page features a user-friendly interface with multiple sections, each dedicated to a specific aspect of the design process:

Design Editor Section: Offers a selection menu for adding various panel elements to the panel.

The body section is particularly interactive, as it supports drag-and-drop functionality, allowing users to reorder panel elements.

Issue: Incorrect Layer Deletion

A notable issue encountered in the control.html interface was with the deletion of specific layers. When attempting to remove a particular layer from a panel, the correct layer was not destroyed.

Troubleshooting and Solutions

To address this issue, several steps were undertaken to diagnose and rectify the problem:

Index Verification: First confirmed that the indexes passed to the destroyLayer method were accurate. This involved adding console.log statements to output the current indexes and verify their alignment with the intended targets.

Unique Keys: In the v-for directive, unique keys for each layer by binding the :key attribute to a unique property of each layer, usually an id, instead of array indices. This helps Vue identify each DOM element distinctly, maintaining the fidelity of operations like reordering and deleting.

Simplified destroyLayer Function: The destroyLayer method was simplified to remove layers directly using splice, without additional reactivity hacks like $set or $forceUpdate, which are typically unnecessary and can mask the root causes of reactivity issues.

https://jsfiddle.net/4vh80j35/

    <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue.js Control Application</title>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.10.2/Sortable.min.js"></script>
<style>
  .container {
    border: 2px solid #000;
    padding: 10px;
    margin-top: 10px;
  }
  .panel {
    border: 1px solid blue;
    margin: 5px;
    padding: 5px;
    position: relative;
  }
  .destroy-btn {
    color: white;
    background-color: red;
    padding: 2px 5px;
    cursor: pointer;
    position: absolute;
    right: 5px;
    top: 5px;
  }
  .layer {
    border: 1px solid green;
    margin: 3px;
    padding: 3px;
    position: relative;
    background-color: #efefef;
  }
  .draggable-container {
    min-height: 50px;
  }
</style>
</head>
<body>

<div id="app">
  <div class="editor">
    <button @click="createPanel">Create Panel</button>
    <div v-if="panels.length > 0">
      <label for="panelSelect">Choose a panel to add a layer:</label>
      <select id="panelSelect" v-model="selectedPanel">
        <option v-for="(panel, index) in panels" :value="index">Panel {{ index + 1 }}</option>
      </select>
      <button @click="createLayer">Add Layer to Selected Panel</button>
    </div>
  </div>
  <div class="container">
    <div class="panel" v-for="(panel, pIndex) in panels" :key="`panel-${pIndex}`" :data-index="pIndex">
      <span class="destroy-btn" @click="destroyPanel(pIndex)">[destroy]</span>
      Panel {{ pIndex + 1 }}
      <div class="draggable-container">
        <div class="layer" v-for="(layer, lIndex) in panel.layers" :key="`layer-${lIndex}`">
          Layer {{ lIndex + 1 }} (in Panel {{ pIndex + 1 }})
          <span class="destroy-btn" @click.stop="destroyLayer(pIndex, lIndex)">[destroy]</span>
        </div>
      </div>
    </div>

    
  </div>
</div>
<script>
  new Vue({
    el: '#app',
    data: {
      panels: [],
      selectedPanel: null
    },
    mounted() {
        this.$nextTick(() => {
          this.panels.forEach((_, index) => {
            this.makeSortable(index);
          });
        });
      },
    methods: {
      createPanel() {
        const newPanelIndex = this.panels.push({ layers: [] }) - 1;
        this.$nextTick(() => {
          this.makeSortable(newPanelIndex);
        });
        if (this.selectedPanel === null) {
          this.selectedPanel = 0;
        }
      },
      createLayer() {
        if (this.selectedPanel !== null && this.selectedPanel < this.panels.length) {
          this.panels[this.selectedPanel].layers.push({});
        }
      },
      makeSortable(panelIndex) {
          const container = this.$el.querySelector(`.panel[data-index="${panelIndex}"] .draggable-container`);
          Sortable.create(container, {
            group: 'shared', // set the group to 'shared' for all containers
            onAdd: (evt) => {
              console.log(`New ${evt.newIndex} from  ${evt.oldIndex} in panel`);
              const item = this.panels[evt.oldIndex].layers.splice(evt.oldIndex, 1)[0];
              this.panels[evt.newIndex].layers.splice(evt.newIndex, 0, item);
            },
            onUpdate: (evt) => {
              const panelIndex = evt.to.dataset.index;
              const item = this.panels[panelIndex].layers.splice(evt.oldIndex, 1)[0];
              this.panels[panelIndex].layers.splice(evt.newIndex, 0, item);
            },
          });
        },
      destroyPanel(index) {
        this.panels.splice(index, 1);
        if (index === this.selectedPanel) {
          this.selectedPanel = (this.panels.length > 0) ? 0 : null;
        }
      },
      destroyLayer(panelIndex, layerIndex) {
        console.log(`Attempting to destroy layer ${layerIndex} of  ${this.panels[panelIndex].layers.length} in panel ${panelIndex}`);
        if (panelIndex < this.panels.length) {
          const layers = this.panels[panelIndex].layers;
          if (layerIndex < layers.length) {
            // Removes the layer from the panel
            layers.splice(layerIndex, 1);
          }
          // If the panel becomes empty and it's the selected one, deselect it
          if (!layers.length && this.selectedPanel === panelIndex) {
            this.selectedPanel = null;
          }
        }
      },
    }
  });
</script>
</body>
</html>
1

There are 1 answers

2
Nikola Pavicevic On

In following example id is added to the every layer so You can observe destroying:

new Vue({
    el: '#app',
    data: {
      panels: [],
      selectedPanel: null
    },
    mounted() {
        this.$nextTick(() => {
          this.panels.forEach((_, index) => {
            this.makeSortable(index);
          });
        });
      },
    methods: {
      createPanel() {
        const newPanelIndex = this.panels.push({ layers: [] }) - 1;
        this.$nextTick(() => {
          this.makeSortable(newPanelIndex);
        });
        if (this.selectedPanel === null) {
          this.selectedPanel = 0;
        }
      },
      createLayer() {
        if (this.selectedPanel !== null && this.selectedPanel < this.panels.length) {
          const id = this.panels[this.selectedPanel].layers.length ? Math.max(...this.panels[this.selectedPanel].layers.map(o => o.id)) + 1 : 0
          this.panels[this.selectedPanel].layers.push({id});
        }
      },
      makeSortable(panelIndex) {
          const container = this.$el.querySelector(`.panel[data-index="${panelIndex}"] .draggable-container`);
          Sortable.create(container, {
            group: 'shared', // set the group to 'shared' for all containers
            onAdd: (evt) => {
              console.log(`New ${evt.newIndex} from  ${evt.oldIndex} in panel`);
              const item = this.panels[evt.oldIndex].layers.splice(evt.oldIndex, 1)[0];
              this.panels[evt.newIndex].layers.splice(evt.newIndex, 0, item);
            },
            onUpdate: (evt) => {
              const panelIndex = evt.to.dataset.index;
              const item = this.panels[panelIndex].layers.splice(evt.oldIndex, 1)[0];
              this.panels[panelIndex].layers.splice(evt.newIndex, 0, item);
            },
          });
        },
      destroyPanel(index) {
        this.panels.splice(index, 1);
        if (index === this.selectedPanel) {
          this.selectedPanel = (this.panels.length > 0) ? 0 : null;
        }
      },
      destroyLayer(panelIndex, layerIndex) {
        console.log(`Attempting to destroy layer ${layerIndex} of  ${this.panels[panelIndex].layers.length} in panel ${panelIndex}`);
        if (panelIndex < this.panels.length) {
          const layers = this.panels[panelIndex].layers;
          if (layerIndex < layers.length) {
            // Removes the layer from the panel
            layers.splice(layerIndex, 1);
          }
          // If the panel becomes empty and it's the selected one, deselect it
          if (!layers.length && this.selectedPanel === panelIndex) {
            this.selectedPanel = null;
          }
        }
      },
    }
  });
  .container {
    border: 2px solid #000;
    padding: 10px;
    margin-top: 10px;
  }
  .panel {
    border: 1px solid blue;
    margin: 5px;
    padding: 5px;
    position: relative;
  }
  .destroy-btn {
    color: white;
    background-color: red;
    padding: 2px 5px;
    cursor: pointer;
    position: absolute;
    right: 5px;
    top: 5px;
  }
  .layer {
    border: 1px solid green;
    margin: 3px;
    padding: 3px;
    position: relative;
    background-color: #efefef;
  }
  .draggable-container {
    min-height: 50px;
  }
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.10.2/Sortable.min.js"></script>
<div id="app">
  <div class="editor">
    <button @click="createPanel">Create Panel</button>
    <div v-if="panels.length > 0">
      <label for="panelSelect">Choose a panel to add a layer:</label>
      <select id="panelSelect" v-model="selectedPanel">
        <option v-for="(panel, index) in panels" :value="index">Panel {{ index + 1 }}</option>
      </select>
      <button @click="createLayer">Add Layer to Selected Panel</button>
    </div>
  </div>
  <div class="container">
    <div class="panel" v-for="(panel, pIndex) in panels" :key="`panel-${pIndex}`" :data-index="pIndex">
      <span class="destroy-btn" @click="destroyPanel(pIndex)">[destroy]</span>
      Panel {{ pIndex + 1 }}
      <div class="draggable-container">
        <div class="layer" v-for="(layer, lIndex) in panel.layers" :key="`layer-${lIndex}`">
          Layer {{ lIndex + 1 }} (in Panel {{ pIndex + 1 }}) id: {{layer.id}}
          <span class="destroy-btn" @click.stop="destroyLayer(pIndex, lIndex)">[destroy]</span>
        </div>
      </div>
    </div>
  </div>
</div>