diff --git a/src/jsMain/kotlin/de/kif/frontend/WebSocketClient.kt b/src/jsMain/kotlin/de/kif/frontend/WebSocketClient.kt index b7470e9..71b0bd0 100644 --- a/src/jsMain/kotlin/de/kif/frontend/WebSocketClient.kt +++ b/src/jsMain/kotlin/de/kif/frontend/WebSocketClient.kt @@ -14,10 +14,14 @@ class WebSocketClient() { private val url = "ws://${window.location.host}/" private lateinit var ws: WebSocket + private var reconnect = false @Suppress("UNUSED_PARAMETER") private fun onOpen(event: Event) { console.log("Connected!") + if (reconnect) { + window.location.reload() + } } private fun onMessage(messageEvent: MessageEvent) { @@ -37,6 +41,7 @@ class WebSocketClient() { @Suppress("UNUSED_PARAMETER") private fun onClose(event: Event) { console.log("Disconnected!") + reconnect = true async(1000) { connect() } diff --git a/src/jsMain/kotlin/de/kif/frontend/extensions.kt b/src/jsMain/kotlin/de/kif/frontend/extensions.kt index d07fa12..41fa374 100644 --- a/src/jsMain/kotlin/de/kif/frontend/extensions.kt +++ b/src/jsMain/kotlin/de/kif/frontend/extensions.kt @@ -6,31 +6,6 @@ import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.startCoroutine - -operator fun HTMLCollection.iterator() = object : Iterator { - private var index = 0 - override fun hasNext(): Boolean { - return index < this@iterator.length - } - - override fun next(): HTMLElement { - return this@iterator.get(index++) as HTMLElement - } - -} - -operator fun NodeList.iterator() = object : Iterator { - private var index = 0 - override fun hasNext(): Boolean { - return index < this@iterator.length - } - - override fun next(): Node { - return this@iterator.get(index++)!! - } - -} - fun launch(context: CoroutineContext = EmptyCoroutineContext, block: suspend () -> Unit) = block.startCoroutine(Continuation(context) { result -> result.onFailure { exception -> diff --git a/src/jsMain/kotlin/de/kif/frontend/views/WorkGroupConstraints.kt b/src/jsMain/kotlin/de/kif/frontend/views/WorkGroupConstraints.kt index 9286364..2183bd9 100644 --- a/src/jsMain/kotlin/de/kif/frontend/views/WorkGroupConstraints.kt +++ b/src/jsMain/kotlin/de/kif/frontend/views/WorkGroupConstraints.kt @@ -1,6 +1,5 @@ package de.kif.frontend.views -import de.kif.frontend.iterator import de.kif.frontend.launch import de.kif.frontend.repository.WorkGroupRepository import de.westermann.kobserve.event.EventListener @@ -8,6 +7,7 @@ import de.westermann.kwebview.View import de.westermann.kwebview.async import de.westermann.kwebview.components.* import de.westermann.kwebview.createHtmlView +import de.westermann.kwebview.iterator import org.w3c.dom.* import kotlin.browser.document diff --git a/src/jsMain/kotlin/de/kif/frontend/views/board/Board.kt b/src/jsMain/kotlin/de/kif/frontend/views/board/Board.kt index 0763ba3..dba753f 100644 --- a/src/jsMain/kotlin/de/kif/frontend/views/board/Board.kt +++ b/src/jsMain/kotlin/de/kif/frontend/views/board/Board.kt @@ -1,7 +1,7 @@ package de.kif.frontend.views.board import de.kif.common.formatDateTime -import de.kif.frontend.iterator +import de.westermann.kwebview.iterator import de.westermann.kwebview.interval import org.w3c.dom.HTMLElement import org.w3c.dom.get diff --git a/src/jsMain/kotlin/de/kif/frontend/views/calendar/Calendar.kt b/src/jsMain/kotlin/de/kif/frontend/views/calendar/Calendar.kt index b5ca675..1e21537 100644 --- a/src/jsMain/kotlin/de/kif/frontend/views/calendar/Calendar.kt +++ b/src/jsMain/kotlin/de/kif/frontend/views/calendar/Calendar.kt @@ -1,22 +1,23 @@ package de.kif.frontend.views.calendar -import de.kif.frontend.iterator import de.kif.frontend.launch +import de.kif.frontend.repository.RoomRepository import de.kif.frontend.repository.ScheduleRepository import de.westermann.kwebview.View +import de.westermann.kwebview.createHtmlView +import de.westermann.kwebview.iterator import org.w3c.dom.* import kotlin.browser.document import kotlin.browser.window class Calendar(calendar: HTMLElement) : View(calendar) { - var calendarEntries: List = emptyList() - var calendarCells: List = emptyList() - val day: Int + val day: Int = calendar.dataset["day"]?.toIntOrNull() ?: -1 - val htmlTag = document.body as HTMLElement + private val htmlTag = document.body as HTMLElement val calendarTable = calendar.getElementsByClassName("calendar-table")[0] as HTMLElement + val calendarTableHeader = calendar.getElementsByClassName("calendar-header")[0] as HTMLElement fun scrollVerticalBy(pixel: Double, scrollBehavior: ScrollBehavior = ScrollBehavior.SMOOTH) { htmlTag.scrollBy(ScrollToOptions(0.0, pixel, scrollBehavior)) @@ -34,58 +35,22 @@ class Calendar(calendar: HTMLElement) : View(calendar) { calendarTable.scrollTo(ScrollToOptions(pixel, 0.0, scrollBehavior)) } + val editable = calendar.dataset["editable"]?.toBoolean() ?: false + + val body = CalendarBody(this, calendar.getElementsByClassName("calendar-body")[0] as HTMLElement) + 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() - calendarEntries -= entry - } - } - } - - (document.getElementById("calendar-check-constraints") as? HTMLElement)?.let { wrap(it) }?.onClick?.addListener { + (document.getElementById("calendar-check-constraints") as? HTMLElement)?.let { wrap(it) } + ?.onClick?.addListener { launch { val errors = ScheduleRepository.checkConstraints() - println(errors) - for ((s, l) in errors.map) { - for (entry in calendarEntries) { + for (entry in body.calendarEntries) { if (entry.scheduleId == s) { entry.error = l.isNotEmpty() } @@ -106,6 +71,29 @@ class Calendar(calendar: HTMLElement) : View(calendar) { it.preventDefault() } + + RoomRepository.onCreate { + val cell = createHtmlView() + cell.dataset["room"] = it.toString() + cell.classList.add("calendar-cell") + calendarTableHeader.appendChild(cell) + + launch { + val room = RoomRepository.get(it) ?: return@launch + val span = createHtmlView() + span.textContent = room.name + cell.appendChild(span) + } + } + + RoomRepository.onDelete { + val str = it.toString() + for (element in calendarTableHeader.children.iterator()) { + if (element.dataset["room"] == str) { + element.remove() + } + } + } } } diff --git a/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarBody.kt b/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarBody.kt new file mode 100644 index 0000000..449a0bb --- /dev/null +++ b/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarBody.kt @@ -0,0 +1,161 @@ +package de.kif.frontend.views.calendar + +import de.kif.frontend.launch +import de.kif.frontend.repository.ScheduleRepository +import de.westermann.kwebview.ViewCollection +import de.westermann.kwebview.interval +import de.westermann.kwebview.iterator +import org.w3c.dom.HTMLElement +import kotlin.browser.document +import kotlin.js.Date +import kotlin.math.max +import kotlin.math.min + + +class CalendarBody(val calendar: Calendar, view: HTMLElement) : ViewCollection(view) { + + val editable = calendar.editable + val day = calendar.day + + var calendarEntries: List = emptyList() + + val calendarCells: List + get() = iterator().asSequence().flatten().toList() + + private suspend fun updateRows(startTime: Int? = null, length: Int = 0) { + if (calendarEntries.isEmpty() && startTime == null && !editable) { + for (row in iterator().asSequence().toList()) { + remove(row) + } + return + } + + var max: Int + var min: Int + + if (startTime != null) { + min = startTime + max = startTime + length + } else { + min = calendarEntries.first().startTime + max = calendarEntries.first().startTime + calendarEntries.first().length + } + + for (entry in calendarEntries) { + max = max(max, entry.startTime + entry.length) + min = min(min, entry.startTime) + } + + if (min > max) { + val h1 = max + max = min + min = h1 + } + + if (editable) { + min = min(min, 0) + max = max(max, 24 * 60) + } + + min = (min / 60 - 1) * 60 + max = (max / 60 + 2) * 60 + + while (isNotEmpty() && min > first().time) { + remove(first()) + } + + while (isNotEmpty() && max < last().time) { + remove(last()) + } + + if (isEmpty()) { + +CalendarRow.create(this, min) + } + + while (min < first().time) { + prepand(CalendarRow.create(this, first().time - 15)) + } + + while (max > last().time + 15) { + append(CalendarRow.create(this, last().time + 15)) + } + } + + init { + calendarEntries = document.getElementsByClassName("calendar-entry") + .iterator().asSequence().map { CalendarEntry(this, it) }.toList() + + wrapContent { + CalendarRow(this, it) + } + + ScheduleRepository.onCreate { + launch { + val schedule = ScheduleRepository.get(it) ?: throw NoSuchElementException() + + updateRows(schedule.time, schedule.workGroup.length) + + calendarEntries += CalendarEntry.create(this, schedule) + } + } + ScheduleRepository.onUpdate { + launch { + val schedule = ScheduleRepository.get(it) ?: throw NoSuchElementException() + + updateRows(schedule.time, schedule.workGroup.length) + + var found = false + for (entry in calendarEntries) { + if (entry.scheduleId == it) { + entry.load(schedule) + found = true + } + } + if (!found) { + calendarEntries += CalendarEntry.create(this, schedule) + } + + updateRows() + } + } + ScheduleRepository.onDelete { + for (entry in calendarEntries) { + if (entry.scheduleId == it) { + entry.html.remove() + calendarEntries -= entry + + launch { + updateRows() + } + } + } + } + + interval(1000) { + val currentTime = Date().let { + it.getHours() * 60 + it.getMinutes() + } + val rowTime = (currentTime / 15) * 15 + + for (row in this) { + if (row.time == rowTime) { + row.classList.clear() + for (str in row.classList) { + if ("now" in str) { + row.classList -= str + } + } + row.classList += "calendar-row" + row.classList += "calendar-now" + row.classList += "calendar-now-${currentTime - rowTime}" + } else { + for (str in row.classList) { + if ("now" in str) { + row.classList -= str + } + } + } + } + } + } +} diff --git a/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarCell.kt b/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarCell.kt index 0e9bc8c..04e6acf 100644 --- a/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarCell.kt +++ b/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarCell.kt @@ -3,12 +3,17 @@ package de.kif.frontend.views.calendar import de.kif.common.model.Room import de.kif.frontend.repository.RoomRepository import de.westermann.kwebview.ViewCollection +import de.westermann.kwebview.createHtmlView +import org.w3c.dom.HTMLDivElement import org.w3c.dom.HTMLElement import org.w3c.dom.get +import org.w3c.dom.set +import kotlin.browser.document + +class CalendarCell(row: CalendarRow, view: HTMLElement) : ViewCollection(view) { + val day = row.day + val time = row.time -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 @@ -22,7 +27,13 @@ class CalendarCell(view: HTMLElement) : ViewCollection(view) { return room } - init { - (view.getElementsByClassName("calendar-link")[0] as? HTMLElement)?.remove() + companion object { + fun create(row: CalendarRow, roomId: Long): CalendarCell { + val view = createHtmlView() + view.classList.add("calendar-cell") + view.dataset["room"] = roomId.toString() + + return CalendarCell(row, view) + } } } \ 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 index be5fed6..42dc9f3 100644 --- a/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarEntry.kt +++ b/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarEntry.kt @@ -1,10 +1,9 @@ package de.kif.frontend.views.calendar import de.kif.common.CALENDAR_GRID_WIDTH -import de.kif.common.model.Room import de.kif.common.model.Schedule import de.kif.common.model.WorkGroup -import de.kif.frontend.iterator +import de.westermann.kwebview.iterator import de.kif.frontend.launch import de.kif.frontend.repository.RepositoryDelegate import de.kif.frontend.repository.ScheduleRepository @@ -17,7 +16,7 @@ import kotlin.dom.appendText import kotlin.dom.isText import kotlin.js.Date -class CalendarEntry(private val calendar: Calendar, view: HTMLElement) : View(view) { +class CalendarEntry(private val calendar: CalendarBody, view: HTMLElement) : View(view) { private lateinit var mouseDelta: Point private var newCell: CalendarCell? = null @@ -31,11 +30,15 @@ class CalendarEntry(private val calendar: Calendar, view: HTMLElement) : View(vi private lateinit var workGroup: WorkGroup + var startTime = dataset["time"]?.toIntOrNull() ?: 0 + var length = dataset["length"]?.toIntOrNull() ?: 0 + var pending by classList.property("pending") var error by classList.property("error") private var nextScroll = 0.0 - var editable: Boolean = false + val editable: Boolean + get() = calendar.editable var moveLookRoom: Long? = null var moveLookTime: Int? = null @@ -48,7 +51,7 @@ class CalendarEntry(private val calendar: Calendar, view: HTMLElement) : View(vi } if (cell != null) { - if (moveLookRoom != null && cell.roomId != moveLookRoom) { + if (moveLookRoom != null && cell.roomId != moveLookRoom) { return } if (moveLookTime != null && cell.time != moveLookTime) { @@ -70,29 +73,29 @@ class CalendarEntry(private val calendar: Calendar, view: HTMLElement) : View(vi return@async } - val width = calendar.calendarTable.clientWidth + val width = calendar.calendar.calendarTable.clientWidth val height = window.innerHeight val rect = html.getBoundingClientRect() if (rect.left < 0.0) { nextScroll = now + 500.0 - calendar.scrollHorizontalBy(rect.left - 80.0) + calendar.calendar.scrollHorizontalBy(rect.left - 80.0) } else if (rect.right > width) { nextScroll = now + 0.500 - calendar.scrollHorizontalBy(rect.right - width + 50.0) + calendar.calendar.scrollHorizontalBy(rect.right - width + 50.0) } if (rect.top < 20.0) { nextScroll = now + 500.0 - calendar.scrollVerticalBy(rect.top - 50.0) + calendar.calendar.scrollVerticalBy(rect.top - 50.0) } else if (rect.bottom > height - 20.0) { nextScroll = now + 500.0 - calendar.scrollVerticalBy(rect.bottom - height + 50.0) + calendar.calendar.scrollVerticalBy(rect.bottom - height + 50.0) } } launch { - calendarTools.setName(cell.getRoom(), cell.time) + calendarTools?.setName(cell.getRoom(), cell.time) } newCell = cell @@ -153,7 +156,7 @@ class CalendarEntry(private val calendar: Calendar, view: HTMLElement) : View(vi ) } - private val calendarTools = CalendarTools(this) + private val calendarTools = if (editable) CalendarTools(this) else null init { onMouseDown { event -> @@ -181,11 +184,13 @@ class CalendarEntry(private val calendar: Calendar, view: HTMLElement) : View(vi event.stopPropagation() } - html.appendChild(calendarTools.html) + if (calendarTools != null) { + html.appendChild(calendarTools.html) - launch { - val s = schedule.get() - calendarTools.update(s) + launch { + val s = schedule.get() + calendarTools.update(s) + } } } @@ -205,7 +210,10 @@ class CalendarEntry(private val calendar: Calendar, view: HTMLElement) : View(vi } load(schedule.workGroup) - calendarTools.update(schedule) + calendarTools?.update(schedule) + + startTime = schedule.time + length = schedule.workGroup.length val time = schedule.time / CALENDAR_GRID_WIDTH * CALENDAR_GRID_WIDTH val cell = calendar.calendarCells.find { @@ -247,7 +255,7 @@ class CalendarEntry(private val calendar: Calendar, view: HTMLElement) : View(vi } companion object { - fun create(calendar: Calendar, schedule: Schedule): CalendarEntry { + fun create(calendar: CalendarBody, schedule: Schedule): CalendarEntry { val entry = CalendarEntry(calendar, createHtmlView()) entry.load(schedule) @@ -255,7 +263,7 @@ class CalendarEntry(private val calendar: Calendar, view: HTMLElement) : View(vi return entry } - fun create(calendar: Calendar, workGroup: WorkGroup): CalendarEntry { + fun create(calendar: CalendarBody, workGroup: WorkGroup): CalendarEntry { val entry = CalendarEntry(calendar, createHtmlView()) entry.load(workGroup) diff --git a/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarRow.kt b/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarRow.kt new file mode 100644 index 0000000..6d84a17 --- /dev/null +++ b/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarRow.kt @@ -0,0 +1,65 @@ +package de.kif.frontend.views.calendar + +import de.kif.frontend.repository.RoomRepository +import de.westermann.kwebview.ViewCollection +import de.westermann.kwebview.createHtmlView +import org.w3c.dom.HTMLDivElement +import org.w3c.dom.HTMLElement +import org.w3c.dom.HTMLSpanElement +import org.w3c.dom.set + +class CalendarRow(calendar: CalendarBody, view: HTMLElement) : ViewCollection(view) { + val day = calendar.day + + val time = dataset["time"]?.toIntOrNull() ?: 0 + + init { + wrapContent { + CalendarCell(this, it) + } + + RoomRepository.onCreate { + +CalendarCell.create(this, it) + } + RoomRepository.onDelete { id -> + find { it.roomId == id }?.let(this@CalendarRow::remove) + } + } + + companion object { + suspend fun create(calendar: CalendarBody, time: Int): CalendarRow { + val view = createHtmlView() + view.classList.add("calendar-row") + view.dataset["time"] = time.toString() + + val row = CalendarRow(calendar, view) + + val rowHeader = createHtmlView() + rowHeader.classList.add("calendar-cell") + if (time % 60 == 0) { + val span = createHtmlView() + + val t = (time % (60 * 24)).let { + if (it < 0) it + 60 * 24 else it + } + val hours = (t / 60).toString().padStart(2, '0') + span.textContent = "$hours:00" + + rowHeader.appendChild(span) + } + row.html.appendChild(rowHeader) + + row.html + + val rooms = RoomRepository.all() + + for (room in rooms) { + if (room.id != null) { + row += CalendarCell.create(row, room.id) + } + } + + return row + } + } +} diff --git a/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarWorkGroup.kt b/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarWorkGroup.kt index f970f55..443f8d1 100644 --- a/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarWorkGroup.kt +++ b/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarWorkGroup.kt @@ -50,7 +50,7 @@ class CalendarWorkGroup( } onMouseDown { - CalendarEntry.create(calendar, workGroup) + CalendarEntry.create(calendar.body, workGroup) } } } diff --git a/src/jsMain/kotlin/de/kif/frontend/views/overview/OverviewMain.kt b/src/jsMain/kotlin/de/kif/frontend/views/overview/OverviewMain.kt index 61ac882..6087039 100644 --- a/src/jsMain/kotlin/de/kif/frontend/views/overview/OverviewMain.kt +++ b/src/jsMain/kotlin/de/kif/frontend/views/overview/OverviewMain.kt @@ -1,6 +1,6 @@ package de.kif.frontend.views.overview -import de.kif.frontend.iterator +import de.westermann.kwebview.iterator import de.kif.frontend.launch import de.kif.frontend.repository.PostRepository import de.westermann.kobserve.event.subscribe diff --git a/src/jsMain/kotlin/de/kif/frontend/views/table/RoomTableLine.kt b/src/jsMain/kotlin/de/kif/frontend/views/table/RoomTableLine.kt index f42af51..878ee44 100644 --- a/src/jsMain/kotlin/de/kif/frontend/views/table/RoomTableLine.kt +++ b/src/jsMain/kotlin/de/kif/frontend/views/table/RoomTableLine.kt @@ -1,7 +1,7 @@ package de.kif.frontend.views.table import de.kif.common.SearchElement -import de.kif.frontend.iterator +import de.westermann.kwebview.iterator import de.kif.frontend.launch import de.kif.frontend.repository.RepositoryDelegate import de.kif.frontend.repository.RoomRepository diff --git a/src/jsMain/kotlin/de/kif/frontend/views/table/TableLayout.kt b/src/jsMain/kotlin/de/kif/frontend/views/table/TableLayout.kt index c55aed2..8a3a0d9 100644 --- a/src/jsMain/kotlin/de/kif/frontend/views/table/TableLayout.kt +++ b/src/jsMain/kotlin/de/kif/frontend/views/table/TableLayout.kt @@ -1,6 +1,6 @@ package de.kif.frontend.views.table -import de.kif.frontend.iterator +import de.westermann.kwebview.iterator import de.westermann.kwebview.components.InputView import org.w3c.dom.HTMLFormElement import org.w3c.dom.HTMLInputElement diff --git a/src/jsMain/kotlin/de/kif/frontend/views/table/WorkGroupTableLine.kt b/src/jsMain/kotlin/de/kif/frontend/views/table/WorkGroupTableLine.kt index 33e8573..3ef51cc 100644 --- a/src/jsMain/kotlin/de/kif/frontend/views/table/WorkGroupTableLine.kt +++ b/src/jsMain/kotlin/de/kif/frontend/views/table/WorkGroupTableLine.kt @@ -3,7 +3,7 @@ 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.westermann.kwebview.iterator import de.kif.frontend.launch import de.kif.frontend.repository.RepositoryDelegate import de.kif.frontend.repository.TrackRepository diff --git a/src/jsMain/kotlin/de/westermann/kwebview/ClassList.kt b/src/jsMain/kotlin/de/westermann/kwebview/ClassList.kt index 19aa695..022fbe7 100644 --- a/src/jsMain/kotlin/de/westermann/kwebview/ClassList.kt +++ b/src/jsMain/kotlin/de/westermann/kwebview/ClassList.kt @@ -1,10 +1,17 @@ package de.westermann.kwebview -import de.westermann.kobserve.event.EventListener import de.westermann.kobserve.Property import de.westermann.kobserve.ReadOnlyProperty +import de.westermann.kobserve.event.EventListener import de.westermann.kobserve.property.property import org.w3c.dom.DOMTokenList +import kotlin.collections.Iterable +import kotlin.collections.Iterator +import kotlin.collections.MutableMap +import kotlin.collections.contains +import kotlin.collections.minusAssign +import kotlin.collections.mutableMapOf +import kotlin.collections.set /** * Represents the css classes of an html element. @@ -12,7 +19,7 @@ import org.w3c.dom.DOMTokenList * @author lars */ class ClassList( - private val list: DOMTokenList + private val list: DOMTokenList ) : Iterable { private val bound: MutableMap = mutableMapOf() @@ -74,11 +81,11 @@ class ClassList( * Set css class present. */ operator fun set(clazz: String, present: Boolean) = - if (present) { - add(clazz) - } else { - remove(clazz) - } + if (present) { + add(clazz) + } else { + remove(clazz) + } /** * Toggle css class. @@ -92,9 +99,9 @@ class ClassList( set(clazz, property.value) bound[clazz] = Bound(property, - property.onChange.reference { - list.toggle(clazz, property.value) - } + property.onChange.reference { + list.toggle(clazz, property.value) + } ) } @@ -130,8 +137,14 @@ class ClassList( override fun toString(): String = list.value + fun clear() { + for (element in this) { + remove(element) + } + } + private data class Bound( - val property: ReadOnlyProperty, - val reference: EventListener? + val property: ReadOnlyProperty, + val reference: EventListener? ) } \ No newline at end of file diff --git a/src/jsMain/kotlin/de/westermann/kwebview/ViewCollection.kt b/src/jsMain/kotlin/de/westermann/kwebview/ViewCollection.kt index f9f3b3e..4b0debf 100644 --- a/src/jsMain/kotlin/de/westermann/kwebview/ViewCollection.kt +++ b/src/jsMain/kotlin/de/westermann/kwebview/ViewCollection.kt @@ -8,7 +8,15 @@ import kotlin.dom.clear */ abstract class ViewCollection(view: HTMLElement = createHtmlView()) : View(view), Collection { - private val children: MutableList = mutableListOf() + protected val children: MutableList = mutableListOf() + + protected inline fun wrapContent(transform: (T) -> V) { + for (element in html.children.iterator()) { + children += transform(element as T) + } + } + + protected inline fun wrapContent(transform: (HTMLElement) -> V) = wrapContent(transform) fun append(view: V) { children += view diff --git a/src/jsMain/kotlin/de/westermann/kwebview/extensions.kt b/src/jsMain/kotlin/de/westermann/kwebview/extensions.kt index ced083b..f461ae4 100644 --- a/src/jsMain/kotlin/de/westermann/kwebview/extensions.kt +++ b/src/jsMain/kotlin/de/westermann/kwebview/extensions.kt @@ -1,8 +1,7 @@ package de.westermann.kwebview import de.westermann.kobserve.event.EventHandler -import org.w3c.dom.DOMRect -import org.w3c.dom.HTMLElement +import org.w3c.dom.* import org.w3c.dom.events.Event import org.w3c.dom.events.EventListener import org.w3c.dom.events.MouseEvent @@ -11,6 +10,30 @@ import org.w3c.xhr.XMLHttpRequest import kotlin.browser.document import kotlin.browser.window +operator fun HTMLCollection.iterator() = object : Iterator { + private var index = 0 + override fun hasNext(): Boolean { + return index < this@iterator.length + } + + override fun next(): HTMLElement { + return this@iterator.get(index++) as HTMLElement + } + +} + +operator fun NodeList.iterator() = object : Iterator { + private var index = 0 + override fun hasNext(): Boolean { + return index < this@iterator.length + } + + override fun next(): Node { + return this@iterator.get(index++)!! + } + +} + inline fun createHtmlView(tag: String? = null): V { var tagName: String if (tag != null) { diff --git a/src/jsMain/resources/style/components/_calendar.scss b/src/jsMain/resources/style/components/_calendar.scss index ee6206a..c49b293 100644 --- a/src/jsMain/resources/style/components/_calendar.scss +++ b/src/jsMain/resources/style/components/_calendar.scss @@ -384,7 +384,7 @@ border-left: none; } - &:nth-child(4n + 2) .calendar-cell::before { + &:nth-child(4n + 1) .calendar-cell::before { border-top: solid 1px var(--table-border-color); } @@ -424,6 +424,10 @@ &.time-to-room { flex-direction: row; + .calendar-body { + display: flex; + } + .calendar-header, .calendar-row { flex-direction: column; line-height: 3rem; @@ -468,7 +472,7 @@ } } - &:nth-child(2n + 2) .calendar-cell::before { + &:nth-child(4n + 1) .calendar-cell::before { content: ''; height: 100%; left: 0; diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Account.kt b/src/jvmMain/kotlin/de/kif/backend/route/Account.kt index c6c5a8d..e651308 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Account.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Account.kt @@ -40,10 +40,6 @@ fun Route.account() { val wikiSections = WikiImporter.loadSections() respondMain { - menuTemplate { - this.user = user - active = MenuTemplate.Tab.ACCOUNT - } content { h1 { +"Account" } div { diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Calendar.kt b/src/jvmMain/kotlin/de/kif/backend/route/Calendar.kt index c931c85..bb6c86a 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Calendar.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Calendar.kt @@ -6,7 +6,6 @@ import de.kif.backend.Configuration import de.kif.backend.isAuthenticated import de.kif.backend.repository.RoomRepository import de.kif.backend.repository.ScheduleRepository -import de.kif.backend.view.MenuTemplate import de.kif.backend.view.respondMain import de.kif.common.CALENDAR_GRID_WIDTH import de.kif.common.model.Permission @@ -51,6 +50,8 @@ private fun DIV.calendarCell(schedule: Schedule?) { }.toString() attributes["data-language"] = schedule.workGroup.language.code attributes["data-id"] = schedule.id.toString() + attributes["data-time"] = schedule.time.toString() + attributes["data-length"] = schedule.workGroup.length.toString() +schedule.workGroup.name } @@ -81,6 +82,8 @@ private fun DIV.renderCalendar( for (room in rooms) { div("calendar-cell") { + attributes["data-room"] = room.id.toString() + span { +room.name } @@ -88,43 +91,46 @@ private fun DIV.renderCalendar( } } - for (i in 0 until minutesOfDay / CALENDAR_GRID_WIDTH) { - val time = ((i * CALENDAR_GRID_WIDTH + from) % MINUTES_OF_DAY).let { - if (it < 0) it + MINUTES_OF_DAY else it - } - val minutes = (time % 60).toString().padStart(2, '0') - val hours = (time / 60).toString().padStart(2, '0') - val timeString = "$hours:$minutes" + div("calendar-body") { + for (i in 0 until minutesOfDay / CALENDAR_GRID_WIDTH) { + val time = ((i * CALENDAR_GRID_WIDTH + from) % MINUTES_OF_DAY).let { + if (it < 0) it + MINUTES_OF_DAY else it + } + val minutes = (time % 60).toString().padStart(2, '0') + val hours = (time / 60).toString().padStart(2, '0') + val timeString = "$hours:$minutes" - val start = i * CALENDAR_GRID_WIDTH + from - val end = (i + 1) * CALENDAR_GRID_WIDTH + from - 1 + val start = i * CALENDAR_GRID_WIDTH + from + val end = (i + 1) * CALENDAR_GRID_WIDTH + from - 1 - var rowClass = "calendar-row" + var rowClass = "calendar-row" - if (currentTime in start..end) { - rowClass += " calendar-now calendar-now-${currentTime - start}" - } - - div(rowClass) { - - div("calendar-cell") { - if (time % gridLabelWidth == 0) { - span { - +timeString - } - } + if (currentTime in start..end) { + rowClass += " calendar-now calendar-now-${currentTime - start}" } - for (room in rooms) { + div(rowClass) { + attributes["data-time"] = start.toString() + attributes["data-day"] = day.toString() + div("calendar-cell") { - attributes["data-time"] = start.toString() - attributes["data-room"] = room.id.toString() - attributes["data-day"] = day.toString() - title = room.name + " - " + timeString + if (time % gridLabelWidth == 0) { + span { + +timeString + } + } + } - val schedule = (start..end).mapNotNull { schedules[room]?.get(it) }.firstOrNull() + for (room in rooms) { + div("calendar-cell") { + attributes["data-room"] = room.id.toString() - calendarCell(schedule) + title = room.name + " - " + timeString + + val schedule = (start..end).mapNotNull { schedules[room]?.get(it) }.firstOrNull() + + calendarCell(schedule) + } } } } @@ -167,9 +173,12 @@ fun Route.calendar() { val day = call.parameters["day"]?.toIntOrNull() ?: return@get val range = ScheduleRepository.getDayRange() + + /* if (!editable && day !in range) { return@get } + */ val rooms = RoomRepository.all() @@ -177,8 +186,8 @@ fun Route.calendar() { CalendarOrientation.values().find { it.name == name } } ?: CalendarOrientation.ROOM_TO_TIME - val h = ScheduleRepository.getByDay(day) - val schedules = h.groupBy { it.room }.mapValues { (_, it) -> + val list = ScheduleRepository.getByDay(day) + val schedules = list.groupBy { it.room }.mapValues { (_, it) -> it.associateBy { it.time } @@ -186,7 +195,7 @@ fun Route.calendar() { var max = 0 var min = 24 * 60 - for (s in h) { + for (s in list) { max = max(max, s.time + s.workGroup.length) min = min(min, s.time) } @@ -205,6 +214,11 @@ fun Route.calendar() { min = (min / 60 - 1) * 60 max = (max / 60 + 2) * 60 + if (!editable && list.isEmpty()) { + min = 0 + max = 0 + } + val refDate = DateTime(Configuration.Schedule.referenceDate.time) val date = refDate + day.days val dateString = DateFormat("EEEE, d. MMMM") @@ -212,15 +226,7 @@ fun Route.calendar() { .format(date) respondMain { - menuTemplate { - this.user = user - active = MenuTemplate.Tab.CALENDAR - } content { - if (rooms.isEmpty()) { - return@content - } - div("header") { div("header-left") { if (editable || day - 1 > range.start) { diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Login.kt b/src/jvmMain/kotlin/de/kif/backend/route/Login.kt index 694cfa6..8915cd5 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Login.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Login.kt @@ -2,12 +2,10 @@ package de.kif.backend.route import de.kif.backend.PortalSession import de.kif.backend.UserPrinciple -import de.kif.backend.view.MainTemplate import de.kif.backend.view.respondMain import io.ktor.application.call import io.ktor.auth.authenticate import io.ktor.auth.principal -import io.ktor.html.respondHtmlTemplate import io.ktor.response.respondRedirect import io.ktor.routing.Route import io.ktor.routing.get diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Overview.kt b/src/jvmMain/kotlin/de/kif/backend/route/Overview.kt index d6f8ede..e280ca4 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Overview.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Overview.kt @@ -78,10 +78,6 @@ fun Route.overview() { val postList = PostRepository.all().asReversed() respondMain { - menuTemplate { - this.user = user - active = MenuTemplate.Tab.BOARD - } content { div("overview") { div("overview-main") { @@ -118,10 +114,6 @@ fun Route.overview() { } respondMain { - menuTemplate { - this.user = user - active = MenuTemplate.Tab.BOARD - } content { div("overview") { createPost(post, editable) @@ -135,10 +127,6 @@ fun Route.overview() { val postId = call.parameters["id"]?.toLongOrNull() ?: return@get val editPost = PostRepository.get(postId) ?: return@get respondMain { - menuTemplate { - this.user = user - active = MenuTemplate.Tab.BOARD - } content { h1 { +"Edit post" } div("post-edit-container") { @@ -365,10 +353,6 @@ fun Route.overview() { get("/post/new") { authenticateOrRedirect(Permission.POST) { user -> respondMain { - menuTemplate { - this.user = user - active = MenuTemplate.Tab.BOARD - } content { h1 { +"Create post" } div("post-edit-container") { diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Room.kt b/src/jvmMain/kotlin/de/kif/backend/route/Room.kt index 9f9bc60..aa193a7 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Room.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Room.kt @@ -33,10 +33,6 @@ fun Route.room() { val list = RoomRepository.all() respondMain { - menuTemplate { - this.user = user - active = MenuTemplate.Tab.ROOM - } content { insert(TableTemplate()) { searchValue = search @@ -113,10 +109,6 @@ fun Route.room() { val roomId = call.parameters["id"]?.toLongOrNull() ?: return@get val editRoom = RoomRepository.get(roomId) ?: return@get respondMain { - menuTemplate { - this.user = user - active = MenuTemplate.Tab.ROOM - } content { h1 { +"Edit room" } form(method = FormMethod.post) { @@ -276,10 +268,6 @@ fun Route.room() { get("/room/new") { authenticateOrRedirect(Permission.ROOM) { user -> respondMain { - menuTemplate { - this.user = user - active = MenuTemplate.Tab.ROOM - } content { h1 { +"Create room" } form(method = FormMethod.post) { diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Track.kt b/src/jvmMain/kotlin/de/kif/backend/route/Track.kt index 785af28..0f05edb 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Track.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Track.kt @@ -90,10 +90,6 @@ fun Route.track() { val search = call.parameters["search"] ?: "" val list = TrackRepository.all() respondMain { - menuTemplate { - this.user = user - active = MenuTemplate.Tab.WORK_GROUP - } content { insert(TableTemplate()) { searchValue = search @@ -150,10 +146,6 @@ fun Route.track() { val trackId = call.parameters["id"]?.toLongOrNull() ?: return@get val editTrack = TrackRepository.get(trackId) ?: return@get respondMain { - menuTemplate { - this.user = user - active = MenuTemplate.Tab.WORK_GROUP - } content { h1 { +"Edit track" } form(method = FormMethod.post) { @@ -219,10 +211,6 @@ fun Route.track() { get("/track/new") { authenticateOrRedirect(Permission.WORK_GROUP) { user -> respondMain { - menuTemplate { - this.user = user - active = MenuTemplate.Tab.WORK_GROUP - } content { h1 { +"Create track" } form(method = FormMethod.post) { diff --git a/src/jvmMain/kotlin/de/kif/backend/route/User.kt b/src/jvmMain/kotlin/de/kif/backend/route/User.kt index 658ee88..eac0d6d 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/User.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/User.kt @@ -33,10 +33,6 @@ fun Route.user() { val search = call.parameters["search"] ?: "" val list = UserRepository.all() respondMain { - menuTemplate { - this.user = user - active = MenuTemplate.Tab.USER - } content { insert(TableTemplate()) { searchValue = search @@ -92,10 +88,6 @@ fun Route.user() { val userId = call.parameters["id"]?.toLongOrNull() ?: return@get val editUser = UserRepository.get(userId) ?: return@get respondMain { - menuTemplate { - this.user = user - active = MenuTemplate.Tab.USER - } content { h1 { +"Edit user" } form(method = FormMethod.post) { @@ -185,10 +177,6 @@ fun Route.user() { get("/user/new") { authenticateOrRedirect(Permission.USER) { user -> respondMain { - menuTemplate { - this.user = user - active = MenuTemplate.Tab.USER - } content { h1 { +"Create user" } form(method = FormMethod.post) { diff --git a/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt b/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt index 00a3a0a..5b5a2a4 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt @@ -29,10 +29,6 @@ fun Route.workGroup() { val search = call.parameters["search"] ?: "" val list = WorkGroupRepository.all() respondMain { - menuTemplate { - this.user = user - active = MenuTemplate.Tab.WORK_GROUP - } content { insert(TableTemplate()) { searchValue = search @@ -166,10 +162,6 @@ fun Route.workGroup() { } respondMain { - menuTemplate { - this.user = user - active = MenuTemplate.Tab.WORK_GROUP - } content { h1 { +"Edit work group" } form(method = FormMethod.post) { @@ -529,10 +521,6 @@ fun Route.workGroup() { authenticateOrRedirect(Permission.WORK_GROUP) { user -> val tracks = TrackRepository.all() respondMain { - menuTemplate { - this.user = user - active = MenuTemplate.Tab.WORK_GROUP - } content { h1 { +"Create work group" } form(method = FormMethod.post) { diff --git a/src/jvmMain/kotlin/de/kif/backend/view/MainTemplate.kt b/src/jvmMain/kotlin/de/kif/backend/view/MainTemplate.kt index 7f1b378..52bfbe5 100644 --- a/src/jvmMain/kotlin/de/kif/backend/view/MainTemplate.kt +++ b/src/jvmMain/kotlin/de/kif/backend/view/MainTemplate.kt @@ -1,21 +1,28 @@ package de.kif.backend.view +import de.kif.backend.PortalSession import de.kif.backend.Resources +import de.kif.backend.authenticate +import de.kif.common.model.User import io.ktor.application.ApplicationCall import io.ktor.application.call import io.ktor.html.* import io.ktor.request.path +import io.ktor.request.uri import io.ktor.response.respondRedirect +import io.ktor.sessions.get +import io.ktor.sessions.sessions import io.ktor.util.pipeline.PipelineContext import kotlinx.html.* class MainTemplate( private val theme: Theme, + private val url: String, + private val user: User?, private val noMenu: Boolean, private val stretch: Boolean ) : Template { val content = Placeholder() - val menuTemplate = TemplatePlaceholder() override fun HTML.apply() { head { @@ -56,7 +63,7 @@ class MainTemplate( } body { if (!noMenu) { - insert(MenuTemplate(), menuTemplate) + insert(MenuTemplate(url, user)) {} } val containerClasses = if (stretch) "container-full" else "container" @@ -88,12 +95,16 @@ class MainTemplate( } enum class Theme { - LIGHT, DARK, PRINCESS -} + LIGHT, DARK, PRINCESS; -private fun String?.toTheme() = this?.let { str -> - Theme.values().find { str == it.name } -} ?: Theme.LIGHT + companion object { + private val loopup = values().toList().associateBy { it.name } + + fun lookup(name: String?): Theme { + return loopup[(name ?: return LIGHT).toUpperCase()] ?: LIGHT + } + } +} suspend fun PipelineContext.respondMain( noMenu: Boolean = false, @@ -101,11 +112,13 @@ suspend fun PipelineContext.respondMain( body: MainTemplate.() -> Unit ) { val param = call.request.queryParameters["theme"] + val url = call.request.uri.substring(1) + val user = call.sessions.get()?.getUser(call) if (param != null) { call.response.cookies.append( name = "theme", - value = param.toTheme().name, + value = Theme.lookup(param).name, maxAge = Int.MAX_VALUE, path = "/" ) @@ -113,7 +126,9 @@ suspend fun PipelineContext.respondMain( } else { call.respondHtmlTemplate( MainTemplate( - call.request.cookies["theme"].toTheme(), + Theme.lookup(call.request.cookies["theme"]), + url, + user, noMenu, stretch ), diff --git a/src/jvmMain/kotlin/de/kif/backend/view/MenuTemplate.kt b/src/jvmMain/kotlin/de/kif/backend/view/MenuTemplate.kt index 364adc0..f92e660 100644 --- a/src/jvmMain/kotlin/de/kif/backend/view/MenuTemplate.kt +++ b/src/jvmMain/kotlin/de/kif/backend/view/MenuTemplate.kt @@ -5,19 +5,21 @@ import de.kif.common.model.User import io.ktor.html.Template import kotlinx.html.* -class MenuTemplate() : Template { - - var active: Tab = Tab.BOARD - var user: User? = null +class MenuTemplate( + private val url: String, + private val user: User? +) : Template { override fun FlowContent.apply() { + val tab = Tab.lookup(url) + nav("menu") { div("container") { div("menu-left") { - a("/", classes = if (active == Tab.BOARD) "active" else null) { - +"Dashboard" + a("/", classes = if (tab == null) "active" else null) { + +"News" } - a("/calendar", classes = if (active == Tab.CALENDAR) "active" else null) { + a("/calendar", classes = if (tab == Tab.CALENDAR) "active" else null) { +"Calendar" } } @@ -28,26 +30,26 @@ class MenuTemplate() : Template { val user = user div("menu-content") { if (user == null) { - a("/account", classes = if (active == Tab.ACCOUNT) "active" else null) { + a("/account", classes = if (tab == Tab.LOGIN) "active" else null) { +"Login" } } else { if (user.checkPermission(Permission.WORK_GROUP)) { - a("/workgroups", classes = if (active == Tab.WORK_GROUP) "active" else null) { + a("/workgroups", classes = if (tab == Tab.WORK_GROUP) "active" else null) { +"Work groups" } } if (user.checkPermission(Permission.ROOM)) { - a("/rooms", classes = if (active == Tab.ROOM) "active" else null) { + a("/rooms", classes = if (tab == Tab.ROOM) "active" else null) { +"Rooms" } } if (user.checkPermission(Permission.USER)) { - a("/users", classes = if (active == Tab.USER) "active" else null) { + a("/users", classes = if (tab == Tab.USER) "active" else null) { +"Users" } } - a("/account", classes = if (active == Tab.ACCOUNT) "active" else null) { + a("/account", classes = if (tab == Tab.ACCOUNT) "active" else null) { +user.username } } @@ -58,6 +60,14 @@ class MenuTemplate() : Template { } enum class Tab { - BOARD, CALENDAR, ACCOUNT, WORK_GROUP, ROOM, PERSON, USER + NEWS, CALENDAR, ACCOUNT, WORK_GROUP, ROOM, USER, LOGIN; + + companion object { + private val lookup = values().toList().associateBy { it.name.take(4).toLowerCase() } + + fun lookup(url: String): Tab? { + return lookup[url.take(4)] + } + } } }