diff --git a/src/customElements/imageCropper.js b/src/customElements/imageCropper.js index a22f52656..0e712a337 100644 --- a/src/customElements/imageCropper.js +++ b/src/customElements/imageCropper.js @@ -52,6 +52,7 @@ export default () => ( } if (Object.values(allInputValues).some(x => x !== 0 && !x)) { + console.error('ImageCropper: missing input values', { cause: Object.keys(allInputValues).filter(x => !allInputValues[x]) }) throw new Error('ImageCropper: missing input values', { cause: Object.keys(allInputValues).filter(x => !allInputValues[x]) }) } diff --git a/src/customElements/pointerListener.js b/src/customElements/pointerListener.js index 587e24197..eeb685c59 100644 --- a/src/customElements/pointerListener.js +++ b/src/customElements/pointerListener.js @@ -6,15 +6,25 @@ export default () => ( this.previousX = null this.previousY = null - this.listeners = { - 'pointermove': (e) => { - if (this.previousX == null || this.previousY == null) { + this.dragImageElement = document.createElement('span') + this.dragImageElement.classList.add('sr-only') + this.dragImageElement.classList.add('pointer-events-none') + this.dragImageElement.setAttribute('aria-hidden', 'true') + document.body.appendChild(this.dragImageElement) + + this.documentListeners = { + 'dragover': (e) => { + if (this.previousX === null || this.previousY === null) { this.previousX = e.clientX this.previousY = e.clientY return } - this.dispatchEvent(new CustomEvent('document-pointermove', { + if (this.previousX === e.clientX && this.previousY === e.clientY) { + return + } + + this.dispatchEvent(new CustomEvent('document-dragover', { detail: { clientX: e.clientX, clientY: e.clientY, @@ -25,22 +35,43 @@ export default () => ( this.previousX = e.clientX this.previousY = e.clientY + } + } + + this.elementListeners = { + 'dragstart': (e) => { + this.dispatchEvent(new CustomEvent('element-dragstart')) + + e.dataTransfer.setDragImage(this.dragImageElement, 0, 0) }, - 'pointerup': () => { - this.dispatchEvent(new CustomEvent('document-pointerup')) + 'dragend': () => { + this.dispatchEvent(new CustomEvent('element-dragend')) } } - Object.keys(this.listeners).forEach((key) => { - document.addEventListener(key, this.listeners[key]) + Object.keys(this.documentListeners).forEach((key) => { + document.addEventListener(key, this.documentListeners[key]) + }) + + Object.keys(this.elementListeners).forEach((key) => { + this.addEventListener(key, this.elementListeners[key]) }) } disconnectedCallback () { - Object.keys(this.listeners).forEach((key) => { - document.removeEventListener(key, this.listeners[key]) + if (this.dragImageElement && document.body.contains(this.dragImageElement)) { + document.body.removeChild(this.dragImageElement) + } + + Object.keys(this.elementListeners).forEach((key) => { + this.removeEventListener(key, this.elementListeners[key]) }) - this.listeners = undefined + + Object.keys(this.documentListeners).forEach((key) => { + document.removeEventListener(key, this.documentListeners[key]) + }) + + this.documentListeners = undefined } } ) diff --git a/src/elm/View/Components.elm b/src/elm/View/Components.elm index ef64f9e1c..625c692dd 100644 --- a/src/elm/View/Components.elm +++ b/src/elm/View/Components.elm @@ -598,54 +598,68 @@ intersectionObserver options = [] -{-| A component that attaches events related to pointers to the document and -passes it into Elm +{-| A component that attaches events related to pointers to the document and the +element and passes it into Elm. + + - `document.onDragOver`: This event is fired when something is being dragged and + the pointer moves. + - `element.onDragEnd`: This event is fired when the user releases the pointer + after a drag operation. + - `element.onDragStart`: This event is fired whenever the user starts dragging + an element. + +Make sure the element has `draggble = true`. + -} pointerListener : - { onPointerMove : - Maybe - ({ x : Float - , y : Float - , previousX : Float - , previousY : Float - } - -> msg - ) - , onPointerUp : Maybe msg + { document : + { onDragOver : + Maybe + ({ x : Float + , y : Float + , previousX : Float + , previousY : Float + } + -> msg + ) + } + , element : + { onDragStart : + Maybe + { dragImage : Maybe String + , listener : msg + } + , onDragEnd : Maybe msg + } } + -> List (Html.Attribute msg) + -> List (Html msg) -> Html msg -pointerListener { onPointerMove, onPointerUp } = +pointerListener { document, element } attrs children = node "pointer-listener" - [ case onPointerMove of - Nothing -> - class "" - - Just pointerMoveListener -> - on "document-pointermove" - (Json.Decode.field "detail" - (Json.Decode.map4 - (\x y previousX previousY -> - pointerMoveListener - { x = x - , y = y - , previousX = previousX - , previousY = previousY - } - ) - (Json.Decode.field "clientX" Json.Decode.float) - (Json.Decode.field "clientY" Json.Decode.float) - (Json.Decode.field "previousX" Json.Decode.float) - (Json.Decode.field "previousY" Json.Decode.float) - ) + (optionalListenerWithArgs "document-dragover" + document.onDragOver + (Json.Decode.field "detail" + (Json.Decode.map4 + (\x y previousX previousY -> + { x = x + , y = y + , previousX = previousX + , previousY = previousY + } ) - , case onPointerUp of - Nothing -> - class "" - - Just pointerUpListener -> - on "document-pointerup" (Json.Decode.succeed pointerUpListener) - ] - [] + (Json.Decode.field "clientX" Json.Decode.float) + (Json.Decode.field "clientY" Json.Decode.float) + (Json.Decode.field "previousX" Json.Decode.float) + (Json.Decode.field "previousY" Json.Decode.float) + ) + ) + :: optionalListener "element-dragend" element.onDragEnd + :: optionalListener "element-dragstart" (Maybe.map .listener element.onDragStart) + :: optionalAttr "element-pointerdown-drag-image" (Maybe.andThen .dragImage element.onDragStart) + :: attrs + ) + children @@ -687,6 +701,26 @@ optionalAttr attr maybeAttr = attribute attr attrValue +optionalListener : String -> Maybe msg -> Html.Attribute msg +optionalListener eventName maybeMsg = + case maybeMsg of + Nothing -> + class "" + + Just msg -> + on eventName (Json.Decode.succeed msg) + + +optionalListenerWithArgs : String -> Maybe (a -> msg) -> Json.Decode.Decoder a -> Html.Attribute msg +optionalListenerWithArgs eventName maybeMsg decoder = + case maybeMsg of + Nothing -> + class "" + + Just msg -> + on eventName (Json.Decode.map msg decoder) + + dateFormatter : List (Html.Attribute msg) -> { language : Translation.Language, date : Time.Posix, translationString : String } diff --git a/src/elm/View/ImageCropper.elm b/src/elm/View/ImageCropper.elm index 2a4d7b1ff..edda6af9a 100644 --- a/src/elm/View/ImageCropper.elm +++ b/src/elm/View/ImageCropper.elm @@ -4,7 +4,7 @@ import Browser.Dom import Dict import File import Html exposing (Html, button, div, img, input, node) -import Html.Attributes exposing (alt, attribute, class, classList, id, src, style, type_, value) +import Html.Attributes exposing (alt, attribute, class, classList, draggable, id, src, style, type_, value) import Html.Events import Icons import Json.Decode @@ -229,31 +229,29 @@ update msg model = ] } - Dragged { x, y, previousX, previousY } -> + Dragged { x, y } -> case model.dimmensions of Loading -> UR.init model Loaded dimmensions -> - let - selection = - calculateSelectionDimmensions model dimmensions - in - { model - | dimmensions = - Loaded - { dimmensions - | topOffset = - clamp dimmensions.container.top - (dimmensions.container.top + dimmensions.container.height - selection.height) - (dimmensions.topOffset + y - previousY) - , leftOffset = - clamp dimmensions.container.left - (dimmensions.container.left + dimmensions.container.width - selection.width) - (dimmensions.leftOffset + x - previousX) - } - } - |> UR.init + if not model.isDragging then + UR.init model + + else + let + selection = + calculateSelectionDimmensions model dimmensions + in + { model + | dimmensions = + Loaded + { dimmensions + | topOffset = y - selection.height / 2 + , leftOffset = x - selection.width / 2 + } + } + |> UR.init ChangedDimmensions percentageString -> case model.dimmensions of @@ -329,7 +327,7 @@ view model { imageUrl, cropperAttributes } = [ src imageUrl , alt "" , id entireImageId - , class "opacity-20 pointer-events-none select-none max-h-full" + , class "opacity-20 pointer-events-none select-none max-w-full max-h-[35vh] lg:max-h-[60vh]" , Html.Events.on "load" (Json.Decode.succeed ImageLoaded) ] [] @@ -377,14 +375,31 @@ viewCropper model dimmensions { imageUrl, cropperAttributes } = floatToPx offset = String.fromFloat offset ++ "px" in - [ div + [ View.Components.pointerListener + { document = + { onDragOver = + if model.isDragging then + Just Dragged + + else + Nothing + } + , element = + { onDragStart = + Just + { dragImage = Nothing + , listener = StartedDragging + } + , onDragEnd = Just StoppedDragging + } + } (class "absolute overflow-hidden border border-dashed border-gray-400 cursor-move z-20 select-none mx-auto" :: classList [ ( "transition-all origin-center", not model.isDragging && not model.isChangingDimmensions && not model.isReflowing ) ] :: style "top" (floatToPx topOffset) :: style "left" (floatToPx leftOffset) :: style "width" (floatToPx selection.width) :: style "height" (floatToPx selection.height) - :: onPointerDown StartedDragging + :: draggable "true" :: List.map (Html.Attributes.map Basics.never) cropperAttributes ) [ img @@ -401,14 +416,6 @@ viewCropper model dimmensions { imageUrl, cropperAttributes } = ] [] ] - , if model.isDragging then - View.Components.pointerListener - { onPointerMove = Just Dragged - , onPointerUp = Just StoppedDragging - } - - else - Html.text "" , node "image-cropper" [ if model.isRequestingCroppedImage then attribute "elm-generate-new-cropped-image" "true" @@ -500,17 +507,6 @@ calculateSelectionDimmensions model dimmensions = } -onPointerDown : msg -> Html.Attribute msg -onPointerDown msg = - Html.Events.custom "pointerdown" - (Json.Decode.succeed - { message = msg - , stopPropagation = True - , preventDefault = True - } - ) - - msgToString : Msg -> List String msgToString msg = case msg of