svelte-drag-and-drop-actions
Version:
HTML5 Drag-and-Drop implemented as Svelte actions
1,507 lines • 56.8 kB
text/typescript
//----------------------------------------------------------------------------//
// Svelte Drag-and-Drop Actions //
//----------------------------------------------------------------------------//
import {
global, throwError,
ValueIsNumber, ValueIsFiniteNumber, ValueIsString, ValueIsNonEmptyString,
ValueIsFunction, ValueIsPlainObject, ValueIsOneOf,
allowedFiniteNumber, allowedIntegerInRange, allowedOrdinal,
allowedString, allowedNonEmptyString,
allowPlainObject, allowedPlainObject,
allowListSatisfying, allowedFunction,
ObjectIsNotEmpty, quoted, constrained
} from 'javascript-interface-library'
import Conversion from 'svelte-coordinate-conversion'
/**** never ever trust module loading if you REALLY need a singleton! ****/
// finding multiple existing singletons if you actually trust them is so hard!
type ContextType = extendedDragAndDropSupport & SupportForHoldingAndPanning
const Context:ContextType = ( // make this package a REAL singleton
'__DragAndDropActions' in global
? global.__DragAndDropActions
: global.__DragAndDropActions = {}
)
//-------------------------------------------------------------------------------
//-- use:asDraggable={options} - "drag" without "drop" --
//-------------------------------------------------------------------------------
export type PositionReference = (
'parent' | 'body' | string | HTMLElement | SVGElement // | MathMLElement
)
export type Position = { x:number, y:number }
export type DragDummy = (
string | HTMLElement | SVGElement | // MathMLElement |
((DraggableExtras:any, Element:HTMLElement|SVGElement) => HTMLElement|SVGElement) |
'standard' | 'none'
) | null | undefined
type DraggableOptions = {
Extras?:any,
relativeTo?:PositionReference, onlyFrom?:string, neverFrom?:string,
Dummy?:DragDummy, DummyOffsetX?:number, DummyOffsetY?:number,
minX?:number, minY?:number, maxX?:number, maxY?:number,
Pannable?:string|HTMLElement|SVGElement,
PanSensorWidth?:number, PanSensorHeight?:number, PanSpeed?:number,
onDragStart?:Position | ((DraggableExtras:any) => Position),
onDragMove?: (x:number,y:number, dx:number,dy:number, DraggableExtras:any) => void,
onDragEnd?: (x:number,y:number, dx:number,dy:number, DraggableExtras:any) => void,
}
/**** parsedDraggableOptions ****/
function parsedDraggableOptions (Options:any):DraggableOptions {
Options = allowedPlainObject('drag options',Options) || {}
let Extras:any, relativeTo:PositionReference
let onlyFrom:string|undefined, neverFrom:string|undefined
let Dummy:DragDummy, DummyOffsetX:number, DummyOffsetY:number
let minX:number, minY:number, maxX:number, maxY:number
let Pannable:string|HTMLElement|SVGElement|undefined
let PanSensorWidth:number, PanSensorHeight:number, PanSpeed:number
let onDragStart:Function, onDragMove:Function, onDragEnd:Function, onDragCancel:Function
Extras = Options.Extras
switch (true) {
case (Options.relativeTo == null):
relativeTo = 'parent'; break
case (Options.relativeTo === 'parent'):
case (Options.relativeTo === 'body'):
case ValueIsNonEmptyString(Options.relativeTo):
case (Options.relativeTo instanceof HTMLElement):
case (Options.relativeTo instanceof SVGElement):
// case (Options.relativeTo instanceof MathMLElement):
relativeTo = Options.relativeTo as PositionReference; break
default: throwError(
'InvalidArgument: invalid position reference given'
)
}
onlyFrom = allowedNonEmptyString ('"onlyFrom" CSS selector',Options.onlyFrom)
neverFrom = allowedNonEmptyString('"neverFrom" CSS selector',Options.neverFrom)
switch (true) {
case (Options.Dummy == null):
Dummy = undefined; break
case (Options.Dummy === 'standard'):
case (Options.Dummy === 'none'):
case ValueIsNonEmptyString(Options.Dummy):
case (Options.Dummy instanceof HTMLElement):
case (Options.Dummy instanceof SVGElement):
// case (Options.Dummy instanceof MathMLElement):
case ValueIsFunction(Options.Dummy):
Dummy = Options.Dummy as DragDummy; break
default: throwError(
'InvalidArgument: invalid drag dummy specification given'
)
}
DummyOffsetX = allowedFiniteNumber('dummy x offset',Options.DummyOffsetX)
DummyOffsetY = allowedFiniteNumber('dummy y offset',Options.DummyOffsetY)
minX = allowedFiniteNumber('min. x position',Options.minX)
if (minX == null) { minX = -Infinity }
minY = allowedFiniteNumber('min. y position',Options.minY)
if (minY == null) { minY = -Infinity }
maxX = allowedFiniteNumber('max. x position',Options.maxX)
if (maxX == null) { maxX = Infinity }
maxY = allowedFiniteNumber('max. y position',Options.maxY)
if (maxY == null) { maxY = Infinity }
switch (true) {
case (Options.Pannable == null):
Pannable = undefined; break
case ValueIsNonEmptyString(Options.Pannable):
case (Options.Pannable instanceof HTMLElement):
case (Options.Pannable instanceof SVGElement):
// case (Options.Pannable instanceof MathMLElement):
Pannable = Options.Pannable; break
default: throwError(
'InvalidArgument: invalid "Pannable" specification given'
)
}
PanSensorWidth = allowedOrdinal ('panning sensor width',Options.PanSensorWidth)
if (PanSensorWidth == null) { PanSensorWidth = 20 }
PanSensorHeight = allowedOrdinal('panning sensor height',Options.PanSensorHeight)
if (PanSensorHeight == null) { PanSensorHeight = 20 }
PanSpeed = allowedOrdinal ('panning speed',Options.PanSpeed)
if (PanSpeed == null) { PanSpeed = 10 }
if (ValueIsPosition(Options.onDragStart)) {
let { x,y } = Options.onDragStart as Position
onDragStart = () => ({x,y})
} else {
onDragStart = allowedFunction('"onDragStart" handler', Options.onDragStart)
}
onDragMove = allowedFunction('"onDragMove" handler', Options.onDragMove)
onDragEnd = allowedFunction('"onDragEnd" handler', Options.onDragEnd)
return {
Extras, relativeTo, onlyFrom,neverFrom, Dummy, DummyOffsetX,DummyOffsetY,
minX,minY, maxX,maxY,
Pannable, PanSensorWidth,PanSensorHeight, PanSpeed,
// @ts-ignore we cannot validate given functions any further
onDragStart, onDragMove, onDragEnd, onDragCancel
}
}
/**** use:asDraggable={options} ****/
export function asDraggable (
Element:HTMLElement|SVGElement, Options:DraggableOptions
) {
let isDragged:boolean
let currentDraggableOptions:DraggableOptions
let PositionReference:Element // element with user coordinate system
let ReferenceDeltaX:number, ReferenceDeltaY:number // mouse -> user coord.s
let PositioningWasDelayed:boolean // workaround for prob. with "drag" events
let DragImage:Element | undefined
let initialPosition:Position // given in user coordinates
let lastPosition: Position // dto.
isDragged = false
currentDraggableOptions = parsedDraggableOptions(Options)
/**** startDragging ****/
function startDragging (originalEvent:DragEvent) {
let Options = currentDraggableOptions
if (fromForbiddenElement(Element,Options,originalEvent)) {
originalEvent.stopPropagation()
originalEvent.preventDefault()
return false
}
PositionReference = PositionReferenceFor(Element,Options)
let relativePosition = Conversion.fromDocumentTo(
'local', { left:originalEvent.pageX, top:originalEvent.pageY }, PositionReference
) // relative to reference element
ReferenceDeltaX = ReferenceDeltaY = 0; initialPosition = { x:0,y:0 }
if (Options.onDragStart == null) {
initialPosition = { x:0,y:0 } // given in user coordinates
} else {
try {
let StartPosition = (Options.onDragStart as Function)(Options.Extras)
if (ValueIsPlainObject(StartPosition)) {
let x = allowedFiniteNumber('x start position',StartPosition.x)
let y = allowedFiniteNumber('y start position',StartPosition.y)
ReferenceDeltaX = x - relativePosition.left
ReferenceDeltaY = y - relativePosition.top
x = constrained(x, Options.minX,Options.maxX)
y = constrained(y, Options.minY,Options.maxY)
initialPosition = { x,y } // given in user coordinates
}
} catch (Signal) {
console.error('"onDragStart" handler failed',Signal)
}
}
lastPosition = initialPosition
PositioningWasDelayed = false // initializes workaround
if (Options.Dummy == null) {
Options.Dummy = 'none' // this is the default for "use.asDraggable"
}
DragImage = DragImageFor(Element,Options)
if ((DragImage != null) && (originalEvent.dataTransfer != null)) {
let OffsetX = Options.DummyOffsetX as number
let OffsetY = Options.DummyOffsetY as number
if ((OffsetX == null) || (OffsetY == null)) {
let PositionInDraggable = Conversion.fromDocumentTo(
'local', { left:originalEvent.pageX, top:originalEvent.pageY }, Element
)
if (OffsetX == null) { OffsetX = PositionInDraggable.left }
if (OffsetY == null) { OffsetY = PositionInDraggable.top }
}
switch (true) {
case (Options.Dummy === 'none'):
originalEvent.dataTransfer.setDragImage(DragImage,0,0)
setTimeout(() => { // remove element after browser took its snapshot
document.body.removeChild(DragImage as HTMLElement)
},0)
break
case ValueIsString(Options.Dummy):
originalEvent.dataTransfer.setDragImage(DragImage, OffsetX,OffsetY)
setTimeout(() => { // remove element after browser took its snapshot
document.body.removeChild(
(DragImage as HTMLElement).parentElement as HTMLElement
)
},0)
break
default:
originalEvent.dataTransfer.setDragImage(DragImage, OffsetX,OffsetY)
}
}
if (originalEvent.dataTransfer != null) {
originalEvent.dataTransfer.effectAllowed = 'none'
}
isDragged = true
setTimeout(() => Element.classList.add('dragged'), 0)
originalEvent.stopPropagation()
}
/**** continueDragging ****/
function continueDragging (originalEvent:DragEvent) {
if (! isDragged) { return false }
let Options = currentDraggableOptions
if (
(originalEvent.screenX === 0) && (originalEvent.screenY === 0) &&
! PositioningWasDelayed
) {
PositioningWasDelayed = true // last "drag" event contains wrong coord.s
} else {
PositioningWasDelayed = false
performPanningFor(
'draggable',
Element, Options, originalEvent.pageX,originalEvent.pageY
)
let relativePosition = Conversion.fromDocumentTo(
'local', { left:originalEvent.pageX, top:originalEvent.pageY }, PositionReference
) // relative to reference element
let x = relativePosition.left + ReferenceDeltaX // in user coordinates
let y = relativePosition.top + ReferenceDeltaY
x = constrained(x, Options.minX,Options.maxX)
y = constrained(y, Options.minY,Options.maxY)
let dx = x - lastPosition.x // calculated AFTER constraining x,y
let dy = y - lastPosition.y // dto.
lastPosition = { x,y }
invokeHandler('onDragMove', Options, x,y, dx,dy, Options.Extras)
}
originalEvent.stopPropagation()
}
/**** finishDragging ****/
function finishDragging (originalEvent:DragEvent) {
if (! isDragged) { return false }
// continueDragging(originalEvent) // NO! positions might be wrong!
let Options = currentDraggableOptions
if (Options.onDragEnd != null) {
let x = constrained(lastPosition.x, Options.minX,Options.maxX)
let y = constrained(lastPosition.y, Options.minY,Options.maxY)
let dx = x - lastPosition.x
let dy = y - lastPosition.y
invokeHandler('onDragEnd', Options, x,y, dx,dy, Options.Extras)
}
isDragged = false
Element.classList.remove('dragged')
originalEvent.stopPropagation()
}
/**** updateDraggableOptions ****/
function updateDraggableOptions (Options:any):void {
Options = parsedDraggableOptions(Options)
if (
(currentDraggableOptions.Extras == null) && (Options.Extras != null)
) {
currentDraggableOptions.Extras = Options.Extras
} // Extras may be set with delay, but remain constant afterwards
currentDraggableOptions.Dummy = (
Options.Dummy || currentDraggableOptions.Dummy
)
currentDraggableOptions.minX = Options.minX
currentDraggableOptions.minY = Options.minY
currentDraggableOptions.maxX = Options.maxX
currentDraggableOptions.maxY = Options.maxY
currentDraggableOptions.Pannable = Options.Pannable
currentDraggableOptions.PanSensorWidth = Options.PanSensorWidth
currentDraggableOptions.PanSensorHeight = Options.PanSensorHeight
currentDraggableOptions.PanSpeed = Options.PanSpeed
currentDraggableOptions.onDragStart = (
Options.onDragStart || currentDraggableOptions.onDragStart
) // may be used to update initial position for subsequent drags
}
Element.setAttribute('draggable','true')
// @ts-ignore we know that the passed event is a DragEvent
Element.addEventListener('dragstart', startDragging)
// @ts-ignore we know that the passed event is a DragEvent
Element.addEventListener('drag', continueDragging)
// @ts-ignore we know that the passed event is a DragEvent
Element.addEventListener('dragend', finishDragging)
return { update:updateDraggableOptions }
}
/**** fromForbiddenElement ****/
function fromForbiddenElement (
Element:HTMLElement|SVGElement, Options:DraggableOptions,
originalEvent:DragEvent
):boolean {
if ((Options.onlyFrom != null) || (Options.neverFrom != null)) {
let x = originalEvent.clientX
let y = originalEvent.clientY
let touchedElement = document.elementFromPoint(x,y) as HTMLElement
// elementFromPoint considers elements with "pointer-events" <> "none" only
// but sometimes, "pointer-events:none" is needed for proper operation
touchedElement = innerElementOf(touchedElement, x,y)
if (Options.onlyFrom != null) {
let fromElement = touchedElement.closest(Options.onlyFrom as string)
if ((Element !== fromElement) && ! Element.contains(fromElement)) {
return true
}
}
if (Options.neverFrom != null) {
let fromElement = touchedElement.closest(Options.neverFrom as string)
if ((Element === fromElement) || Element.contains(fromElement)) {
return true
}
}
}
return false
}
/**** innerElementOf ****/
function innerElementOf (Candidate:HTMLElement, x:number,y:number):HTMLElement {
let innerElements = Candidate.children
for (let i = 0, l = innerElements.length; i < l; i++) {
let innerElement = innerElements[i] as HTMLElement
let Position = Conversion.fromLocalTo(
'viewport', { left:0, top:0 }, innerElement
)
if ((x < Position.left) || (y < Position.top)) { continue }
if (x > Position.left+innerElement.offsetWidth-1) { continue }
if (y > Position.top+innerElement.offsetHeight-1) { continue }
return innerElementOf(innerElement, x,y)
}
return Candidate // this is the innermost element at (x,y)
}
/**** extended Drag-and-Drop Support ****/
type extendedDragAndDropSupport = {
currentDroppableExtras:any, // extras for currently dragged droppable
currentDropZoneExtras:any, // extras for currently hovered drop zone
currentDropZoneElement:Element|undefined, // dto. as Element
DroppableWasDropped:boolean, // indicates a successful drop operation
currentDropZonePosition:Position|undefined, // position relative to DropZone
currentDropOperation:DropOperation|undefined, // actual drop operation
currentTypeTransferred:string|undefined, // actual type of transferred data
currentDataTransferred:any, // actual data transferred
}
/**** Support for Holding and Panning ****/
type SupportForHoldingAndPanning = {
HoldPosition?:Position, // current position to compare against
HoldTimer?:any,
HoldWasTriggeredForElement?:HTMLElement | SVGElement,
DropZonePanning?:boolean
} // because we trigger it once only
//-------------------------------------------------------------------------------
//-- use:asDroppable={options} - "drag" and "drop" --
//-------------------------------------------------------------------------------
export const DropOperations = [ 'copy', 'move', 'link' ]
export type DropOperation = typeof DropOperations[number]
export type DataOfferSet = { [Type:string]:string }
type DroppableOptions = DraggableOptions & {
Operations?:string,// consisting of 'copy', 'move', 'link' (blank-separated)
DataToOffer?:DataOfferSet,
onDropZoneEnter?: (x:number,y:number, DropZoneExtras:any, DroppableExtras:any) => void,
onDropZoneHover?: (x:number,y:number, DropZoneExtras:any, DroppableExtras:any) => void,
onDropZoneLeave?: (DropZoneExtras:any, DroppableExtras:any) => void,
onDropped?: (x:number,y:number, Operation:DropOperation,
TypeTransferred:string, DataTransferred:any,
DropZoneExtras:any, DroppableExtras:any) => void,
}
/**** parsedDroppableOptions ****/
function parsedDroppableOptions (Options:any):DroppableOptions {
Options = allowedPlainObject('drop options',Options) || {}
let Operations:string, DataToOffer:DataOfferSet
let onDropZoneEnter:Function, onDropZoneHover:Function, onDropZoneLeave:Function
let onDropped:Function
Operations = parsedOperations('list of allowed operations',Options.Operations,'copy')
DataToOffer = Object.assign(
{}, allowedPlainObject('data to be offered',Options.DataToOffer)
)
if ('none' in DataToOffer) throwError(
'InvalidArgument: "none" is not a valid data type'
)
onDropZoneEnter = allowedFunction('"onDropZoneEnter" handler',Options.onDropZoneEnter)
onDropZoneHover = allowedFunction('"onDropZoneHover" handler',Options.onDropZoneHover)
onDropZoneLeave = allowedFunction('"onDropZoneLeave" handler',Options.onDropZoneLeave)
onDropped = allowedFunction('"onDropped" handler', Options.onDropped)
return {
Operations, DataToOffer,
// @ts-ignore we cannot validate given functions any further
onDropZoneEnter, onDropZoneHover, onDropZoneLeave, onDropped
}
}
/**** use:asDroppable={options} ****/
export function asDroppable (
Element:HTMLElement|SVGElement, Options:DroppableOptions
) {
let isDragged:boolean
let currentDraggableOptions:DraggableOptions
let currentDroppableOptions:DroppableOptions
let PositionReference:Element // element with user coordinate system
let ReferenceDeltaX:number, ReferenceDeltaY:number // mouse -> user coord.s
let PositioningWasDelayed:boolean // workaround for prob. with "drag" events
let DragImage:Element | undefined
let initialPosition:Position // given in user coordinates
let lastPosition: Position // dto.
let lastDropZoneElement:HTMLElement|SVGElement|undefined
let lastDropZoneExtras:any
isDragged = false
currentDraggableOptions = parsedDraggableOptions(Options)
currentDroppableOptions = parsedDroppableOptions(Options)
/**** startDragging ****/
function startDragging (originalEvent:DragEvent) {
let Options = Object.assign(
{}, currentDraggableOptions, currentDroppableOptions
)
if (fromForbiddenElement(Element,Options,originalEvent)) {
originalEvent.stopPropagation()
originalEvent.preventDefault()
return false
}
PositionReference = PositionReferenceFor(Element,Options)
let relativePosition = Conversion.fromDocumentTo(
'local', { left:originalEvent.pageX, top:originalEvent.pageY }, PositionReference
) // relative to reference element
ReferenceDeltaX = ReferenceDeltaY = 0; initialPosition = { x:0,y:0 }
if (Options.onDragStart == null) {
initialPosition = { x:0,y:0 } // given in user coordinates
} else {
try {
let StartPosition = (Options.onDragStart as Function)(Options.Extras)
if (ValueIsPlainObject(StartPosition)) {
let x = allowedFiniteNumber('x start position',StartPosition.x)
let y = allowedFiniteNumber('y start position',StartPosition.y)
ReferenceDeltaX = x - relativePosition.left
ReferenceDeltaY = y - relativePosition.top
x = constrained(x, Options.minX,Options.maxX)
y = constrained(y, Options.minY,Options.maxY)
initialPosition = { x,y } // given in user coordinates
}
} catch (Signal) {
console.error('"onDragStart" handler failed',Signal)
}
}
lastPosition = initialPosition
lastDropZoneElement = undefined
lastDropZoneExtras = undefined
PositioningWasDelayed = false // initializes workaround
if (Options.Dummy == null) {
Options.Dummy = 'standard' // this is the default for "use.asDroppable"
}
DragImage = DragImageFor(Element,Options)
if ((DragImage != null) && (originalEvent.dataTransfer != null)) {
let OffsetX = Options.DummyOffsetX as number
let OffsetY = Options.DummyOffsetY as number
if ((OffsetX == null) || (OffsetY == null)) {
let PositionInDraggable = Conversion.fromDocumentTo(
'local', { left:originalEvent.pageX, top:originalEvent.pageY }, Element
)
if (OffsetX == null) { OffsetX = PositionInDraggable.left }
if (OffsetY == null) { OffsetY = PositionInDraggable.top }
}
switch (true) {
case (Options.Dummy === 'none'):
originalEvent.dataTransfer.setDragImage(DragImage,0,0)
setTimeout(() => { // remove element after browser took its snapshot
document.body.removeChild(DragImage as HTMLElement)
},0)
break
case ValueIsString(Options.Dummy):
originalEvent.dataTransfer.setDragImage(DragImage, OffsetX,OffsetY)
setTimeout(() => { // remove element after browser took its snapshot
document.body.removeChild(
(DragImage as HTMLElement).parentElement as HTMLElement
)
},0)
break
default:
originalEvent.dataTransfer.setDragImage(DragImage, OffsetX,OffsetY)
}
}
if (originalEvent.dataTransfer != null) {
let allowedEffects = allowedEffectsFrom(Options.Operations as string)
originalEvent.dataTransfer.effectAllowed = allowedEffects
if (ObjectIsNotEmpty(Options.DataToOffer)) {
for (let Type in Options.DataToOffer) {
if (Options.DataToOffer.hasOwnProperty(Type)) {
originalEvent.dataTransfer.setData(
Type, Options.DataToOffer[Type]
)
}
}
}
}
Context.currentDroppableExtras = Options.Extras
Context.currentDropZoneExtras = undefined
Context.currentDropZonePosition = undefined
Context.currentDropZoneElement = undefined
Context.DroppableWasDropped = false
Context.currentDropOperation = undefined
Context.currentTypeTransferred = undefined
Context.currentDataTransferred = undefined
isDragged = true
setTimeout(() => Element.classList.add('dragged'), 0)
originalEvent.stopPropagation()
}
/**** continueDragging ****/
function continueDragging (originalEvent:DragEvent) {
if (! isDragged) { return false }
let Options = Object.assign(
{}, currentDraggableOptions, currentDroppableOptions
)
if (
(originalEvent.screenX === 0) && (originalEvent.screenY === 0) &&
! PositioningWasDelayed
) {
PositioningWasDelayed = true // last "drag" event contains wrong coord.s
} else {
PositioningWasDelayed = false
performPanningFor(
'draggable',
Element, Options, originalEvent.pageX,originalEvent.pageY
)
let relativePosition = Conversion.fromDocumentTo(
'local', { left:originalEvent.pageX, top:originalEvent.pageY }, PositionReference
) // relative to reference element
let x = relativePosition.left + ReferenceDeltaX // in user coordinates
let y = relativePosition.top + ReferenceDeltaY
x = constrained(x, Options.minX,Options.maxX)
y = constrained(y, Options.minY,Options.maxY)
let dx = x - lastPosition.x // calculated AFTER constraining x,y
let dy = y - lastPosition.y // dto.
lastPosition = { x,y }
invokeHandler('onDragMove', Options, x,y, dx,dy, Options.Extras)
}
if (Context.currentDropZoneElement === lastDropZoneElement) {
if (Context.currentDropZoneElement != null) {
invokeHandler(
'onDropZoneHover', Options,
(Context.currentDropZonePosition as Position).x,(Context.currentDropZonePosition as Position).y,
Context.currentDropZoneExtras, Options.Extras
)
}
} else {
if (Context.currentDropZoneElement == null) {
Element.classList.remove('droppable')
invokeHandler('onDropZoneLeave', Options, lastDropZoneExtras, Options.Extras)
} else {
Element.classList.add('droppable')
invokeHandler(
'onDropZoneEnter', Options,
(Context.currentDropZonePosition as Position).x,(Context.currentDropZonePosition as Position).y,
lastDropZoneExtras, Options.Extras
)
}
lastDropZoneElement = Context.currentDropZoneElement as HTMLElement
lastDropZoneExtras = Context.currentDropZoneExtras
}
originalEvent.stopPropagation()
}
/**** finishDragging ****/
function finishDragging (originalEvent:DragEvent) {
if (! isDragged) { return false }
// continueDragging(originalEvent) // NO! positions might be wrong!
let Options = Object.assign(
{}, currentDraggableOptions, currentDroppableOptions
)
if (Context.DroppableWasDropped) {
invokeHandler(
'onDropped', Options,
(Context.currentDropZonePosition as Position).x,(Context.currentDropZonePosition as Position).y,
Context.currentDropOperation, Context.currentTypeTransferred, Context.currentDataTransferred,
Context.currentDropZoneExtras, Options.Extras
)
Context.currentDropZoneExtras = undefined
Context.currentDropZonePosition = undefined
Context.currentDropZoneElement = undefined
Context.DroppableWasDropped = false
Context.currentDropOperation = undefined
Context.currentTypeTransferred = undefined
Context.currentDataTransferred = undefined
}
if (Options.onDragEnd != null) {
let x = constrained(lastPosition.x, Options.minX,Options.maxX)
let y = constrained(lastPosition.y, Options.minY,Options.maxY)
let dx = x - lastPosition.x
let dy = y - lastPosition.y
invokeHandler('onDragEnd', Options, x,y, dx,dy, Options.Extras)
}
Context.currentDroppableExtras = undefined
isDragged = false
Element.classList.remove('dragged','droppable')
originalEvent.stopPropagation()
}
/**** updateDraggableOptions ****/
function updateDraggableOptions (Options:any):void {
Options = parsedDraggableOptions(Options)
if (
(currentDraggableOptions.Extras == null) && (Options.Extras != null)
) {
currentDraggableOptions.Extras = Options.Extras
} // Extras may be set with delay, but remain constant afterwards
currentDraggableOptions.Dummy = (
Options.Dummy || currentDraggableOptions.Dummy
)
currentDraggableOptions.minX = Options.minX
currentDraggableOptions.minY = Options.minY
currentDraggableOptions.maxX = Options.maxX
currentDraggableOptions.maxY = Options.maxY
currentDraggableOptions.Pannable = Options.Pannable
currentDraggableOptions.PanSensorWidth = Options.PanSensorWidth
currentDraggableOptions.PanSensorHeight = Options.PanSensorHeight
currentDraggableOptions.PanSpeed = Options.PanSpeed
currentDraggableOptions.onDragStart = (
Options.onDragStart || currentDraggableOptions.onDragStart
) // may be used to update initial position for subsequent drags
}
/**** updateDroppableOptions ****/
function updateDroppableOptions (Options:any):void {
Options = parsedDroppableOptions(Options)
currentDroppableOptions.Operations = Options.Operations
currentDroppableOptions.DataToOffer = Options.DataToOffer
}
Element.setAttribute('draggable','true')
// @ts-ignore we know that the passed event is a DragEvent
Element.addEventListener('dragstart', startDragging)
// @ts-ignore we know that the passed event is a DragEvent
Element.addEventListener('drag', continueDragging)
// @ts-ignore we know that the passed event is a DragEvent
Element.addEventListener('dragend', finishDragging)
return {
update:(Options:any) => {
updateDraggableOptions(Options)
updateDroppableOptions(Options)
}
}
}
//-------------------------------------------------------------------------------
//-- use:asDropZone={options} - for drop zones --
//-------------------------------------------------------------------------------
export type TypeAcceptanceSet = { [Type:string]:string }
// values consist of 'copy', 'move', 'link' (blank-separated)
type DropZoneOptions = {
Extras?:any,
TypesToAccept?:TypeAcceptanceSet,
HoldDelay?:number,
Pannable?:string|'this'|HTMLElement|SVGElement,
PanSensorWidth?:number, PanSensorHeight?:number, PanSpeed?:number,
onDroppableEnter?:(x:number,y:number, Operation:DropOperation, offeredTypeList:string[],
DroppableExtras:any, DropZoneExtras:any) => boolean|undefined,
onDroppableMove?: (x:number,y:number, Operation:DropOperation, offeredTypeList:string[],
DroppableExtras:any, DropZoneExtras:any) => boolean|undefined,
onDroppableHold?: (x:number,y:number, DroppableExtras:any, DropZoneExtras:any) => void,
onDroppableLeave?:(DroppableExtras:any, DropZoneExtras:any) => void,
onDrop?: (x:number,y:number, Operation:DropOperation, DataOffered:any,
DroppableExtras:any, DropZoneExtras:any) => string | undefined,
}
/**** parsedDropZoneOptions ****/
function parsedDropZoneOptions (Options:any):DropZoneOptions {
Options = allowedPlainObject('drop zone options',Options) || {}
let Extras:any, TypesToAccept:TypeAcceptanceSet, HoldDelay:number
let Pannable:string|'this'|HTMLElement|SVGElement|undefined
let PanSensorWidth:number, PanSensorHeight:number, PanSpeed:number
let onDroppableEnter:Function, onDroppableMove:Function, onDroppableLeave:Function
let onDroppableHold:Function, onDroppableRelease:Function, onDrop:Function
Extras = Options.Extras
allowPlainObject('data types to be accepted',Options.TypesToAccept)
TypesToAccept = Object.create(null)
if ((Options.TypesToAccept != null) && ('none' in Options.TypesToAccept)) throwError(
'InvalidArgument: "none" is not a valid data type'
)
for (let Type in Options.TypesToAccept) {
if (Options.TypesToAccept.hasOwnProperty(Type)) {
TypesToAccept[Type] = parsedOperations(
'list of accepted operations for type ' + quoted(Type),
Options.TypesToAccept[Type]
)
}
}
HoldDelay = allowedIntegerInRange('min. time to hold',Options.HoldDelay, 0) as number
switch (true) {
case (Options.Pannable == null):
Pannable = undefined; break
case (Options.Pannable === 'this'):
case ValueIsNonEmptyString(Options.Pannable):
case (Options.Pannable instanceof HTMLElement):
case (Options.Pannable instanceof SVGElement):
// case (Options.Pannable instanceof MathMLElement):
Pannable = Options.Pannable; break
default: throwError(
'InvalidArgument: invalid "Pannable" specification given'
)
}
PanSensorWidth = allowedOrdinal ('panning sensor width',Options.PanSensorWidth)
if (PanSensorWidth == null) { PanSensorWidth = 20 }
PanSensorHeight = allowedOrdinal('panning sensor height',Options.PanSensorHeight)
if (PanSensorHeight == null) { PanSensorHeight = 20 }
PanSpeed = allowedOrdinal ('panning speed',Options.PanSpeed)
if (PanSpeed == null) { PanSpeed = 10 }
onDroppableEnter = allowedFunction('"onDroppableEnter" handler', Options.onDroppableEnter)
onDroppableMove = allowedFunction('"onDroppableMove" handler', Options.onDroppableMove)
onDroppableLeave = allowedFunction('"onDroppableLeave" handler', Options.onDroppableLeave)
onDroppableHold = allowedFunction('"onDroppableHold" handler', Options.onDroppableHold)
onDroppableRelease = allowedFunction('"onDroppableRelease" handler',Options.onDroppableRelease)
onDrop = allowedFunction('"onDrop" handler', Options.onDrop)
return {
Extras, TypesToAccept, HoldDelay,
Pannable, PanSensorWidth,PanSensorHeight, PanSpeed,
// @ts-ignore we cannot validate given functions any further
onDroppableEnter, onDroppableMove, onDroppableLeave,
// @ts-ignore we cannot validate given functions any further
onDroppableHold, onDroppableRelease, onDrop,
}
}
/**** use:asDropZone={options} ****/
export function asDropZone (
Element:HTMLElement|SVGElement, Options:DropZoneOptions
) {
let currentDropZoneOptions:DropZoneOptions
currentDropZoneOptions = parsedDropZoneOptions(Options)
/**** enteredByDroppable ****/
function enteredByDroppable (originalEvent:DragEvent) {
let Options = currentDropZoneOptions
performPanningFor(
'dropzone',
Element, Options, originalEvent.pageX,originalEvent.pageY
)
let DropZonePosition = asPosition(Conversion.fromDocumentTo(
'local', { left:originalEvent.pageX, top:originalEvent.pageY }, Element
)) // relative to DropZone element
if (
ValueIsNumber(Options.HoldDelay) && (Options.HoldDelay as number > 0) &&
(Context.HoldWasTriggeredForElement !== Element)
) { startHoldTimer(DropZonePosition) }
if (
(originalEvent.dataTransfer == null) ||
(originalEvent.dataTransfer.effectAllowed === 'none')
) { return }
let wantedOperation:any = originalEvent.dataTransfer.dropEffect
if (wantedOperation === 'none') { // workaround for browser bug
switch (originalEvent.dataTransfer.effectAllowed) {
case 'copy': case 'move': case 'link':
wantedOperation = originalEvent.dataTransfer.effectAllowed; break
default:
wantedOperation = undefined
}
}
let TypesToAccept = Options.TypesToAccept as TypeAcceptanceSet
let offeredTypeList = originalEvent.dataTransfer.types.filter((Type) =>
(Type in TypesToAccept) &&
(TypesToAccept[Type] !== '') // "getData" is not available here
) // cannot use "originalEvent.dataTransfer.dropEffect" due to browser bug
if (offeredTypeList.length === 0) { return }
let accepted:boolean|undefined = ResultOfHandler(
'onDroppableEnter', Options,
DropZonePosition.x, DropZonePosition.y,
wantedOperation, offeredTypeList, Context.currentDroppableExtras, Options.Extras
)
if (accepted === false) { // i.e. explicit "false" result required
return
} else {
Context.currentDropZoneExtras = Options.Extras
Context.currentDropZoneElement = Element
Context.currentDropZonePosition = DropZonePosition
Element.classList.add('hovered')
originalEvent.preventDefault()
originalEvent.stopPropagation()
}
}
/**** hoveredByDroppable ****/
// warning: I've already seen leftByDroppable followed by hoveredByDropable!
function hoveredByDroppable (originalEvent:DragEvent) {
let Options = currentDropZoneOptions
performPanningFor(
'dropzone',
Element, Options, originalEvent.pageX,originalEvent.pageY
)
let DropZonePosition = asPosition(Conversion.fromDocumentTo(
'local', { left:originalEvent.pageX, top:originalEvent.pageY }, Element
)) // relative to DropZone element
if (
ValueIsNumber(Options.HoldDelay) && (Options.HoldDelay as number > 0) &&
(Context.HoldWasTriggeredForElement !== Element)
) {
if (Context.HoldPosition == null) { // see above for reasoning
startHoldTimer(DropZonePosition)
} else {
continueHoldTimer(DropZonePosition)
}
}
if (
(originalEvent.dataTransfer == null) ||
(originalEvent.dataTransfer.effectAllowed === 'none') ||
(Context.currentDropZoneElement != null) && (Context.currentDropZoneElement !== Element)
) {
Element.classList.remove('hovered')
return
}
// in some browsers, it may be that (currentDropZone !== Element)!
let wantedOperation:any = originalEvent.dataTransfer.dropEffect
if (wantedOperation === 'none') { // workaround for browser bug
switch (originalEvent.dataTransfer.effectAllowed) {
case 'copy': case 'move': case 'link':
wantedOperation = originalEvent.dataTransfer.effectAllowed; break
default:
wantedOperation = undefined
}
}
let TypesToAccept = Options.TypesToAccept as TypeAcceptanceSet
let offeredTypeList = originalEvent.dataTransfer.types.filter((Type) =>
(Type in TypesToAccept) &&
(TypesToAccept[Type] !== '') // "getData" is not available here
) // cannot use "originalEvent.dataTransfer.dropEffect" due to browser bug
if (offeredTypeList.length === 0) {
if (Context.currentDropZoneElement === Element) {
Context.currentDropZoneExtras = undefined
Context.currentDropZoneElement = undefined
Context.currentDropZonePosition = undefined
}
Element.classList.remove('hovered')
return
}
Context.currentDropZonePosition = DropZonePosition
let accepted = ResultOfHandler(
'onDroppableMove', Options,
Context.currentDropZonePosition.x, Context.currentDropZonePosition.y,
wantedOperation, offeredTypeList, Context.currentDroppableExtras, Options.Extras
)
if (accepted === false) { // i.e. explicit "false" result required
Context.currentDropZoneExtras = undefined
Context.currentDropZoneElement = undefined
Context.currentDropZonePosition = undefined
Element.classList.remove('hovered')
} else { // warning: sometimes (currentDropZone !== Element)!
Context.currentDropZoneExtras = Options.Extras
Context.currentDropZoneElement = Element
// Context.currentDropZonePosition has already been set before
Element.classList.add('hovered')
originalEvent.preventDefault() // never allow default action!
// originalEvent.stopPropagation()
return false // special return value when drop seems acceptable
}
}
/**** leftByDroppable ****/
function leftByDroppable (originalEvent:DragEvent) {
Element.classList.remove('hovered')
Context.DropZonePanning = false
stopHoldTimer()
let Options = currentDropZoneOptions
if (Context.currentDropZoneElement === Element) {
if (Context.currentTypeTransferred == null) { // see explanation below
Context.currentDropZoneExtras = undefined
Context.currentDropZoneElement = undefined
Context.DroppableWasDropped = false
Context.currentDropZonePosition = undefined
Context.currentTypeTransferred = undefined
Context.currentDataTransferred = undefined
invokeHandler('onDroppableLeave', Options, Context.currentDroppableExtras, Options.Extras)
} // swallow "dragleave" right after successful "drop"
originalEvent.preventDefault()
originalEvent.stopPropagation()
}
}
/**** droppedByDroppable ****/
function droppedByDroppable (originalEvent:DragEvent) {
Element.classList.remove('hovered')
Context.DropZonePanning = false
stopHoldTimer()
if (
(originalEvent.dataTransfer == null) ||
(originalEvent.dataTransfer.effectAllowed === 'none') ||
(Context.currentDropZoneElement !== Element)
) { return }
// originalEvent.preventDefault()
originalEvent.stopPropagation()
let Options = currentDropZoneOptions
let wantedOperation:any = originalEvent.dataTransfer.dropEffect
if (wantedOperation === 'none') { // workaround for browser bug
switch (originalEvent.dataTransfer.effectAllowed) {
case 'copy': case 'move': case 'link':
wantedOperation = originalEvent.dataTransfer.effectAllowed; break
default:
wantedOperation = undefined
}
}
let TypesToAccept = Options.TypesToAccept as TypeAcceptanceSet
let offeredTypeList = originalEvent.dataTransfer.types.filter((Type) =>
(Type in TypesToAccept) && (
(wantedOperation == null) ||
(TypesToAccept[Type].indexOf(wantedOperation) >= 0)
)
) // cannot use "originalEvent.dataTransfer.dropEffect" due to browser bug
if (offeredTypeList.length === 0) {
Context.currentDropZoneExtras = undefined
Context.currentDropZonePosition = undefined
Context.DroppableWasDropped = false
Context.currentDropOperation = undefined
Context.currentTypeTransferred = undefined
Context.currentDataTransferred = undefined
invokeHandler('onDroppableLeave', Options, Context.currentDroppableExtras, Options.Extras)
return
}
Context.currentDropZonePosition = asPosition(Conversion.fromDocumentTo(
'local', { left:originalEvent.pageX, top:originalEvent.pageY }, Element
)) // relative to DropZone element
let offeredData:any = {}
offeredTypeList.forEach(
// @ts-ignore originalEvent.dataTransfer definitely exists
(Type) => offeredData[Type] = originalEvent.dataTransfer.getData(Type)
)
let acceptedType = ResultOfHandler(
'onDrop', Options,
Context.currentDropZonePosition.x, Context.currentDropZonePosition.y,
wantedOperation, offeredData, Context.currentDroppableExtras, Options.Extras
)
switch (true) {
case (acceptedType == null):
Context.DroppableWasDropped = true
Context.currentDropOperation = wantedOperation
Context.currentTypeTransferred = undefined
Context.currentDataTransferred = undefined
break
case ValueIsOneOf(acceptedType,offeredTypeList):
Context.DroppableWasDropped = true
Context.currentDropOperation = wantedOperation
Context.currentTypeTransferred = acceptedType
Context.currentDataTransferred = offeredData[acceptedType]
break
default: // handler should return false in case of failure
Context.DroppableWasDropped = false
Context.currentDropZoneExtras = undefined
Context.currentDropZonePosition = undefined
Context.currentDropOperation = undefined
Context.currentTypeTransferred = undefined
Context.currentDataTransferred = undefined
// invokeHandler('onDroppableLeave', Options, currentDroppableExtras, Options.Extras)
}
Context.currentDropZoneElement = undefined
}
/**** startHoldTimer ****/
function startHoldTimer (DropZonePosition:Position):void {
Context.HoldPosition = DropZonePosition
if (Context.HoldTimer != null) {
clearTimeout(Context.HoldTimer)
}
Context.HoldTimer = setTimeout(triggerHold, Options.HoldDelay)
}
/**** continueHoldTimer ****/
function continueHoldTimer (DropZonePosition:Position):void {
let Offset = (
((Context.HoldPosition as Position).x-DropZonePosition.x)**2 +
((Context.HoldPosition as Position).y-DropZonePosition.y)**2
)
if (Offset > 25) {
Context.HoldPosition = DropZonePosition
clearTimeout(Context.HoldTimer)
Context.HoldTimer = setTimeout(triggerHold, Options.HoldDelay)
}
}
/**** stopHoldTimer ****/
function stopHoldTimer () {
delete Context.HoldPosition
if (Context.HoldTimer != null) {
clearTimeout(Context.HoldTimer)
delete Context.HoldTimer
}
delete Context.HoldWasTriggeredForElement
}
/**** triggerHold ****/
function triggerHold () {
let DropZonePosition = ( // sometimes, there is no "enteredByDroppable"
Context.currentDropZonePosition || Context.HoldPosition
)
delete Context.HoldPosition
delete Context.HoldTimer
Context.HoldWasTriggeredForElement = Element
invokeHandler(
'onDroppableHold', Options,
(DropZonePosition as Position).x, (DropZonePosition as Position).y,
Context.currentDroppableExtras, Options.Extras
)
}
/**** updateDropZoneOptions ****/
function updateDropZoneOptions (Options:any):void {
Options = parsedDropZoneOptions(Options)
if (
(currentDropZoneOptions.Extras == null) && (Options.Extras != null)
) {
currentDropZoneOptions.Extras = Options.Extras
} // Extras may be set with delay, but remain constant afterwards
currentDropZoneOptions.TypesToAccept = Options.TypesToAccept
currentDropZoneOptions.HoldDelay = Options.HoldDelay
currentDropZoneOptions.Pannable = Options.Pannable
currentDropZoneOptions.PanSensorWidth = Options.PanSensorWidth
currentDropZoneOptions.PanSensorHeight = Options.PanSensorHeight
currentDropZoneOptions.PanSpeed = Options.PanSpeed
}
// @ts-ignore we know that the passed event is a DragEvent
Element.addEventListener('dragenter', enteredByDroppable)
// @ts-ignore we know that the passed event is a DragEvent
Element.addEventListener('dragover', hoveredByDroppable)
// @ts-ignore we know that the passed event is a DragEvent
Element.addEventListener('dragleave', leftByDroppable)
// @ts-ignore we know that the passed event is a DragEvent
Element.addEventListener('drop', droppedByDroppable)
return { update:updateDropZoneOptions }
}
/**** ValueIsPosition ****/
function ValueIsPosition (Candidate:any):boolean {
return (
ValueIsPlainObject(Candidate) &&
ValueIsFiniteNumber(Candidate.x) && ValueIsFiniteNumber(Candidate.y)
)
}
/**** asPosition ****/
function asPosition (Value:any):Position {
return { x:Value.left, y:Value.top }
}
/**** PositionReferenceFor ****/
function PositionReferenceFor (
Element:HTMLElement|SVGElement,
Options:DraggableOptions|DroppableOptions
):Element {
let PositionReference:Element | undefined | null
switch (true) {
case (Options.relativeTo === 'parent'):
PositionReference = Element.parentElement
break
case (Options.relativeTo === 'body'):
PositionReference = document.body
break
case (Options.relativeTo instanceof HTMLElement):
case (Options.relativeTo instanceof SVGElement):
// case (Options.relativeTo instanceof MathMLElement):
PositionReference = Options.relativeTo as Element
if (
(PositionReference != document.body) &&
! document.body.contains(PositionReference)
) throwError(
'InvalidArgument: the HTML element given as "relativeTo" option ' +
'is not part of this HTML document'
)
break
default: // CSS selector
PositionReference = Element.closest(Options.relativeTo as string)
}
return (PositionReference == null ? document.body : PositionReference)
}
/**** DragImageFor ****/
function DragImageFor (
Element:HTMLElement|SVGElement,
Options:DraggableOptions|DroppableOptions
):Element|undefined {
switch (true) {
case (Options.Dummy === 'standard'):
return undefined
case (Options.Dummy === 'none'):
let invisibleDragImage = document.createElement('div')
invisibleDragImage.setAttribute('style',
'display:block; position:absolute; width:1px; height:1px; ' +
'background:transparent; border:none; margin:0px; padding:0px; ' +
'cursor:auto'
)
document.body.appendChild(invisibleDragImage)
return invisibleDragImage
case ValueIsNonEmptyString(Options.Dummy): // may flicker shortly
let auxiliaryElement = document.createElement('div')
auxiliaryElement.style.display = 'block'
auxiliaryElement.style.position = 'absolute'
auxiliaryElement.style.left = (document.body.scrollWidth + 100)+'px'
document.body.appendChild(auxiliaryElement)
auxiliaryElement.innerHTML = Options.Dummy as string
return auxiliaryElement.children[0]
case (Options.Dummy instanceof HTMLElement):
case (Options.Dummy instanceof SVGElement):
// case (Options.Dummy instanceof MathMLElement):
return Options.Dummy as Element
case ValueIsFunction(Options.Dummy):
let Candidate:HTMLElement | SVGElement | undefined = undefined
try {
Candidate = (Options.Dummy as Function)(Options.Extras, Element)
} catch (Signal) {
console.error('RuntimeError: creating draggable dummy failed',Signal)
}
if (Candidate != null) {
if ((Candidate instanceof HTMLElement) || (Candidate instanceof SVGElement)) {
return Candidate
} else {
console.error(
'InvalidArgument: the newly created draggable dummy is ' +
'neither an HTML nor an SVG element'
)
}
}
}
}
/**** performPanningFor ****/
function performPanningFor (
Type:'draggable'|'dropzone',
Element:HTMLElement | SVGElement, Options:DraggableOptions,
xOnPage:number,yOnPage:number
):void {
if ((Type === 'draggable') && Context.DropZonePanning) { return }
if (
(Options.Pannable == null) ||
((Options.PanSensorWidth === 0) && (Options.PanSensorHeight === 0)) ||
(Options.PanSpeed === 0)
) { Context.DropZonePanning = false; return }
let pannableElement:Element|undefined|null
switch (true) {
case ValueIsNonEmptyString(Options.Pannable):
pannableElement = Element.parentElement
if (pannableElement != null) {
pannableElement = pannableElement.closest(Options.Pannable as string)
}
break
case (Options.Pannable === 'this') && (Type === 'dropzone'):
pannableElement = Element
break
case (Options.Pannable instanceof HTMLElement):
case (Options.Pannable instanceof SVGElement):
// case (Options.Pannable instanceof MathMLElement):
pannableElement = Options.Pannable as HTMLElement
}
if (pannableElement == null) { Context.DropZonePanning = false; return }
let { left:xInPannable, top:yInPannable } = Conversion.fromDocumentTo(
'local', { left:xOnPage, top:yOnPage }, pannableElement
)
if ((xInPannable >= 0) && (xInPannable < (Options.PanSensorWidth as number))) {
pannableElement.scrollLeft = Math.max(
0,pannableElement.scrollLeft - (Options.PanSpeed as number)
)
}
let PannableWidth = pannableElement.clientWidth // w/o scrollbar
if (
(xInPannable >= PannableWidth-(Options.PanSensorWidth as number)) &&
(xInPannable < PannableWidth)
) {
pannableElement.scrollLeft = Math.min(
pannableElement.scrollLeft + (Options.PanSpeed as number),
pannableElement.scrollWidth-PannableWidth
)
}
if ((yInPannable >= 0) && (yInPannable < (Options.PanSensorHeight as number))) {
pannableElement.scrollTop = Math.max(
0,pannableElement.scrollTop - (Options.PanSpeed as number)
)
}
let PannableHeight = pannableElement.clientHeight // w/o scrollbar
if (
(yInPannable >= PannableHeight-(Options.PanSensorHeight as number)) &&
(yInPannable < PannableHeight)
) {
pannableElement.scrollTop = Math.min(
pannableElement.scrollTop + (Options.PanSpeed as number),
pannableElement.scrollHeight-PannableHeight
)
}
Context.DropZonePanning = (Type === 'dropzone')
}
/**** parsedOperations ****/
function parsedOperations (
Description:string, Argument:any, Default:string='copy move link'
):string {
let Operations = allowedString(Description,Argument) || Default
switch (Operations.trim()) {
case 'all': return 'copy move link'
case 'none': return ''
}
let OperationList = Operations.trim().replace(/\s+/g,' ').split(' ')
allowListSatisfying(
Description,OperationList,
(Operation:string) => ValueIsOneOf(Operation,DropOperations)
)
return OperationList.reduce(
(Result:string, Operation:string) => (
Result.indexOf(Operation) < 0 ? Result + Operation + ' ': Result
),' '
)
}
/**** allowedEffectsFrom ****/
type allowedEffects = (
'none'|'copy'|'copyLink'|'copyMove'|'link'|'linkMove'|'move'|'all'
)
function allowedEffectsFrom (Operations:string):allowedEffects {
let EffectIndex = ( // Horner's method
(Operations.indexOf('move') < 0 ? 0 : 1) * 2 +
(Operations.indexOf('link') < 0 ? 0 : 1)
) * 2 +
(Operations.indexOf('copy') < 0 ? 0 : 1)
return [
'none','copy','link','copyLink','move','copyMove','linkMove','all'
][EffectIndex] as allowedEffects
}
/**** invokeHandler ****/
function invokeHandler (Name:string, Options:any, ...Arguments:any):any {
if (Options[Name] != null) {
try {
return Options[Name].apply(null,Arguments)
} catch (Signal) {
console.error(quoted(Name) + ' handler failed',Signal)
}
}
}
const ResultOfHandler = invokeHandler