diff --git a/build.gradle b/build.gradle index 58d694d..e2f226c 100644 --- a/build.gradle +++ b/build.gradle @@ -29,6 +29,7 @@ repositories { } def ktor_version = '1.1.5' def serialization_version = '0.11.0' +def observable_version = '0.9.3' kotlin { jvm() { @@ -53,7 +54,7 @@ kotlin { commonMain { dependencies { implementation kotlin('stdlib-common') - implementation "de.westermann:KObserve-metadata:0.9.1" + implementation "de.westermann:KObserve-metadata:$observable_version" implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:$serialization_version" } @@ -84,7 +85,7 @@ kotlin { implementation 'org.mindrot:jbcrypt:0.4' - implementation "de.westermann:KObserve-jvm:0.9.1" + implementation "de.westermann:KObserve-jvm:$observable_version" api 'io.github.microutils:kotlin-logging:1.6.23' api 'ch.qos.logback:logback-classic:1.2.3' @@ -103,7 +104,7 @@ kotlin { implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:$serialization_version" - implementation "de.westermann:KObserve-js:0.9.1" + implementation "de.westermann:KObserve-js:$observable_version" } } jsTest { diff --git a/src/commonMain/kotlin/de/kif/common/Search.kt b/src/commonMain/kotlin/de/kif/common/Search.kt new file mode 100644 index 0000000..fadef97 --- /dev/null +++ b/src/commonMain/kotlin/de/kif/common/Search.kt @@ -0,0 +1,120 @@ +package de.kif.common + +import kotlinx.serialization.Serializable + +@Serializable +data class SearchElement( + val fields: Map = emptyMap(), + val flags: Map = emptyMap(), + val numbers: Map = emptyMap() +) { + + fun stringify(): String { + return Message.json.stringify(serializer(), this) + } + + companion object { + fun parse(data: String): SearchElement { + return Message.json.parse(serializer(), data) + } + } +} + +object Search { + + private val regex = """ + ((\w+)\s?[:=]\s?)? + ((\[.*]|\+\w+|-\w+|!\w+)| + ((\w+)\s?([<=>]+)\s?(\d+))| + (\w+|"(.*)")) + """.trimIndent().replace("\n", "").toRegex() + + fun match(search: String, element: SearchElement): Boolean { + val matches = regex.findAll(search) + + val fields = mutableMapOf() + val flags = mutableMapOf() + val numbers = mutableMapOf>() + + for (match in matches) { + val name = match.groups[2]?.value ?: "" + val field = match.groups[10]?.value ?: match.groups[9]?.value + val flag = match.groups[4]?.value + val numberName = match.groups[6]?.value + val numberRelation = match.groups[7]?.value + val numberDigits = match.groups[8]?.value?.toDoubleOrNull() + + if (flag != null) { + val h = flag.replace("[\\[\\]+!\\-]".toRegex(), "") + val b = ("-" !in flag && "!" !in flag) + flags[h] = b + } else if (numberName != null && numberRelation != null && numberDigits != null) { + when (numberRelation) { + "<" -> numbers[numberName] = Double.NEGATIVE_INFINITY..(numberDigits - Double.MIN_VALUE) + "<=" -> numbers[numberName] = Double.NEGATIVE_INFINITY..numberDigits + "==" -> numbers[numberName] = numberDigits..numberDigits + ">" -> numbers[numberName] = (numberDigits + Double.MIN_VALUE)..Double.POSITIVE_INFINITY + ">=" -> numbers[numberName] = numberDigits..Double.POSITIVE_INFINITY + } + } else if (field != null) { + val old = fields[name] + fields[name] = if (old == null) field else "$old $field" + } + } + + val fieldRadio = if (fields.isEmpty()) 1.0 else fields.count { (searchKey, searchValue) -> + for ((elementKey, elementValue) in element.fields) { + if (elementKey.contains(searchKey, true) && elementValue.contains(searchValue, true)) { + return@count true + } + } + for ((elementKey, elementValue) in element.flags) { + if (elementValue && elementKey.contains(searchValue, true)) { + return@count true + } + } + for ((elementKey, _) in element.numbers) { + if (elementKey.contains(searchValue, true)) { + return@count true + } + } + false + } / fields.size.toDouble() + + for ((searchKey, searchValue) in flags) { + for ((elementKey, elementValue) in element.flags) { + if (elementKey.contains(searchKey, true) && searchValue != elementValue) + return false + } + } + + for ((searchKey, searchValue) in numbers) { + for ((elementKey, elementValue) in element.numbers) { + if (elementKey.contains(searchKey, true) && elementValue !in searchValue) + return false + } + } + + //println("$fieldRadio (${fieldRadio >= 0.5}) for $element") + return fieldRadio >= 0.5 + } +} + +/* +fun main() { + val element = SearchElement( + mapOf( + "name" to "lorem" + ), mapOf( + "beamer" to true, + "room" to true, + "day" to false + ), mapOf( + "places" to 500.0 + ) + ) + println(Search.match("""search text data:"text to search" name = hans""", element)) + + println(Search.match("""lorem [beamer] places >= 100 +room -day""", element)) +} +*/ diff --git a/src/commonMain/kotlin/de/kif/common/model/Model.kt b/src/commonMain/kotlin/de/kif/common/model/Model.kt index 779afd2..5a7d2e1 100644 --- a/src/commonMain/kotlin/de/kif/common/model/Model.kt +++ b/src/commonMain/kotlin/de/kif/common/model/Model.kt @@ -1,3 +1,7 @@ package de.kif.common.model -interface Model \ No newline at end of file +import de.kif.common.SearchElement + +interface Model { + fun createSearch(): SearchElement +} \ No newline at end of file diff --git a/src/commonMain/kotlin/de/kif/common/model/Room.kt b/src/commonMain/kotlin/de/kif/common/model/Room.kt index dda4e61..47e6eee 100644 --- a/src/commonMain/kotlin/de/kif/common/model/Room.kt +++ b/src/commonMain/kotlin/de/kif/common/model/Room.kt @@ -1,5 +1,6 @@ package de.kif.common.model +import de.kif.common.SearchElement import kotlinx.serialization.Serializable @Serializable @@ -8,4 +9,15 @@ data class Room( val name: String, val places: Int, val projector: Boolean -) : Model +) : Model { + + override fun createSearch() = SearchElement( + mapOf( + "name" to name + ), mapOf( + "projector" to projector + ), mapOf( + "places" to places.toDouble() + ) + ) +} diff --git a/src/commonMain/kotlin/de/kif/common/model/Schedule.kt b/src/commonMain/kotlin/de/kif/common/model/Schedule.kt index ec568cf..dfa7b25 100644 --- a/src/commonMain/kotlin/de/kif/common/model/Schedule.kt +++ b/src/commonMain/kotlin/de/kif/common/model/Schedule.kt @@ -1,5 +1,6 @@ package de.kif.common.model +import de.kif.common.SearchElement import kotlinx.serialization.Serializable @Serializable @@ -9,4 +10,16 @@ data class Schedule( val room: Room, val day: Int, val time: Int -) : Model +) : Model { + + override fun createSearch() = SearchElement( + mapOf( + "workgroup" to workGroup.name, + "room" to room.name, + "track" to (workGroup.track?.name ?: ""), + "language" to workGroup.language.localeName + ), mapOf(), mapOf( + "day" to day.toDouble() + ) + ) +} diff --git a/src/commonMain/kotlin/de/kif/common/model/Track.kt b/src/commonMain/kotlin/de/kif/common/model/Track.kt index a671ef6..7fbc33d 100644 --- a/src/commonMain/kotlin/de/kif/common/model/Track.kt +++ b/src/commonMain/kotlin/de/kif/common/model/Track.kt @@ -1,10 +1,18 @@ package de.kif.common.model +import de.kif.common.SearchElement import kotlinx.serialization.Serializable @Serializable data class Track( val id: Long?, - var name: String, - var color: Color -) : Model + val name: String, + val color: Color +) : Model { + + override fun createSearch() = SearchElement( + mapOf( + "name" to name + ) + ) +} diff --git a/src/commonMain/kotlin/de/kif/common/model/User.kt b/src/commonMain/kotlin/de/kif/common/model/User.kt index 5d86bd8..2ee0ec5 100644 --- a/src/commonMain/kotlin/de/kif/common/model/User.kt +++ b/src/commonMain/kotlin/de/kif/common/model/User.kt @@ -1,5 +1,6 @@ package de.kif.common.model +import de.kif.common.SearchElement import kotlinx.serialization.Serializable @Serializable @@ -13,4 +14,10 @@ data class User( fun checkPermission(permission: Permission): Boolean { return permission in permissions || Permission.ADMIN in permissions } + + override fun createSearch() = SearchElement( + mapOf( + "username" to username + ) + ) } diff --git a/src/commonMain/kotlin/de/kif/common/model/WorkGroup.kt b/src/commonMain/kotlin/de/kif/common/model/WorkGroup.kt index c4767fa..224c352 100644 --- a/src/commonMain/kotlin/de/kif/common/model/WorkGroup.kt +++ b/src/commonMain/kotlin/de/kif/common/model/WorkGroup.kt @@ -1,5 +1,6 @@ package de.kif.common.model +import de.kif.common.SearchElement import kotlinx.serialization.Serializable @Serializable @@ -12,4 +13,19 @@ data class WorkGroup( val resolution: Boolean, val length: Int, val language: Language -) : Model +) : Model { + + override fun createSearch() = SearchElement( + mapOf( + "name" to name, + "track" to (track?.name ?: ""), + "language" to language.localeName + ), mapOf( + "projector" to projector, + "resolution" to resolution + ), mapOf( + "interested" to interested.toDouble(), + "length" to length.toDouble() + ) + ) +} diff --git a/src/jsMain/kotlin/de/kif/frontend/main.kt b/src/jsMain/kotlin/de/kif/frontend/main.kt index 8207573..c54ffcd 100644 --- a/src/jsMain/kotlin/de/kif/frontend/main.kt +++ b/src/jsMain/kotlin/de/kif/frontend/main.kt @@ -1,6 +1,7 @@ package de.kif.frontend -import de.kif.frontend.views.initCalendar +import de.kif.frontend.views.calendar.initCalendar +import de.kif.frontend.views.initTableLayout import de.westermann.kwebview.components.init import kotlin.browser.document @@ -10,4 +11,7 @@ fun main() = init { if (document.getElementsByClassName("calendar").length > 0) { initCalendar() } + if (document.getElementsByClassName("table-layout").length > 0) { + initTableLayout() + } } diff --git a/src/jsMain/kotlin/de/kif/frontend/repository/RoomRepository.kt b/src/jsMain/kotlin/de/kif/frontend/repository/RoomRepository.kt index 6920c3b..2d0b68d 100644 --- a/src/jsMain/kotlin/de/kif/frontend/repository/RoomRepository.kt +++ b/src/jsMain/kotlin/de/kif/frontend/repository/RoomRepository.kt @@ -23,7 +23,7 @@ object RoomRepository : Repository { } override suspend fun create(model: Room): Long { - return repositoryPost("/api/rooms", Message.json.stringify(Room.serializer(), model))?.toLong() + return repositoryPost("/api/rooms", Message.json.stringify(Room.serializer(), model)) ?: throw IllegalStateException("Cannot create model!") } diff --git a/src/jsMain/kotlin/de/kif/frontend/repository/ScheduleRepository.kt b/src/jsMain/kotlin/de/kif/frontend/repository/ScheduleRepository.kt index 6217fcc..4d7d74b 100644 --- a/src/jsMain/kotlin/de/kif/frontend/repository/ScheduleRepository.kt +++ b/src/jsMain/kotlin/de/kif/frontend/repository/ScheduleRepository.kt @@ -23,7 +23,7 @@ object ScheduleRepository : Repository { } override suspend fun create(model: Schedule): Long { - return repositoryPost("/api/schedules", Message.json.stringify(Schedule.serializer(), model))?.toLong() + return repositoryPost("/api/schedules", Message.json.stringify(Schedule.serializer(), model)) ?: throw IllegalStateException("Cannot create model!") } diff --git a/src/jsMain/kotlin/de/kif/frontend/repository/TrackRepository.kt b/src/jsMain/kotlin/de/kif/frontend/repository/TrackRepository.kt index 5adaa61..7e4acd2 100644 --- a/src/jsMain/kotlin/de/kif/frontend/repository/TrackRepository.kt +++ b/src/jsMain/kotlin/de/kif/frontend/repository/TrackRepository.kt @@ -23,7 +23,7 @@ object TrackRepository : Repository { } override suspend fun create(model: Track): Long { - return repositoryPost("/api/tracks", Message.json.stringify(Track.serializer(), model))?.toLong() + return repositoryPost("/api/tracks", Message.json.stringify(Track.serializer(), model)) ?: throw IllegalStateException("Cannot create model!") } diff --git a/src/jsMain/kotlin/de/kif/frontend/repository/UserRepository.kt b/src/jsMain/kotlin/de/kif/frontend/repository/UserRepository.kt index b837141..83f013a 100644 --- a/src/jsMain/kotlin/de/kif/frontend/repository/UserRepository.kt +++ b/src/jsMain/kotlin/de/kif/frontend/repository/UserRepository.kt @@ -23,7 +23,7 @@ object UserRepository : Repository { } override suspend fun create(model: User): Long { - return repositoryPost("/api/users", Message.json.stringify(User.serializer(), model))?.toLong() + return repositoryPost("/api/users", Message.json.stringify(User.serializer(), model)) ?: throw IllegalStateException("Cannot create model!") } diff --git a/src/jsMain/kotlin/de/kif/frontend/repository/WorkGroupRepository.kt b/src/jsMain/kotlin/de/kif/frontend/repository/WorkGroupRepository.kt index 1b85517..e38fbed 100644 --- a/src/jsMain/kotlin/de/kif/frontend/repository/WorkGroupRepository.kt +++ b/src/jsMain/kotlin/de/kif/frontend/repository/WorkGroupRepository.kt @@ -23,7 +23,7 @@ object WorkGroupRepository : Repository { } override suspend fun create(model: WorkGroup): Long { - return repositoryPost("/api/workgroups", Message.json.stringify(WorkGroup.serializer(), model))?.toLong() + return repositoryPost("/api/workgroups", Message.json.stringify(WorkGroup.serializer(), model)) ?: throw IllegalStateException("Cannot create model!") } diff --git a/src/jsMain/kotlin/de/kif/frontend/views/Calendar.kt b/src/jsMain/kotlin/de/kif/frontend/views/Calendar.kt deleted file mode 100644 index 380cd17..0000000 --- a/src/jsMain/kotlin/de/kif/frontend/views/Calendar.kt +++ /dev/null @@ -1,359 +0,0 @@ -package de.kif.frontend.views - -import de.kif.common.CALENDAR_GRID_WIDTH -import de.kif.common.model.Room -import de.kif.common.model.Schedule -import de.kif.frontend.iterator -import de.kif.frontend.launch -import de.kif.frontend.repository.RepositoryDelegate -import de.kif.frontend.repository.RoomRepository -import de.kif.frontend.repository.ScheduleRepository -import de.westermann.kwebview.* -import de.westermann.kwebview.components.Body -import org.w3c.dom.HTMLAnchorElement -import org.w3c.dom.HTMLElement -import org.w3c.dom.events.EventListener -import org.w3c.dom.events.MouseEvent -import org.w3c.dom.get -import kotlin.browser.document -import kotlin.dom.appendText -import kotlin.dom.isText - -class CalendarTools(entry: CalendarEntry, view: HTMLElement) : View(view) { - - init { - var linkM10: HTMLAnchorElement? = null - var linkM5: HTMLAnchorElement? = null - var linkReset: HTMLAnchorElement? = null - var linkP5: HTMLAnchorElement? = null - var linkP10: HTMLAnchorElement? = null - var linkDel: HTMLAnchorElement? = null - - for (element in html.children) { - when { - element.classList.contains("calendar-tools-m10") -> linkM10 = element as? HTMLAnchorElement - element.classList.contains("calendar-tools-m5") -> linkM5 = element as? HTMLAnchorElement - element.classList.contains("calendar-tools-reset") -> linkReset = element as? HTMLAnchorElement - element.classList.contains("calendar-tools-p5") -> linkP5 = element as? HTMLAnchorElement - element.classList.contains("calendar-tools-p10") -> linkP10 = element as? HTMLAnchorElement - element.classList.contains("calendar-tools-del") -> linkDel = element as? HTMLAnchorElement - } - } - - linkM10 = linkM10 ?: run { - val link = createHtmlView() - link.classList.add("calendar-tools-m10") - link.textContent = "-10" - html.appendChild(link) - link - } - linkM10.removeAttribute("href") - linkM10.addEventListener("click", EventListener { - entry.pending = true - launch { - val s = entry.schedule.get() - ScheduleRepository.update(s.copy(time = s.time - 10)) - } - }) - - linkM5 = linkM5 ?: run { - val link = createHtmlView() - link.classList.add("calendar-tools-m5") - link.textContent = "-5" - html.appendChild(link) - link - } - linkM5.removeAttribute("href") - linkM5.addEventListener("click", EventListener { - entry.pending = true - launch { - val s = entry.schedule.get() - ScheduleRepository.update(s.copy(time = s.time - 5)) - } - }) - - linkReset = linkReset ?: run { - val link = createHtmlView() - link.classList.add("calendar-tools-reset") - link.textContent = "reset" - html.appendChild(link) - link - } - linkReset.removeAttribute("href") - linkReset.addEventListener("click", EventListener { - entry.pending = true - launch { - val s = entry.schedule.get() - ScheduleRepository.update(s.copy(time = s.time / CALENDAR_GRID_WIDTH * CALENDAR_GRID_WIDTH)) - } - }) - - linkP5 = linkP5 ?: run { - val link = createHtmlView() - link.classList.add("calendar-tools-p5") - link.textContent = "+5" - html.appendChild(link) - link - } - linkP5.removeAttribute("href") - linkP5.addEventListener("click", EventListener { - entry.pending = true - launch { - val s = entry.schedule.get() - ScheduleRepository.update(s.copy(time = s.time + 5)) - } - }) - - linkP10 = linkP10 ?: run { - val link = createHtmlView() - link.classList.add("calendar-tools-p10") - link.textContent = "+10" - html.appendChild(link) - link - } - linkP10.removeAttribute("href") - linkP10.addEventListener("click", EventListener { - entry.pending = true - launch { - val s = entry.schedule.get() - ScheduleRepository.update(s.copy(time = s.time + 10)) - } - }) - - linkDel = linkDel ?: run { - val link = createHtmlView() - link.classList.add("calendar-tools-del") - link.textContent = "del" - html.appendChild(link) - link - } - linkDel.removeAttribute("href") - linkDel.addEventListener("click", EventListener { - entry.pending = true - launch { - ScheduleRepository.delete(entry.scheduleId) - } - }) - } -} - -class CalendarEntry(view: HTMLElement) : View(view) { - - private lateinit var mouseDelta: Point - private var newCell: CalendarCell? = null - - private var language by dataset.property("language") - - val scheduleId = dataset["id"]?.toLongOrNull() ?: 0 - - val schedule = RepositoryDelegate(ScheduleRepository, scheduleId) - - var pending by classList.property("pending") - - private fun onMove(event: MouseEvent) { - val position = event.toPoint() - mouseDelta - - val cell = calendarCells.find { - position in it.dimension - } - - - if (cell != null) { - cell += this - - if (newCell == null) { - style { - left = "0" - top = "0.1rem" - } - } - - newCell = cell - } - - event.preventDefault() - event.stopPropagation() - } - - private fun onFinishMove(event: MouseEvent) { - classList -= "drag" - - newCell?.let { cell -> - launch { - val newTime = cell.time - val newRoom = cell.getRoom() - - pending = true - - val s = schedule.get().copy(room = newRoom, time = newTime) - - ScheduleRepository.update(s) - } - } - newCell = null - - for (it in listeners) { - it.detach() - } - listeners = emptyList() - - event.preventDefault() - event.stopPropagation() - } - - private var listeners: List> = emptyList() - - init { - onMouseDown { event -> - if (event.target != html || "pending" in classList) { - event.stopPropagation() - return@onMouseDown - } - - launch { - classList += "drag" - - val s = schedule.get() - val time = s.time / CALENDAR_GRID_WIDTH * CALENDAR_GRID_WIDTH - - val p = calendarCells.find { - it.day == s.day && it.time == time && it.roomId == s.room.id - }?.dimension?.center ?: dimension.center - - mouseDelta = event.toPoint() - p - - listeners = listOf( - Body.onMouseMove.reference(this::onMove), - Body.onMouseUp.reference(this::onFinishMove), - Body.onMouseLeave.reference(this::onFinishMove) - ) - } - event.preventDefault() - event.stopPropagation() - } - - var calendarTools: CalendarTools? = null - for (item in html.children) { - if (item.classList.contains("calendar-tools")) { - calendarTools = CalendarTools(this, item) - break - } - } - if (calendarTools == null) { - calendarTools = CalendarTools(this, createHtmlView()) - html.appendChild(calendarTools.html) - } - } - - fun load(schedule: Schedule) { - pending = false - - language = schedule.workGroup.language.code - - this.schedule.set(schedule) - - style { - val size = schedule.workGroup.length / CALENDAR_GRID_WIDTH.toDouble() - val pos = (schedule.time % CALENDAR_GRID_WIDTH) / CALENDAR_GRID_WIDTH.toDouble() - - val ps = "${pos * 100}%" - val sz = "${size * 100}%" - - left = ps - top = "calc($ps + 0.1rem)" - - width = sz - height = "calc($sz - 0.2rem)" - - if (schedule.workGroup.track?.color != null) { - backgroundColor = schedule.workGroup.track.color.toString() - color = schedule.workGroup.track.color.calcTextColor().toString() - } - } - - for (element in html.childNodes) { - if (element.isText) { - html.removeChild(element) - } - } - - html.appendText(schedule.workGroup.name) - - - val time = schedule.time / CALENDAR_GRID_WIDTH * CALENDAR_GRID_WIDTH - val cell = calendarCells.find { - it.day == schedule.day && it.time == time && it.roomId == schedule.room.id - } - - if (cell != null && cell.html != html.parentElement) { - cell += this - } - } - - companion object { - fun create(schedule: Schedule): CalendarEntry { - val entry = CalendarEntry(createHtmlView()) - - entry.load(schedule) - - return entry - } - } -} - -class CalendarCell(view: HTMLElement) : ViewCollection(view) { - val day = dataset["day"]?.toIntOrNull() ?: 0 - val time = dataset["time"]?.toIntOrNull() ?: 0 - val roomId = dataset["room"]?.toLongOrNull() ?: 0 - - private lateinit var room: Room - - suspend fun getRoom(): Room { - if (this::room.isInitialized) { - return room - } - - room = RoomRepository.get(roomId) ?: throw NoSuchElementException() - return room - } -} - -var calendarEntries: List = emptyList() -var calendarCells: List = emptyList() - -fun initCalendar() { - calendarEntries = document.getElementsByClassName("calendar-entry") - .iterator().asSequence().map(::CalendarEntry).toList() - - calendarCells = document.getElementsByClassName("calendar-cell") - .iterator().asSequence().filter { it.dataset["time"] != null }.map(::CalendarCell).toList() - - ScheduleRepository.onCreate { - launch { - val schedule = ScheduleRepository.get(it) ?: throw NoSuchElementException() - CalendarEntry.create(schedule) - } - } - ScheduleRepository.onUpdate { - launch { - val schedule = ScheduleRepository.get(it) ?: throw NoSuchElementException() - var found = false - for (entry in calendarEntries) { - if (entry.scheduleId == it) { - entry.load(schedule) - found = true - } - } - if (!found) { - CalendarEntry.create(schedule) - } - } - } - ScheduleRepository.onDelete { - for (entry in calendarEntries) { - if (entry.scheduleId == it) { - entry.html.remove() - } - } - } -} \ No newline at end of file diff --git a/src/jsMain/kotlin/de/kif/frontend/views/TableLayout.kt b/src/jsMain/kotlin/de/kif/frontend/views/TableLayout.kt new file mode 100644 index 0000000..e56eb14 --- /dev/null +++ b/src/jsMain/kotlin/de/kif/frontend/views/TableLayout.kt @@ -0,0 +1,32 @@ +package de.kif.frontend.views + +import de.kif.common.Search +import de.kif.common.SearchElement +import de.kif.frontend.iterator +import de.westermann.kwebview.components.InputView +import org.w3c.dom.HTMLFormElement +import org.w3c.dom.HTMLInputElement +import org.w3c.dom.HTMLTableElement +import org.w3c.dom.get +import kotlin.browser.document + +fun initTableLayout() { + val form = document.getElementsByClassName("table-layout-search")[0] as HTMLFormElement + form.onsubmit = { false } + + 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")!!) + } + + 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" + } + } +} diff --git a/src/jsMain/kotlin/de/kif/frontend/views/calendar/Calendar.kt b/src/jsMain/kotlin/de/kif/frontend/views/calendar/Calendar.kt new file mode 100644 index 0000000..abf806c --- /dev/null +++ b/src/jsMain/kotlin/de/kif/frontend/views/calendar/Calendar.kt @@ -0,0 +1,65 @@ +package de.kif.frontend.views.calendar + +import de.kif.frontend.iterator +import de.kif.frontend.launch +import de.kif.frontend.repository.ScheduleRepository +import de.westermann.kwebview.View +import org.w3c.dom.HTMLElement +import org.w3c.dom.get +import kotlin.browser.document + + +class Calendar(calendar: HTMLElement): View(calendar) { + var calendarEntries: List = emptyList() + var calendarCells: List = emptyList() + + val day: Int + + init { + val editable = calendar.dataset["editable"]?.toBoolean() ?: false + day = calendar.dataset["day"]?.toIntOrNull() ?: -1 + + calendarEntries = document.getElementsByClassName("calendar-entry") + .iterator().asSequence().map{ CalendarEntry(this, it) }.onEach { it.editable = editable }.toList() + + calendarCells = document.getElementsByClassName("calendar-cell") + .iterator().asSequence().filter { it.dataset["time"] != null }.map(::CalendarCell).toList() + + if (editable) { + CalendarEdit(this, calendar.querySelector(".calendar-edit") as HTMLElement) + } + + ScheduleRepository.onCreate { + launch { + val schedule = ScheduleRepository.get(it) ?: throw NoSuchElementException() + calendarEntries += CalendarEntry.create(this, schedule).also { it.editable = editable } + } + } + ScheduleRepository.onUpdate { + launch { + val schedule = ScheduleRepository.get(it) ?: throw NoSuchElementException() + var found = false + for (entry in calendarEntries) { + if (entry.scheduleId == it) { + entry.load(schedule) + found = true + } + } + if (!found) { + calendarEntries += CalendarEntry.create(this, schedule).also { it.editable = editable } + } + } + } + ScheduleRepository.onDelete { + for (entry in calendarEntries) { + if (entry.scheduleId == it) { + entry.html.remove() + } + } + } + } +} + +fun initCalendar() { + Calendar(document.getElementsByClassName("calendar")[0] as? HTMLElement ?: return) +} \ No newline at end of file diff --git a/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarCell.kt b/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarCell.kt new file mode 100644 index 0000000..0e9bc8c --- /dev/null +++ b/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarCell.kt @@ -0,0 +1,28 @@ +package de.kif.frontend.views.calendar + +import de.kif.common.model.Room +import de.kif.frontend.repository.RoomRepository +import de.westermann.kwebview.ViewCollection +import org.w3c.dom.HTMLElement +import org.w3c.dom.get + +class CalendarCell(view: HTMLElement) : ViewCollection(view) { + val day = dataset["day"]?.toIntOrNull() ?: 0 + val time = dataset["time"]?.toIntOrNull() ?: 0 + val roomId = dataset["room"]?.toLongOrNull() ?: 0 + + private lateinit var room: Room + + suspend fun getRoom(): Room { + if (this::room.isInitialized) { + return room + } + + room = RoomRepository.get(roomId) ?: throw NoSuchElementException() + return room + } + + init { + (view.getElementsByClassName("calendar-link")[0] as? HTMLElement)?.remove() + } +} \ No newline at end of file diff --git a/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarEdit.kt b/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarEdit.kt new file mode 100644 index 0000000..11fd68b --- /dev/null +++ b/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarEdit.kt @@ -0,0 +1,76 @@ +package de.kif.frontend.views.calendar + +import de.kif.common.Search +import de.kif.frontend.launch +import de.kif.frontend.repository.WorkGroupRepository +import de.westermann.kobserve.list.filterObservable +import de.westermann.kobserve.list.observableListOf +import de.westermann.kobserve.list.sortObservable +import de.westermann.kwebview.View +import de.westermann.kwebview.components.Button +import de.westermann.kwebview.components.InputView +import de.westermann.kwebview.components.ListView +import de.westermann.kwebview.extra.listFactory +import org.w3c.dom.HTMLButtonElement +import org.w3c.dom.HTMLElement +import org.w3c.dom.HTMLInputElement + +class CalendarEdit( + private val calendar: Calendar, view: HTMLElement +) : View(view) { + + private val toggleEditButton = + Button.wrap(view.querySelector(".calendar-edit-top button") as HTMLButtonElement) + + val search = + InputView.wrap(view.querySelector(".calendar-edit-search input") as HTMLInputElement) + + val listView = ListView.wrap( + view.querySelector(".calendar-edit-list") as HTMLElement + ) + + private var loaded = false + + val workGroupList = observableListOf() + private val sortedList = workGroupList.sortObservable(compareBy { + it.workGroup.name + }).filterObservable(search.valueProperty) { entry, search -> + val s = entry.workGroup.createSearch() + Search.match(search, s) + } + + private fun load() { + if (loaded) return + loaded = true + + launch { + for (workGroup in WorkGroupRepository.all()) { + workGroupList += CalendarWorkGroup(calendar, this, workGroup) + } + } + } + + init { + toggleEditButton.onClick { + calendar.classList.toggle("edit") + + if (!loaded) { + load() + } + } + + WorkGroupRepository.onCreate { + if (loaded) { + launch { + workGroupList += CalendarWorkGroup( + calendar, + this, + WorkGroupRepository.get(it)!! + ) + } + } + } + + listView.listFactory(sortedList) + } +} \ No newline at end of file diff --git a/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarEntry.kt b/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarEntry.kt new file mode 100644 index 0000000..9763d67 --- /dev/null +++ b/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarEntry.kt @@ -0,0 +1,222 @@ +package de.kif.frontend.views.calendar + +import de.kif.common.CALENDAR_GRID_WIDTH +import de.kif.common.model.Schedule +import de.kif.common.model.WorkGroup +import de.kif.frontend.iterator +import de.kif.frontend.launch +import de.kif.frontend.repository.RepositoryDelegate +import de.kif.frontend.repository.ScheduleRepository +import de.westermann.kwebview.Point +import de.westermann.kwebview.View +import de.westermann.kwebview.components.Body +import de.westermann.kwebview.createHtmlView +import de.westermann.kwebview.toPoint +import org.w3c.dom.HTMLElement +import org.w3c.dom.events.MouseEvent +import kotlin.dom.appendText +import kotlin.dom.isText + +class CalendarEntry(private val calendar: Calendar, view: HTMLElement) : View(view) { + + private lateinit var mouseDelta: Point + private var newCell: CalendarCell? = null + + private var language by dataset.property("language") + + var scheduleId = dataset["id"]?.toLongOrNull() ?: -1 + + val schedule = + RepositoryDelegate(ScheduleRepository, scheduleId) + + private lateinit var workGroup: WorkGroup + + var pending by classList.property("pending") + + var editable: Boolean = false + + private fun onMove(event: MouseEvent) { + val position = event.toPoint() - mouseDelta + + val cell = calendar.calendarCells.find { + position in it.dimension + } + + if (cell != null) { + cell += this + + if (newCell == null) { + style { + left = "0" + top = "0.1rem" + } + } + + newCell = cell + } + + event.preventDefault() + event.stopPropagation() + } + + private fun onFinishMove(event: MouseEvent) { + classList -= "drag" + + newCell?.let { cell -> + launch { + val newTime = cell.time + val newRoom = cell.getRoom() + + pending = true + + if (scheduleId < 0) { + ScheduleRepository.create( + Schedule( + null, + workGroup, + newRoom, + calendar.day, + newTime + ) + ) + html.remove() + } else { + ScheduleRepository.update(schedule.get().copy(room = newRoom, time = newTime)) + } + } + } + newCell = null + + for (it in listeners) { + it.detach() + } + listeners = emptyList() + + event.preventDefault() + event.stopPropagation() + } + + private var listeners: List> = emptyList() + + fun startDrag() { + listeners = listOf( + Body.onMouseMove.reference(this::onMove), + Body.onMouseUp.reference(this::onFinishMove), + Body.onMouseLeave.reference(this::onFinishMove) + ) + } + + init { + onMouseDown { event -> + if (!editable || event.target != html || "pending" in classList) { + event.stopPropagation() + return@onMouseDown + } + + launch { + classList += "drag" + + val s = schedule.get() + val time = s.time / CALENDAR_GRID_WIDTH * CALENDAR_GRID_WIDTH + + val p = calendar.calendarCells.find { + it.day == s.day && it.time == time && it.roomId == s.room.id + }?.dimension?.center ?: dimension.center + + mouseDelta = event.toPoint() - p + + startDrag() + } + event.preventDefault() + event.stopPropagation() + } + + var calendarTools: CalendarTools? = null + for (item in html.children) { + if (item.classList.contains("calendar-tools")) { + calendarTools = CalendarTools(this, item) + break + } + } + if (calendarTools == null) { + calendarTools = CalendarTools(this, createHtmlView()) + html.appendChild(calendarTools.html) + } + } + + fun load(schedule: Schedule) { + pending = false + + if (schedule.id != null) scheduleId = schedule.id + this.schedule.set(schedule) + + style { + val pos = (schedule.time % CALENDAR_GRID_WIDTH) / CALENDAR_GRID_WIDTH.toDouble() + + val ps = "${pos * 100}%" + + left = ps + top = "calc($ps + 0.1rem)" + } + + load(schedule.workGroup) + + val time = schedule.time / CALENDAR_GRID_WIDTH * CALENDAR_GRID_WIDTH + val cell = calendar.calendarCells.find { + it.day == schedule.day && it.time == time && it.roomId == schedule.room.id + } + + if (cell != null && cell.html != html.parentElement) { + cell += this + } + } + + fun load(workGroup: WorkGroup) { + pending = false + + language = workGroup.language.code + this.workGroup = workGroup + + style { + val size = workGroup.length / CALENDAR_GRID_WIDTH.toDouble() + + val sz = "${size * 100}%" + + width = sz + height = "calc($sz - 0.2rem)" + + if (workGroup.track?.color != null) { + backgroundColor = workGroup.track.color.toString() + color = workGroup.track.color.calcTextColor().toString() + } + } + + for (element in html.childNodes) { + if (element.isText) { + html.removeChild(element) + } + } + + html.appendText(workGroup.name) + } + + companion object { + fun create(calendar: Calendar, schedule: Schedule): CalendarEntry { + val entry = CalendarEntry(calendar, createHtmlView()) + + entry.load(schedule) + + return entry + } + + fun create(calendar: Calendar, workGroup: WorkGroup): CalendarEntry { + val entry = CalendarEntry(calendar, createHtmlView()) + + entry.load(workGroup) + entry.mouseDelta = Point.ZERO + entry.startDrag() + + return entry + } + } +} diff --git a/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarTools.kt b/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarTools.kt new file mode 100644 index 0000000..227f130 --- /dev/null +++ b/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarTools.kt @@ -0,0 +1,129 @@ +package de.kif.frontend.views.calendar + +import de.kif.common.CALENDAR_GRID_WIDTH +import de.kif.frontend.iterator +import de.kif.frontend.launch +import de.kif.frontend.repository.ScheduleRepository +import de.westermann.kwebview.View +import de.westermann.kwebview.createHtmlView +import org.w3c.dom.HTMLAnchorElement +import org.w3c.dom.HTMLElement +import org.w3c.dom.events.EventListener + +class CalendarTools(entry: CalendarEntry, view: HTMLElement) : View(view) { + + init { + var linkM10: HTMLAnchorElement? = null + var linkM5: HTMLAnchorElement? = null + var linkReset: HTMLAnchorElement? = null + var linkP5: HTMLAnchorElement? = null + var linkP10: HTMLAnchorElement? = null + var linkDel: HTMLAnchorElement? = null + + for (element in html.children) { + when { + element.classList.contains("calendar-tools-m10") -> linkM10 = element as? HTMLAnchorElement + element.classList.contains("calendar-tools-m5") -> linkM5 = element as? HTMLAnchorElement + element.classList.contains("calendar-tools-reset") -> linkReset = element as? HTMLAnchorElement + element.classList.contains("calendar-tools-p5") -> linkP5 = element as? HTMLAnchorElement + element.classList.contains("calendar-tools-p10") -> linkP10 = element as? HTMLAnchorElement + element.classList.contains("calendar-tools-del") -> linkDel = element as? HTMLAnchorElement + } + } + + linkM10 = linkM10 ?: run { + val link = createHtmlView() + link.classList.add("calendar-tools-m10") + link.textContent = "-10" + html.appendChild(link) + link + } + linkM10.removeAttribute("href") + linkM10.addEventListener("click", EventListener { + entry.pending = true + launch { + val s = entry.schedule.get() + ScheduleRepository.update(s.copy(time = s.time - 10)) + } + }) + + linkM5 = linkM5 ?: run { + val link = createHtmlView() + link.classList.add("calendar-tools-m5") + link.textContent = "-5" + html.appendChild(link) + link + } + linkM5.removeAttribute("href") + linkM5.addEventListener("click", EventListener { + entry.pending = true + launch { + val s = entry.schedule.get() + ScheduleRepository.update(s.copy(time = s.time - 5)) + } + }) + + linkReset = linkReset ?: run { + val link = createHtmlView() + link.classList.add("calendar-tools-reset") + link.textContent = "reset" + html.appendChild(link) + link + } + linkReset.removeAttribute("href") + linkReset.addEventListener("click", EventListener { + entry.pending = true + launch { + val s = entry.schedule.get() + ScheduleRepository.update(s.copy(time = s.time / CALENDAR_GRID_WIDTH * CALENDAR_GRID_WIDTH)) + } + }) + + linkP5 = linkP5 ?: run { + val link = createHtmlView() + link.classList.add("calendar-tools-p5") + link.textContent = "+5" + html.appendChild(link) + link + } + linkP5.removeAttribute("href") + linkP5.addEventListener("click", EventListener { + entry.pending = true + launch { + val s = entry.schedule.get() + ScheduleRepository.update(s.copy(time = s.time + 5)) + } + }) + + linkP10 = linkP10 ?: run { + val link = createHtmlView() + link.classList.add("calendar-tools-p10") + link.textContent = "+10" + html.appendChild(link) + link + } + linkP10.removeAttribute("href") + linkP10.addEventListener("click", EventListener { + entry.pending = true + launch { + val s = entry.schedule.get() + ScheduleRepository.update(s.copy(time = s.time + 10)) + } + }) + + linkDel = linkDel ?: run { + val link = createHtmlView() + link.classList.add("calendar-tools-del") + link.textContent = "del" + html.appendChild(link) + link + } + linkDel.removeAttribute("href") + linkDel.addEventListener("click", EventListener { + entry.pending = true + launch { + ScheduleRepository.delete(entry.scheduleId) + } + }) + } +} \ No newline at end of file diff --git a/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarWorkGroup.kt b/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarWorkGroup.kt new file mode 100644 index 0000000..f970f55 --- /dev/null +++ b/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarWorkGroup.kt @@ -0,0 +1,56 @@ +package de.kif.frontend.views.calendar + +import de.kif.common.model.WorkGroup +import de.kif.frontend.launch +import de.kif.frontend.repository.WorkGroupRepository +import de.westermann.kwebview.View + +class CalendarWorkGroup( + private val calendar: Calendar, + private val calendarEdit: CalendarEdit, + workGroup: WorkGroup +) : View() { + private var language by dataset.property("language") + + lateinit var workGroup: WorkGroup + private set + + private fun load(workGroup: WorkGroup) { + this.workGroup = workGroup + + html.textContent = workGroup.name + + language = workGroup.language.code + + style { + if (workGroup.track?.color != null) { + backgroundColor = workGroup.track.color.toString() + color = workGroup.track.color.calcTextColor().toString() + } + } + } + + init { + load(workGroup) + + val references: MutableList> = mutableListOf() + + references += WorkGroupRepository.onUpdate.reference { + if (it == workGroup.id) { + launch { + load(WorkGroupRepository.get(it)!!) + } + } + } + + references += WorkGroupRepository.onDelete.reference { + if (it == workGroup.id) { + calendarEdit.workGroupList -= this + } + } + + onMouseDown { + CalendarEntry.create(calendar, workGroup) + } + } +} diff --git a/src/jsMain/kotlin/de/westermann/kwebview/ViewCollection.kt b/src/jsMain/kotlin/de/westermann/kwebview/ViewCollection.kt index c9af304..f9f3b3e 100644 --- a/src/jsMain/kotlin/de/westermann/kwebview/ViewCollection.kt +++ b/src/jsMain/kotlin/de/westermann/kwebview/ViewCollection.kt @@ -6,7 +6,7 @@ import kotlin.dom.clear /** * @author lars */ -abstract class ViewCollection(view: HTMLElement = createHtmlView()) : View(view), Iterable { +abstract class ViewCollection(view: HTMLElement = createHtmlView()) : View(view), Collection { private val children: MutableList = mutableListOf() @@ -29,6 +29,36 @@ abstract class ViewCollection(view: HTMLElement = createHtmlView()) : } } + fun replace(oldView: V, newView: V) { + if (children.contains(oldView)) { + children.add(children.indexOf(oldView), newView) + html.insertBefore(newView.html, oldView.html) + children -= oldView + html.removeChild(oldView.html) + } + } + + fun add(view: V) = append(view) + + fun add(index: Int, view: V) { + if (index >= size) { + append(view) + } else { + html.insertBefore(view.html, children[index].html) + children.add(index, view) + } + } + + operator fun get(index: Int): V { + return children[index] + } + + fun removeAt(index: Int) { + if (index in 0 until size) { + remove(children[index]) + } + } + fun toForeground(view: V) { if (view in children && children.indexOf(view) < children.size - 1) { remove(view) @@ -48,8 +78,7 @@ abstract class ViewCollection(view: HTMLElement = createHtmlView()) : operator fun minusAssign(view: V) = remove(view) - val isEmpty: Boolean - get() = children.isEmpty() + override fun isEmpty(): Boolean = children.isEmpty() fun clear() { children.clear() @@ -58,16 +87,14 @@ abstract class ViewCollection(view: HTMLElement = createHtmlView()) : override fun iterator(): Iterator = children.iterator() - val size: Int + override val size: Int get() = children.size - operator fun contains(view: V) = children.contains(view) + override fun contains(element: V) = children.contains(element) + + override fun containsAll(elements: Collection): Boolean = children.containsAll(elements) operator fun V.unaryPlus() { append(this) } - - companion object { - fun wrap(htmlElement: HTMLElement) = object : ViewCollection(htmlElement) {} - } } diff --git a/src/jsMain/kotlin/de/westermann/kwebview/ViewForLabel.kt b/src/jsMain/kotlin/de/westermann/kwebview/ViewForLabel.kt index 88330e8..573a6a5 100644 --- a/src/jsMain/kotlin/de/westermann/kwebview/ViewForLabel.kt +++ b/src/jsMain/kotlin/de/westermann/kwebview/ViewForLabel.kt @@ -5,7 +5,9 @@ import org.w3c.dom.HTMLInputElement import kotlin.math.abs import kotlin.random.Random -abstract class ViewForLabel : View(createHtmlView()) { +abstract class ViewForLabel( + view: HTMLInputElement = createHtmlView() +) : View(view) { override val html = super.html as HTMLInputElement private var label: Label? = null diff --git a/src/jsMain/kotlin/de/westermann/kwebview/components/Button.kt b/src/jsMain/kotlin/de/westermann/kwebview/components/Button.kt index dbed7f5..50d561a 100644 --- a/src/jsMain/kotlin/de/westermann/kwebview/components/Button.kt +++ b/src/jsMain/kotlin/de/westermann/kwebview/components/Button.kt @@ -14,7 +14,7 @@ import org.w3c.dom.HTMLButtonElement * * @author lars */ -class Button() : ViewCollection(createHtmlView()) { +class Button(view: HTMLButtonElement = createHtmlView()) : ViewCollection(view) { constructor(text: String) : this() { this.text = text @@ -38,6 +38,10 @@ class Button() : ViewCollection(createHtmlView()) { } val textProperty: Property = property(this::text) + + companion object { + fun wrap(view: HTMLButtonElement) = Button(view) + } } @KWebViewDsl diff --git a/src/jsMain/kotlin/de/westermann/kwebview/components/InputView.kt b/src/jsMain/kotlin/de/westermann/kwebview/components/InputView.kt index ea6b839..a99203a 100644 --- a/src/jsMain/kotlin/de/westermann/kwebview/components/InputView.kt +++ b/src/jsMain/kotlin/de/westermann/kwebview/components/InputView.kt @@ -3,8 +3,8 @@ package de.westermann.kwebview.components import de.westermann.kobserve.Property import de.westermann.kobserve.ReadOnlyProperty import de.westermann.kobserve.ValidationProperty -import de.westermann.kobserve.property.property import de.westermann.kobserve.not +import de.westermann.kobserve.property.property import de.westermann.kwebview.* import org.w3c.dom.HTMLInputElement import org.w3c.dom.events.Event @@ -12,9 +12,10 @@ import org.w3c.dom.events.EventListener import org.w3c.dom.events.KeyboardEvent class InputView( - type: InputType, - initValue: String = "" -) : ViewForLabel() { + type: InputType, + initValue: String = "", + view: HTMLInputElement = createHtmlView() +) : ViewForLabel(view) { fun bind(property: ReadOnlyProperty) { valueProperty.bind(property) @@ -108,12 +109,17 @@ class InputView( html.addEventListener("keyup", changeListener) html.addEventListener("keypress", changeListener) } + + companion object { + fun wrap(view: HTMLInputElement) = InputView(InputType.SEARCH, view.value, view) + } } enum class InputType(val html: String) { TEXT("text"), NUMBER("number"), - PASSWORD("password"); + PASSWORD("password"), + SEARCH("search"); companion object { fun find(html: String): InputType? = values().find { it.html == html } @@ -122,33 +128,49 @@ enum class InputType(val html: String) { @KWebViewDsl fun ViewCollection.inputView(text: String = "", init: InputView.() -> Unit = {}) = - InputView(InputType.TEXT, text).also(this::append).also(init) + InputView(InputType.TEXT, text).also(this::append).also(init) @KWebViewDsl fun ViewCollection.inputView(text: ReadOnlyProperty, init: InputView.() -> Unit = {}) = - InputView(InputType.TEXT, text.value).also(this::append).also { it.bind(text) }.also(init) + InputView(InputType.TEXT, text.value).also(this::append).also { it.bind(text) }.also(init) @KWebViewDsl fun ViewCollection.inputView(text: Property, init: InputView.() -> Unit = {}) = - InputView(InputType.TEXT, text.value).also(this::append).also { it.bind(text) }.also(init) + InputView(InputType.TEXT, text.value).also(this::append).also { it.bind(text) }.also(init) @KWebViewDsl fun ViewCollection.inputView(text: ValidationProperty, init: InputView.() -> Unit = {}) = - InputView(InputType.TEXT, text.value).also(this::append).also { it.bind(text) }.also(init) + InputView(InputType.TEXT, text.value).also(this::append).also { it.bind(text) }.also(init) @KWebViewDsl -fun ViewCollection.inputView(type: InputType = InputType.TEXT, text: String = "", init: InputView.() -> Unit = {}) = - InputView(type, text).also(this::append).also(init) +fun ViewCollection.inputView( + type: InputType = InputType.TEXT, + text: String = "", + init: InputView.() -> Unit = {} +) = + InputView(type, text).also(this::append).also(init) @KWebViewDsl -fun ViewCollection.inputView(type: InputType = InputType.TEXT, text: ReadOnlyProperty, init: InputView.() -> Unit = {}) = - InputView(type, text.value).also(this::append).also { it.bind(text) }.also(init) +fun ViewCollection.inputView( + type: InputType = InputType.TEXT, + text: ReadOnlyProperty, + init: InputView.() -> Unit = {} +) = + InputView(type, text.value).also(this::append).also { it.bind(text) }.also(init) @KWebViewDsl -fun ViewCollection.inputView(type: InputType = InputType.TEXT, text: Property, init: InputView.() -> Unit = {}) = - InputView(type, text.value).also(this::append).also { it.bind(text) }.also(init) +fun ViewCollection.inputView( + type: InputType = InputType.TEXT, + text: Property, + init: InputView.() -> Unit = {} +) = + InputView(type, text.value).also(this::append).also { it.bind(text) }.also(init) @KWebViewDsl -fun ViewCollection.inputView(type: InputType = InputType.TEXT, text: ValidationProperty, init: InputView.() -> Unit = {}) = - InputView(type, text.value).also(this::append).also { it.bind(text) }.also(init) +fun ViewCollection.inputView( + type: InputType = InputType.TEXT, + text: ValidationProperty, + init: InputView.() -> Unit = {} +) = + InputView(type, text.value).also(this::append).also { it.bind(text) }.also(init) diff --git a/src/jsMain/kotlin/de/westermann/kwebview/components/ListView.kt b/src/jsMain/kotlin/de/westermann/kwebview/components/ListView.kt new file mode 100644 index 0000000..9f94607 --- /dev/null +++ b/src/jsMain/kotlin/de/westermann/kwebview/components/ListView.kt @@ -0,0 +1,30 @@ +package de.westermann.kwebview.components + +import de.westermann.kwebview.KWebViewDsl +import de.westermann.kwebview.View +import de.westermann.kwebview.ViewCollection +import de.westermann.kwebview.createHtmlView +import org.w3c.dom.HTMLDivElement +import org.w3c.dom.HTMLElement + +class ListView(view: HTMLElement = createHtmlView()) : ViewCollection(view) { + override val html = super.html as HTMLDivElement + + companion object { + fun wrap(view: HTMLElement) = ListView(view) + } +} + +@KWebViewDsl +fun ViewCollection>.listView( + vararg classes: String, + init: ListView.() -> Unit = {} +): ListView { + val view = ListView() + for (c in classes) { + view.classList += c + } + append(view) + init(view) + return view +} diff --git a/src/jsMain/kotlin/de/westermann/kwebview/extra/ListFactory.kt b/src/jsMain/kotlin/de/westermann/kwebview/extra/ListFactory.kt new file mode 100644 index 0000000..0b26850 --- /dev/null +++ b/src/jsMain/kotlin/de/westermann/kwebview/extra/ListFactory.kt @@ -0,0 +1,62 @@ +package de.westermann.kwebview.extra + +import de.westermann.kobserve.list.ObservableReadOnlyList +import de.westermann.kwebview.View +import de.westermann.kwebview.ViewCollection +import de.westermann.kwebview.async + +fun ViewCollection.listFactory( + list: ObservableReadOnlyList, + factory: (T) -> V, + animateAdd: Int? = null, + animateRemove: Int? = null +) { + for (element in list) { + +factory(element) + } + list.onAdd { (index, element) -> + val view = factory(element) + add(index, view) + + if (animateAdd != null) { + classList += "animate-add" + view.classList += "active" + + async(animateAdd) { + classList -= "animate-add" + view.classList -= "active" + } + } + } + list.onRemove { (index) -> + @Suppress("UNCHECKED_CAST") val view = this[index] as V + + if (animateRemove == null) { + remove(view) + } else { + classList += "animate-remove" + view.classList += "active" + + async(animateRemove) { + classList -= "animate-remove" + view.classList -= "active" + remove(view) + } + } + } + list.onUpdate { (oldIndex, newIndex, element) -> + removeAt(oldIndex) + add(newIndex, factory(element)) + } +} + +fun ViewCollection.listFactory( + list: ObservableReadOnlyList, + animateAdd: Int? = null, + animateRemove: Int? = null +) = listFactory( + list, + { it }, + animateAdd, + animateRemove +) \ No newline at end of file diff --git a/src/jsMain/resources/style/style.scss b/src/jsMain/resources/style/style.scss index 7ba7fca..747f5f0 100644 --- a/src/jsMain/resources/style/style.scss +++ b/src/jsMain/resources/style/style.scss @@ -220,28 +220,6 @@ a { .table-layout-search { float: left; padding-bottom: 0.5rem !important; - - input { - padding-right: 4rem; - } - - .btn-search { - position: absolute; - top: 0; - height: 2.5rem; - line-height: 2.5rem; - right: -3px; - border-bottom-left-radius: 0; - border-top-left-radius: 0; - - margin-top: 1px; - margin-right: 2px; - } - - input:focus ~ .btn-search { - border-color: $primary-color; - border-width: 2px; - } } .table-layout-action { @@ -470,13 +448,106 @@ form { width: 100%; position: relative; margin-top: 1rem; +} - & > div { - width: calc(100% - 6rem); - overflow-x: scroll; - overflow-y: visible; - margin-left: 6rem; - padding-bottom: 1rem; +.calendar-table { + float: left; + width: 100%; + overflow-x: scroll; + overflow-y: visible; + padding-bottom: 1rem; + position: relative; + transition: width $transitionTime; +} + +.calendar-edit { + width: 16rem; + display: block; + position: absolute; + right: 0; + top: -3rem; + padding-top: 3rem; + overflow: hidden; + + .calendar-edit-main { + position: relative; + left: 16rem; + opacity: 0; + transition: left $transitionTime, opacity$transitionTime; + } +} + +.calendar-edit-top { + position: absolute; + right: 0; + top: 0; +} + +.calendar-edit-search { + padding: 0.5rem; +} + +.calendar-work-group { + position: relative; + display: block; + border-radius: 0.2rem; + line-height: 2rem; + font-size: 0.8rem; + white-space: nowrap; + text-overflow: ellipsis; + + background-color: $primary-color; + color: $primary-text-color; + + padding: 0 0.5rem; + margin: 0.5rem; + + &::after { + content: attr(data-language); + bottom: 0; + right: 1rem; + opacity: 0.6; + position: absolute; + text-transform: uppercase; + } + + &:hover .calendar-tools { + display: block; + } + + &.drag { + box-shadow: 0 0.1rem 0.2rem rgba($text-primary-color, 0.8); + z-index: 2; + + .calendar-tools { + display: none; + } + } + + &.pending::before { + content: ''; + display: block; + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: $background-primary-color; + opacity: 0.6; + } + + @include no-select() +} + +.calendar[data-editable = "true"].edit { + .calendar-table { + width: calc(100% - 16rem); + border-right: solid 1px rgba($text-primary-color, 0.1); + } + + .calendar-edit-main { + left: 0; + opacity: 1; } } @@ -507,6 +578,10 @@ form { } } +.calendar[data-editable=false] .calendar-tools { + display: none !important; +} + .calendar-entry { position: absolute; display: block; @@ -555,6 +630,8 @@ form { background-color: $background-primary-color; opacity: 0.6; } + + @include no-select() } .calendar-table-time-to-room { @@ -618,7 +695,6 @@ form { } .calendar-cell:first-child { - position: absolute; width: 6rem; left: 0; text-align: center; @@ -663,13 +739,6 @@ form { line-height: 2rem; height: 1.3rem; - &:nth-child(4n + 2)::before { - content: ''; - position: absolute; - width: calc(100% - 6rem); - border-top: solid 1px rgba($text-primary-color, 0.1); - } - .calendar-cell { position: relative; width: 12rem; @@ -677,6 +746,7 @@ form { &::before { content: ''; height: 100%; + width: 100%; left: 0; top: 0; border-left: solid 1px rgba($text-primary-color, 0.1); @@ -694,18 +764,22 @@ form { left: 0.1rem !important; right: 0.1rem; } + + a { + position: relative; + } } - .calendar-cell:first-child:not(:empty)::before { - width: 100%; - top: 0; + .calendar-cell:first-child::before { border-left: none; + } + + &:nth-child(4n + 2) .calendar-cell::before { border-top: solid 1px rgba($text-primary-color, 0.1); } } .calendar-cell:first-child { - position: absolute; width: 6rem; left: 0; text-align: center; diff --git a/src/jvmMain/kotlin/de/kif/backend/Main.kt b/src/jvmMain/kotlin/de/kif/backend/Main.kt index 38c4abb..08abe04 100644 --- a/src/jvmMain/kotlin/de/kif/backend/Main.kt +++ b/src/jvmMain/kotlin/de/kif/backend/Main.kt @@ -7,9 +7,7 @@ import de.kif.common.model.User import io.ktor.application.Application import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.runBlocking -import kotlin.coroutines.suspendCoroutine object Main { @Suppress("UnusedMainParameter") @@ -34,6 +32,8 @@ object Main { password = System.console()?.readPassword()?.toString() ?: readLine() } + println("Create root user '$username' with pw '${"*".repeat(password.length)}'") + UserRepository.create( User( null, diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Calendar.kt b/src/jvmMain/kotlin/de/kif/backend/route/Calendar.kt index 2a58521..ed78f62 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Calendar.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Calendar.kt @@ -5,11 +5,11 @@ import de.kif.backend.isAuthenticated import de.kif.backend.repository.RoomRepository import de.kif.backend.repository.ScheduleRepository import de.kif.backend.repository.WorkGroupRepository -import de.kif.backend.util.Search import de.kif.backend.view.MainTemplate import de.kif.backend.view.MenuTemplate import de.kif.backend.view.TableTemplate import de.kif.common.CALENDAR_GRID_WIDTH +import de.kif.common.Search import de.kif.common.model.Permission import de.kif.common.model.Room import de.kif.common.model.Schedule @@ -304,19 +304,21 @@ fun Route.calendar() { a("/calendar/${day + 1}") { +">" } } div("header-right") { - a("/calendar/$day/room-to-time") { + a("/calendar/$day/rtt") { +"Room to time" } - a("/calendar/$day/time-to-room") { + a("/calendar/$day/ttr") { +"Time to room" } } } div("calendar") { + val editable = user != null attributes["data-day"] = day.toString() + attributes["data-editable"] = editable.toString() - div { + div("calendar-table") { when (orientation) { CalendarOrientation.ROOM_TO_TIME -> renderRoomToTime( day, @@ -324,7 +326,7 @@ fun Route.calendar() { max, rooms, schedules, - user != null + editable ) CalendarOrientation.TIME_TO_ROOM -> renderTimeToRoom( day, @@ -332,13 +334,33 @@ fun Route.calendar() { max, rooms, schedules, - user != null + editable ) } } + + if (editable) { + div("calendar-edit") { + div("calendar-edit-top") { + button(classes = "form-btn") { + +"Edit" + } + } + div("calendar-edit-main") { + div("calendar-edit-search") { + input(InputType.search, name = "search", classes = "form-control") { + placeholder = "Search" + value = "" + } + } + div("calendar-edit-list") { + + } + } + } + } } } - } } @@ -392,10 +414,11 @@ fun Route.calendar() { } for (u in list) { - if (Search.match(search, u.name)) { + val s = u.createSearch() + if (Search.match(search, s)) { val href = "/calendar/$day/${room.id}/$time/${u.id}" entry { - attributes["data-search"] = Search.pack(u.name) + attributes["data-search"] = s.stringify() td { a(href) { +u.name diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Room.kt b/src/jvmMain/kotlin/de/kif/backend/route/Room.kt index d6c5ee4..2a45e43 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Room.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Room.kt @@ -2,7 +2,7 @@ package de.kif.backend.route import de.kif.backend.authenticateOrRedirect import de.kif.backend.repository.RoomRepository -import de.kif.backend.util.Search +import de.kif.common.Search import de.kif.backend.view.MainTemplate import de.kif.backend.view.MenuTemplate import de.kif.backend.view.TableTemplate @@ -64,9 +64,10 @@ fun Route.room() { } for (u in list) { - if (Search.match(search, u.name)) { + val s = u.createSearch() + if (Search.match(search, s)) { entry { - attributes["data-search"] = Search.pack(u.name) + attributes["data-search"] = s.stringify() td { +u.name } diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Track.kt b/src/jvmMain/kotlin/de/kif/backend/route/Track.kt index 50e8f2e..fc83748 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Track.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Track.kt @@ -3,7 +3,7 @@ package de.kif.backend.route import de.kif.backend.authenticateOrRedirect import de.kif.backend.repository.TrackRepository -import de.kif.backend.util.Search +import de.kif.common.Search import de.kif.backend.view.MainTemplate import de.kif.backend.view.MenuTemplate import de.kif.backend.view.TableTemplate @@ -118,9 +118,10 @@ fun Route.track() { } for (u in list) { - if (Search.match(search, u.name)) { + val s = u.createSearch() + if (Search.match(search, s)) { entry { - attributes["data-search"] = Search.pack(u.name) + attributes["data-search"] = s.stringify() td { +u.name } diff --git a/src/jvmMain/kotlin/de/kif/backend/route/User.kt b/src/jvmMain/kotlin/de/kif/backend/route/User.kt index 0fe31f0..d53c77c 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/User.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/User.kt @@ -4,7 +4,7 @@ package de.kif.backend.route import de.kif.backend.authenticateOrRedirect import de.kif.backend.hashPassword import de.kif.backend.repository.UserRepository -import de.kif.backend.util.Search +import de.kif.common.Search import de.kif.backend.view.MainTemplate import de.kif.backend.view.MenuTemplate import de.kif.backend.view.TableTemplate @@ -66,9 +66,10 @@ fun Route.user() { } for (u in list) { - if (Search.match(search, u.username)) { + val s = u.createSearch() + if (Search.match(search, s)) { entry { - attributes["data-search"] = Search.pack(u.username) + attributes["data-search"] = s.stringify() td { +u.username } diff --git a/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt b/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt index 7f4efea..af753a4 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt @@ -4,10 +4,10 @@ package de.kif.backend.route import de.kif.backend.authenticateOrRedirect import de.kif.backend.repository.TrackRepository import de.kif.backend.repository.WorkGroupRepository -import de.kif.backend.util.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.Language import de.kif.common.model.Permission import de.kif.common.model.WorkGroup @@ -20,12 +20,9 @@ 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.find -import kotlin.collections.firstOrNull -import kotlin.collections.mapValues import kotlin.collections.set fun Route.workGroup() { @@ -84,34 +81,36 @@ fun Route.workGroup() { } for (u in list) { - if (Search.match(search, u.name)) { - entry { - attributes["data-search"] = Search.pack(u.name) - td { - +u.name - } - td { - +u.interested.toString() - } - td { - +(u.track?.name ?: "") - } - td { - +u.projector.toString() - } - td { - +u.resolution.toString() - } - td { - +u.length.toString() - } - td { - +u.language.toString() - } - td(classes = "action") { - a("/workgroup/${u.id}") { - i("material-icons") { +"edit" } - } + val s = u.createSearch() + 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.interested.toString() + } + td { + +(u.track?.name ?: "") + } + td { + +u.projector.toString() + } + td { + +u.resolution.toString() + } + td { + +u.length.toString() + } + td { + +u.language.localeName + } + td(classes = "action") { + a("/workgroup/${u.id}") { + i("material-icons") { +"edit" } } } } @@ -171,30 +170,31 @@ fun Route.workGroup() { htmlFor = "track" +"Track" } - div("input-group") { - select( - classes = "form-control" - ) { - name = "track" + //div("input-group") { + select( + classes = "form-control" + ) { + name = "track" + option { + selected = (editWorkGroup.track?.id ?: -1) < 0 + value = "-1" + +"None" + } + for (track in tracks) { option { - selected = (editWorkGroup.track?.id ?: -1) < 0 - value = "-1" - +"None" - } - for (track in tracks) { - option { - selected = editWorkGroup.track?.id == track.id - value = track.id.toString() - +track.name - } + selected = editWorkGroup.track?.id == track.id + value = track.id.toString() + +track.name } } - + } + /* a("/tracks", classes = "form-btn") { i("material-icons") { +"edit" } } } + */ } div("form-group") { label { diff --git a/src/jvmMain/kotlin/de/kif/backend/util/Search.kt b/src/jvmMain/kotlin/de/kif/backend/util/Search.kt deleted file mode 100644 index 5467a32..0000000 --- a/src/jvmMain/kotlin/de/kif/backend/util/Search.kt +++ /dev/null @@ -1,29 +0,0 @@ -package de.kif.backend.util - -object Search { - private fun permute(list: List): List> { - if (list.size <= 1) return listOf(list) - - val perm = mutableListOf>() - - for (elem in list) { - val p = permute(list - elem) - - for (x in p) { - perm += x + elem - } - } - - return perm - } - - fun match(search: String, vararg params: String): Boolean { - val s = search.toLowerCase().replace(" ", "") - - return permute(params.map { it.toLowerCase().replace(" ", "") }).any { - it.joinToString("").contains(s) - } - } - - fun pack(vararg params: String): String = params.joinToString("|") { it.toLowerCase() } -} diff --git a/src/jvmMain/kotlin/de/kif/backend/view/TableTemplate.kt b/src/jvmMain/kotlin/de/kif/backend/view/TableTemplate.kt index 2d76c41..0474106 100644 --- a/src/jvmMain/kotlin/de/kif/backend/view/TableTemplate.kt +++ b/src/jvmMain/kotlin/de/kif/backend/view/TableTemplate.kt @@ -15,12 +15,14 @@ class TableTemplate() : Template { override fun FlowContent.apply() { div("table-layout") { form(classes = "form-group table-layout-search") { - input(InputType.search, name = "search", classes = "form-control") { - placeholder = "Search" - value = searchValue - } - button(type = ButtonType.submit, classes = "form-btn btn-search") { - i("material-icons") { +"search" } + div("input-group") { + input(InputType.search, name = "search", classes = "form-control") { + placeholder = "Search" + value = searchValue + } + button(type = ButtonType.submit, classes = "form-btn btn-search") { + i("material-icons") { +"search" } + } } } div("table-layout-action") { diff --git a/src/jvmTest/resources/sample.http b/src/jvmTest/resources/sample.http new file mode 100644 index 0000000..caaf0e8 --- /dev/null +++ b/src/jvmTest/resources/sample.http @@ -0,0 +1,75 @@ +POST http://localhost:8080/api/authenticate +Content-Type: application/json + +{ + "username": "admin", + "password": "admin" +} + + +> {% + client.assert(response.body.OK === true, "Login failed!"); + client.global.set("auth_token", response.headers.valueOf("Set-Cookie")); + %} + +### + +POST http://localhost:8080/api/rooms +Content-Type: application/json +Cookie: {{auth_token}} + +{ + "name": "E006", + "places": 20, + "projector": true +} + +### + +POST http://localhost:8080/api/rooms +Content-Type: application/json +Cookie: {{auth_token}} + +{ + "name": "E007", + "places": 20, + "projector": true +} + +### + +POST http://localhost:8080/api/rooms +Content-Type: application/json +Cookie: {{auth_token}} + +{ + "name": "E008", + "places": 20, + "projector": true +} + +### + +POST http://localhost:8080/api/rooms +Content-Type: application/json +Cookie: {{auth_token}} + +{ + "name": "E009", + "places": 20, + "projector": true +} + +### + +POST http://localhost:8080/api/rooms +Content-Type: application/json +Cookie: {{auth_token}} + +{ + "name": "E010", + "places": 20, + "projector": true +} + +###