From e88db9c75c9465f8daae30432e6b3da2dfc09244 Mon Sep 17 00:00:00 2001 From: Lars Westermann Date: Sat, 18 May 2019 18:53:46 +0200 Subject: [PATCH] Add direct edit for work groups and rooms --- .../de/kif/frontend/views/TableLayout.kt | 21 +-- .../kif/frontend/views/table/RoomTableLine.kt | 77 ++++++++++ .../de/kif/frontend/views/table/TableLine.kt | 79 +++++++++++ .../views/table/WorkGroupTableLine.kt | 133 ++++++++++++++++++ .../kwebview/components/TextView.kt | 27 +++- src/jsMain/resources/style/style.scss | 38 +++++ .../kotlin/de/kif/backend/route/Room.kt | 42 ++++-- .../kotlin/de/kif/backend/route/Track.kt | 31 ++-- .../kotlin/de/kif/backend/route/User.kt | 37 +++-- .../kotlin/de/kif/backend/route/WorkGroup.kt | 51 +++++-- .../de/kif/backend/view/TableTemplate.kt | 5 +- 11 files changed, 468 insertions(+), 73 deletions(-) create mode 100644 src/jsMain/kotlin/de/kif/frontend/views/table/RoomTableLine.kt create mode 100644 src/jsMain/kotlin/de/kif/frontend/views/table/TableLine.kt create mode 100644 src/jsMain/kotlin/de/kif/frontend/views/table/WorkGroupTableLine.kt diff --git a/src/jsMain/kotlin/de/kif/frontend/views/TableLayout.kt b/src/jsMain/kotlin/de/kif/frontend/views/TableLayout.kt index e56eb14..9904e6d 100644 --- a/src/jsMain/kotlin/de/kif/frontend/views/TableLayout.kt +++ b/src/jsMain/kotlin/de/kif/frontend/views/TableLayout.kt @@ -1,8 +1,9 @@ package de.kif.frontend.views -import de.kif.common.Search -import de.kif.common.SearchElement import de.kif.frontend.iterator +import de.kif.frontend.views.table.RoomTableLine +import de.kif.frontend.views.table.TableLine +import de.kif.frontend.views.table.WorkGroupTableLine import de.westermann.kwebview.components.InputView import org.w3c.dom.HTMLFormElement import org.w3c.dom.HTMLInputElement @@ -17,16 +18,20 @@ fun initTableLayout() { val table = document.getElementsByClassName("table-layout-table")[0] as HTMLTableElement val list = table.getElementsByTagName("tr").iterator().asSequence().filter { - it.dataset.get("search") != null - }.associateWith { - SearchElement.parse(it.dataset.get("search")!!) - } + it.dataset["search"] != null + }.map { + when (it.dataset["edit"]) { + "workgroup" -> WorkGroupTableLine(it) + "room" -> RoomTableLine(it) + else -> TableLine(it) + } + }.toList() val input = form.getElementsByTagName("input")[0] as HTMLInputElement val search = InputView.wrap(input) search.valueProperty.onChange { - for ((row, s) in list) { - row.style.display = if (Search.match(search.value, s)) "table-row" else "none" + for (row in list) { + row.search(search.value) } } } diff --git a/src/jsMain/kotlin/de/kif/frontend/views/table/RoomTableLine.kt b/src/jsMain/kotlin/de/kif/frontend/views/table/RoomTableLine.kt new file mode 100644 index 0000000..f42af51 --- /dev/null +++ b/src/jsMain/kotlin/de/kif/frontend/views/table/RoomTableLine.kt @@ -0,0 +1,77 @@ +package de.kif.frontend.views.table + +import de.kif.common.SearchElement +import de.kif.frontend.iterator +import de.kif.frontend.launch +import de.kif.frontend.repository.RepositoryDelegate +import de.kif.frontend.repository.RoomRepository +import de.westermann.kwebview.components.TextView +import org.w3c.dom.HTMLElement +import org.w3c.dom.HTMLSpanElement +import org.w3c.dom.get + +class RoomTableLine(view: HTMLElement) : TableLine(view) { + + var lineId = dataset["id"]?.toLongOrNull() ?: -1 + + private val room = + RepositoryDelegate(RoomRepository, lineId) + + private val spanRoomName: TextView + private val spanRoomPlaces: TextView + private val spanRoomProjector: TextView + + override var searchElement: SearchElement = super.searchElement + + init { + val spans = view.getElementsByTagName("span").iterator().asSequence().toList() + + spanRoomName = + TextView.wrap(spans.first { it.dataset["editType"] == "room-name" } as HTMLSpanElement) + spanRoomPlaces = + TextView.wrap(spans.first { it.dataset["editType"] == "room-places" } as HTMLSpanElement) + spanRoomProjector = + TextView.wrap(spans.first { it.dataset["editType"] == "room-projector" } as HTMLSpanElement) + + setupEditable(spanRoomName) { + launch { + val wg = room.get() + if (wg.name != it) { + RoomRepository.update(wg.copy(name = it)) + } + } + } + + setupEditable(spanRoomPlaces, "\\d+".toRegex()) { + val number = it.toIntOrNull() ?: return@setupEditable + launch { + val wg = room.get() + if (wg.places != number) { + RoomRepository.update(wg.copy(places = number)) + } + } + } + + setupBoolean(spanRoomProjector) { + launch { + val wg = room.get() + RoomRepository.update(wg.copy(projector = !wg.projector)) + } + } + + + RoomRepository.onUpdate { + if (it != lineId) return@onUpdate + + launch { + val wg = RoomRepository.get(it) ?: return@launch + room.set(wg) + searchElement = wg.createSearch() + + spanRoomName.text = wg.name + spanRoomPlaces.text = wg.places.toString() + spanRoomProjector.text = wg.projector.toString() + } + } + } +} \ No newline at end of file diff --git a/src/jsMain/kotlin/de/kif/frontend/views/table/TableLine.kt b/src/jsMain/kotlin/de/kif/frontend/views/table/TableLine.kt new file mode 100644 index 0000000..ecd55d2 --- /dev/null +++ b/src/jsMain/kotlin/de/kif/frontend/views/table/TableLine.kt @@ -0,0 +1,79 @@ +package de.kif.frontend.views.table + +import de.kif.common.Search +import de.kif.common.SearchElement +import de.westermann.kwebview.View +import de.westermann.kwebview.components.ListView +import de.westermann.kwebview.components.TextView +import de.westermann.kwebview.components.textView +import org.w3c.dom.HTMLElement + +open class TableLine(line: HTMLElement) : View(line) { + + open val searchElement: SearchElement = SearchElement.parse(dataset["search"]!!) + + fun search(value: String) { + style.display = if (Search.match(value, searchElement)) "table-row" else "none" + } + + protected fun setupEditable(view: TextView, regex: Regex = ".*".toRegex(), onSave: (String) -> Unit) { + view.contentEditable = true + + view.onKeyDown { + if (it.keyCode == 13) { + it.preventDefault() + + view.blur() + return@onKeyDown + } + } + + view.onKeyUp { + view.classList["error"] = !regex.matches(view.text) + } + + view.onBlur { + view.classList["error"] = !regex.matches(view.text) + if (!view.classList["error"]) { + onSave(view.text) + } + } + } + + protected fun setupBoolean(view: TextView, onSave: () -> Unit) { + view.classList += "no-select" + view.tabIndex = 0 + view.onDblClick { + onSave() + } + view.onKeyDown { + if (it.keyCode != 32) return@onKeyDown + onSave() + } + } + + protected fun setupList(view: TextView, list: List, transform: (T) -> String, onSave: (T?) -> Unit) { + view.classList += "no-select" + view.tabIndex = 0 + + val listView = ListView() + listView.classList += "table-select-box" + + for (elem in list) { + val text = if (elem == null) "" else transform(elem) + listView.textView(text) { + onClick { + onSave(elem) + view.blur() + } + } + } + + view.onFocus { + view.html.appendChild(listView.html) + } + view.onBlur { + listView.html.remove() + } + } +} diff --git a/src/jsMain/kotlin/de/kif/frontend/views/table/WorkGroupTableLine.kt b/src/jsMain/kotlin/de/kif/frontend/views/table/WorkGroupTableLine.kt new file mode 100644 index 0000000..33e8573 --- /dev/null +++ b/src/jsMain/kotlin/de/kif/frontend/views/table/WorkGroupTableLine.kt @@ -0,0 +1,133 @@ +package de.kif.frontend.views.table + +import de.kif.common.SearchElement +import de.kif.common.model.Language +import de.kif.common.model.Track +import de.kif.frontend.iterator +import de.kif.frontend.launch +import de.kif.frontend.repository.RepositoryDelegate +import de.kif.frontend.repository.TrackRepository +import de.kif.frontend.repository.WorkGroupRepository +import de.westermann.kwebview.components.TextView +import org.w3c.dom.HTMLElement +import org.w3c.dom.HTMLSpanElement +import org.w3c.dom.get + +class WorkGroupTableLine(view: HTMLElement) : TableLine(view) { + + var lineId = dataset["id"]?.toLongOrNull() ?: -1 + + private val workGroup = + RepositoryDelegate(WorkGroupRepository, lineId) + + private val spanWorkGroupName: TextView + private val spanWorkGroupLength: TextView + private val spanWorkGroupInterested: TextView + private val spanWorkGroupTrack: TextView + private val spanWorkGroupProjector: TextView + private val spanWorkGroupResolution: TextView + private val spanWorkGroupLanguage: TextView + + override var searchElement: SearchElement = super.searchElement + + init { + val spans = view.getElementsByTagName("span").iterator().asSequence().toList() + + spanWorkGroupName = + TextView.wrap(spans.first { it.dataset["editType"] == "workgroup-name" } as HTMLSpanElement) + spanWorkGroupLength = + TextView.wrap(spans.first { it.dataset["editType"] == "workgroup-length" } as HTMLSpanElement) + spanWorkGroupInterested = + TextView.wrap(spans.first { it.dataset["editType"] == "workgroup-interested" } as HTMLSpanElement) + spanWorkGroupTrack = + TextView.wrap(spans.first { it.dataset["editType"] == "workgroup-track" } as HTMLSpanElement) + spanWorkGroupProjector = + TextView.wrap(spans.first { it.dataset["editType"] == "workgroup-projector" } as HTMLSpanElement) + spanWorkGroupResolution = + TextView.wrap(spans.first { it.dataset["editType"] == "workgroup-resolution" } as HTMLSpanElement) + spanWorkGroupLanguage = + TextView.wrap(spans.first { it.dataset["editType"] == "workgroup-language" } as HTMLSpanElement) + + setupEditable(spanWorkGroupName) { + launch { + val wg = workGroup.get() + if (wg.name != it) { + WorkGroupRepository.update(wg.copy(name = it)) + } + } + } + + setupEditable(spanWorkGroupLength, "\\d+".toRegex()) { + val number = it.toIntOrNull() ?: return@setupEditable + launch { + val wg = workGroup.get() + if (wg.length != number) { + WorkGroupRepository.update(wg.copy(length = number)) + } + } + } + + setupEditable(spanWorkGroupInterested, "\\d+".toRegex()) { + val number = it.toIntOrNull() ?: return@setupEditable + launch { + val wg = workGroup.get() + if (wg.interested != number) { + WorkGroupRepository.update(wg.copy(interested = number)) + } + } + } + + setupBoolean(spanWorkGroupProjector) { + launch { + val wg = workGroup.get() + WorkGroupRepository.update(wg.copy(projector = !wg.projector)) + } + } + + setupBoolean(spanWorkGroupResolution) { + launch { + val wg = workGroup.get() + WorkGroupRepository.update(wg.copy(resolution = !wg.resolution)) + } + } + + setupList(spanWorkGroupLanguage, Language.values().sortedBy { it.localeName }, { it.localeName }) { + if (it == null) return@setupList + launch { + val wg = workGroup.get() + if (wg.language == it) return@launch + WorkGroupRepository.update(wg.copy(language = it)) + } + } + + launch { + val tracks = listOf(null) + TrackRepository.all() + + setupList(spanWorkGroupTrack, tracks, { it.name }) { + launch x@{ + val wg = workGroup.get() + if (wg.track == it) return@x + WorkGroupRepository.update(wg.copy(track = it)) + } + } + } + + WorkGroupRepository.onUpdate { + if (it != lineId) return@onUpdate + + launch { + val wg = WorkGroupRepository.get(it) ?: return@launch + workGroup.set(wg) + searchElement = wg.createSearch() + + spanWorkGroupName.text = wg.name + spanWorkGroupLength.text = wg.length.toString() + spanWorkGroupInterested.text = wg.interested.toString() + spanWorkGroupTrack.text = wg.track?.name ?: "" + spanWorkGroupProjector.text = wg.projector.toString() + spanWorkGroupResolution.text = wg.resolution.toString() + spanWorkGroupLanguage.text = wg.language.localeName + } + } + } +} \ No newline at end of file diff --git a/src/jsMain/kotlin/de/westermann/kwebview/components/TextView.kt b/src/jsMain/kotlin/de/westermann/kwebview/components/TextView.kt index 0472cb9..57a507f 100644 --- a/src/jsMain/kotlin/de/westermann/kwebview/components/TextView.kt +++ b/src/jsMain/kotlin/de/westermann/kwebview/components/TextView.kt @@ -3,10 +3,7 @@ package de.westermann.kwebview.components import de.westermann.kobserve.Property import de.westermann.kobserve.ReadOnlyProperty import de.westermann.kobserve.property.property -import de.westermann.kwebview.KWebViewDsl -import de.westermann.kwebview.View -import de.westermann.kwebview.ViewCollection -import de.westermann.kwebview.createHtmlView +import de.westermann.kwebview.* import org.w3c.dom.HTMLSpanElement /** @@ -15,8 +12,9 @@ import org.w3c.dom.HTMLSpanElement * @author lars */ class TextView( - value: String = "" -) : View(createHtmlView()) { + value: String = "", + view: HTMLSpanElement = createHtmlView() +) : View(view) { override val html = super.html as HTMLSpanElement @@ -37,9 +35,26 @@ class TextView( val textProperty: Property = property(this::text) + var contentEditable: Boolean + get() = html.isContentEditable + set(value) { + html.contentEditable = value.toString() + } + + private var internalTabIndex by AttributeDelegate("tabIndex") + var tabIndex: Int? + get() = internalTabIndex?.toIntOrNull() + set(value) { + internalTabIndex = value?.toString() + } + init { text = value } + + companion object { + fun wrap(view: HTMLSpanElement) = TextView(view.textContent ?: "", view) + } } @KWebViewDsl diff --git a/src/jsMain/resources/style/style.scss b/src/jsMain/resources/style/style.scss index c2c3166..4eedb78 100644 --- a/src/jsMain/resources/style/style.scss +++ b/src/jsMain/resources/style/style.scss @@ -27,6 +27,8 @@ $bg-enabled-color: rgba($primary-color, .5); $lever-disabled-color: $background-primary-color; $lever-enabled-color: $primary-color; +$error-background-color: #FFCDD2; + body, html { color: $text-primary-color; background: $background-secondary-color; @@ -40,6 +42,10 @@ body, html { padding: 0; } +.no-select { + @include no-select() +} + a { text-decoration: none; outline: none; @@ -248,6 +254,10 @@ a { tr { border-top: solid 1px rgba($text-primary-color, 0.1); + &:nth-child(odd) { + //background-color: rgba($text-primary-color, 0.01); + } + &:first-child { background-color: rgba($text-primary-color, 0.06); height: 2.5rem; @@ -263,6 +273,34 @@ a { } } } + + span { + display: block; + position: relative; + + &:empty:before { + content: "\200b"; + } + + &.error { + background-color: $error-background-color; + } + } + + .table-select-box { + position: absolute; + z-index: 1; + background: $background-primary-color; + width: 100%; + border: solid 1px rgba($text-primary-color, 0.1); + + span { + padding: 0 0.5rem; + &:hover { + background-color: rgba($text-primary-color, 0.06); + } + } + } } .form-control { diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Room.kt b/src/jvmMain/kotlin/de/kif/backend/route/Room.kt index 2a45e43..4676784 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Room.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Room.kt @@ -2,10 +2,10 @@ package de.kif.backend.route import de.kif.backend.authenticateOrRedirect import de.kif.backend.repository.RoomRepository -import de.kif.common.Search import de.kif.backend.view.MainTemplate import de.kif.backend.view.MenuTemplate import de.kif.backend.view.TableTemplate +import de.kif.common.Search import de.kif.common.model.Permission import de.kif.common.model.Room import io.ktor.application.call @@ -17,11 +17,11 @@ import io.ktor.routing.Route import io.ktor.routing.get import io.ktor.routing.post import io.ktor.util.toMap +import kotlinx.css.CSSBuilder +import kotlinx.css.Display import kotlinx.html.* import kotlin.collections.component1 import kotlin.collections.component2 -import kotlin.collections.firstOrNull -import kotlin.collections.mapValues import kotlin.collections.set fun Route.room() { @@ -65,22 +65,38 @@ fun Route.room() { for (u in list) { val s = u.createSearch() - if (Search.match(search, s)) { - entry { - attributes["data-search"] = s.stringify() - td { + entry { + attributes["style"] = CSSBuilder().apply { + display = if (Search.match(search, s)) Display.tableRow else Display.none + }.toString() + attributes["data-search"] = s.stringify() + attributes["data-edit"] = "room" + attributes["data-id"] = u.id.toString() + + td { + span { + attributes["data-edit-type"] = "room-name" + +u.name } - td { + } + td { + span { + attributes["data-edit-type"] = "room-places" + +u.places.toString() } - td { + } + td { + span { + attributes["data-edit-type"] = "room-projector" + +u.projector.toString() } - td(classes = "action") { - a("/room/${u.id}") { - i("material-icons") { +"edit" } - } + } + td(classes = "action") { + a("/room/${u.id}") { + i("material-icons") { +"edit" } } } } diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Track.kt b/src/jvmMain/kotlin/de/kif/backend/route/Track.kt index fc83748..644e22c 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Track.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Track.kt @@ -3,10 +3,10 @@ package de.kif.backend.route import de.kif.backend.authenticateOrRedirect import de.kif.backend.repository.TrackRepository -import de.kif.common.Search import de.kif.backend.view.MainTemplate import de.kif.backend.view.MenuTemplate import de.kif.backend.view.TableTemplate +import de.kif.common.Search import de.kif.common.model.Color import de.kif.common.model.Permission import de.kif.common.model.Track @@ -20,6 +20,7 @@ import io.ktor.routing.get import io.ktor.routing.post import io.ktor.util.toMap import kotlinx.css.CSSBuilder +import kotlinx.css.Display import kotlinx.html.* import kotlin.collections.set import kotlin.random.Random @@ -119,22 +120,24 @@ fun Route.track() { for (u in list) { val s = u.createSearch() - if (Search.match(search, s)) { - entry { - attributes["data-search"] = s.stringify() - td { - +u.name - } - td { - +u.color.toString() - } - td(classes = "action") { - a("/track/${u.id}") { - i("material-icons") { +"edit" } - } + entry { + attributes["style"] = CSSBuilder().apply { + display = if (Search.match(search, s)) Display.tableRow else Display.none + }.toString() + attributes["data-search"] = s.stringify() + td { + +u.name + } + td { + +u.color.toString() + } + td(classes = "action") { + a("/track/${u.id}") { + i("material-icons") { +"edit" } } } } + } } } diff --git a/src/jvmMain/kotlin/de/kif/backend/route/User.kt b/src/jvmMain/kotlin/de/kif/backend/route/User.kt index d53c77c..ef07acb 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/User.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/User.kt @@ -4,10 +4,10 @@ package de.kif.backend.route import de.kif.backend.authenticateOrRedirect import de.kif.backend.hashPassword import de.kif.backend.repository.UserRepository -import de.kif.common.Search import de.kif.backend.view.MainTemplate import de.kif.backend.view.MenuTemplate import de.kif.backend.view.TableTemplate +import de.kif.common.Search import de.kif.common.model.Permission import de.kif.common.model.User import io.ktor.application.call @@ -19,16 +19,12 @@ import io.ktor.routing.Route import io.ktor.routing.get import io.ktor.routing.post import io.ktor.util.toMap +import kotlinx.css.CSSBuilder +import kotlinx.css.Display import kotlinx.html.* import kotlin.collections.component1 import kotlin.collections.component2 -import kotlin.collections.filter -import kotlin.collections.firstOrNull -import kotlin.collections.joinToString -import kotlin.collections.mapNotNull -import kotlin.collections.mapValues import kotlin.collections.set -import kotlin.collections.toSet fun Route.user() { get("/users") { param -> @@ -67,19 +63,20 @@ fun Route.user() { for (u in list) { val s = u.createSearch() - if (Search.match(search, s)) { - entry { - attributes["data-search"] = s.stringify() - td { - +u.username - } - td { - +u.permissions.joinToString(", ") { it.toString().toLowerCase() } - } - td(classes = "action") { - a("/user/${u.id}") { - i("material-icons") { +"edit" } - } + entry { + attributes["style"] = CSSBuilder().apply { + display = if (Search.match(search, s)) Display.tableRow else Display.none + }.toString() + attributes["data-search"] = s.stringify() + td { + +u.username + } + td { + +u.permissions.joinToString(", ") { it.toString().toLowerCase() } + } + td(classes = "action") { + a("/user/${u.id}") { + i("material-icons") { +"edit" } } } } diff --git a/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt b/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt index af753a4..5e1c53b 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt @@ -57,6 +57,9 @@ fun Route.workGroup() { th { +"Name" } + th { + +"Length" + } th { +"Interested" } @@ -69,9 +72,6 @@ fun Route.workGroup() { th { +"Resolution" } - th { - +"Length" - } th { +"Language" } @@ -87,26 +87,57 @@ fun Route.workGroup() { display = if (Search.match(search, s)) Display.tableRow else Display.none }.toString() attributes["data-search"] = s.stringify() + attributes["data-edit"] = "workgroup" + attributes["data-id"] = u.id.toString() + td { - +u.name + span { + attributes["data-edit-type"] = "workgroup-name" + + +u.name + } } td { - +u.interested.toString() + span { + attributes["data-edit-type"] = "workgroup-length" + + +u.length.toString() + } } td { - +(u.track?.name ?: "") + span { + attributes["data-edit-type"] = "workgroup-interested" + + +u.interested.toString() + } } td { - +u.projector.toString() + span { + attributes["data-edit-type"] = "workgroup-track" + + +(u.track?.name ?: "") + } } td { - +u.resolution.toString() + span { + attributes["data-edit-type"] = "workgroup-projector" + + +u.projector.toString() + } } td { - +u.length.toString() + span { + attributes["data-edit-type"] = "workgroup-resolution" + + +u.resolution.toString() + } } td { - +u.language.localeName + span { + attributes["data-edit-type"] = "workgroup-language" + + +u.language.localeName + } } td(classes = "action") { a("/workgroup/${u.id}") { diff --git a/src/jvmMain/kotlin/de/kif/backend/view/TableTemplate.kt b/src/jvmMain/kotlin/de/kif/backend/view/TableTemplate.kt index 0474106..61f1b59 100644 --- a/src/jvmMain/kotlin/de/kif/backend/view/TableTemplate.kt +++ b/src/jvmMain/kotlin/de/kif/backend/view/TableTemplate.kt @@ -4,7 +4,7 @@ import io.ktor.html.* import kotlinx.html.* -class TableTemplate() : Template { +class TableTemplate(private val classes: String = "") : Template { var searchValue = "" @@ -13,7 +13,8 @@ class TableTemplate() : Template { val entry = PlaceholderList() override fun FlowContent.apply() { - div("table-layout") { + val c = "table-layout" + if (classes.isEmpty()) "" else " $classes" + div(c) { form(classes = "form-group table-layout-search") { div("input-group") { input(InputType.search, name = "search", classes = "form-control") {