Reorderable list input component
Adapted by Philip van Heemstra (https://www.vanheemstra.com/) Based on article and demo by Darin Senneff (https://www.darins.page/) https://www.darins.page/articles/designing-a-reorderable-list-component
by PhilQ
HTML
<div id="demo">
<div class="ranking-input ranking-input-is-lowfi">
<a href="#ranking-input-skip-end" class="ranking-input-skip-link" id="ranking-input-skip-start">↓ Jump to list end</a>
<span class="ranking-input-visually-hidden ranking-input-heading">Fruit ranking</span>
<ol>
<li aria-labelledby="ranking-input-marker1 ranking-input-name1" class="ranking-input-item-lowfi">
<div id="ranking-input-marker1" class="ranking-input-item--marker">1.</div>
<div class="ranking-input-item--inner">
<div id="ranking-input-name1" class="ranking-input-item--text">Apple</div>
<img decoding="async" class="ranking-input-item--photo" src="https://assets.codepen.io/128542/apple.png" width="72" height="72" alt="Apple" />
<div class="ranking-input-item--move-group">
<label for="ranking-input-move1">Position<span class="ranking-input-visually-hidden"> of Apple </span>:</label>
<input type="text" name="fruit__positions[apple]" id="ranking-input-move1" inputmode="numeric" pattern="[0-9]*" autocomplete="off" />
</div>
<input type="hidden" name="fruit[]" value="apple" class="ranking-input-item--input" autocomplete="off" />
</div>
</li>
<li aria-labelledby="ranking-input-marker2 ranking-input-name2" class="ranking-input-item-lowfi">
<div id="ranking-input-marker2" class="ranking-input-item--marker">2.</div>
<div class="ranking-input-item--inner">
<div id="ranking-input-name2" class="ranking-input-item--text">Banana</div>
<img decoding="async" class="ranking-input-item--photo" src="https://assets.codepen.io/128542/banana.png" width="72" height="72" alt="Banana" />
<div class="ranking-input-item--move-group">
<label for="ranking-input-move2">Position<span class="ranking-input-visually-hidden"> of Banana </span>:</label>
<input type="text" name="fruit__positions[banana]"...
SCSS
/* Demo styles */
body {
background: #eee;
padding: 5rem;
font-family: sans-serif;
}
#demo {
width: 100%;
max-width: 500px;
margin: 5rem auto;
}
/* Component styles */
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
@mixin breakpoint($point) {
@media (max-width: #{$point}px) { @content ; }
}
$prefix: 'ranking-input';
.#{$prefix} {
--color--grey-3: #F5F6F7;
--color--grey-40: #8D9297;
--color-lowfi-background: var(--color--grey-3);
--color-lowfi-seperator: var(--color--grey-40);
--color--grey-50: #71767A;
--color--grey-60: #565C65;
--color-border: var(--color--grey-50);
--color-background: #fff;
--color-text: var(--color--grey-60);
--color--teal-4: #EAFBFF;
--color--teal-50: #0097BD;
--color--teal-60: #006D88;
--color-focus-border: var(--color--teal-50);
--color-focus-background: var(--color--teal-4);
--color-focus-text: var(--color--teal-60);
--color-hover-border: var(--color--teal-50);
--color-hover-background: var(--color--teal-4);
--color-hover-text: var(--color--teal-60);
--color--violet-4: #fef2ff;
--color--violet-50: #be32d0;
--color--violet-60: #93348c;
--color-dragged-over-border: var(--color--violet-50);
--color-dragged-over-background: var(--color--violet-4);
--color-dragged-over-text: var(--color--violet-60);
--button-gap: 1em;
--dot-color: rgba(0,0,0,.6);
--dot-distance: 1em;
--drag-border-scale: .98;
--animation-duration: .25;
--row-gap: 2em;
--marker-width: 2em;
--marker-gap: 2em;
--gap-indicator-scale-x: .5;
--gap-indicator-overhang: 4em;
--gap-border-width: 8px;
--focus-border-width: 4px;
margin: calc(2 * var(--row-gap)) 0 var(--row-gap);
padding: 0 0 var(--row-gap);
position: relative;
text-align: left;
}
.#{$prefix}-gap {
height: var(--row-gap);
margin-left: calc(var(--marker-width) + var(--marker-gap));
position: relative;
width: calc(100% - (var(--marker-width) + var(--marker-gap)));
&::before {
background: none;
border-top: dashed...
JavaScript
// Initiating component on element
document.addEventListener("DOMContentLoaded", () => {
// Unique prefix for component (as used in SCSS and HTML)
const prefix = 'ranking-input';
// (optional) Remove duplicate SVGs
document.querySelectorAll(`.${prefix}-svg`).forEach((el, idx) => {
if ( idx > 0 ) {
el.remove();
}
});
// Initiate first non-initiated
let el = document.querySelector(`.${prefix}-is-lowfi`);
if ( el ) {
// Note: comment out next line to see lowfi form.
el.vhw_ranking_input = new RankingInput( el, prefix );
}
});
/**
* Ranking input component class
*
* Adapted by Philip van Heemstra (https://www.vanheemstra.com/)
*
* Based on article and demo by Darin Senneff (https://www.darins.page/)
* https://www.darins.page/articles/designing-a-reorderable-list-component
*
* @todo Refactor and cleanup
*/
class RankingInput {
static idx = 0;
prefix = 'prefix'; // placeholder
handle = 'prefix1'; // placeholder
wrapper;
oldList;
items;
buttons;
listEnd;
status;
wrapperFrag;
gapHeight;
firstGap;
curName;
curItem;
curButton;
curDraggedEl;
grabCoords;
statusTimer;
constructor( wrapper, prefix = 'prefix' ) {
this.prefix = prefix;
this.handle = `${this.prefix}${++RankingInput.idx}`;
this.wrapper = wrapper;
wrapper.classList.remove(`${this.prefix}-is-lowfi`);
this.setup();
}
debounce(func, delay) {
let inDebounce;
return function () {
const context = this;
const args = arguments;
clearTimeout(inDebounce);
inDebounce = setTimeout(() => func.apply(context, args), delay);
};
}
itemDragStart(e) {
//console.log("dragstart");
// add drag styling
e.target.parentElement.classList.add("dragging");
this.items.forEach((item) => {
item.classList.remove("dragged-over");
...