Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
479 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import { Primer } from '../primer' | ||
import { addListeners, removeListeners } from './handler' | ||
|
||
export class Dnd extends Primer { | ||
public readonly options: Dnd.Options | ||
public disabled: boolean | ||
|
||
constructor(options: Dnd.Options) { | ||
super() | ||
this.options = options | ||
this.disabled = options.disabled != null ? options.disabled : false | ||
if (options.element == null || options.element.nodeType !== 1) { | ||
throw new Error('The trigger element for dnd is illegal.') | ||
} | ||
Dnd.stamp(options.element, this) | ||
} | ||
|
||
enable() { | ||
this.disabled = false | ||
} | ||
|
||
disable() { | ||
this.disabled = true | ||
} | ||
} | ||
|
||
export namespace Dnd { | ||
export const delay = 300 | ||
export const events = { | ||
prepare: 'prepare', | ||
dragStart: 'dragStart', | ||
dragging: 'dragging', | ||
dragEnter: 'dragEnter', | ||
dragOver: 'dragOver', | ||
dragLeave: 'dragLeave', | ||
dragEnd: 'dragEnd', | ||
drop: 'drop', | ||
} | ||
|
||
export type HTMLElementOrFunc = | ||
| HTMLElement | ||
| ((this: Dnd, trigger: HTMLElement) => HTMLElement) | ||
|
||
export interface Options { | ||
element: HTMLElement | ||
containers: | ||
| HTMLElement[] | ||
| ((this: Dnd, trigger: HTMLElement) => HTMLElement[]) | ||
preview?: HTMLElementOrFunc | ||
region?: HTMLElementOrFunc | ||
fully?: boolean | ||
axis?: 'x' | 'y' | ||
disabled?: boolean | ||
} | ||
|
||
export interface State { | ||
e: MouseEvent | TouchEvent | ||
instance: Dnd | ||
element: HTMLElement | ||
preview: HTMLElement | ||
region: HTMLElement | ||
containers: HTMLElement[] | ||
activeContainer?: HTMLElement | null | ||
isPreparing: boolean | ||
isDragging: boolean | ||
pageX: number | ||
pageY: number | ||
diffX: number | ||
diffY: number | ||
data?: any | ||
} | ||
|
||
const cache: WeakMap<HTMLElement, Dnd> = new WeakMap() | ||
|
||
export function stamp(trigger: HTMLElement, dnd: Dnd) { | ||
cache.set(trigger, dnd) | ||
} | ||
|
||
export function getInstance(trigger: HTMLElement) { | ||
return cache.get(trigger) || null | ||
} | ||
|
||
export function enable() { | ||
addListeners(['mousedown', 'touchstart']) | ||
} | ||
|
||
export function disable() { | ||
removeListeners(['mousedown', 'touchstart']) | ||
} | ||
} | ||
|
||
Dnd.enable() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,299 @@ | ||
import { clearSelection } from '../../util' | ||
import { DomEvent } from '../dom-event' | ||
import { Dnd } from './dnd' | ||
import { | ||
getParents, | ||
getDndElement, | ||
getOffset, | ||
outerWidth, | ||
outerHeight, | ||
isContained, | ||
} from './util' | ||
|
||
const win = window | ||
const doc = win.document | ||
|
||
let data: Dnd.State | null = null | ||
let timer: number | ||
let isTouch: boolean | ||
|
||
type EventName = | ||
| 'mousedown' | ||
| 'mousemove' | ||
| 'mouseup' | ||
| 'touchstart' | ||
| 'touchmove' | ||
| 'touchend' | ||
|
||
export function addListeners(names: EventName[]) { | ||
names.forEach(name => DomEvent.addListener(doc, name, process)) | ||
} | ||
|
||
export function removeListeners(names: EventName[]) { | ||
names.forEach(name => DomEvent.removeListener(doc, name, process)) | ||
} | ||
|
||
function process(e: MouseEvent) { | ||
updatePosition(e) | ||
const state = data! | ||
const eventName = e.type as EventName | ||
|
||
if (state != null) { | ||
state.e = e | ||
} | ||
|
||
if (eventName === 'mousedown' || eventName === 'touchstart') { | ||
isTouch = eventName === 'touchstart' | ||
addListeners(['mouseup', 'touchend']) | ||
onMouseDown(e) | ||
} else if ( | ||
(eventName === 'mousemove' || eventName === 'touchmove') && | ||
state != null | ||
) { | ||
if (state.isPreparing) { | ||
onDragStart() | ||
} | ||
|
||
if (state.isDragging) { | ||
onDragging() | ||
onDragEnterLeaveOver() | ||
} | ||
|
||
clearSelection() | ||
e.preventDefault() | ||
} else if ( | ||
(eventName === 'mouseup' || eventName === 'touchend') && | ||
state != null | ||
) { | ||
removeListeners(['mouseup', 'touchend']) | ||
if (timer) { | ||
clearTimeout(timer) | ||
timer = 0 | ||
return | ||
} | ||
|
||
if (state.isDragging) { | ||
state.isDragging = false | ||
onDrop() | ||
} else if (state.isPreparing) { | ||
state.isPreparing = false | ||
state.instance.trigger(Dnd.events.dragEnd, state) | ||
clear() | ||
} | ||
} | ||
} | ||
|
||
function onMouseDown(e: MouseEvent | TouchEvent) { | ||
const delay = isTouch ? 200 : Dnd.delay || 300 | ||
timer = window.setTimeout(() => { | ||
timer = 0 | ||
prepare(e) | ||
}, delay) | ||
} | ||
|
||
function prepare(e: MouseEvent | TouchEvent) { | ||
let element: HTMLElement | null = null | ||
let instance: Dnd | null = null | ||
const parents = getParents(e.target as HTMLElement) | ||
for (let i = 0, ii = parents.length; i < ii; i += 1) { | ||
element = parents[i] | ||
instance = Dnd.getInstance(element) | ||
if (instance != null) { | ||
break | ||
} | ||
} | ||
|
||
// null or disabled | ||
if (instance == null || instance.disabled) { | ||
return | ||
} | ||
|
||
const options = instance.options | ||
|
||
data = {} as Dnd.State | ||
data.e = e | ||
data.element = element! | ||
data.instance = instance | ||
data.isPreparing = true | ||
data.isDragging = false | ||
|
||
data.preview = getDndElement( | ||
instance, | ||
data.element, | ||
options.preview, | ||
() => data!.element.cloneNode(true) as HTMLElement, | ||
) | ||
|
||
data.region = getDndElement(instance, data.element, options.region, doc.body) | ||
data.containers = | ||
typeof options.containers === 'function' | ||
? options.containers.call(instance, data.element) | ||
: options.containers | ||
|
||
updatePosition(e) | ||
|
||
// 将代理元素插入文档,设置样式等 | ||
data.instance.trigger(Dnd.events.prepare, data) | ||
|
||
const offset = getOffset(data.element) | ||
const width = outerWidth(data.element) | ||
const height = outerHeight(data.element) | ||
const rateX = (data.pageX - offset.left) / width | ||
const rateY = (data.pageY - offset.top) / height | ||
|
||
previewWidth = outerWidth(data.preview) | ||
previewHeight = outerHeight(data.preview) | ||
|
||
data.diffX = rateX * previewWidth | ||
data.diffY = rateY * previewHeight | ||
|
||
data.preview.style.left = `${data.pageX - data.diffX}px` | ||
data.preview.style.top = `${data.pageY - data.diffY}px` | ||
|
||
addListeners(['mousemove', 'mouseup', 'touchmove', 'touchend']) | ||
} | ||
|
||
let fixTop: number | ||
let fixLeft: number | ||
let regionOffset: { left: number; top: number } | null | ||
let regionHeight: number | ||
let regionWidth: number | ||
let previewWidth: number | ||
let previewHeight: number | ||
|
||
function onDragStart() { | ||
const state = data! | ||
|
||
state.isPreparing = false | ||
state.isDragging = true | ||
|
||
const previewOffset = getOffset(state.preview) | ||
const parentOffset = getOffset(state.preview.parentNode as HTMLElement) | ||
const style = state.preview.style | ||
|
||
// 修正值 | ||
fixLeft = | ||
previewOffset.left - parentOffset.left - parseFloat(style.left || '0') | ||
fixTop = previewOffset.top - parentOffset.top - parseFloat(style.top || '0') | ||
|
||
// 区域 | ||
regionOffset = getOffset(state.region) | ||
regionWidth = outerWidth(state.region) | ||
regionHeight = outerHeight(state.region) | ||
|
||
state.instance.trigger(Dnd.events.dragStart, state) | ||
} | ||
|
||
function onDragging() { | ||
const state = data! | ||
const axis = state.instance.options.axis | ||
const top = state.pageY - state.diffY | ||
const left = state.pageX - state.diffX | ||
|
||
// 限制在指定的区域内 | ||
const getLeft = () => { | ||
const offset = regionOffset! | ||
if ( | ||
left >= offset.left && | ||
left + previewWidth <= offset.left + regionWidth | ||
) { | ||
return left - fixLeft | ||
} | ||
|
||
if (left < offset.left) { | ||
return offset.left - fixLeft | ||
} | ||
|
||
return offset.left + regionWidth - previewWidth - fixLeft | ||
} | ||
|
||
const getTop = () => { | ||
const offset = regionOffset! | ||
if (top >= offset.top && top + previewHeight <= offset.top + regionHeight) { | ||
return top - fixTop | ||
} | ||
|
||
if (top <= offset.top) { | ||
return offset.top - fixTop | ||
} | ||
|
||
return offset.top + regionHeight - previewHeight - fixTop | ||
} | ||
|
||
const style = state.preview.style | ||
if (axis !== 'y') { | ||
style.left = `${getLeft()}px` | ||
} | ||
if (axis !== 'x') { | ||
style.top = `${getTop()}px` | ||
} | ||
|
||
state.instance.trigger(Dnd.events.dragging, state) | ||
} | ||
|
||
function onDragEnterLeaveOver() { | ||
const state = data! | ||
const containers = state.containers | ||
const fully = state.instance.options.fully | ||
|
||
if (!containers || !containers.length) { | ||
return | ||
} | ||
|
||
if (state.activeContainer) { | ||
if (!isContained(state.activeContainer, state.preview, fully)) { | ||
state.instance.trigger(Dnd.events.dragLeave, state) | ||
state.activeContainer = null | ||
} else { | ||
state.instance.trigger(Dnd.events.dragOver, state) | ||
} | ||
} else { | ||
for (let i = 0, ii = containers.length; i < ii; i += 1) { | ||
const container = containers[i] | ||
if (isContained(container, state.preview, fully)) { | ||
state.activeContainer = container | ||
state.instance.trigger(Dnd.events.dragEnter, state) | ||
break | ||
} | ||
} | ||
} | ||
} | ||
|
||
function onDrop() { | ||
data!.instance.trigger(Dnd.events.drop, data) | ||
clear() | ||
} | ||
|
||
function clear() { | ||
removeListeners(['mousemove', 'mouseup', 'touchmove', 'touchend']) | ||
|
||
data = null | ||
isTouch = false | ||
|
||
fixLeft = 0 | ||
fixTop = 0 | ||
regionOffset = null | ||
regionHeight = 0 | ||
regionWidth = 0 | ||
previewWidth = 0 | ||
previewHeight = 0 | ||
} | ||
|
||
function updatePosition(e: MouseEvent | TouchEvent) { | ||
if (data == null) { | ||
return | ||
} | ||
|
||
if (isTouch) { | ||
const evt = e as TouchEvent | ||
const changedTouches = evt.changedTouches | ||
if (changedTouches && changedTouches.length) { | ||
data.pageX = changedTouches[0].pageX | ||
data.pageY = changedTouches[0].pageY | ||
} | ||
} else { | ||
const evt = e as MouseEvent | ||
data.pageX = evt.pageX | ||
data.pageY = evt.pageY | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './dnd' |
Oops, something went wrong.