Usin vanilla HTML drag
This commit is contained in:
@ -5506,6 +5506,7 @@ h4 .context-menu-trigger, h4 .context-menu-trigger:active {
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: #32b5f2;
|
||||
border-radius: 100px;
|
||||
opacity: 0;
|
||||
}
|
||||
.list__item--no-interaction {
|
||||
@ -5700,6 +5701,9 @@ h4 .context-menu-trigger, h4 .context-menu-trigger:active {
|
||||
:root .dragging .list--tracks .list__item:hover:before, :root .dragging .list--tracks .list__item--hover:before, .list--tracks .list__item.touch-drag-hover:before {
|
||||
opacity: 1;
|
||||
}
|
||||
.list--tracks .list__item--dropping:before {
|
||||
opacity: 1;
|
||||
}
|
||||
.list--tracks .list__item.has-touch-drag-zone {
|
||||
padding-left: 45px;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -57,173 +57,14 @@ const Track = ({
|
||||
context,
|
||||
stream_title,
|
||||
play_state,
|
||||
selected_tracks,
|
||||
is_selected,
|
||||
is_dropping,
|
||||
can_sort,
|
||||
show_source_icon,
|
||||
getItemIndex,
|
||||
handleDrag,
|
||||
handleDrop,
|
||||
handleDoubleTap,
|
||||
handleTouchDrag,
|
||||
handleTap,
|
||||
dragger,
|
||||
onContextMenu,
|
||||
onClick,
|
||||
onDoubleClick,
|
||||
events,
|
||||
}) => {
|
||||
const [hover, setHover] = useState(false);
|
||||
const index = getItemIndex();
|
||||
const [currentEvent, setCurrentEvent] = useState({
|
||||
start_time: 0,
|
||||
end_time: 0,
|
||||
});
|
||||
|
||||
// const onMouseEnter = () => setHover(true);
|
||||
// const onMouseLeave = () => setHover(false);
|
||||
// const onDoubleClick = (e) => handleDoubleClick(e, key);
|
||||
// const updateCurrentEvent = (data) => setCurrentEvent((prev) => ({ ...prev, ...data }));
|
||||
|
||||
const handleContextMenu = (e) => onContextMenu(item, index, e);
|
||||
const handleClick = (e) => onClick(item, index, e);
|
||||
const handleDoubleClick = (e) => onDoubleClick(item, index, e);
|
||||
|
||||
// const onMouseDown = (e) => {
|
||||
// const target = $(e.target);
|
||||
|
||||
// // Clicked a nested link (ie Artist name), so no dragging required
|
||||
// if (target.is('a')) {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// // Only listen for left mouse clicks
|
||||
// if (e.button === 0) {
|
||||
// updateCurrentEvent({
|
||||
// start_position: {
|
||||
// x: e.pageX,
|
||||
// y: e.pageY,
|
||||
// },
|
||||
// });
|
||||
|
||||
// // Not left click, then ensure no dragging
|
||||
// } else {
|
||||
// updateCurrentEvent({ start_position: false });
|
||||
// }
|
||||
// };
|
||||
|
||||
// const onMouseMove = (e) => {
|
||||
// if (handleDrag === undefined) return false;
|
||||
|
||||
// if (currentEvent.start_position) {
|
||||
// const start_x = currentEvent.start_position.x;
|
||||
// const start_y = currentEvent.start_position.y;
|
||||
// const threshold = 5;
|
||||
|
||||
// // Have we dragged outside of our threshold zone?
|
||||
// if (
|
||||
// e.pageX > start_x + threshold
|
||||
// || e.pageX < start_x - threshold
|
||||
// || e.pageY > start_y + threshold
|
||||
// || e.pageY < start_y - threshold
|
||||
// ) {
|
||||
// // Handover to parent for dragging. We can unset all our behaviour now.
|
||||
// handleDrag(e, key);
|
||||
// updateCurrentEvent({ start_position: false });
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
|
||||
// const onMouseUp = (e) => {
|
||||
// const target = $(e.target);
|
||||
|
||||
// // Only listen for left clicks
|
||||
// if (e.button === 0) {
|
||||
// if (dragger) {
|
||||
// e.preventDefault();
|
||||
|
||||
// if (handleDrop !== undefined) {
|
||||
// handleDrop(e, key);
|
||||
// }
|
||||
// } else if (!target.is('a') && target.closest('a').length <= 0) {
|
||||
// handleClick(e, key);
|
||||
// updateCurrentEvent({ start_position: false });
|
||||
// }
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // Not left click, then ensure no dragging
|
||||
// updateCurrentEvent({ start_position: false });
|
||||
// }
|
||||
|
||||
|
||||
// const onTouchStart = (e) => {
|
||||
// const target = $(e.target);
|
||||
// const timestamp = Math.floor(Date.now());
|
||||
|
||||
// if (target.hasClass('drag-zone')) {
|
||||
// handleTouchDrag(e, key);
|
||||
// e.preventDefault();
|
||||
// }
|
||||
|
||||
// // Save touch start details
|
||||
// updateCurrentEvent({
|
||||
// start_time: timestamp,
|
||||
// start_position: {
|
||||
// x: e.touches[0].clientX,
|
||||
// y: e.touches[0].clientY,
|
||||
// },
|
||||
// });
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// const onTouchEnd = (e) => {
|
||||
// const target = $(e.target);
|
||||
// const timestamp = Math.floor(Date.now());
|
||||
// const tap_distance_threshold = 10; // Max distance (px) between touchstart and touchend to qualify as a tap
|
||||
// const tap_time_threshold = 200; // Max time (ms) between touchstart and touchend to qualify as a tap
|
||||
// const end_position = {
|
||||
// x: e.changedTouches[0].clientX,
|
||||
// y: e.changedTouches[0].clientY,
|
||||
// };
|
||||
|
||||
// // Too long between touchstart and touchend
|
||||
// if (currentEvent.start_time + tap_time_threshold < timestamp) {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// // Make sure there's enough distance between start and end before we handle
|
||||
// // this event as a 'tap'
|
||||
// if (
|
||||
// currentEvent.start_position.x + tap_distance_threshold > end_position.x
|
||||
// && currentEvent.start_position.x - tap_distance_threshold < end_position.x
|
||||
// && currentEvent.start_position.y + tap_distance_threshold > end_position.y
|
||||
// && currentEvent.start_position.y - tap_distance_threshold < end_position.y
|
||||
// ) {
|
||||
// // Clicked a nested link (ie Artist name), so no dragging required
|
||||
// if (!target.is('a')) {
|
||||
// e.preventDefault();
|
||||
// }
|
||||
|
||||
// // Context trigger
|
||||
// if (target.hasClass('touch-contextable')) {
|
||||
// // Update our selection. By not passing touch = true selection will work like a regular
|
||||
// // click this.props.handleSelection(e);
|
||||
// handleContextMenu(e, key);
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// // We received a touchend within 300ms ago, so handle as double-tap
|
||||
// if ((timestamp - currentEvent.end_time) > 0 && (timestamp - currentEvent.end_time) <= 300) {
|
||||
// handleDoubleTap(e, key);
|
||||
// e.preventDefault();
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// handleTap(e, key);
|
||||
// }
|
||||
|
||||
// updateCurrentEvent({ end_time: timestamp });
|
||||
// }
|
||||
|
||||
if (!item) return null;
|
||||
|
||||
@ -271,32 +112,36 @@ const Track = ({
|
||||
}
|
||||
|
||||
const track_middle_column = <MiddleColumn context={context} item={item} />;
|
||||
if (is_selected(getItemIndex())) className += ' list__item--selected';
|
||||
if (is_selected(index)) className += ' list__item--selected';
|
||||
if (is_dropping(index)) className += ' list__item--dropping';
|
||||
if (can_sort) className += ' list__item--can-sort';
|
||||
if (item.type !== undefined) className += ` list__item--${item.type}`;
|
||||
if (item.playing) className += ' list__item--playing';
|
||||
if (item.loading) className += ' list__item--loading';
|
||||
if (item.is_playable === false) className += ' list__item--disabled';
|
||||
if (hover) className += ' list__item--hover';
|
||||
if (track_middle_column) className += ' list__item--has-middle-column';
|
||||
if (track_details.length > 0) className += ' list__item--has-details';
|
||||
|
||||
const onClick = (e) => events.onClick(item, index, e);
|
||||
const onDoubleClick = (e) => events.onDoubleClick(item, index, e);
|
||||
const onContextMenu = (e) => events.onContextMenu(item, index, e);
|
||||
const onDragStart = (e) => events.onDragStart(item, index, e);
|
||||
const onDragEnter = (e) => events.onDragEnter(item, index, e);
|
||||
const onDragLeave = (e) => events.onDragLeave(item, index, e);
|
||||
const onDrop = (e) => events.onDrop(item, index, e);
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<div
|
||||
className={className}
|
||||
onClick={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
// onMouseEnter={onMouseEnter}
|
||||
// onMouseLeave={onMouseLeave}
|
||||
// onMouseDown={onMouseDown}
|
||||
// onMouseUp={onMouseUp}
|
||||
// onMouseMove={onMouseMove}
|
||||
// onDoubleClick={onDoubleClick}
|
||||
// onContextMenu={onContextMenu}
|
||||
// onTouchStart={onTouchStart}
|
||||
// onTouchEnd={onTouchEnd}
|
||||
onClick={onClick}
|
||||
onDoubleClick={onDoubleClick}
|
||||
onContextMenu={onContextMenu}
|
||||
onDragStart={onDragStart}
|
||||
onDragEnter={onDragEnter}
|
||||
onDragLeave={onDragLeave}
|
||||
onDrop={() => console.debug("DROP")}
|
||||
draggable="true"
|
||||
>
|
||||
<div className="list__item__column list__item__column--name">
|
||||
<div className="list__item__column__item--name">
|
||||
@ -333,7 +178,7 @@ const Track = ({
|
||||
)}
|
||||
<ContextMenuTrigger
|
||||
className="list__item__column__item--context-menu-trigger subtle"
|
||||
onTrigger={handleContextMenu}
|
||||
onTrigger={onContextMenu}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { uniqBy } from 'lodash';
|
||||
import Track from './Track';
|
||||
import * as mopidyActions from '../services/mopidy/actions';
|
||||
import * as uiActions from '../services/ui/actions';
|
||||
@ -26,7 +27,6 @@ const TrackList = ({
|
||||
dragger,
|
||||
uiActions: {
|
||||
createNotification,
|
||||
setSelectedTracks,
|
||||
showContextMenu,
|
||||
dragStart,
|
||||
},
|
||||
@ -35,48 +35,16 @@ const TrackList = ({
|
||||
},
|
||||
}) => {
|
||||
const [selected, setSelected] = useState([]);
|
||||
const [dropTarget, setDropTarget] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', onKeyDown, false);
|
||||
// window.addEventListener('touchmove', onTouchMove, false);
|
||||
// window.addEventListener('touchend', onTouchEnd, false);
|
||||
|
||||
window.addEventListener('dragend', events.onDragEnd, false);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', onKeyDown, false);
|
||||
// window.removeEventListener('touchmove', onTouchMove, false);
|
||||
// window.removeEventListener('touchend', onTouchEnd, false);
|
||||
}
|
||||
}, [selected_tracks]);
|
||||
|
||||
const selectionIndexByItemIndex = (index) => (
|
||||
selected && selected.length > 0
|
||||
? selected.findIndex(({ index: selectedIndex }) => selectedIndex === index)
|
||||
: -1
|
||||
);
|
||||
|
||||
const nextSelected = (prev, item, index, e, sticky = false) => {
|
||||
const alreadySelected = selectionIndexByItemIndex(index);
|
||||
|
||||
if (e.shiftKey) {
|
||||
const { index: lastSelectedIndex } = selected[selected.length-1] || { index };
|
||||
const next = [...prev, { index, item }];
|
||||
const from = lastSelectedIndex < index ? lastSelectedIndex : index;
|
||||
const to = lastSelectedIndex > index ? lastSelectedIndex : index;
|
||||
for (let i = from; i < to; i++) {
|
||||
next.push({ index: i, item: tracks[i] });
|
||||
}
|
||||
return next;
|
||||
}
|
||||
if (e.ctrlKey) {
|
||||
if (alreadySelected >= 0 && !sticky) {
|
||||
const next = [...prev];
|
||||
next.splice(alreadySelected, 1);
|
||||
return next;
|
||||
}
|
||||
return [...prev, { item, index }];
|
||||
}
|
||||
return [{ item, index }];
|
||||
}
|
||||
window.removeEventListener('dragend', events.onDragEnd, false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onKeyDown = (e) => {
|
||||
const {
|
||||
@ -123,45 +91,55 @@ const TrackList = ({
|
||||
}
|
||||
}
|
||||
|
||||
const onClick = (item, index, e) => {
|
||||
e.preventDefault();
|
||||
e.persist();
|
||||
const events = {
|
||||
onDragStart: (item, index, e) => {
|
||||
const items = getOrUpdateSelected(item, index, e).map(({ item: selectedItem }) => selectedItem);
|
||||
console.debug('DRAGGING', items.length)
|
||||
},
|
||||
onDrop: (item, index, e) => {
|
||||
console.debug('DROP')
|
||||
reorderTracks(arrayOf('index', selected), index);
|
||||
setSelected([]);
|
||||
},
|
||||
onDragEnter: (item, index, e) => {
|
||||
console.debug('onDragEnter', index);
|
||||
setDropTarget(index);
|
||||
// e.preventDefault();
|
||||
},
|
||||
onDragLeave: (item, index, e) => {
|
||||
console.debug('onDragLeave', index);
|
||||
// e.preventDefault();
|
||||
},
|
||||
onDragEnd: (e) => {
|
||||
console.debug('onDragEnd');
|
||||
setDropTarget(null);
|
||||
},
|
||||
onClick: (item, index, e) => {
|
||||
// e.preventDefault();
|
||||
e.persist();
|
||||
setSelected((prev) => nextSelected(prev, item, index, e));
|
||||
},
|
||||
onDoubleClick: (item, index) => {
|
||||
// if (context_menu) hideContextMenu();
|
||||
setSelected([{ item, index }]);
|
||||
playURIs([item.uri], context);
|
||||
},
|
||||
onContextMenu: (item, index, e) => {
|
||||
// Do our best to stop any flow-on events
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.cancelBubble = true;
|
||||
const items = getOrUpdateSelected(item, index, e).map(({ item: selectedItem }) => selectedItem);
|
||||
|
||||
setSelected((prev) => nextSelected(prev, item, index, e));
|
||||
};
|
||||
|
||||
const onDoubleClick = (item, index) => {
|
||||
// if (context_menu) hideContextMenu();
|
||||
setSelected([{ item, index }]);
|
||||
playURIs([item.uri], context);
|
||||
};
|
||||
|
||||
const onContextMenu = (item, index, e) => {
|
||||
// Do our best to stop any flow-on events
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.cancelBubble = true;
|
||||
|
||||
const alreadySelected = selectionIndexByItemIndex(index);
|
||||
let items = [];
|
||||
if (alreadySelected > -1) {
|
||||
console.debug('ALREADY')
|
||||
items = selected;
|
||||
} else {
|
||||
items = nextSelected(selected, item, index, e);
|
||||
setSelected(items);
|
||||
}
|
||||
console.debug({ alreadySelected, items })
|
||||
items = items.map(({ item: selectedItem }) => selectedItem);
|
||||
|
||||
showContextMenu({
|
||||
e,
|
||||
context,
|
||||
...(items.length === 1
|
||||
? { type: 'track', item: items[0] }
|
||||
: { type: 'tracks', items }
|
||||
),
|
||||
});
|
||||
showContextMenu({
|
||||
e,
|
||||
context,
|
||||
...(items.length === 1
|
||||
? { type: 'track', item: items[0] }
|
||||
: { type: 'tracks', items }
|
||||
),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const onRemoveTracks = () => {
|
||||
@ -175,61 +153,57 @@ const TrackList = ({
|
||||
removeTracks(selected.map(({ index }) => index));
|
||||
};
|
||||
|
||||
const selectionIndexByItemIndex = (index) => (
|
||||
selected && selected.length > 0
|
||||
? selected.findIndex(({ index: selectedIndex }) => selectedIndex === index)
|
||||
: -1
|
||||
);
|
||||
|
||||
/**
|
||||
* Digest our selected tracks
|
||||
*
|
||||
* @param tracks = mixed (defaults to stored value)
|
||||
* @param indexex_only = boolean (do we just want an array of indexes)
|
||||
* @return mixed
|
||||
* */
|
||||
// const digestTracksKeys = (keys = selected_tracks, indexes_only = false) => {
|
||||
// if (!keys) {
|
||||
// return false;
|
||||
// }
|
||||
// Based on a single track's event, procure the next selected array and update the state
|
||||
// Target not selected = select it
|
||||
// Target selected = leave as-is and return current selection
|
||||
const getOrUpdateSelected = (item, index, e) => {
|
||||
const alreadySelected = selectionIndexByItemIndex(index);
|
||||
let items = [];
|
||||
if (alreadySelected > -1) {
|
||||
items = selected;
|
||||
} else {
|
||||
items = nextSelected(selected, item, index, e);
|
||||
setSelected(items);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
// // Accommodate a single key
|
||||
// let singleton = false;
|
||||
// if (!(keys instanceof Array)) {
|
||||
// singleton = true;
|
||||
// keys = [keys];
|
||||
// }
|
||||
const nextSelected = (prev, item, index, e, sticky = false) => {
|
||||
const alreadySelected = selectionIndexByItemIndex(index);
|
||||
|
||||
// // Construct a basic track object, based on our unique track key
|
||||
// // This is enough to perform interactions (dragging, selecting, etc)
|
||||
// const array = [];
|
||||
// for (const key of keys) {
|
||||
// const key_components = key.split('@@');
|
||||
|
||||
// if (indexes_only) {
|
||||
// array.push(key_components[0]);
|
||||
// } else {
|
||||
// array.push({
|
||||
// key,
|
||||
// index: parseInt(key_components[0]),
|
||||
// tlid: parseInt(key_components[1]),
|
||||
// uri: key_components[2],
|
||||
// provider: uriSource(key_components[2]),
|
||||
// context: key_components[3],
|
||||
// context_uri: key_components[4],
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (singleton && array.length > 0) {
|
||||
// return array[0];
|
||||
// }
|
||||
// return array;
|
||||
// }
|
||||
if (e.shiftKey) {
|
||||
const { index: lastSelectedIndex } = selected[selected.length-1] || { index };
|
||||
const next = [...prev, { index, item }];
|
||||
const from = lastSelectedIndex < index ? lastSelectedIndex : index;
|
||||
const to = lastSelectedIndex > index ? lastSelectedIndex : index;
|
||||
for (let i = from; i < to; i++) {
|
||||
next.push({ index: i, item: tracks[i] });
|
||||
}
|
||||
return uniqBy(next, 'index');
|
||||
}
|
||||
if (e.ctrlKey) {
|
||||
if (alreadySelected >= 0 && !sticky) {
|
||||
const next = [...prev];
|
||||
next.splice(alreadySelected, 1);
|
||||
return next;
|
||||
}
|
||||
return [...prev, { item, index }];
|
||||
}
|
||||
return [{ item, index }];
|
||||
};
|
||||
|
||||
if (!tracks || Object.prototype.toString.call(tracks) !== '[object Array]') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const is_selected = (index) => {
|
||||
if (selected.length <= 0) return false;
|
||||
return selected.filter(({ index: selIndex }) => index === selIndex).length > 0;
|
||||
};
|
||||
const is_selected = (index) => selected.find(({ index: i }) => index === i);
|
||||
const is_dropping = (index) => dropTarget === index;
|
||||
|
||||
return (
|
||||
<SmartList
|
||||
@ -244,14 +218,13 @@ const TrackList = ({
|
||||
selected_tracks,
|
||||
can_sort: context?.can_edit,
|
||||
is_selected,
|
||||
is_dropping,
|
||||
mini_zones: slim_mode || isTouchDevice(),
|
||||
onClick,
|
||||
onContextMenu,
|
||||
onDoubleClick,
|
||||
events,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
play_state: state.mopidy.play_state,
|
||||
|
||||
@ -39,6 +39,7 @@
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: colour('blue');
|
||||
border-radius: 100px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@ -287,6 +288,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
&--dropping {
|
||||
&:before {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.has-touch-drag-zone {
|
||||
padding-left: 45px;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user