Skip to content

Indicator

Adds an absolute positioned element to indicate the drop position of the element.



Demo


vue
<script lang="ts" setup>
import createDragDropObservable, {
  addClasses,
  autoScroll,
  dragImage,
  indicator,
  reorderItems,
} from 'dndrxjs'
import { onMounted, onUnmounted, ref } from 'vue'
import data from './data/MOCK_DATA_1000.json'

const items = ref<{ name: string, id: string }[]>(data)
const container = ref<HTMLElement | null>(null)
const checked = ref<Record<string, boolean>>({})

onMounted(() => {
  // #region subscription
  const subscription = createDragDropObservable({
    container: container.value!,
    dropPositionFn: () => 'around',
    getSelectedElements: () =>
      Array.from(container.value!.querySelectorAll(`[data-selected="true"]`)),
  })
    .pipe(
      addClasses(),
      indicator({
        offset: 2,
        indicatorClasses: {
          initial: 'custom-indicator',
          vertical: 'custom-indicator-vertical',
          horizontal: 'custom-indicator-horizontal',
          after: 'custom-indicator-after',
          in: 'custom-indicator-in',
          before: 'custom-indicator-before',
        },
      }),
      autoScroll(),
      dragImage(),
    )
    .subscribe(({ type, dragElements, dropElement, position }) => {
      if (!!dropElement && type === 'DragEnd') {
        const index = Number.parseInt(dropElement.getAttribute('data-index')!)
        const selectedItems = dragElements.map(
          e =>
            items.value.find(item => item.id === e.getAttribute('data-id'))!,
        )
        if (position === 'after') {
          items.value = reorderItems(items.value, selectedItems, index + 1)
        }
        else if (position === 'before') {
          items.value = reorderItems(items.value, selectedItems, index)
        }
      }
    })
  // #endregion subscription
  onUnmounted(() => subscription.unsubscribe())
})
</script>

<template>
  <div
    ref="container"
    class="demo"
    style="overflow: auto; max-height: 400px; padding: 10px"
  >
    <ul class="list">
      <li
        v-for="(item, index) in items"
        :key="item.id"
        :data-id="item.id"
        :data-index="index"
        class="list-item"
        style="margin-bottom: 4px"
        :data-selected="checked[item.id]"
        @click="checked[item.id] = !checked[item.id]"
      >
        <img src="/handle.svg">
        <input v-model="checked[item.id]" type="checkbox">
        <span>{{ item.name }}</span>
      </li>
    </ul>
  </div>
</template>

<style type="text/css">
.custom-indicator {
  color: purple;
  background: currentColor;
  pointer-events: none;
  position: absolute;
  display: none;
}

.custom-indicator-before,
.custom-indicator-after {
  display: block;
}

.custom-indicator-after:before,
.custom-indicator-before:before {
  width: 10px;
  height: 10px;
  content: "";
  display: block;
  border: 2px solid currentColor;
  border-radius: 10px;
  position: absolute;
  left: -10px;
  top: -4.5px;
}

.custom-indicator-after.custom-indicator-vertical {
  width: var(--indicator-w);
  height: 2px;
  top: calc(
    var(--indicator-y) + var(--indicator-h) - 1px + var(--indicator-offset)
  );
  left: var(--indicator-x);
}
.custom-indicator-after.custom-indicator-horizontal {
  height: var(--indicator-h);
  width: 2px;
  top: var(--indicator-y);
  left: calc(
    var(--indicator-x) + var(--indicator-w) - 1px + var(--indicator-offset)
  );
}
.custom-indicator-before.custom-indicator-horizontal {
  width: 2px;
  height: var(--indicator-h);
  left: calc(var(--indicator-x) - 1px - var(--indicator-offset));
  top: var(--indicator-y);
}
.custom-indicator-before.custom-indicator-vertical {
  width: var(--indicator-w);
  height: 2px;
  top: calc(var(--indicator-y) - 1px - var(--indicator-offset));
  left: var(--indicator-x);
}
</style>

Types

ts
import type { DropPosition } from '../types'

export interface IndicatorMiddlewareOptions {
  /**
   * Defaults to:
   * {
   *  initial: "indicator",
   *  vertical: "indicator-vertical",
   *  horizontal: "indicator-vertical",
   *  after: "indicator-after",
   *  in: "indicator-in",
   *  before: "indicator-before",
   * }
   */
  indicatorClasses: IndicatorClasses
  /**
   * Offset for the before and after.
   */
  offset: number
}

export type GetIndicatorStyleFn = (
  x: number,
  y: number,
  width: number,
  height: number,
  position: DropPosition,
  vertical: boolean,
  offset: number,
) => () => string

export type IndicatorClasses = Partial<
  Record<DropPosition | 'initial' | 'vertical' | 'horizontal', string>
>