<script setup>
// Reusable drag-to-reorder list for the repeater rows used across the program,
// workshop and page editors (slider texts, objectives, discovery points, Q&A, …).
//
// vuedraggable derives each row's :key from the element itself, which makes
// editable string lists lose input focus on every keystroke (the value IS the
// key). To avoid that we keep an internal array of { key, value } rows with
// stable synthetic keys, so reordering works for both primitive and object
// rows and editing never remounts an input.
import { ref, watch } from "vue";
import draggable from "vuedraggable";

const props = defineProps({
  modelValue: { type: Array, default: () => [] },
  disabled: { type: Boolean, default: false },
  // Extra classes for each row wrapper (e.g. spacing to match existing markup).
  rowClass: { type: [String, Array, Object], default: "" },
});

const emit = defineEmits(["update:modelValue", "reorder"]);

let seq = 0;
const rows = ref([]);
// When we emit a change ourselves, skip the resulting modelValue echo so we
// don't rebuild rows (which would drop stable keys / input focus).
let skipNextSync = false;

const buildRows = (list) =>
  (Array.isArray(list) ? list : []).map((value, i) => ({
    key: rows.value[i] ? rows.value[i].key : ++seq,
    value,
  }));

rows.value = buildRows(props.modelValue);

// Deep so in-place push/splice on the bound array (some parents mutate rather
// than reassign) still re-syncs the rows. Reuses keys by position, so editing
// an existing row never remounts its input.
watch(
  () => props.modelValue,
  (list) => {
    if (skipNextSync) {
      skipNextSync = false;
      return;
    }
    rows.value = buildRows(list);
  },
  { deep: true },
);

const commit = () => {
  skipNextSync = true;
  emit(
    "update:modelValue",
    rows.value.map((r) => r.value),
  );
};

// Replace one row's value (for slots that edit primitives via :update).
const setValue = (index, value) => {
  if (!rows.value[index]) return;
  rows.value[index].value = value;
  commit();
};

// vuedraggable reorders `rows` in place before @end fires.
const onEnd = () => {
  commit();
  emit("reorder");
};
</script>

<template>
  <draggable
    :list="rows"
    item-key="key"
    handle=".dl-handle"
    :disabled="disabled"
    :animation="180"
    ghost-class="dl-ghost"
    class="dl-list"
    @end="onEnd"
  >
    <template #item="{ element, index }">
      <div class="dl-row" :class="rowClass">
        <span
          v-if="!disabled"
          class="dl-handle"
          title="Drag to reorder"
          aria-label="Drag to reorder"
        >
          <VIcon size="20">bx-menu</VIcon>
        </span>
        <div class="dl-row-content">
          <slot
            name="item"
            :element="element.value"
            :index="index"
            :update="(v) => setValue(index, v)"
          />
        </div>
      </div>
    </template>
  </draggable>
</template>

<style scoped>
.dl-list {
  width: 100%;
}
.dl-row {
  display: flex;
  align-items: center;
  gap: 8px;
}
.dl-row-content {
  flex: 1 1 auto;
  min-width: 0;
}
.dl-handle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: grab;
  color: rgb(var(--v-theme-on-surface), 0.4);
  border-radius: 6px;
  padding: 2px;
  flex: 0 0 auto;
  touch-action: none;
}
.dl-handle:hover {
  color: rgb(var(--v-theme-primary));
  background: rgba(var(--v-theme-on-surface), 0.05);
}
.dl-handle:active {
  cursor: grabbing;
}
.dl-ghost {
  opacity: 0.5;
}
.dl-ghost .dl-row-content {
  background: rgba(var(--v-theme-primary), 0.06);
  border-radius: 8px;
}
</style>
