diff --git a/example-development/showcase/src/Main.elm b/example-development/showcase/src/Main.elm index 98143d5..edc03fc 100644 --- a/example-development/showcase/src/Main.elm +++ b/example-development/showcase/src/Main.elm @@ -639,7 +639,7 @@ pickApplesInterface state = (gamepads |> Dict.foldr (\_ gamepad _ -> gamepad |> Just) Nothing) ) , let - rectangleAtCellLocation : Color.Color -> PickApplesLocation -> Web.DomNode state_ + rectangleAtCellLocation : Color.Color -> PickApplesLocation -> Web.Dom.Node state_ rectangleAtCellLocation fill cellLocation = Web.Svg.element "rect" [ Svg.LocalExtra.fillUniform fill @@ -650,7 +650,7 @@ pickApplesInterface state = ] [] - worldUi : Web.DomNode state_ + worldUi : Web.Dom.Node state_ worldUi = Web.Svg.element "rect" [ Svg.LocalExtra.fillUniform Color.black @@ -692,7 +692,7 @@ pickApplesInterface state = in segmented.currentSegment :: segmented.finishedSegments - headTailUi : Web.DomNode future_ + headTailUi : Web.Dom.Node future_ headTailUi = let segments : List (List { x : Int, y : Int }) @@ -700,7 +700,7 @@ pickApplesInterface state = (state.headLocation :: state.tailSegments) |> splitIntoSegmentsThatDoNotWrapAround - legsUi : Web.DomNode future_ + legsUi : Web.Dom.Node future_ legsUi = segments |> List.concat @@ -740,7 +740,7 @@ pickApplesInterface state = ) |> Web.Svg.element "g" [] - warpAnimationUi : Web.DomNode future_ + warpAnimationUi : Web.Dom.Node future_ warpAnimationUi = case segments |> List.reverse of (lastPoint :: beforeLastPoint) :: _ -> @@ -830,7 +830,7 @@ pickApplesInterface state = } ] - headUi : Web.DomNode future_ + headUi : Web.Dom.Node future_ headUi = [ Svg.LocalExtra.polygon (facePoints state.headLocation 0.8) [ Svg.LocalExtra.fillUniform (Color.rgba 0 0.5 1 0.5) @@ -867,7 +867,7 @@ pickApplesInterface state = ] |> Web.Svg.element "g" [] - appleUi : Web.DomNode future_ + appleUi : Web.Dom.Node future_ appleUi = [ Svg.LocalExtra.circle { radius = cellSideLength * 0.45 @@ -954,7 +954,7 @@ pickApplesInterface state = ] |> Web.Svg.element "g" [] - pickedAppleCountUi : Web.DomNode future_ + pickedAppleCountUi : Web.Dom.Node future_ pickedAppleCountUi = Web.Svg.element "text" [ Svg.LocalExtra.fillUniform (Color.rgba 0.3 1 0.5 0.13) @@ -969,7 +969,7 @@ pickApplesInterface state = ] [ state.pickedAppleCount |> String.fromInt |> Web.Dom.text ] - controlsUi : Web.DomNode state_ + controlsUi : Web.Dom.Node state_ controlsUi = Web.Svg.element "text" [ Svg.LocalExtra.fillUniform (Color.rgb 0.3 0.7 0.5) @@ -1244,7 +1244,7 @@ mapWithExitInterface = -- Ui -uiFrame : List (Web.Dom.Modifier state) -> List (Web.DomNode state) -> Web.DomNode state +uiFrame : List (Web.Dom.Modifier state) -> List (Web.Dom.Node state) -> Web.Dom.Node state uiFrame modifiers subs = Web.Dom.element "div" ([ Web.Dom.style "font-size" "2em" @@ -1263,7 +1263,7 @@ uiFrame modifiers subs = subs -narrativeUiFrame : List (Web.Dom.Modifier state_) -> List (Web.DomNode state_) -> Web.DomNode state_ +narrativeUiFrame : List (Web.Dom.Modifier state_) -> List (Web.Dom.Node state_) -> Web.Dom.Node state_ narrativeUiFrame modifiers subs = uiFrame modifiers @@ -1275,7 +1275,7 @@ narrativeUiFrame modifiers subs = ] -buttonUi : List (Web.Dom.Modifier ()) -> List (Web.DomNode ()) -> Web.DomNode () +buttonUi : List (Web.Dom.Modifier ()) -> List (Web.Dom.Node ()) -> Web.Dom.Node () buttonUi modifiers subs = Web.Dom.element "button" ([ Web.Dom.listenTo "click" @@ -1296,7 +1296,7 @@ buttonUi modifiers subs = subs -textInputUi : Maybe String -> Web.DomNode (Result Json.Decode.Error String) +textInputUi : Maybe String -> Web.Dom.Node (Result Json.Decode.Error String) textInputUi currentInputValue = Web.Dom.element "input" [ Web.Dom.attribute "type" "text" @@ -1325,7 +1325,7 @@ textInputUi currentInputValue = [] -clockUi : { posix : Time.Posix, timezone : Time.Zone } -> Web.DomNode state_ +clockUi : { posix : Time.Posix, timezone : Time.Zone } -> Web.Dom.Node state_ clockUi state = let hour : Int @@ -1360,7 +1360,7 @@ clockUi state = ] -clockHandUi : { width : Int, length : Float, turns : Float } -> Web.DomNode state_ +clockHandUi : { width : Int, length : Float, turns : Float } -> Web.Dom.Node state_ clockHandUi config = let clockTurns : Float diff --git a/example-development/showcase/src/Svg/LocalExtra.elm b/example-development/showcase/src/Svg/LocalExtra.elm index aa64f91..e370657 100644 --- a/example-development/showcase/src/Svg/LocalExtra.elm +++ b/example-development/showcase/src/Svg/LocalExtra.elm @@ -10,7 +10,7 @@ import Web.Svg line : { start : { x : Float, y : Float }, end : { x : Float, y : Float } } -> List (Web.Dom.Modifier future) - -> Web.DomNode future + -> Web.Dom.Node future line lineGeometry additionalModifiers = Web.Svg.element "line" ([ Web.Dom.attribute "x1" (lineGeometry.start.x |> String.fromFloat) @@ -23,7 +23,7 @@ line lineGeometry additionalModifiers = [] -circle : { position : { x : Float, y : Float }, radius : Float } -> List (Web.Dom.Modifier future) -> Web.DomNode future +circle : { position : { x : Float, y : Float }, radius : Float } -> List (Web.Dom.Modifier future) -> Web.Dom.Node future circle geometry additionalModifiers = Web.Svg.element "circle" ([ Web.Dom.attribute "cx" ((geometry.position.x |> String.fromFloat) ++ "px") @@ -35,7 +35,7 @@ circle geometry additionalModifiers = [] -ellipse : { position : { x : Float, y : Float }, radiusX : Float, radiusY : Float } -> List (Web.Dom.Modifier future) -> Web.DomNode future +ellipse : { position : { x : Float, y : Float }, radiusX : Float, radiusY : Float } -> List (Web.Dom.Modifier future) -> Web.Dom.Node future ellipse geometry additionalModifiers = Web.Svg.element "ellipse" ([ Web.Dom.attribute "cx" ((geometry.position.x |> String.fromFloat) ++ "px") @@ -71,7 +71,7 @@ strokeUniform color = Web.Dom.attribute "stroke" (color |> Color.toCssString) -polygon : List { x : Float, y : Float } -> List (Web.Dom.Modifier future) -> Web.DomNode future +polygon : List { x : Float, y : Float } -> List (Web.Dom.Modifier future) -> Web.Dom.Node future polygon points_ additionalModifiers = Web.Svg.element "polyline" ([ points points_ ] @@ -85,7 +85,7 @@ strokeWidth pixels = Web.Dom.attribute "stroke-width" ((pixels |> String.fromFloat) ++ "px") -polyline : List { x : Float, y : Float } -> List (Web.Dom.Modifier future) -> Web.DomNode future +polyline : List { x : Float, y : Float } -> List (Web.Dom.Modifier future) -> Web.Dom.Node future polyline points_ additionalModifiers = Web.Svg.element "polyline" ([ points points_ ] diff --git a/runner/index.ts b/runner/index.ts index 4f6d2a5..88ff6a0 100644 --- a/runner/index.ts +++ b/runner/index.ts @@ -86,7 +86,9 @@ export function programStart(appConfig: { ports: ElmPorts, domElement: HTMLEleme delete notifications[config.id] } } - case "RemoveDom": return (_config: null) => { appConfig.domElement.replaceChildren() } + case "RemoveDom": return (config: { path: number[] }) => { + removeDom(config.path) + } case "RemoveHttpRequest": return (config: string) => { const maybeAbortController = httpRequestAbortControllers[config] if (maybeAbortController) { @@ -239,11 +241,7 @@ export function programStart(appConfig: { ports: ElmPorts, domElement: HTMLEleme } const interfaceWithFutureImplementation: (tag: string) => ((config: any, sendToElm: (v: any) => void) => void) = tag => { switch (tag) { - case "AddDomRender": return (config, sendToElm) => { - appConfig.domElement.replaceChildren() // remove all subs - appConfig.domElement.appendChild(createDomNode([], config, sendToElm)) - } - case "EditDom": return (config, sendToElm) => { + case "EditDom": return (config: { path: number[], replacement: any }, sendToElm) => { editDom(config.path, config.replacement, sendToElm) } case "AddAudioSourceLoad": return audioSourceLoad @@ -371,56 +369,76 @@ export function programStart(appConfig: { ports: ElmPorts, domElement: HTMLEleme } }) + function removeDom(path: number[]): void { + const toRemove = domInElementAt(appConfig.domElement, 0, path) + if (toRemove) { + while (toRemove.nextSibling) { + toRemove.nextSibling.remove() + } + toRemove.remove() + } + } + function domInElementAt(parent: Element, indexInParent: number, subPath: number[]): ChildNode | null { + const currentDom = parent.childNodes.item(indexInParent) + return (subPath.length === 0) ? + currentDom + : (currentDom && (currentDom instanceof Element)) ? + domInElementAt(currentDom, subPath.at(0) ?? 0, subPath.toSpliced(0, 1)) + : null + } + function domElementOrDummyInElementAt(parent: Element, indexInParent: number, subPath: number[]): ChildNode { + if (subPath.length === 0) { + return domElementSubAtIndexOrDummy(parent, indexInParent) + } else { + return domElementOrDummyInElementAt( + domElementSubAtIndexOrDummy(parent, indexInParent), + subPath.at(0) ?? 0, + subPath.toSpliced(0, 1) + ) + } + } + function domElementSubAtIndexOrDummy(parent: Element, indexInParent: number): Element { + const currentDom = parent.childNodes.item(indexInParent) + if ((parent.childNodes.length >= indexInParent + 1) && currentDom) { + if (currentDom instanceof Element) { + return currentDom + } else { + const newDummy = document.createElement("div") + parent.replaceChild(newDummy, currentDom) + return newDummy + } + } else { + while (parent.childNodes.length <= indexInParent - 1) { + parent.appendChild(document.createElement("div")) + } + const newDummy = document.createElement("div") + parent.appendChild(newDummy) + return newDummy + } + } + function editDom( path: number[], replacement: { tag: "Node" | "Styles" | "Attributes" | "AttributesNamespaced" | "ScrollToPosition" | "ScrollToShow" | "ScrollPositionRequest" | "EventListens", value: any }, sendToElm: (v: any) => void ) { - if (path.length === 0) { - const parentDomNode = appConfig.domElement - switch (replacement.tag) { - case "Node": { - parentDomNode.replaceChildren() // remove all subs - parentDomNode.appendChild(createDomNode([], replacement.value, sendToElm)) - break - } - case "Styles": case "Attributes": case "AttributesNamespaced": case "ScrollToPosition": case "ScrollToShow": case "ScrollPositionRequest": case "EventListens": { + switch (replacement.tag) { + case "Node": { + const oldDomNodeToEdit = domElementOrDummyInElementAt(appConfig.domElement, 0, path) + const newDomNode = createDomNode(replacement.value, oldDomNodeToEdit.childNodes, sendToElm) + oldDomNodeToEdit.parentElement?.replaceChild(newDomNode, oldDomNodeToEdit) + break + } + case "Styles": case "Attributes": case "AttributesNamespaced": case "ScrollToPosition": case "ScrollToShow": case "ScrollPositionRequest": case "EventListens": { + const oldDomNodeToEdit = domInElementAt(appConfig.domElement, 0, path) + if (oldDomNodeToEdit && (oldDomNodeToEdit instanceof Element)) { editDomModifiers( - parentDomNode.firstChild as (Element & ElementCSSInlineStyle), + oldDomNodeToEdit as (Element & ElementCSSInlineStyle), { tag: replacement.tag, value: replacement.value }, - path, sendToElm ) - break - } - } - } else { - let parentDomNode = appConfig.domElement.firstChild - if (parentDomNode) { - path.slice(1, path.length).reverse().forEach(subIndex => { - const subNode = parentDomNode?.childNodes[subIndex] - if (subNode) { - parentDomNode = subNode - } - }) - const oldDomNode: ChildNode | undefined = parentDomNode.childNodes[path[0] ?? 0] - if (oldDomNode) { - switch (replacement.tag) { - case "Node": { - parentDomNode.replaceChild(createDomNode(path, replacement.value, sendToElm), oldDomNode) - break - } - case "Styles": case "Attributes": case "AttributesNamespaced": case "EventListens": { - editDomModifiers( - oldDomNode as (Element & ElementCSSInlineStyle), - { tag: replacement.tag, value: replacement.value }, - path, - sendToElm - ) - break - } - } } + break } } } @@ -465,7 +483,6 @@ function editDomModifiers( tag: "Styles" | "Attributes" | "AttributesNamespaced" | "ScrollToPosition" | "ScrollToShow" | "ScrollPositionRequest" | "EventListens", value: any }, - path: number[], sendToElm: (v: any) => void ) { switch (replacement.tag) { @@ -505,7 +522,7 @@ function editDomModifiers( break } case "ScrollPositionRequest": { - sendToElm({ fromLeft: domElementToEdit.scrollLeft, fromTop: domElementToEdit.scrollTop }) + domElementAddScrollPositionRequest(domElementToEdit, sendToElm) break } case "EventListens": { @@ -517,7 +534,7 @@ function editDomModifiers( } return true }) - domElementAddEventListens(domElementToEdit, replacement.value, path, sendToElm) + domElementAddEventListens(domElementToEdit, replacement.value, sendToElm) break } } @@ -560,13 +577,13 @@ function getMeta(name: string): HTMLMetaElement { } } -function createDomNode(path: number[], node: { tag: "Text" | "Element", value: any }, sendToElm: (v: any) => void): Element | Text { +function createDomNode(node: { tag: "Text" | "Element", value: any }, subs: NodeListOf, sendToElm: (v: any) => void): Element | Text { switch (node.tag) { case "Text": { return document.createTextNode(node.value) } case "Element": { - const createdDomElement: (Element & ElementCSSInlineStyle) = + const createdDomElement: HTMLElement = node.value.namespace ? document.createElementNS(node.value.namespace, noScript(node.value.tag)) : @@ -582,21 +599,28 @@ function createDomNode(path: number[], node: { tag: "Text" | "Element", value: a node.value.scrollIntoView({ inline: node.value.scrollToShow.x, block: node.value.scrollToShow.y }) } if (node.value.scrollPositionRequest) { - sendToElm({ path: path, scrollPosition: { fromLeft: createdDomElement.scrollLeft, fromTop: createdDomElement.scrollTop } }) + domElementAddScrollPositionRequest(createdDomElement, sendToElm) } - domElementAddEventListens(createdDomElement, node.value.eventListens, path, sendToElm) - node.value.subs.forEach((sub: any, subIndex: number) => { - createdDomElement.appendChild( - createDomNode([subIndex].concat(path), sub, sendToElm) - ) - }) + domElementAddEventListens(createdDomElement, node.value.eventListens, sendToElm) + createdDomElement.append(...subs) return createdDomElement } } } +function domElementAddScrollPositionRequest(domElement: Element, sendToElm: (v: any) => void) { + // guarantee it has painted drawn at least once + window.requestAnimationFrame(_timestamp => { + window.requestAnimationFrame(_timestamp => { + sendToElm({ + tag: "ScrollPositionRequest", + value: { fromLeft: domElement.scrollLeft, fromTop: domElement.scrollTop } + }) + }) + }) +} function domElementAddStyles(domElement: Element & ElementCSSInlineStyle, styles: { key: string, value: string }[]) { styles.forEach(styleSingle => { - domElement.style.setProperty(styleSingle.key, styleSingle.value) + domElement?.style.setProperty(styleSingle.key, styleSingle.value) }) } function domElementAddAttributes(domElement: Element, attributes: { key: string, value: string }[]) { @@ -623,14 +647,14 @@ function domElementAddAttributesNamespaced(domElement: Element, attributesNamesp function domElementAddEventListens( domElement: Element, eventListens: { name: string, defaultActionHandling: "DefaultActionPrevent" | "DefaultActionExecute" }[], - path: number[], sendToElm: (v: any) => void + sendToElm: (v: any) => void ) { eventListens.forEach(eventListen => { const abortController: AbortController = new AbortController() domElement.addEventListener( eventListen.name, (triggeredEvent) => { - sendToElm({ path: path, name: eventListen.name, event: triggeredEvent }) + sendToElm({ tag: "EventListen", value: { name: eventListen.name, event: triggeredEvent } }) switch (eventListen.defaultActionHandling) { case "DefaultActionPrevent": { triggeredEvent.preventDefault() diff --git a/src/Json/Codec.elm b/src/Json/Codec.elm index a31a7d4..9ed233c 100644 --- a/src/Json/Codec.elm +++ b/src/Json/Codec.elm @@ -3,7 +3,7 @@ module Json.Codec exposing , RecordJsonCodecBeingBuilt, record, field, recordFinish , enum, ChoiceJsonCodecBeingBuilt, choice, variant , unit, bool, int, float, string - , nullable, list, array, dict + , nullable, list, dict ) {-| Simple decoder-encoder pair for json @@ -16,11 +16,10 @@ similar to [miniBill/elm-codec](https://dark.elm.dmy.fr/packages/miniBill/elm-co @docs enum, ChoiceJsonCodecBeingBuilt, choice, variant @docs unit, bool, int, float, string -@docs nullable, list, array, dict +@docs nullable, list, dict -} -import Array exposing (Array) import Dict exposing (Dict) import Json.Decode import Json.Encode @@ -80,11 +79,6 @@ list elementJsonCodec = } -array : JsonCodec element -> JsonCodec (Array element) -array elementJsonCodec = - map Array.fromList Array.toList (list elementJsonCodec) - - dict : JsonCodec { key : comparableKey, value : value } -> JsonCodec (Dict comparableKey value) dict entryJsonCodec = map diff --git a/src/StructuredId.elm b/src/StructuredId.elm index 801e45c..30a6af6 100644 --- a/src/StructuredId.elm +++ b/src/StructuredId.elm @@ -93,11 +93,11 @@ toComparableRope = String.cons ' ' string |> Rope.singleton List elements -> - elements - |> List.foldl + Rope.append ")" + (List.foldl (\el soFar -> - (el |> toComparableRope) - |> Rope.prependTo soFar + Rope.prependTo soFar (toComparableRope el) ) (Rope.singleton "(") - |> Rope.append ")" + elements + ) diff --git a/src/Web.elm b/src/Web.elm index 381a24b..322d380 100644 --- a/src/Web.elm +++ b/src/Web.elm @@ -1,7 +1,7 @@ module Web exposing ( ProgramConfig, program, Program , Interface, interfaceBatch, interfaceNone, interfaceFutureMap - , DomNode(..), DomElement, DomElementVisibilityAlignment(..), DefaultActionHandling(..) + , DomElementHeader, DomElementVisibilityAlignment(..), DefaultActionHandling(..) , Audio, AudioSource, AudioSourceLoadError(..), AudioProcessing(..), AudioParameterTimeline, EditAudioDiff(..) , HttpRequest, HttpBody(..), HttpExpect(..), HttpError(..), HttpMetadata , SocketConnectionEvent(..), SocketId(..) @@ -11,8 +11,8 @@ module Web exposing , WindowVisibility(..) , programInit, programUpdate, programSubscriptions , ProgramState(..), ProgramEvent(..) - , InterfaceSingle(..), InterfaceSingleWithFuture(..), InterfaceSingleRequest(..), InterfaceSingleListen(..), InterfaceSingleWithoutFuture(..) - , interfaceDiffs, findInterfaceAssociatedWithDiffComingBack, interfaceFutureJsonDecoder, InterfaceDiff(..), InterfaceWithFutureDiff(..), InterfaceWithoutFutureDiff(..), EditDomDiff, ReplacementInEditDomDiff(..), InterfaceSingleRequestId(..), InterfaceSingleListenId(..), DomElementId, DomNodeId(..), HttpRequestId, HttpExpectId(..) + , InterfaceSingle(..), InterfaceSingleWithFuture(..), InterfaceSingleRequest(..), InterfaceSingleListen(..), InterfaceSingleWithoutFuture(..), DomElementHeaderInfo, DomTextOrElementHeader(..), DomTextOrElementHeaderInfo(..) + , interfaceDiffs, findInterfaceAssociatedWithDiffComingBack, interfaceFutureJsonDecoder, InterfaceDiff(..), InterfaceWithFutureDiff(..), InterfaceWithoutFutureDiff(..), EditDomDiff, ReplacementInEditDomDiff(..), InterfaceSingleRequestId(..), InterfaceSingleListenId(..), HttpRequestId, HttpExpectId(..) ) {-| A state-interface program that can run in the browser @@ -31,7 +31,7 @@ You can also [embed](#embed) a state-interface program as part of an existing ap Types used by [`Web.Dom`](Web-Dom) -@docs DomNode, DomElement, DomElementVisibilityAlignment, DefaultActionHandling +@docs DomElementHeader, DomElementVisibilityAlignment, DefaultActionHandling ## Audio @@ -102,14 +102,13 @@ Under the hood, [`Web.program`](Web#program) is then defined as just ## internals, safe to ignore for users @docs ProgramState, ProgramEvent -@docs InterfaceSingle, InterfaceSingleWithFuture, InterfaceSingleRequest, InterfaceSingleListen, InterfaceSingleWithoutFuture -@docs interfaceDiffs, findInterfaceAssociatedWithDiffComingBack, interfaceFutureJsonDecoder, InterfaceDiff, InterfaceWithFutureDiff, InterfaceWithoutFutureDiff, EditDomDiff, ReplacementInEditDomDiff, InterfaceSingleRequestId, InterfaceSingleListenId, DomElementId, DomNodeId, HttpRequestId, HttpExpectId +@docs InterfaceSingle, InterfaceSingleWithFuture, InterfaceSingleRequest, InterfaceSingleListen, InterfaceSingleWithoutFuture, DomElementHeaderInfo, DomTextOrElementHeader, DomTextOrElementHeaderInfo +@docs interfaceDiffs, findInterfaceAssociatedWithDiffComingBack, interfaceFutureJsonDecoder, InterfaceDiff, InterfaceWithFutureDiff, InterfaceWithoutFutureDiff, EditDomDiff, ReplacementInEditDomDiff, InterfaceSingleRequestId, InterfaceSingleListenId, HttpRequestId, HttpExpectId -} import Angle exposing (Angle) import AppUrl exposing (AppUrl) -import Array exposing (Array) import Bytes exposing (Bytes) import Bytes.LocalExtra import Dict exposing (Dict) @@ -231,7 +230,7 @@ type AudioSourceLoadError {-| An [`InterfaceSingle`](#InterfaceSingle) that will notify elm some time in the future. -} type InterfaceSingleWithFuture future - = DomNodeRender (DomNode future) + = DomNodeRender { path : List Int, node : DomTextOrElementHeader future } | AudioSourceLoad { url : String, on : Result AudioSourceLoadError AudioSource -> future } | SocketConnect { address : String, on : SocketConnectionEvent -> future } | NotificationShow { id : String, message : String, details : String, on : NotificationClicked -> future } @@ -401,16 +400,17 @@ type HttpExpectId | IdHttpExpectWhatever -{-| Plain text or a [`DomElement`](#DomElement) for use in an [`Interface`](#Interface). +{-| Plain text or a [`DomElementHeader`](#DomElementHeader) for use in an [`Interface`](#Interface). -} -type DomNode future +type DomTextOrElementHeader future = DomText String - | DomElement (DomElement future) + | DomElementHeader (DomElementHeader future) -{-| A tagged DOM node that can itself contain child [node](#DomNode)s +{-| Everything about a [tagged DOM element](Web-Dom#Element) +except potential sub-[node](Web-Dom#Node)s -} -type alias DomElement future = +type alias DomElementHeader future = RecordWithoutConstructorFunction { namespace : Maybe String , tag : String @@ -426,11 +426,10 @@ type alias DomElement future = { on : Json.Decode.Value -> future , defaultActionHandling : DefaultActionHandling } - , subs : Array (DomNode future) } -{-| What part of the [`DomElement`](Web#DomElement) should be visible +{-| What part of the [`Web.Dom.Element`](Web-Dom#Element) should be visible - `DomElementStart`: mostly for text to read - `DomElementEnd`: mostly for text to write @@ -486,9 +485,9 @@ type InterfaceSingleListenId | IdGamepadsChangeListen -{-| Safe to ignore. Identifier for a [`DomElement`](#DomElement) +{-| Safe to ignore. [`DomElementHeader`](#DomElementHeader) without the actual receiving functions for events -} -type alias DomElementId = +type alias DomElementHeaderInfo = RecordWithoutConstructorFunction { namespace : Maybe String , tag : String @@ -499,15 +498,14 @@ type alias DomElementId = , scrollToShow : Maybe { x : DomElementVisibilityAlignment, y : DomElementVisibilityAlignment } , scrollPositionRequest : Bool , eventListens : Dict String DefaultActionHandling - , subs : Array DomNodeId } -{-| Safe to ignore. Identifier for a [`DomNode`](#DomNode) +{-| Safe to ignore. [`DomTextOrElementHeader`](#DomTextOrElementHeader) without the actual receiving functions for events -} -type DomNodeId - = DomTextId String - | DomElementId DomElementId +type DomTextOrElementHeaderInfo + = DomTextInfo String + | DomElementHeaderInfo DomElementHeaderInfo {-| Identifier for a [`Web.Socket`](Web-Socket) that can be used to [communicate](Web-Socket#communicate) @@ -586,7 +584,7 @@ and sometimes to nest events (like `Cmd.map/Task.map/Sub.map/...` in The Elm Arc ] |> Web.Dom.render - treeUi : ... -> Web.DomNode TreeUiEvent + treeUi : ... -> Web.Dom.Node TreeUiEvent In all these examples, you end up converting the narrow future representation of part of the interface to a broader representation for the parent interface @@ -617,23 +615,13 @@ interfaceSingleFutureMap futureChange = |> InterfaceWithFuture -domNodeFutureMap : (future -> mappedFuture) -> (DomNode future -> DomNode mappedFuture) -domNodeFutureMap futureChange = - \domElementToMap -> - case domElementToMap of - DomText text -> - DomText text - - DomElement domElement -> - domElement |> domElementFutureMap futureChange |> DomElement - - interfaceWithFutureMap : (future -> mappedFuture) -> (InterfaceSingleWithFuture future -> InterfaceSingleWithFuture mappedFuture) interfaceWithFutureMap futureChange = \interface -> case interface of - DomNodeRender domElementToRender -> - domElementToRender |> domNodeFutureMap futureChange |> DomNodeRender + DomNodeRender toRender -> + { path = toRender.path, node = toRender.node |> domNodeFutureMap futureChange } + |> DomNodeRender AudioSourceLoad load -> { url = load.url, on = \event -> load.on event |> futureChange } @@ -658,6 +646,41 @@ interfaceWithFutureMap futureChange = listen |> interfaceListenFutureMap futureChange |> Listen +domNodeFutureMap : (future -> mappedFuture) -> (DomTextOrElementHeader future -> DomTextOrElementHeader mappedFuture) +domNodeFutureMap futureChange = + \domElementToMap -> + case domElementToMap of + DomText text -> + DomText text + + DomElementHeader domElement -> + domElement |> domElementHeaderFutureMap futureChange |> DomElementHeader + + +domElementHeaderFutureMap : (future -> mappedFuture) -> (DomElementHeader future -> DomElementHeader mappedFuture) +domElementHeaderFutureMap futureChange = + \domElementToMap -> + { namespace = domElementToMap.namespace + , tag = domElementToMap.tag + , styles = domElementToMap.styles + , attributes = domElementToMap.attributes + , attributesNamespaced = domElementToMap.attributesNamespaced + , scrollToPosition = domElementToMap.scrollToPosition + , scrollToShow = domElementToMap.scrollToShow + , scrollPositionRequest = + domElementToMap.scrollPositionRequest + |> Maybe.map (\request position -> position |> request |> futureChange) + , eventListens = + domElementToMap.eventListens + |> Dict.map + (\_ listen -> + { on = \event -> listen.on event |> futureChange + , defaultActionHandling = listen.defaultActionHandling + } + ) + } + + interfaceRequestFutureMap : (future -> mappedFuture) -> (InterfaceSingleRequest future -> InterfaceSingleRequest mappedFuture) interfaceRequestFutureMap futureChange = \interfaceSingleRequest -> @@ -775,32 +798,6 @@ interfaceListenFutureMap futureChange = (\event -> event |> toFuture |> futureChange) |> GamepadsChangeListen -domElementFutureMap : (future -> mappedFuture) -> (DomElement future -> DomElement mappedFuture) -domElementFutureMap futureChange = - \domElementToMap -> - { namespace = domElementToMap.namespace - , tag = domElementToMap.tag - , styles = domElementToMap.styles - , attributes = domElementToMap.attributes - , attributesNamespaced = domElementToMap.attributesNamespaced - , scrollToPosition = domElementToMap.scrollToPosition - , scrollToShow = domElementToMap.scrollToShow - , scrollPositionRequest = - domElementToMap.scrollPositionRequest - |> Maybe.map (\request position -> position |> request |> futureChange) - , eventListens = - domElementToMap.eventListens - |> Dict.map - (\_ listen -> - { on = \event -> listen.on event |> futureChange - , defaultActionHandling = listen.defaultActionHandling - } - ) - , subs = - domElementToMap.subs |> Array.map (\node -> node |> domNodeFutureMap futureChange) - } - - {-| Ignore the specific variants, this is just exposed so can for example simulate it more easily in tests, add a debugger etc. @@ -820,132 +817,6 @@ type ProgramEvent appState | AppEventToNewAppState appState -domElementToId : DomElement future_ -> DomElementId -domElementToId = - \domElement -> - { namespace = domElement.namespace - , tag = domElement.tag - , styles = domElement.styles - , attributes = domElement.attributes - , attributesNamespaced = domElement.attributesNamespaced - , scrollToPosition = domElement.scrollToPosition - , scrollToShow = domElement.scrollToShow - , scrollPositionRequest = - case domElement.scrollPositionRequest of - Nothing -> - False - - Just _ -> - True - , eventListens = - domElement.eventListens |> Dict.map (\_ listen -> listen.defaultActionHandling) - , subs = - domElement.subs |> Array.map domNodeToId - } - - -domNodeDiff : - List Int - -> ( DomNode state, DomNode state ) - -> List EditDomDiff -domNodeDiff path = - \( aNode, bNode ) -> - case ( aNode, bNode ) of - ( DomText _, DomElement bElement ) -> - [ { path = path, replacement = bElement |> domElementToId |> DomElementId |> ReplacementDomNode } ] - - ( DomElement _, DomText bText ) -> - [ { path = path, replacement = bText |> DomTextId |> ReplacementDomNode } ] - - ( DomText aText, DomText bText ) -> - if aText == bText then - [] - - else - [ { path = path, replacement = bText |> DomTextId |> ReplacementDomNode } ] - - ( DomElement aElement, DomElement bElement ) -> - ( aElement, bElement ) |> domElementDiff path - - -domElementDiff : - List Int - -> (( DomElement future, DomElement future ) -> List EditDomDiff) -domElementDiff path = - \( aElement, bElement ) -> - if - (aElement.tag == bElement.tag) - && ((aElement.subs |> Array.length) == (bElement.subs |> Array.length)) - then - let - modifierDiffs : List ReplacementInEditDomDiff - modifierDiffs = - [ if aElement.styles == bElement.styles then - Nothing - - else - ReplacementDomElementStyles bElement.styles |> Just - , if aElement.attributes == bElement.attributes then - Nothing - - else - ReplacementDomElementAttributes bElement.attributes |> Just - , if aElement.attributesNamespaced == bElement.attributesNamespaced then - Nothing - - else - ReplacementDomElementAttributesNamespaced bElement.attributesNamespaced |> Just - , if aElement.scrollToPosition == bElement.scrollToPosition then - Nothing - - else - ReplacementDomElementScrollToPosition bElement.scrollToPosition |> Just - , if aElement.scrollToShow == bElement.scrollToShow then - Nothing - - else - ReplacementDomElementScrollToShow bElement.scrollToShow |> Just - , case ( aElement.scrollPositionRequest, bElement.scrollPositionRequest ) of - ( Nothing, Nothing ) -> - Nothing - - ( Just _, Just _ ) -> - Nothing - - ( Just _, Nothing ) -> - Nothing - - ( Nothing, Just _ ) -> - ReplacementDomElementScrollPositionRequest |> Just - , let - bElementEventListensId : Dict String DefaultActionHandling - bElementEventListensId = - bElement.eventListens |> Dict.map (\_ v -> v.defaultActionHandling) - in - if - (aElement.eventListens |> Dict.map (\_ v -> v.defaultActionHandling)) - == bElementEventListensId - then - Nothing - - else - ReplacementDomElementEventListens bElementEventListensId |> Just - ] - |> List.filterMap identity - in - (modifierDiffs - |> List.map (\replacement -> { path = path, replacement = replacement }) - ) - ++ (List.map2 (\( subIndex, aSub ) bSub -> domNodeDiff (subIndex :: path) ( aSub, bSub )) - (aElement.subs |> Array.toIndexedList) - (bElement.subs |> Array.toList) - |> List.concat - ) - - else - [ { path = path, replacement = bElement |> domElementToId |> DomElementId |> ReplacementDomNode } ] - - {-| Determine which outgoing effects need to be executed based on the difference between old and updated interfaces To for example determine the initial effects, use @@ -1124,8 +995,8 @@ toRemoveDiff = Request (HttpRequest request) -> RemoveHttpRequest (request |> httpRequestToId) |> Just - DomNodeRender _ -> - RemoveDom |> Just + DomNodeRender toRender -> + RemoveDom { path = toRender.path } |> Just Request (WindowSizeRequest _) -> Nothing @@ -1166,9 +1037,14 @@ toMergeDiff = \oldAndNewInterfaceSingles -> case oldAndNewInterfaceSingles of ( InterfaceWithFuture (DomNodeRender domElementPreviouslyRendered), InterfaceWithFuture (DomNodeRender domElementToRender) ) -> - ( domElementPreviouslyRendered, domElementToRender ) - |> domNodeDiff [] - |> List.map (\diff -> diff |> EditDom |> InterfaceWithFutureDiff) + ( domElementPreviouslyRendered.node, domElementToRender.node ) + |> domTextOrElementHeaderDiff + |> List.map + (\diff -> + { path = domElementPreviouslyRendered.path, replacement = diff } + |> EditDom + |> InterfaceWithFutureDiff + ) ( InterfaceWithoutFuture (AudioPlay previouslyPlayed), InterfaceWithoutFuture (AudioPlay toPlay) ) -> ( previouslyPlayed, toPlay ) @@ -1190,6 +1066,110 @@ toMergeDiff = [] +domElementHeaderToInfo : DomElementHeader future_ -> DomElementHeaderInfo +domElementHeaderToInfo = + \domElement -> + { namespace = domElement.namespace + , tag = domElement.tag + , styles = domElement.styles + , attributes = domElement.attributes + , attributesNamespaced = domElement.attributesNamespaced + , scrollToPosition = domElement.scrollToPosition + , scrollToShow = domElement.scrollToShow + , scrollPositionRequest = + case domElement.scrollPositionRequest of + Nothing -> + False + + Just _ -> + True + , eventListens = + domElement.eventListens |> Dict.map (\_ listen -> listen.defaultActionHandling) + } + + +domTextOrElementHeaderDiff : ( DomTextOrElementHeader state, DomTextOrElementHeader state ) -> List ReplacementInEditDomDiff +domTextOrElementHeaderDiff = + \( aNode, bNode ) -> + case ( aNode, bNode ) of + ( DomText _, DomElementHeader bElement ) -> + [ bElement |> domElementHeaderToInfo |> DomElementHeaderInfo |> ReplacementDomNode ] + + ( DomElementHeader _, DomText bText ) -> + [ bText |> DomTextInfo |> ReplacementDomNode ] + + ( DomText aText, DomText bText ) -> + if aText == bText then + [] + + else + [ bText |> DomTextInfo |> ReplacementDomNode ] + + ( DomElementHeader aElement, DomElementHeader bElement ) -> + ( aElement, bElement ) |> domElementHeaderDiff + + +domElementHeaderDiff : ( DomElementHeader future, DomElementHeader future ) -> List ReplacementInEditDomDiff +domElementHeaderDiff = + \( aElement, bElement ) -> + if aElement.tag == bElement.tag then + [ if aElement.styles == bElement.styles then + Nothing + + else + ReplacementDomElementStyles bElement.styles |> Just + , if aElement.attributes == bElement.attributes then + Nothing + + else + ReplacementDomElementAttributes bElement.attributes |> Just + , if aElement.attributesNamespaced == bElement.attributesNamespaced then + Nothing + + else + ReplacementDomElementAttributesNamespaced bElement.attributesNamespaced |> Just + , if aElement.scrollToPosition == bElement.scrollToPosition then + Nothing + + else + ReplacementDomElementScrollToPosition bElement.scrollToPosition |> Just + , if aElement.scrollToShow == bElement.scrollToShow then + Nothing + + else + ReplacementDomElementScrollToShow bElement.scrollToShow |> Just + , case ( aElement.scrollPositionRequest, bElement.scrollPositionRequest ) of + ( Nothing, Nothing ) -> + Nothing + + ( Just _, Just _ ) -> + Nothing + + ( Just _, Nothing ) -> + Nothing + + ( Nothing, Just _ ) -> + ReplacementDomElementScrollPositionRequest |> Just + , let + bElementEventListensId : Dict String DefaultActionHandling + bElementEventListensId = + bElement.eventListens |> Dict.map (\_ v -> v.defaultActionHandling) + in + if + (aElement.eventListens |> Dict.map (\_ v -> v.defaultActionHandling)) + == bElementEventListensId + then + Nothing + + else + ReplacementDomElementEventListens bElementEventListensId |> Just + ] + |> List.filterMap identity + + else + [ bElement |> domElementHeaderToInfo |> DomElementHeaderInfo |> ReplacementDomNode ] + + audioDiff : ( Audio, Audio ) -> List EditAudioDiff audioDiff = \( previous, new ) -> @@ -1217,16 +1197,6 @@ audioDiff = |> List.filterMap identity -domNodeToId : DomNode future_ -> DomNodeId -domNodeToId domNode = - case domNode of - DomText text -> - DomTextId text - - DomElement element -> - DomElementId (element |> domElementToId) - - interfaceSingleRequestToId : InterfaceSingleRequest future_ -> InterfaceSingleRequestId interfaceSingleRequestToId = \interfaceSingleRequest -> @@ -1278,8 +1248,8 @@ toAddDiff = InterfaceWithFuture interfaceWithFuture -> (case interfaceWithFuture of - DomNodeRender domNode -> - domNode |> domNodeToId |> AddDomRender + DomNodeRender domNodeAndPath -> + { path = domNodeAndPath.path, replacement = domNodeAndPath.node |> domNodeToId |> ReplacementDomNode } |> EditDom AudioSourceLoad load -> AddAudioSourceLoad load.url @@ -1299,13 +1269,25 @@ toAddDiff = |> InterfaceWithFutureDiff +domNodeToId : DomTextOrElementHeader future_ -> DomTextOrElementHeaderInfo +domNodeToId domNode = + case domNode of + DomText text -> + DomTextInfo text + + DomElementHeader element -> + DomElementHeaderInfo (element |> domElementHeaderToInfo) + + interfaceSingleWithFutureIdToStructuredId : InterfaceSingleWithFutureId -> StructuredId interfaceSingleWithFutureIdToStructuredId = \idInterfaceWithFuture -> StructuredId.ofVariant (case idInterfaceWithFuture of - IdDomNodeRender -> - ( "DomNodeRender", [] ) + IdDomNodeRender path -> + ( "DomNodeRender" + , [ path.path |> List.map StructuredId.ofInt |> StructuredId.ofList ] + ) IdAudioSourceLoad sourceUrl -> ( "AudioSourceLoad" @@ -1531,8 +1513,8 @@ interfaceSingleWithFutureToId : InterfaceSingleWithFuture future_ -> InterfaceSi interfaceSingleWithFutureToId = \idInterfaceWithFuture -> case idInterfaceWithFuture of - DomNodeRender _ -> - IdDomNodeRender + DomNodeRender toRender -> + IdDomNodeRender { path = toRender.path } AudioSourceLoad audioSourceLoad -> IdAudioSourceLoad audioSourceLoad.url @@ -1747,30 +1729,11 @@ interfaceSingleListenIdJsonCodec = Json.Codec.unit -domNodeIdJsonCodec : JsonCodec DomNodeId -domNodeIdJsonCodec = - Json.Codec.choice - (\domTextId domElementId domNodeId -> - case domNodeId of - DomTextId text -> - domTextId text - - DomElementId element -> - domElementId element - ) - |> Json.Codec.variant ( DomTextId, "Text" ) Json.Codec.string - |> Json.Codec.variant ( DomElementId, "Element" ) - (Json.Codec.lazy (\() -> domElementIdJsonCodec)) - - interfaceWithFutureDiffJsonCodec : JsonCodec InterfaceWithFutureDiff interfaceWithFutureDiffJsonCodec = Json.Codec.choice - (\addDomRender addEditDom addAudioSourceLoad addSocketConnect addNotificationShow addListen addRequest interfaceWithFutureDiff -> + (\addEditDom addAudioSourceLoad addSocketConnect addNotificationShow addListen addRequest interfaceWithFutureDiff -> case interfaceWithFutureDiff of - AddDomRender domNodeId -> - addDomRender domNodeId - EditDom editDomDiff -> addEditDom editDomDiff @@ -1789,8 +1752,6 @@ interfaceWithFutureDiffJsonCodec = AddRequest request -> addRequest request ) - |> Json.Codec.variant ( AddDomRender, "AddDomRender" ) - domNodeIdJsonCodec |> Json.Codec.variant ( EditDom, "EditDom" ) (Json.Codec.record (\path replacement -> { path = path, replacement = replacement }) |> Json.Codec.field ( .path, "path" ) (Json.Codec.list Json.Codec.int) @@ -2150,6 +2111,52 @@ replacementInEditDomDiffJsonCodec = |> Json.Codec.variant ( ReplacementDomElementEventListens, "EventListens" ) domElementEventListensJsonCodec +domNodeIdJsonCodec : JsonCodec DomTextOrElementHeaderInfo +domNodeIdJsonCodec = + Json.Codec.choice + (\domTextId domElementId domNodeId -> + case domNodeId of + DomTextInfo text -> + domTextId text + + DomElementHeaderInfo element -> + domElementId element + ) + |> Json.Codec.variant ( DomTextInfo, "Text" ) Json.Codec.string + |> Json.Codec.variant ( DomElementHeaderInfo, "Element" ) + (Json.Codec.lazy (\() -> domElementHeaderInfoJsonCodec)) + + +domElementHeaderInfoJsonCodec : JsonCodec DomElementHeaderInfo +domElementHeaderInfoJsonCodec = + Json.Codec.record + (\namespace tag styles attributes attributesNamespaced scrollToPosition scrollToShow scrollPositionRequest eventListens -> + { namespace = namespace + , tag = tag + , styles = styles + , attributes = attributes + , attributesNamespaced = attributesNamespaced + , scrollToPosition = scrollToPosition + , scrollToShow = scrollToShow + , scrollPositionRequest = scrollPositionRequest + , eventListens = eventListens + } + ) + |> Json.Codec.field ( .namespace, "namespace" ) (Json.Codec.nullable Json.Codec.string) + |> Json.Codec.field ( .tag, "tag" ) Json.Codec.string + |> Json.Codec.field ( .styles, "styles" ) domElementStylesJsonCodec + |> Json.Codec.field ( .attributes, "attributes" ) domElementAttributesJsonCodec + |> Json.Codec.field ( .attributesNamespaced, "attributesNamespaced" ) domElementAttributesNamespacedJsonCodec + |> Json.Codec.field ( .scrollToPosition, "scrollToPosition" ) + (Json.Codec.nullable domElementScrollPositionJsonCodec) + |> Json.Codec.field ( .scrollToShow, "scrollToShow" ) + (Json.Codec.nullable domElementVisibilityAlignmentsJsonDecoder) + |> Json.Codec.field ( .scrollPositionRequest, "scrollPositionRequest" ) Json.Codec.bool + |> Json.Codec.field ( .eventListens, "eventListens" ) + domElementEventListensJsonCodec + |> Json.Codec.recordFinish + + tagValueToJson : ( String, Json.Encode.Value ) -> Json.Encode.Value tagValueToJson = \( tag, value ) -> @@ -2159,6 +2166,27 @@ tagValueToJson = ] +tagValueJsonDecoder : String -> (Json.Decode.Decoder value -> Json.Decode.Decoder value) +tagValueJsonDecoder name valueJsonDecoder = + Json.Decode.map2 (\() variantValue -> variantValue) + (Json.Decode.field "tag" (onlyStringJsonDecoder name)) + (Json.Decode.field "value" valueJsonDecoder) + + +onlyStringJsonDecoder : String -> Json.Decode.Decoder () +onlyStringJsonDecoder specificAllowedString = + Json.Decode.string + |> Json.Decode.andThen + (\str -> + if str == specificAllowedString then + () |> Json.Decode.succeed + + else + ([ "expected only \"", specificAllowedString, "\"" ] |> String.concat) + |> Json.Decode.fail + ) + + interfaceDiffToJson : InterfaceDiff -> Json.Encode.Value interfaceDiffToJson = \interfaceDiff -> @@ -2258,8 +2286,10 @@ interfaceWithoutFutureDiffToJson = ] ) - RemoveDom -> - ( "RemoveDom", Json.Encode.null ) + RemoveDom path -> + ( "RemoveDom" + , Json.Encode.object [ ( "path", path.path |> Json.Encode.list Json.Encode.int ) ] + ) RemoveHttpRequest httpRequestId -> ( "RemoveHttpRequest", httpRequestId |> httpRequestIdJsonCodec.toJson ) @@ -2441,11 +2471,8 @@ interfaceWithFutureDiffToId : InterfaceWithFutureDiff -> InterfaceSingleWithFutu interfaceWithFutureDiffToId = \idInterfaceWithFuture -> case idInterfaceWithFuture of - AddDomRender _ -> - IdDomNodeRender - - EditDom _ -> - IdDomNodeRender + EditDom toEdit -> + IdDomNodeRender { path = toEdit.path } AddNotificationShow show -> IdNotificationShow { id = show.id } @@ -2502,50 +2529,40 @@ for the transformed event data coming back interfaceFutureJsonDecoder : InterfaceSingleWithFuture future -> Json.Decode.Decoder future interfaceFutureJsonDecoder interface = case interface of - DomNodeRender domElementToRender -> - let - domElementAtPathJsonDecoder = - Json.Decode.field "path" (Json.Decode.list Json.Decode.int) - |> Json.Decode.andThen - (\path -> - case domElementToRender |> domElementAtReversePath (path |> List.reverse) of - Nothing -> - Json.Decode.fail "origin element of event not found" - - Just (DomText _) -> - Json.Decode.fail "origin element of event leads to text, not element" - - Just (DomElement foundDomElement) -> - foundDomElement |> Json.Decode.succeed + DomNodeRender toRender -> + case toRender.node of + DomText _ -> + Json.Decode.fail "received event from a text node" + + DomElementHeader domElement -> + Json.Decode.oneOf + ([ tagValueJsonDecoder "EventListen" + (Json.Decode.map2 (\name event -> { name = name, event = event }) + (Json.Decode.field "name" Json.Decode.string) + (Json.Decode.field "event" Json.Decode.value) ) - in - Json.Decode.oneOf - [ Json.Decode.map3 (\domElementAtPath name event -> { domElementAtPath = domElementAtPath, name = name, event = event }) - domElementAtPathJsonDecoder - (Json.Decode.field "name" Json.Decode.string) - (Json.Decode.field "event" Json.Decode.value) - |> Json.Decode.andThen - (\specificEvent -> - case specificEvent.domElementAtPath.eventListens |> Dict.get specificEvent.name of - Nothing -> - Json.Decode.fail "received event for element without listen" - - Just eventListen -> - eventListen.on specificEvent.event |> Json.Decode.succeed - ) - , Json.Decode.map2 (\domElementAtPath scrollPosition -> { domElementAtPath = domElementAtPath, scrollPosition = scrollPosition }) - domElementAtPathJsonDecoder - (Json.Decode.field "scrollPosition" domElementScrollPositionJsonCodec.jsonDecoder) - |> Json.Decode.andThen - (\specificEvent -> - case specificEvent.domElementAtPath.scrollPositionRequest of - Nothing -> - Json.Decode.fail "received scroll position for element without listen" + |> Json.Decode.andThen + (\specificEvent -> + case domElement.eventListens |> Dict.get specificEvent.name of + Nothing -> + Json.Decode.fail "received event of a kind that isn't listened for" + + Just eventListen -> + eventListen.on specificEvent.event |> Json.Decode.succeed + ) + |> Just + , case domElement.scrollPositionRequest of + Nothing -> + Nothing - Just request -> - request specificEvent.scrollPosition |> Json.Decode.succeed + Just request -> + tagValueJsonDecoder "ScrollPositionRequest" + domElementScrollPositionJsonCodec.jsonDecoder + |> Json.Decode.map request + |> Just + ] + |> List.filterMap identity ) - ] AudioSourceLoad load -> Json.Decode.map load.on @@ -3309,26 +3326,6 @@ windowVisibilityCodec = ) -domElementAtReversePath : List Int -> (DomNode future -> Maybe (DomNode future)) -domElementAtReversePath path domNode = - case path of - [] -> - Just domNode - - subIndex :: parentsOfSub -> - case domNode of - DomText _ -> - Nothing - - DomElement domElement -> - case Array.get subIndex domElement.subs of - Nothing -> - Nothing - - Just subNodeAtIndex -> - domElementAtReversePath parentsOfSub subNodeAtIndex - - {-| Controller information on button presses, thumbstick positions etc. - `primaryButton`: The most common action like "enter"/"confirm" or jump @@ -3354,7 +3351,7 @@ domElementAtReversePath path domNode = - `upButton`, `downBottom`, `leftButton`, `rightButton`: exactly one step in a direction, usually in a (quick) menu/inventory - - \`homeButton: Usually turns the gamepad on/off, or changes the state of the game + - `homeButton`: Usually turns the gamepad on/off, or changes the state of the game - `touchpadButton`: Not present on most gamepads. While the touchpad is often used for controlling the mouse, it can also be used as a simple button @@ -3433,38 +3430,6 @@ type alias GamepadButtonMap = } -domElementIdJsonCodec : JsonCodec DomElementId -domElementIdJsonCodec = - Json.Codec.record - (\namespace tag styles attributes attributesNamespaced scrollToPosition scrollToShow scrollPositionRequest eventListens subs -> - { namespace = namespace - , tag = tag - , styles = styles - , attributes = attributes - , attributesNamespaced = attributesNamespaced - , scrollToPosition = scrollToPosition - , scrollToShow = scrollToShow - , scrollPositionRequest = scrollPositionRequest - , eventListens = eventListens - , subs = subs - } - ) - |> Json.Codec.field ( .namespace, "namespace" ) (Json.Codec.nullable Json.Codec.string) - |> Json.Codec.field ( .tag, "tag" ) Json.Codec.string - |> Json.Codec.field ( .styles, "styles" ) domElementStylesJsonCodec - |> Json.Codec.field ( .attributes, "attributes" ) domElementAttributesJsonCodec - |> Json.Codec.field ( .attributesNamespaced, "attributesNamespaced" ) domElementAttributesNamespacedJsonCodec - |> Json.Codec.field ( .scrollToPosition, "scrollToPosition" ) - (Json.Codec.nullable domElementScrollPositionJsonCodec) - |> Json.Codec.field ( .scrollToShow, "scrollToShow" ) - (Json.Codec.nullable domElementVisibilityAlignmentsJsonDecoder) - |> Json.Codec.field ( .scrollPositionRequest, "scrollPositionRequest" ) Json.Codec.bool - |> Json.Codec.field ( .eventListens, "eventListens" ) - domElementEventListensJsonCodec - |> Json.Codec.field ( .subs, "subs" ) (Json.Codec.array domNodeIdJsonCodec) - |> Json.Codec.recordFinish - - {-| The "update" part for an embedded program -} programUpdate : ProgramConfig state -> (ProgramEvent state -> ProgramState state -> ( ProgramState state, Cmd (ProgramEvent state) )) @@ -3578,7 +3543,7 @@ type InterfaceWithoutFutureDiff = Add InterfaceSingleWithoutFuture | EditAudio { url : String, startTime : Time.Posix, replacement : EditAudioDiff } | RemoveHttpRequest HttpRequestId - | RemoveDom + | RemoveDom { path : List Int } | RemoveSocketConnect { address : String } | RemoveAudio { url : String, startTime : Time.Posix } | EditNotification { id : String, message : String, details : String } @@ -3589,7 +3554,7 @@ type InterfaceWithoutFutureDiff {-| Actions that will notify elm some time in the future -} type InterfaceSingleWithFutureId - = IdDomNodeRender + = IdDomNodeRender { path : List Int } | IdAudioSourceLoad String | IdSocketConnect { address : String } | IdNotificationShow { id : String } @@ -3600,8 +3565,7 @@ type InterfaceSingleWithFutureId {-| Actions that will notify elm some time in the future -} type InterfaceWithFutureDiff - = AddDomRender DomNodeId - | EditDom EditDomDiff + = EditDom EditDomDiff | AddSocketConnect { address : String } | AddAudioSourceLoad String | AddNotificationShow { id : String, message : String, details : String } @@ -3686,7 +3650,7 @@ type alias EditDomDiff = {-| What parts of a node are replaced. Either all modifiers of a certain kind or the whole node -} type ReplacementInEditDomDiff - = ReplacementDomNode DomNodeId + = ReplacementDomNode DomTextOrElementHeaderInfo | ReplacementDomElementStyles (Dict String String) | ReplacementDomElementAttributes (Dict String String) | ReplacementDomElementAttributesNamespaced (Dict ( String, String ) String) diff --git a/src/Web/Dom.elm b/src/Web/Dom.elm index 46bc2b5..a1d25d7 100644 --- a/src/Web/Dom.elm +++ b/src/Web/Dom.elm @@ -6,9 +6,10 @@ module Web.Dom exposing , scrollToShow, scrollPositionRequest, scrollToPosition , modifierFutureMap, modifierBatch, modifierNone , futureMap, render + , Node(..), Element ) -{-| Helpers for [DOM node types](Web#DomNode) as part of an [`Interface`](Web#Interface). +{-| Helpers for [DOM nodes](#Node) as part of an [`Interface`](Web#Interface). These are primitives used for svg and html. Compare with [`elm/virtual-dom`](https://dark.elm.dmy.fr/packages/elm/virtual-dom/latest/) @@ -20,37 +21,69 @@ Compare with [`elm/virtual-dom`](https://dark.elm.dmy.fr/packages/elm/virtual-do @docs scrollToShow, scrollPositionRequest, scrollToPosition @docs modifierFutureMap, modifierBatch, modifierNone @docs futureMap, render +@docs Node, Element -} -import Array import Dict import Json.Decode import List.LocalExtra +import RecordWithoutConstructorFunction exposing (RecordWithoutConstructorFunction) import Rope exposing (Rope) -import Web exposing (DomElement, DomNode) +import Web -{-| An [`Interface`](Web#Interface) for displaying a given [`DomNode`](Web#DomNode). +{-| An [`Interface`](Web#Interface) for displaying a given [`Web.Dom.Node`](Web-Dom#Node). -} -render : DomNode future -> Web.Interface future +render : Node future -> Web.Interface future render = \domNode -> domNode - |> Web.DomNodeRender - |> Web.InterfaceWithFuture - |> Rope.singleton - - -{-| Wire events from this [`DomNode`](Web#DomNode) to a specific event. + |> nodeFlatten + |> List.map (\nodeAndPath -> nodeAndPath |> Web.DomNodeRender |> Web.InterfaceWithFuture) + |> Rope.fromList + + +nodeFlatten : Node future -> List { path : List Int, node : Web.DomTextOrElementHeader future } +nodeFlatten = + \node -> node |> nodeFlattenToRope |> Rope.toList + + +nodeFlattenToRope : Node future -> Rope { path : List Int, node : Web.DomTextOrElementHeader future } +nodeFlattenToRope = + \node -> + case node of + Text string -> + { path = [], node = Web.DomText string } |> Rope.singleton + + Element element_ -> + Rope.prepend + { path = [], node = Web.DomElementHeader element_.header } + (List.foldl + (\sub soFar -> + { subIndex = soFar.subIndex + 1 + , rope = + Rope.appendTo + soFar.rope + (Rope.map (\layerPart -> { layerPart | path = soFar.subIndex :: layerPart.path }) + (nodeFlattenToRope sub) + ) + } + ) + { subIndex = 0, rope = Rope.empty } + element_.subs + ).rope + + +{-| Wire events from this [`Web.Dom.Node`](Web-Dom#Node) to a specific event, for example buttonUi "start" |> Web.Dom.futureMap (\Clicked -> StartButtonClicked) -with e.g. +with - buttonUi : List (Web.DomNode ()) -> Web.DomNode ButtonEvent - buttonUi subs = + buttonUi : String -> Web.Dom.Node ButtonEvent + buttonUi label = Web.Dom.element "button" [ Web.Dom.listenTo "click" |> Web.Dom.modifierFutureMap (\_ -> Clicked) @@ -61,19 +94,28 @@ with e.g. = Clicked -} -futureMap : (future -> mappedFuture) -> (DomNode future -> DomNode mappedFuture) +futureMap : (future -> mappedFuture) -> (Node future -> Node mappedFuture) futureMap futureChange = \domElementToMap -> case domElementToMap of - Web.DomText string -> - Web.DomText string + Text string -> + Text string - Web.DomElement domElement -> - domElement |> elementFutureMap futureChange |> Web.DomElement + Element domElement -> + domElement |> elementFutureMap futureChange |> Element -elementFutureMap : (future -> mappedFuture) -> (DomElement future -> DomElement mappedFuture) +elementFutureMap : (future -> mappedFuture) -> (Element future -> Element mappedFuture) elementFutureMap futureChange = + \domElementToMap -> + { header = domElementToMap.header |> domElementHeaderFutureMap futureChange + , subs = + domElementToMap.subs |> List.map (\node -> node |> futureMap futureChange) + } + + +domElementHeaderFutureMap : (future -> mappedFuture) -> (Web.DomElementHeader future -> Web.DomElementHeader mappedFuture) +domElementHeaderFutureMap futureChange = \domElementToMap -> { namespace = domElementToMap.namespace , tag = domElementToMap.tag @@ -93,129 +135,129 @@ elementFutureMap futureChange = , defaultActionHandling = listen.defaultActionHandling } ) - , subs = - domElementToMap.subs |> Array.map (\node -> node |> futureMap futureChange) } -{-| Plain text [`DomNode`](Web#DomNode) +{-| Plain text [DOM `Node`](#Node) -} -text : String -> DomNode future_ +text : String -> Node future_ text = - Web.DomText + Text -elementWithMaybeNamespace : Maybe String -> String -> List (Modifier future) -> List (DomNode future) -> DomNode future +elementWithMaybeNamespace : Maybe String -> String -> List (Modifier future) -> List (Node future) -> Node future elementWithMaybeNamespace maybeNamespace tag modifiers subs = let modifierList : List (ModifierSingle future) modifierList = modifiers |> modifierBatch |> Rope.toList in - { namespace = maybeNamespace - , tag = tag - , scrollToPosition = - modifierList - |> List.LocalExtra.firstJustMap - (\modifier -> - case modifier of - ScrollToPosition position -> - position |> Just - - _ -> - Nothing - ) - , scrollToShow = - modifierList - |> List.LocalExtra.firstJustMap - (\modifier -> - case modifier of - ScrollToShow alignment -> - alignment |> Just - - _ -> - Nothing - ) - , scrollPositionRequest = - modifierList - |> List.LocalExtra.firstJustMap - (\modifier -> - case modifier of - ScrollPositionRequest positionRequest -> - positionRequest |> Just - - _ -> - Nothing - ) - , eventListens = - modifierList - |> List.filterMap - (\modifier -> - case modifier of - Listen listen -> - ( listen.eventName - , { on = listen.on - , defaultActionHandling = listen.defaultActionHandling - } - ) - |> Just - - _ -> - Nothing - ) - |> Dict.fromList - , styles = - modifierList - |> List.filterMap - (\modifier -> - case modifier of - Style keyValue -> - ( keyValue.key, keyValue.value ) |> Just - - _ -> - Nothing - ) - |> Dict.fromList - , attributes = - modifierList - |> List.filterMap - (\modifier -> - case modifier of - Attribute keyValue -> - case keyValue.namespace of - Just _ -> - Nothing - - Nothing -> - ( keyValue.key, keyValue.value ) |> Just - - _ -> - Nothing - ) - |> Dict.fromList - , attributesNamespaced = - modifierList - |> List.filterMap - (\modifier -> - case modifier of - Attribute keyValue -> - case keyValue.namespace of - Just namespace -> - ( ( namespace, keyValue.key ), keyValue.value ) |> Just - - Nothing -> - Nothing - - _ -> - Nothing - ) - |> Dict.fromList - , subs = subs |> Array.fromList + { header = + { namespace = maybeNamespace + , tag = tag + , scrollToPosition = + modifierList + |> List.LocalExtra.firstJustMap + (\modifier -> + case modifier of + ScrollToPosition position -> + position |> Just + + _ -> + Nothing + ) + , scrollToShow = + modifierList + |> List.LocalExtra.firstJustMap + (\modifier -> + case modifier of + ScrollToShow alignment -> + alignment |> Just + + _ -> + Nothing + ) + , scrollPositionRequest = + modifierList + |> List.LocalExtra.firstJustMap + (\modifier -> + case modifier of + ScrollPositionRequest positionRequest -> + positionRequest |> Just + + _ -> + Nothing + ) + , eventListens = + modifierList + |> List.filterMap + (\modifier -> + case modifier of + Listen listen -> + ( listen.eventName + , { on = listen.on + , defaultActionHandling = listen.defaultActionHandling + } + ) + |> Just + + _ -> + Nothing + ) + |> Dict.fromList + , styles = + modifierList + |> List.filterMap + (\modifier -> + case modifier of + Style keyValue -> + ( keyValue.key, keyValue.value ) |> Just + + _ -> + Nothing + ) + |> Dict.fromList + , attributes = + modifierList + |> List.filterMap + (\modifier -> + case modifier of + Attribute keyValue -> + case keyValue.namespace of + Just _ -> + Nothing + + Nothing -> + ( keyValue.key, keyValue.value ) |> Just + + _ -> + Nothing + ) + |> Dict.fromList + , attributesNamespaced = + modifierList + |> List.filterMap + (\modifier -> + case modifier of + Attribute keyValue -> + case keyValue.namespace of + Just namespace -> + ( ( namespace, keyValue.key ), keyValue.value ) |> Just + + Nothing -> + Nothing + + _ -> + Nothing + ) + |> Dict.fromList + } + , subs = subs } - |> Web.DomElement + |> Element -{-| Create a DOM element with a given tag, [`Modifier`](#Modifier)s and sub-[node](Web#DomNode)s. +{-| Create a DOM element with a given tag, [`Modifier`](#Modifier)s and sub-[node](Web-Dom#Node)s. For example to get `

flying

` Web.Dom.element "p" @@ -225,12 +267,12 @@ For example to get `

flying

` To create SVG elements, use [`Web.Svg.element`](Web-Svg#element) -} -element : String -> List (Modifier future) -> List (DomNode future) -> DomNode future +element : String -> List (Modifier future) -> List (Node future) -> Node future element tag modifiers subs = elementWithMaybeNamespace Nothing tag modifiers subs -{-| Create a DOM element with a given namespace, tag, [`Modifier`](#Modifier)s and sub-[node](Web#DomNode)s. +{-| Create a DOM element with a given namespace, tag, [`Modifier`](#Modifier)s and sub-[node](Web-Dom#Node)s. For example, [`Web.Svg`](Web-Svg) defines its elements using element : String -> List (Modifier future) -> List (DomNode future) -> DomNode future @@ -238,7 +280,7 @@ For example, [`Web.Svg`](Web-Svg) defines its elements using Web.Dom.elementNamespaced "http://www.w3.org/2000/svg" tag modifiers subs -} -elementNamespaced : String -> String -> List (Modifier future) -> List (DomNode future) -> DomNode future +elementNamespaced : String -> String -> List (Modifier future) -> List (Node future) -> Node future elementNamespaced namespace tag modifiers subs = elementWithMaybeNamespace (namespace |> Just) tag modifiers subs @@ -329,7 +371,7 @@ style key value = { key = key, value = value } |> Style |> Rope.singleton -{-| Listen for a specific DOM event on the [`DomElement`](Web#DomElement). +{-| Listen for a specific DOM event on the [`Web.Dom.Element`](Web-Dom#Element). Use [`modifierFutureMap`](#modifierFutureMap) to wire this to a specific event. If you want to override the browser's default behavior for that event, @@ -446,3 +488,19 @@ scrollToShow : -> Modifier future_ scrollToShow preferredAlignment = ScrollToShow preferredAlignment |> Rope.singleton + + +{-| Plain text or an [`Element`](#Element). Create using [`text`](#text) and [`element`](#element) +-} +type Node future + = Text String + | Element (Element future) + + +{-| A tagged DOM node that can itself contain child [node](#Node)s +-} +type alias Element future = + RecordWithoutConstructorFunction + { header : Web.DomElementHeader future + , subs : List (Node future) + } diff --git a/src/Web/Svg.elm b/src/Web/Svg.elm index df5df7b..d9e3d59 100644 --- a/src/Web/Svg.elm +++ b/src/Web/Svg.elm @@ -1,6 +1,6 @@ module Web.Svg exposing (element) -{-| Helpers for svg [DOM node types](Web#DomNode) +{-| Helpers for svg [DOM nodes](Web-Dom#Node) @docs element @@ -8,13 +8,12 @@ for text, attributes etc use the helpers in [`Web.Dom`](Web-Dom) -} -import Web exposing (DomNode) -import Web.Dom exposing (Modifier) +import Web.Dom -{-| Create an SVG element [`DomNode`](Web#DomNode). +{-| Create an SVG element [`Web.Dom.Node`](Web-Dom#Node). with a given tag, [`Modifier`](Web-Dom#Modifier)s and sub-nodes. -} -element : String -> List (Modifier future) -> List (DomNode future) -> DomNode future +element : String -> List (Web.Dom.Modifier future) -> List (Web.Dom.Node future) -> Web.Dom.Node future element tag modifiers subs = Web.Dom.elementNamespaced "http://www.w3.org/2000/svg" tag modifiers subs