All Downloads are FREE. Search and download functionalities are using the official Maven repository.

elmish.tree.TreeView.kt Maven / Gradle / Ivy

/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package elmish.tree

import data.mapAt
import elmish.View
import elmish.attributes
import elmish.div
import elmish.empty
import elmish.li
import elmish.ul


object TreeView {

    data class Model(
        val tree: Tree
    )

    sealed class Intent {
        data class Toggle(val focus: Tree.Focus) : Intent()
    }

    fun  view(model: Model): View> =
        viewTree>(model.tree.focus()) { focus ->
            div(
                attributes {
                    onClick { Intent.Toggle(focus) }
                },
                focus.tree.label.toString()
            )
        }

    fun  step(intent: Intent, model: Model): Model = when (intent) {
        is Intent.Toggle -> model.copy(
            tree = intent.focus.update {
                copy(state = state.toggle())
            }
        )
    }
}


/**
 * A persistent tree view model.
 */
data class Tree(
    val label: T,
    val children: List> = emptyList(),
    val state: ViewState = ViewState.Collapsed
) {

    enum class ViewState {

        Collapsed,
        Expanded;

        fun toggle(): ViewState = when (this) {
            Collapsed -> Expanded
            Expanded -> Collapsed
        }
    }

    /**
     * Creates an updatable reference to this tree.
     */
    fun focus(): Focus = Focus.Original(
        this
    )

    fun isNotEmpty(): Boolean =
        children.isNotEmpty()

    /**
     * Propagates changes to a particular node in the tree all the way
     * up to the root (the [focused][focus] tree).
     */
    sealed class Focus {

        abstract val tree: Tree

        abstract fun update(f: Tree.() -> Tree): Tree

        val children
            get() = tree.children.indices.asSequence().map(::child)

        fun child(index: Int): Focus = Child(
            this,
            index,
            tree.children[index]
        )

        data class Original(
            override val tree: Tree
        ) : Focus() {

            override fun update(f: Tree.() -> Tree): Tree = f(tree)
        }

        data class Child(
            val parent: Focus,
            val index: Int,
            override val tree: Tree
        ) : Focus() {

            override fun update(f: Tree.() -> Tree): Tree = parent.update {
                copy(children = children.mapAt(index, f))
            }
        }
    }
}


fun  viewTree(
    focus: Tree.Focus,
    viewLabel: (Tree.Focus) -> View
): View = ul(
    viewSubTree(focus, viewLabel)
)


fun  viewSubTree(
    focus: Tree.Focus,
    viewLabel: (Tree.Focus) -> View
): View = focus.tree.run {
    li(
        viewLabel(focus),
        children.takeIf { state == Tree.ViewState.Expanded && it.isNotEmpty() }?.run {
            viewExpanded(focus, viewLabel)
        } ?: empty
    )
}


fun  viewExpanded(focus: Tree.Focus, viewLabel: Tree.Focus.() -> View): View =
    ul(viewChildrenOf(focus, viewLabel))


fun  viewChildrenOf(
    focus: Tree.Focus,
    viewLabel: (Tree.Focus) -> View
): List> = viewSubTrees(focus.children, viewLabel)


fun  viewSubTrees(
    subTrees: Sequence>,
    viewLabel: (Tree.Focus) -> View
): List> = subTrees
    .map { subTree -> viewSubTree(subTree, viewLabel) }
    .toList()