Drag and Drop List

A list that can be reordered.

by Perry Kaufman

HTML

<section class="pk-section">
  <h1 class="pk-h1"> Drag and Drop List</h1>
  <p class="pk-p b">By Perry Kaufman</p>
  <p class="pk-p">Click and drag an item to reorder the list!</p>
  <form class="pk-list-form"></form>
  <ul class="pk-list"></ul>
</section>

SCSS

/* Fonts */
@import url('https://fonts.googleapis.com/css?family=Roboto');
$fonts: 'Roboto', arial, sans-serif;

/* Colors */
$black: black;
$gray: gray;
$lightgray: lightgray;
$lightteal: rgb(194,227,227);
$teal: rgb(6,152,152);
$white: white;

/* Sizing */
$box-dim: 20px;
$form-input-padding: 2px 5px;
$item-border: 1px;
$item-padding: 5px;
$item-height: 40px;
$item-width: 280px;
$list-padding-lat: 0px;
$list-padding-vert: 5px;
$list-width: 280px;
$slot-padding: 3px;

/* Mixins */
@mixin user-select {
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

@mixin user-drag {
  user-drag: none; 
  -webkit-user-drag: none;
}

@mixin transform($x) {
  -webkit-transform: $x;
  -ms-transform: $x;
  transform: $x;
}

/* Shared */

%pk-box {
  border-radius: 5px;
  height: $box-dim;
  width: $box-dim;
}

/* Utility */

.b {
  font-weight: 600;
}

.hidden {
  height: 0;
  opacity: 0;
  left: -10000px;
  position: absolute;
  width: 0;
}

/* General */
* {
  box-sizing: inherit;
}

html, body {
  box-sizing: border-box;
  font-family: $fonts;
  font-size: 16px;
  margin: 0;
  min-width: 300px;
  padding: 0;
}

.pk-section {
  align-items: center;
  display: flex;
  flex-flow: column nowrap;
  font-size: inherit;
}

.pk-h1 {
  font-size: 2em;
}

.pk-p {
  margin: 0 0 10px 0;
}

/* List */
.pk-list {
  list-style: none;
  margin: 0;
  padding: $list-padding-vert $list-padding-lat;
  position: relative;
  width: $list-width;
}

.pk-item-slot {
  height: $item-height;
  padding: $slot-padding 0;
  width: $item-width;
}

.pk-item {
  align-items: center;
  background: $lightgray;
  border: $item-border solid $gray;
  border-radius: 5px;
  display: grid;
  grid-template-areas: "left middle right";
  grid-template-columns: 20px 1fr 20px;
  grid-template-rows: auto;
  height: $item-height - ($slot-padding * 2);
  padding: 0 5px;
  width: $item-width;
  z-index: 1;
  
  .pk-checkbox {
     grid-area: left;
     justify-self: start;
...

JavaScript

//initalization list
const ITEMS = [
	'Apple',
  'Banana',
	'Kiwi',
  'Orange',
  'Pineapple',
  'Watermelon'
]

//make checkbox element
const PK_ELEMENTS = {
	createCheckboxElement() {
  	const checkbox = document.createElement('label')
    checkbox.classList.add('pk-checkbox')
    const input = document.createElement('input')
    input.dataset.type='select'
    input.classList.add('hidden')
    input.classList.add('pk-checkbox-input')
    input.setAttribute('type', 'checkbox')
    checkbox.appendChild(input)
    const check = document.createElement('span')
    check.classList.add('check')
    checkbox.appendChild(check)
    
    return checkbox
  }
}

//id generator
function* idGen(start) {
	while(true) yield start++
}

/*
 * create a list from a root UL/OL element and an optional array
 */
class List {
	constructor(element, array) {
  	this.element = element
    this.items = []
    this.mouse = {clientX: 0, clientY: 0}
    this._moving = null
    this._nextKey = idGen(0)
    this._listeners = []
    this._scrolling = null
    if (array) array.forEach((text) => this.addItem(text))
    
    this._init()
  }
  _init() {
  	this.element.addEventListener('pk-move', (e) => this._move(e))
  }
  _cancel(e) {
  	//temporary solution
    if (!e.relatedTarget) this._drop(e)
  }
  _drag(e) {  
  	//update item position
    this._updatePosition(e)
    
    //update slot position
    this._updateDropArea(e)
  }
  _drop(e) {
    //reset
    const item = this._moving
    item.resetPosition()
    item.moving = false
    this._moving = null
    
    //stop scrolling
    this._scrolling = null
    
    //remove listeners
    this._listeners.forEach((current) => {
      	document.removeEventListener(current.type, current.listener)
    	})
    this._listeners = []
  }
  _move(e) {
      //setup
      this._moving = this.getItem(e.target)
      const item = this._moving
      this._updatePosition(e)
      this._updateMouse(e)
      item.moving = true
            
      //add...