Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
445 lines
12 KiB
JavaScript
Executable file
445 lines
12 KiB
JavaScript
Executable file
!(function(define){'use strict';define(function(require,exports,module){
|
|
|
|
/**
|
|
* Mini logger
|
|
* @type {Function}
|
|
*/
|
|
var debug = 0 ? console.log.bind(console, '[FastList<>Edit]') : function() {};
|
|
|
|
// Pointer/touch abstraction
|
|
var startEvent = ('ontouchstart' in window) ? 'touchstart' : 'mousedown';
|
|
var moveEvent = ('ontouchstart' in window) ? 'touchmove' : 'mousemove';
|
|
var endEvent = ('ontouchstart' in window) ? 'touchend' : 'mouseup';
|
|
|
|
var schedule = FastList.scheduler;
|
|
|
|
/**
|
|
* Exports
|
|
*/
|
|
|
|
exports = module.exports = fastListEdit;
|
|
|
|
function fastListEdit(list) {
|
|
list.editing = false;
|
|
|
|
/**
|
|
* Toggles the edit mode on and off
|
|
*
|
|
* Returns the DomScheduler promise of the edit mode transition
|
|
*
|
|
* @return {Promise}
|
|
*/
|
|
list.toggleEditMode = function() {
|
|
list.editing = !list.editing;
|
|
|
|
if (list.editing) list._startTouchListeners();
|
|
else list._stopTouchListeners();
|
|
|
|
return toggleEditClass(
|
|
list.els.itemContainer,
|
|
list.els.itemsInDOM,
|
|
list.editing
|
|
);
|
|
};
|
|
|
|
list._startTouchListeners = function() {
|
|
on(list.els.itemContainer, startEvent, list.handleEventEdit);
|
|
on(list.els.itemContainer, endEvent, list.handleEventEdit);
|
|
};
|
|
|
|
list._stopTouchListeners = function() {
|
|
off(list.els.itemContainer, startEvent, list.handleEventEdit);
|
|
off(list.els.itemContainer, endEvent, list.handleEventEdit);
|
|
};
|
|
|
|
list.handleEventEdit = function(evt) {
|
|
switch (evt.type) {
|
|
case startEvent:
|
|
if (!evt.target.classList.contains('cursor')) {
|
|
return;
|
|
}
|
|
evt.preventDefault();
|
|
list._reorderStart(evt);
|
|
break;
|
|
case endEvent:
|
|
if (!evt.target.classList.contains('cursor') &&
|
|
endEvent === 'touchend') {
|
|
return;
|
|
}
|
|
evt.preventDefault();
|
|
list._reorderEnd(evt);
|
|
break;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Starts the reordering gestures
|
|
*
|
|
* - initializes the reordering context (state)
|
|
* - attaches (see DomScheduler) a block to the move events
|
|
* - sets up the css to make the dragged element pop up
|
|
*
|
|
* @param {TouchEvent} evt the touchstart event
|
|
*/
|
|
list._reorderStart = function(evt) {
|
|
debug('reorder start');
|
|
var ctx = list.reorderingContext;
|
|
|
|
// Already tracking an item, bailing out
|
|
if (ctx.item) return;
|
|
|
|
var li = evt.target.parentNode.parentNode;
|
|
var touch = evt.touches && evt.touches[0] || evt;
|
|
|
|
ctx.item = li;
|
|
ctx.initialY = touch.pageY;
|
|
ctx.identifier = touch.identifier;
|
|
ctx.moveUp = new Set();
|
|
ctx.moveDown = new Set();
|
|
ctx.moveHandler = list._reorderMove.bind(list);
|
|
|
|
var listenToMove = (function() {
|
|
schedule.attachDirect(
|
|
list.els.itemContainer,
|
|
moveEvent,
|
|
ctx.moveHandler);
|
|
}).bind(list);
|
|
|
|
setupForDragging(li, true)
|
|
.then(listenToMove)
|
|
.then(toggleDraggingClass.bind(null, li, true));
|
|
};
|
|
|
|
/**
|
|
* Scheduled callback for move events during reordering
|
|
*
|
|
* - updates the position of the dragged item
|
|
* - rearranges the other items
|
|
*
|
|
* @param {TouchEvent} evt the touchmove event
|
|
*/
|
|
list._reorderMove = function(evt) {
|
|
debug('reorder move');
|
|
var ctx = list.reorderingContext;
|
|
if (!ctx.item) return;
|
|
|
|
// Multi touch
|
|
if (evt.touches && evt.touches.length > 1) return;
|
|
|
|
var li = ctx.item;
|
|
var touch = evt.touches && evt.touches[0] || evt;
|
|
var position = touch.pageY;
|
|
var delta = position - ctx.initialY;
|
|
|
|
list._rearrange(delta);
|
|
tweakTransform(li, delta);
|
|
|
|
// TODO: scroll when close to the beginning/end of the viewport
|
|
};
|
|
|
|
/**
|
|
* Rearranges the items below te dragged one
|
|
*
|
|
* - computes the transitions needed based on the context
|
|
* - triggers scheduled transitions for each one
|
|
*
|
|
* @param {Number} delta the distance traveled by the reordering gesture
|
|
*/
|
|
list._rearrange = function(delta) {
|
|
debug('rearrange', delta);
|
|
|
|
var ctx = list.reorderingContext;
|
|
var upCount = ctx.moveUp.size;
|
|
var downCount = ctx.moveDown.size;
|
|
|
|
computeChanges(
|
|
list.reorderingContext,
|
|
list.geometry,
|
|
list.els.itemsInDOM,
|
|
delta
|
|
);
|
|
|
|
if (ctx.moveUp.size === upCount &&
|
|
ctx.moveDown.size === downCount) {
|
|
return;
|
|
}
|
|
|
|
applyChanges(
|
|
list.reorderingContext,
|
|
list.geometry,
|
|
list.els.itemsInDOM
|
|
);
|
|
|
|
debug('rearranged');
|
|
};
|
|
|
|
/**
|
|
* Ends the reordering gesture
|
|
*
|
|
* - computes any remaining pending changes
|
|
* - detaches the block from the move events
|
|
* - schedules a transition to move the dragged item in place
|
|
* - schedules a transition to move other items in place
|
|
*
|
|
* @param {TouchEvent} evt the touchend event
|
|
*/
|
|
list._reorderEnd = function(evt) {
|
|
debug('reorder end');
|
|
|
|
var ctx = list.reorderingContext;
|
|
var li = ctx.item;
|
|
|
|
if (!li) return;
|
|
|
|
schedule.detachDirect(
|
|
list.els.itemContainer,
|
|
moveEvent,
|
|
ctx.moveHandler);
|
|
|
|
var touch = evt.touches && evt.touches[0] || evt;
|
|
if (touch.identifier == ctx.identifier) {
|
|
var position = touch.pageY;
|
|
var delta = position - ctx.initialY;
|
|
computeChanges(ctx, list.geometry, list.els.itemsInDOM, delta);
|
|
}
|
|
|
|
Promise.all([
|
|
applyChanges(ctx, list.geometry, list.els.itemsInDOM),
|
|
moveInPlace(ctx, list.geometry)
|
|
]).then(list._reorderFinish.bind(list, li))
|
|
.then(toggleDraggingClass.bind(null, li, false));
|
|
};
|
|
|
|
/**
|
|
* Final stage of the reordering
|
|
*
|
|
* In parallell
|
|
* - Commits the change to the document
|
|
* - cleans up the css on the dragged element
|
|
*
|
|
*/
|
|
list._reorderFinish = function(li) {
|
|
return Promise.all([
|
|
list._commitToDocument(),
|
|
setupForDragging(li, false)
|
|
]);
|
|
};
|
|
|
|
/**
|
|
* Commits the reordering changes to the document
|
|
*
|
|
* Inside a scheduled mutation
|
|
* - re-renders the list with the new ordering
|
|
* - cleans up the reordering context
|
|
*
|
|
*/
|
|
list._commitToDocument = function() {
|
|
debug('commit to document');
|
|
var ctx = list.reorderingContext;
|
|
var li = ctx.item;
|
|
var itemsInDOM = list.els.itemsInDOM;
|
|
var items = list.els.items;
|
|
var source = list.source;
|
|
|
|
return schedule.mutation((function() {
|
|
// Nothing to do
|
|
if (!ctx.moveUp && !ctx.moveDown) {
|
|
return;
|
|
}
|
|
|
|
var index = items.indexOf(li);
|
|
var section = li.dataset.section;
|
|
var newIndex = index + ctx.moveUp.size - ctx.moveDown.size;
|
|
|
|
items.splice(index, 1);
|
|
items.splice(newIndex, 0, li);
|
|
var c = source.removeAtIndex(index);
|
|
source.insertAtIndex(newIndex, c, section);
|
|
|
|
itemsInDOM.forEach(function(item) {
|
|
resetTransition(item);
|
|
resetTransform(item);
|
|
});
|
|
|
|
list.updateSections();
|
|
list.render();
|
|
|
|
ctx.item = null;
|
|
ctx.initialY = null;
|
|
ctx.identifier = null;
|
|
ctx.moveUp = null;
|
|
ctx.moveDown = null;
|
|
ctx.moveHandler = null;
|
|
}).bind(list));
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Internals
|
|
*/
|
|
|
|
function resetTransform(item) {
|
|
debug('reset transform');
|
|
var position = parseInt(item.dataset.position);
|
|
item.style.webkitTransform =
|
|
item.style.transform = 'translateY(' + position + 'px)';
|
|
item.dataset.tweakDelta = '';
|
|
}
|
|
|
|
function tweakTransform(item, delta) {
|
|
debug('tweak transform', delta);
|
|
var position = ~~item.dataset.position + delta;
|
|
item.style.webkitTransform =
|
|
item.style.transform = 'translateY(' + position + 'px)';
|
|
item.dataset.tweakDelta = delta;
|
|
}
|
|
|
|
function resetTransition(item) {
|
|
item.style.transition = '';
|
|
item.style.webkitTransition = '';
|
|
}
|
|
|
|
function tweakAndTransition(item, tweak) {
|
|
return schedule.feedback(function() {
|
|
debug('tweak and transition', item, tweak);
|
|
item.style.transition = 'transform 0.15s ease';
|
|
item.style.webkitTransition = '-webkit-transform 0.15s ease';
|
|
if (tweak === 0) {
|
|
setTimeout(resetTransform.bind(null, item));
|
|
return;
|
|
}
|
|
setTimeout(tweakTransform.bind(null, item, tweak));
|
|
}, item, 'transitionend')
|
|
.then(resetTransition.bind(null, item));
|
|
}
|
|
|
|
function toggleEditClass(list, domItems, editing) {
|
|
if (!domItems.length) return Promise.resolve();
|
|
|
|
return schedule.feedback(function() {
|
|
for (var i = 0; i < domItems.length; i++) {
|
|
var item = domItems[i];
|
|
var overlay = item.querySelector('.overlay');
|
|
overlay.dataset.anim = editing ? 'reveal' : 'hide';
|
|
item.classList.toggle('edit', editing);
|
|
}
|
|
}, list, 'animationend');
|
|
}
|
|
|
|
function setupForDragging(item, on) {
|
|
return schedule.mutation(function() {
|
|
item.parentNode.classList.toggle('reordering', on);
|
|
item.style.zIndex = on ? '1000' : '';
|
|
item.style.boxShadow = on ? '0 0 3px 1px #bcbcbc' : '';
|
|
});
|
|
}
|
|
|
|
function toggleDraggingClass(item, on) {
|
|
return schedule.feedback(function() {
|
|
var overlay = item.querySelector('.overlay');
|
|
overlay.dataset.anim = on ? 'hide' : 'reveal';
|
|
}, item, 'animationend');
|
|
}
|
|
|
|
function computeChanges(context, geometry, domItems, delta) {
|
|
if (!context.item) return;
|
|
|
|
var draggedItem = context.item;
|
|
var draggedOriginal = parseInt(draggedItem.dataset.position);
|
|
var draggedTweaked = draggedOriginal + delta;
|
|
|
|
for (var i = 0; i < domItems.length; i++) {
|
|
var item = domItems[i];
|
|
if (item === draggedItem) continue;
|
|
|
|
// TODO: support re-ordering accross sections
|
|
if (item.dataset.section !== draggedItem.dataset.section) continue;
|
|
|
|
var itemOriginal = parseInt(item.dataset.position);
|
|
var itemTweaked = itemOriginal + parseInt(item.dataset.tweakDelta);
|
|
|
|
if ((itemOriginal < draggedOriginal) && (draggedTweaked < itemOriginal)) {
|
|
context.moveDown.add(item);
|
|
} else {
|
|
if (draggedTweaked > itemTweaked) {
|
|
context.moveDown.delete(item);
|
|
}
|
|
}
|
|
|
|
if ((itemOriginal > draggedOriginal) && (draggedTweaked > itemOriginal)) {
|
|
context.moveUp.add(item);
|
|
} else {
|
|
if (draggedTweaked < itemTweaked) {
|
|
context.moveUp.delete(item);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function applyChanges(context, geometry, domItems) {
|
|
debug('apply changes');
|
|
if (!context.item) return Promise.resolve();
|
|
|
|
var itemHeight = geometry.itemHeight;
|
|
var draggedItem = context.item;
|
|
var promises = [];
|
|
|
|
for (var i = 0; i < domItems.length; i++) {
|
|
var item = domItems[i];
|
|
if (item === draggedItem) continue;
|
|
|
|
// Reset
|
|
if (item.dataset.tweakDelta &&
|
|
!context.moveUp.has(item) && !context.moveDown.has(item)) {
|
|
|
|
promises.push(tweakAndTransition(item, 0));
|
|
}
|
|
|
|
// Moving down
|
|
if (context.moveDown.has(item)) {
|
|
if (parseInt(item.dataset.tweakDelta) !== itemHeight) {
|
|
promises.push(tweakAndTransition(item, itemHeight));
|
|
}
|
|
}
|
|
|
|
// Moving up
|
|
if (context.moveUp.has(item)) {
|
|
if (parseInt(item.dataset.tweakDelta) !== itemHeight * -1) {
|
|
promises.push(tweakAndTransition(item, itemHeight * -1));
|
|
}
|
|
}
|
|
}
|
|
|
|
return Promise.all(promises);
|
|
}
|
|
|
|
function moveInPlace(context, geometry) {
|
|
debug('move in place');
|
|
var li = context.item;
|
|
|
|
// We're already in place
|
|
if (!li.dataset.tweakDelta) return Promise.resolve();
|
|
|
|
var position = parseInt(li.dataset.position);
|
|
var taintedPosition = position + parseInt(li.dataset.tweakDelta);
|
|
var itemHeight = geometry.itemHeight;
|
|
var newPosition = position + context.moveUp.size * itemHeight -
|
|
context.moveDown.size * itemHeight;
|
|
|
|
var duration = (Math.abs(taintedPosition - newPosition) / itemHeight) * 150;
|
|
|
|
return schedule.feedback(function() {
|
|
li.style.transition = 'transform ' + duration + 'ms linear';
|
|
li.style.webkitTransition = '-webkit-transform ' + duration + 'ms linear';
|
|
setTimeout(tweakTransform.bind(null, li, (newPosition - position)));
|
|
}, li, 'transitionend');
|
|
}
|
|
|
|
// Shorthand
|
|
function on(el, name, fn) { el.addEventListener(name, fn); }
|
|
function off(el, name, fn) { el.removeEventListener(name, fn); }
|
|
|
|
});})((typeof define)[0]=='f'&&define.amd?define:(function(n,n2,w){'use strict';
|
|
return(typeof module)[0]=='o'?function(c){c(require,exports,module);}:
|
|
function(c){var m={exports:{}};c(function(n){w[n];},m.exports,m);
|
|
w[n]=w[n2]=m.exports;};})('fastListEdit','fast-list-edit',this));
|