Add dynamic resizing of calendar

This commit is contained in:
Lars Westermann 2019-06-07 21:19:52 +02:00
parent 990cdaf1a4
commit dce2567160
Signed by: lars.westermann
GPG key ID: 9D417FA5BB9D5E1D
28 changed files with 476 additions and 254 deletions

View file

@ -14,10 +14,14 @@ class WebSocketClient() {
private val url = "ws://${window.location.host}/" private val url = "ws://${window.location.host}/"
private lateinit var ws: WebSocket private lateinit var ws: WebSocket
private var reconnect = false
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
private fun onOpen(event: Event) { private fun onOpen(event: Event) {
console.log("Connected!") console.log("Connected!")
if (reconnect) {
window.location.reload()
}
} }
private fun onMessage(messageEvent: MessageEvent) { private fun onMessage(messageEvent: MessageEvent) {
@ -37,6 +41,7 @@ class WebSocketClient() {
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
private fun onClose(event: Event) { private fun onClose(event: Event) {
console.log("Disconnected!") console.log("Disconnected!")
reconnect = true
async(1000) { async(1000) {
connect() connect()
} }

View file

@ -6,31 +6,6 @@ import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.startCoroutine import kotlin.coroutines.startCoroutine
operator fun HTMLCollection.iterator() = object : Iterator<HTMLElement> {
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<Node> {
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) = fun launch(context: CoroutineContext = EmptyCoroutineContext, block: suspend () -> Unit) =
block.startCoroutine(Continuation(context) { result -> block.startCoroutine(Continuation(context) { result ->
result.onFailure { exception -> result.onFailure { exception ->

View file

@ -1,6 +1,5 @@
package de.kif.frontend.views package de.kif.frontend.views
import de.kif.frontend.iterator
import de.kif.frontend.launch import de.kif.frontend.launch
import de.kif.frontend.repository.WorkGroupRepository import de.kif.frontend.repository.WorkGroupRepository
import de.westermann.kobserve.event.EventListener import de.westermann.kobserve.event.EventListener
@ -8,6 +7,7 @@ import de.westermann.kwebview.View
import de.westermann.kwebview.async import de.westermann.kwebview.async
import de.westermann.kwebview.components.* import de.westermann.kwebview.components.*
import de.westermann.kwebview.createHtmlView import de.westermann.kwebview.createHtmlView
import de.westermann.kwebview.iterator
import org.w3c.dom.* import org.w3c.dom.*
import kotlin.browser.document import kotlin.browser.document

View file

@ -1,7 +1,7 @@
package de.kif.frontend.views.board package de.kif.frontend.views.board
import de.kif.common.formatDateTime import de.kif.common.formatDateTime
import de.kif.frontend.iterator import de.westermann.kwebview.iterator
import de.westermann.kwebview.interval import de.westermann.kwebview.interval
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
import org.w3c.dom.get import org.w3c.dom.get

View file

@ -1,22 +1,23 @@
package de.kif.frontend.views.calendar package de.kif.frontend.views.calendar
import de.kif.frontend.iterator
import de.kif.frontend.launch import de.kif.frontend.launch
import de.kif.frontend.repository.RoomRepository
import de.kif.frontend.repository.ScheduleRepository import de.kif.frontend.repository.ScheduleRepository
import de.westermann.kwebview.View import de.westermann.kwebview.View
import de.westermann.kwebview.createHtmlView
import de.westermann.kwebview.iterator
import org.w3c.dom.* import org.w3c.dom.*
import kotlin.browser.document import kotlin.browser.document
import kotlin.browser.window import kotlin.browser.window
class Calendar(calendar: HTMLElement) : View(calendar) { class Calendar(calendar: HTMLElement) : View(calendar) {
var calendarEntries: List<CalendarEntry> = emptyList()
var calendarCells: List<CalendarCell> = 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 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) { fun scrollVerticalBy(pixel: Double, scrollBehavior: ScrollBehavior = ScrollBehavior.SMOOTH) {
htmlTag.scrollBy(ScrollToOptions(0.0, pixel, scrollBehavior)) htmlTag.scrollBy(ScrollToOptions(0.0, pixel, scrollBehavior))
@ -34,58 +35,22 @@ class Calendar(calendar: HTMLElement) : View(calendar) {
calendarTable.scrollTo(ScrollToOptions(pixel, 0.0, scrollBehavior)) calendarTable.scrollTo(ScrollToOptions(pixel, 0.0, scrollBehavior))
} }
init {
val editable = calendar.dataset["editable"]?.toBoolean() ?: false val editable = calendar.dataset["editable"]?.toBoolean() ?: false
day = calendar.dataset["day"]?.toIntOrNull() ?: -1
calendarEntries = document.getElementsByClassName("calendar-entry") val body = CalendarBody(this, calendar.getElementsByClassName("calendar-body")[0] as HTMLElement)
.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()
init {
if (editable) { if (editable) {
CalendarEdit(this, calendar.querySelector(".calendar-edit") as HTMLElement) CalendarEdit(this, calendar.querySelector(".calendar-edit") as HTMLElement)
} }
ScheduleRepository.onCreate { (document.getElementById("calendar-check-constraints") as? HTMLElement)?.let { wrap(it) }
launch { ?.onClick?.addListener {
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 {
launch { launch {
val errors = ScheduleRepository.checkConstraints() val errors = ScheduleRepository.checkConstraints()
println(errors)
for ((s, l) in errors.map) { for ((s, l) in errors.map) {
for (entry in calendarEntries) { for (entry in body.calendarEntries) {
if (entry.scheduleId == s) { if (entry.scheduleId == s) {
entry.error = l.isNotEmpty() entry.error = l.isNotEmpty()
} }
@ -106,6 +71,29 @@ class Calendar(calendar: HTMLElement) : View(calendar) {
it.preventDefault() it.preventDefault()
} }
RoomRepository.onCreate {
val cell = createHtmlView<HTMLElement>()
cell.dataset["room"] = it.toString()
cell.classList.add("calendar-cell")
calendarTableHeader.appendChild(cell)
launch {
val room = RoomRepository.get(it) ?: return@launch
val span = createHtmlView<HTMLSpanElement>()
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()
}
}
}
} }
} }

View file

@ -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<CalendarRow>(view) {
val editable = calendar.editable
val day = calendar.day
var calendarEntries: List<CalendarEntry> = emptyList()
val calendarCells: List<CalendarCell>
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
}
}
}
}
}
}
}

View file

@ -3,12 +3,17 @@ package de.kif.frontend.views.calendar
import de.kif.common.model.Room import de.kif.common.model.Room
import de.kif.frontend.repository.RoomRepository import de.kif.frontend.repository.RoomRepository
import de.westermann.kwebview.ViewCollection import de.westermann.kwebview.ViewCollection
import de.westermann.kwebview.createHtmlView
import org.w3c.dom.HTMLDivElement
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
import org.w3c.dom.get import org.w3c.dom.get
import org.w3c.dom.set
import kotlin.browser.document
class CalendarCell(row: CalendarRow, view: HTMLElement) : ViewCollection<CalendarEntry>(view) {
val day = row.day
val time = row.time
class CalendarCell(view: HTMLElement) : ViewCollection<CalendarEntry>(view) {
val day = dataset["day"]?.toIntOrNull() ?: 0
val time = dataset["time"]?.toIntOrNull() ?: 0
val roomId = dataset["room"]?.toLongOrNull() ?: 0 val roomId = dataset["room"]?.toLongOrNull() ?: 0
private lateinit var room: Room private lateinit var room: Room
@ -22,7 +27,13 @@ class CalendarCell(view: HTMLElement) : ViewCollection<CalendarEntry>(view) {
return room return room
} }
init { companion object {
(view.getElementsByClassName("calendar-link")[0] as? HTMLElement)?.remove() fun create(row: CalendarRow, roomId: Long): CalendarCell {
val view = createHtmlView<HTMLDivElement>()
view.classList.add("calendar-cell")
view.dataset["room"] = roomId.toString()
return CalendarCell(row, view)
}
} }
} }

View file

@ -1,10 +1,9 @@
package de.kif.frontend.views.calendar package de.kif.frontend.views.calendar
import de.kif.common.CALENDAR_GRID_WIDTH import de.kif.common.CALENDAR_GRID_WIDTH
import de.kif.common.model.Room
import de.kif.common.model.Schedule import de.kif.common.model.Schedule
import de.kif.common.model.WorkGroup 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.launch
import de.kif.frontend.repository.RepositoryDelegate import de.kif.frontend.repository.RepositoryDelegate
import de.kif.frontend.repository.ScheduleRepository import de.kif.frontend.repository.ScheduleRepository
@ -17,7 +16,7 @@ import kotlin.dom.appendText
import kotlin.dom.isText import kotlin.dom.isText
import kotlin.js.Date 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 lateinit var mouseDelta: Point
private var newCell: CalendarCell? = null private var newCell: CalendarCell? = null
@ -31,11 +30,15 @@ class CalendarEntry(private val calendar: Calendar, view: HTMLElement) : View(vi
private lateinit var workGroup: WorkGroup private lateinit var workGroup: WorkGroup
var startTime = dataset["time"]?.toIntOrNull() ?: 0
var length = dataset["length"]?.toIntOrNull() ?: 0
var pending by classList.property("pending") var pending by classList.property("pending")
var error by classList.property("error") var error by classList.property("error")
private var nextScroll = 0.0 private var nextScroll = 0.0
var editable: Boolean = false val editable: Boolean
get() = calendar.editable
var moveLookRoom: Long? = null var moveLookRoom: Long? = null
var moveLookTime: Int? = null var moveLookTime: Int? = null
@ -70,29 +73,29 @@ class CalendarEntry(private val calendar: Calendar, view: HTMLElement) : View(vi
return@async return@async
} }
val width = calendar.calendarTable.clientWidth val width = calendar.calendar.calendarTable.clientWidth
val height = window.innerHeight val height = window.innerHeight
val rect = html.getBoundingClientRect() val rect = html.getBoundingClientRect()
if (rect.left < 0.0) { if (rect.left < 0.0) {
nextScroll = now + 500.0 nextScroll = now + 500.0
calendar.scrollHorizontalBy(rect.left - 80.0) calendar.calendar.scrollHorizontalBy(rect.left - 80.0)
} else if (rect.right > width) { } else if (rect.right > width) {
nextScroll = now + 0.500 nextScroll = now + 0.500
calendar.scrollHorizontalBy(rect.right - width + 50.0) calendar.calendar.scrollHorizontalBy(rect.right - width + 50.0)
} }
if (rect.top < 20.0) { if (rect.top < 20.0) {
nextScroll = now + 500.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) { } else if (rect.bottom > height - 20.0) {
nextScroll = now + 500.0 nextScroll = now + 500.0
calendar.scrollVerticalBy(rect.bottom - height + 50.0) calendar.calendar.scrollVerticalBy(rect.bottom - height + 50.0)
} }
} }
launch { launch {
calendarTools.setName(cell.getRoom(), cell.time) calendarTools?.setName(cell.getRoom(), cell.time)
} }
newCell = cell 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 { init {
onMouseDown { event -> onMouseDown { event ->
@ -181,6 +184,7 @@ class CalendarEntry(private val calendar: Calendar, view: HTMLElement) : View(vi
event.stopPropagation() event.stopPropagation()
} }
if (calendarTools != null) {
html.appendChild(calendarTools.html) html.appendChild(calendarTools.html)
launch { launch {
@ -188,6 +192,7 @@ class CalendarEntry(private val calendar: Calendar, view: HTMLElement) : View(vi
calendarTools.update(s) calendarTools.update(s)
} }
} }
}
fun load(schedule: Schedule) { fun load(schedule: Schedule) {
pending = false pending = false
@ -205,7 +210,10 @@ class CalendarEntry(private val calendar: Calendar, view: HTMLElement) : View(vi
} }
load(schedule.workGroup) 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 time = schedule.time / CALENDAR_GRID_WIDTH * CALENDAR_GRID_WIDTH
val cell = calendar.calendarCells.find { val cell = calendar.calendarCells.find {
@ -247,7 +255,7 @@ class CalendarEntry(private val calendar: Calendar, view: HTMLElement) : View(vi
} }
companion object { companion object {
fun create(calendar: Calendar, schedule: Schedule): CalendarEntry { fun create(calendar: CalendarBody, schedule: Schedule): CalendarEntry {
val entry = CalendarEntry(calendar, createHtmlView()) val entry = CalendarEntry(calendar, createHtmlView())
entry.load(schedule) entry.load(schedule)
@ -255,7 +263,7 @@ class CalendarEntry(private val calendar: Calendar, view: HTMLElement) : View(vi
return entry return entry
} }
fun create(calendar: Calendar, workGroup: WorkGroup): CalendarEntry { fun create(calendar: CalendarBody, workGroup: WorkGroup): CalendarEntry {
val entry = CalendarEntry(calendar, createHtmlView()) val entry = CalendarEntry(calendar, createHtmlView())
entry.load(workGroup) entry.load(workGroup)

View file

@ -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<CalendarCell>(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<HTMLDivElement>()
view.classList.add("calendar-row")
view.dataset["time"] = time.toString()
val row = CalendarRow(calendar, view)
val rowHeader = createHtmlView<HTMLElement>()
rowHeader.classList.add("calendar-cell")
if (time % 60 == 0) {
val span = createHtmlView<HTMLSpanElement>()
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
}
}
}

View file

@ -50,7 +50,7 @@ class CalendarWorkGroup(
} }
onMouseDown { onMouseDown {
CalendarEntry.create(calendar, workGroup) CalendarEntry.create(calendar.body, workGroup)
} }
} }
} }

View file

@ -1,6 +1,6 @@
package de.kif.frontend.views.overview 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.launch
import de.kif.frontend.repository.PostRepository import de.kif.frontend.repository.PostRepository
import de.westermann.kobserve.event.subscribe import de.westermann.kobserve.event.subscribe

View file

@ -1,7 +1,7 @@
package de.kif.frontend.views.table package de.kif.frontend.views.table
import de.kif.common.SearchElement import de.kif.common.SearchElement
import de.kif.frontend.iterator import de.westermann.kwebview.iterator
import de.kif.frontend.launch import de.kif.frontend.launch
import de.kif.frontend.repository.RepositoryDelegate import de.kif.frontend.repository.RepositoryDelegate
import de.kif.frontend.repository.RoomRepository import de.kif.frontend.repository.RoomRepository

View file

@ -1,6 +1,6 @@
package de.kif.frontend.views.table package de.kif.frontend.views.table
import de.kif.frontend.iterator import de.westermann.kwebview.iterator
import de.westermann.kwebview.components.InputView import de.westermann.kwebview.components.InputView
import org.w3c.dom.HTMLFormElement import org.w3c.dom.HTMLFormElement
import org.w3c.dom.HTMLInputElement import org.w3c.dom.HTMLInputElement

View file

@ -3,7 +3,7 @@ package de.kif.frontend.views.table
import de.kif.common.SearchElement import de.kif.common.SearchElement
import de.kif.common.model.Language import de.kif.common.model.Language
import de.kif.common.model.Track 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.launch
import de.kif.frontend.repository.RepositoryDelegate import de.kif.frontend.repository.RepositoryDelegate
import de.kif.frontend.repository.TrackRepository import de.kif.frontend.repository.TrackRepository

View file

@ -1,10 +1,17 @@
package de.westermann.kwebview package de.westermann.kwebview
import de.westermann.kobserve.event.EventListener
import de.westermann.kobserve.Property import de.westermann.kobserve.Property
import de.westermann.kobserve.ReadOnlyProperty import de.westermann.kobserve.ReadOnlyProperty
import de.westermann.kobserve.event.EventListener
import de.westermann.kobserve.property.property import de.westermann.kobserve.property.property
import org.w3c.dom.DOMTokenList 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. * Represents the css classes of an html element.
@ -130,6 +137,12 @@ class ClassList(
override fun toString(): String = list.value override fun toString(): String = list.value
fun clear() {
for (element in this) {
remove(element)
}
}
private data class Bound( private data class Bound(
val property: ReadOnlyProperty<Boolean>, val property: ReadOnlyProperty<Boolean>,
val reference: EventListener<Unit>? val reference: EventListener<Unit>?

View file

@ -8,7 +8,15 @@ import kotlin.dom.clear
*/ */
abstract class ViewCollection<V : View>(view: HTMLElement = createHtmlView()) : View(view), Collection<V> { abstract class ViewCollection<V : View>(view: HTMLElement = createHtmlView()) : View(view), Collection<V> {
private val children: MutableList<V> = mutableListOf() protected val children: MutableList<V> = mutableListOf()
protected inline fun <reified T : HTMLElement> wrapContent(transform: (T) -> V) {
for (element in html.children.iterator()) {
children += transform(element as T)
}
}
protected inline fun wrapContent(transform: (HTMLElement) -> V) = wrapContent<HTMLElement>(transform)
fun append(view: V) { fun append(view: V) {
children += view children += view

View file

@ -1,8 +1,7 @@
package de.westermann.kwebview package de.westermann.kwebview
import de.westermann.kobserve.event.EventHandler import de.westermann.kobserve.event.EventHandler
import org.w3c.dom.DOMRect import org.w3c.dom.*
import org.w3c.dom.HTMLElement
import org.w3c.dom.events.Event import org.w3c.dom.events.Event
import org.w3c.dom.events.EventListener import org.w3c.dom.events.EventListener
import org.w3c.dom.events.MouseEvent import org.w3c.dom.events.MouseEvent
@ -11,6 +10,30 @@ import org.w3c.xhr.XMLHttpRequest
import kotlin.browser.document import kotlin.browser.document
import kotlin.browser.window import kotlin.browser.window
operator fun HTMLCollection.iterator() = object : Iterator<HTMLElement> {
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<Node> {
private var index = 0
override fun hasNext(): Boolean {
return index < this@iterator.length
}
override fun next(): Node {
return this@iterator.get(index++)!!
}
}
inline fun <reified V : HTMLElement> createHtmlView(tag: String? = null): V { inline fun <reified V : HTMLElement> createHtmlView(tag: String? = null): V {
var tagName: String var tagName: String
if (tag != null) { if (tag != null) {

View file

@ -384,7 +384,7 @@
border-left: none; 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); border-top: solid 1px var(--table-border-color);
} }
@ -424,6 +424,10 @@
&.time-to-room { &.time-to-room {
flex-direction: row; flex-direction: row;
.calendar-body {
display: flex;
}
.calendar-header, .calendar-row { .calendar-header, .calendar-row {
flex-direction: column; flex-direction: column;
line-height: 3rem; line-height: 3rem;
@ -468,7 +472,7 @@
} }
} }
&:nth-child(2n + 2) .calendar-cell::before { &:nth-child(4n + 1) .calendar-cell::before {
content: ''; content: '';
height: 100%; height: 100%;
left: 0; left: 0;

View file

@ -40,10 +40,6 @@ fun Route.account() {
val wikiSections = WikiImporter.loadSections() val wikiSections = WikiImporter.loadSections()
respondMain { respondMain {
menuTemplate {
this.user = user
active = MenuTemplate.Tab.ACCOUNT
}
content { content {
h1 { +"Account" } h1 { +"Account" }
div { div {

View file

@ -6,7 +6,6 @@ import de.kif.backend.Configuration
import de.kif.backend.isAuthenticated import de.kif.backend.isAuthenticated
import de.kif.backend.repository.RoomRepository import de.kif.backend.repository.RoomRepository
import de.kif.backend.repository.ScheduleRepository import de.kif.backend.repository.ScheduleRepository
import de.kif.backend.view.MenuTemplate
import de.kif.backend.view.respondMain import de.kif.backend.view.respondMain
import de.kif.common.CALENDAR_GRID_WIDTH import de.kif.common.CALENDAR_GRID_WIDTH
import de.kif.common.model.Permission import de.kif.common.model.Permission
@ -51,6 +50,8 @@ private fun DIV.calendarCell(schedule: Schedule?) {
}.toString() }.toString()
attributes["data-language"] = schedule.workGroup.language.code attributes["data-language"] = schedule.workGroup.language.code
attributes["data-id"] = schedule.id.toString() attributes["data-id"] = schedule.id.toString()
attributes["data-time"] = schedule.time.toString()
attributes["data-length"] = schedule.workGroup.length.toString()
+schedule.workGroup.name +schedule.workGroup.name
} }
@ -81,6 +82,8 @@ private fun DIV.renderCalendar(
for (room in rooms) { for (room in rooms) {
div("calendar-cell") { div("calendar-cell") {
attributes["data-room"] = room.id.toString()
span { span {
+room.name +room.name
} }
@ -88,6 +91,7 @@ private fun DIV.renderCalendar(
} }
} }
div("calendar-body") {
for (i in 0 until minutesOfDay / CALENDAR_GRID_WIDTH) { for (i in 0 until minutesOfDay / CALENDAR_GRID_WIDTH) {
val time = ((i * CALENDAR_GRID_WIDTH + from) % MINUTES_OF_DAY).let { val time = ((i * CALENDAR_GRID_WIDTH + from) % MINUTES_OF_DAY).let {
if (it < 0) it + MINUTES_OF_DAY else it if (it < 0) it + MINUTES_OF_DAY else it
@ -106,6 +110,8 @@ private fun DIV.renderCalendar(
} }
div(rowClass) { div(rowClass) {
attributes["data-time"] = start.toString()
attributes["data-day"] = day.toString()
div("calendar-cell") { div("calendar-cell") {
if (time % gridLabelWidth == 0) { if (time % gridLabelWidth == 0) {
@ -117,9 +123,8 @@ private fun DIV.renderCalendar(
for (room in rooms) { for (room in rooms) {
div("calendar-cell") { div("calendar-cell") {
attributes["data-time"] = start.toString()
attributes["data-room"] = room.id.toString() attributes["data-room"] = room.id.toString()
attributes["data-day"] = day.toString()
title = room.name + " - " + timeString title = room.name + " - " + timeString
val schedule = (start..end).mapNotNull { schedules[room]?.get(it) }.firstOrNull() val schedule = (start..end).mapNotNull { schedules[room]?.get(it) }.firstOrNull()
@ -131,6 +136,7 @@ private fun DIV.renderCalendar(
} }
} }
} }
}
fun Route.calendar() { fun Route.calendar() {
@ -167,9 +173,12 @@ fun Route.calendar() {
val day = call.parameters["day"]?.toIntOrNull() ?: return@get val day = call.parameters["day"]?.toIntOrNull() ?: return@get
val range = ScheduleRepository.getDayRange() val range = ScheduleRepository.getDayRange()
/*
if (!editable && day !in range) { if (!editable && day !in range) {
return@get return@get
} }
*/
val rooms = RoomRepository.all() val rooms = RoomRepository.all()
@ -177,8 +186,8 @@ fun Route.calendar() {
CalendarOrientation.values().find { it.name == name } CalendarOrientation.values().find { it.name == name }
} ?: CalendarOrientation.ROOM_TO_TIME } ?: CalendarOrientation.ROOM_TO_TIME
val h = ScheduleRepository.getByDay(day) val list = ScheduleRepository.getByDay(day)
val schedules = h.groupBy { it.room }.mapValues { (_, it) -> val schedules = list.groupBy { it.room }.mapValues { (_, it) ->
it.associateBy { it.associateBy {
it.time it.time
} }
@ -186,7 +195,7 @@ fun Route.calendar() {
var max = 0 var max = 0
var min = 24 * 60 var min = 24 * 60
for (s in h) { for (s in list) {
max = max(max, s.time + s.workGroup.length) max = max(max, s.time + s.workGroup.length)
min = min(min, s.time) min = min(min, s.time)
} }
@ -205,6 +214,11 @@ fun Route.calendar() {
min = (min / 60 - 1) * 60 min = (min / 60 - 1) * 60
max = (max / 60 + 2) * 60 max = (max / 60 + 2) * 60
if (!editable && list.isEmpty()) {
min = 0
max = 0
}
val refDate = DateTime(Configuration.Schedule.referenceDate.time) val refDate = DateTime(Configuration.Schedule.referenceDate.time)
val date = refDate + day.days val date = refDate + day.days
val dateString = DateFormat("EEEE, d. MMMM") val dateString = DateFormat("EEEE, d. MMMM")
@ -212,15 +226,7 @@ fun Route.calendar() {
.format(date) .format(date)
respondMain { respondMain {
menuTemplate {
this.user = user
active = MenuTemplate.Tab.CALENDAR
}
content { content {
if (rooms.isEmpty()) {
return@content
}
div("header") { div("header") {
div("header-left") { div("header-left") {
if (editable || day - 1 > range.start) { if (editable || day - 1 > range.start) {

View file

@ -2,12 +2,10 @@ package de.kif.backend.route
import de.kif.backend.PortalSession import de.kif.backend.PortalSession
import de.kif.backend.UserPrinciple import de.kif.backend.UserPrinciple
import de.kif.backend.view.MainTemplate
import de.kif.backend.view.respondMain import de.kif.backend.view.respondMain
import io.ktor.application.call import io.ktor.application.call
import io.ktor.auth.authenticate import io.ktor.auth.authenticate
import io.ktor.auth.principal import io.ktor.auth.principal
import io.ktor.html.respondHtmlTemplate
import io.ktor.response.respondRedirect import io.ktor.response.respondRedirect
import io.ktor.routing.Route import io.ktor.routing.Route
import io.ktor.routing.get import io.ktor.routing.get

View file

@ -78,10 +78,6 @@ fun Route.overview() {
val postList = PostRepository.all().asReversed() val postList = PostRepository.all().asReversed()
respondMain { respondMain {
menuTemplate {
this.user = user
active = MenuTemplate.Tab.BOARD
}
content { content {
div("overview") { div("overview") {
div("overview-main") { div("overview-main") {
@ -118,10 +114,6 @@ fun Route.overview() {
} }
respondMain { respondMain {
menuTemplate {
this.user = user
active = MenuTemplate.Tab.BOARD
}
content { content {
div("overview") { div("overview") {
createPost(post, editable) createPost(post, editable)
@ -135,10 +127,6 @@ fun Route.overview() {
val postId = call.parameters["id"]?.toLongOrNull() ?: return@get val postId = call.parameters["id"]?.toLongOrNull() ?: return@get
val editPost = PostRepository.get(postId) ?: return@get val editPost = PostRepository.get(postId) ?: return@get
respondMain { respondMain {
menuTemplate {
this.user = user
active = MenuTemplate.Tab.BOARD
}
content { content {
h1 { +"Edit post" } h1 { +"Edit post" }
div("post-edit-container") { div("post-edit-container") {
@ -365,10 +353,6 @@ fun Route.overview() {
get("/post/new") { get("/post/new") {
authenticateOrRedirect(Permission.POST) { user -> authenticateOrRedirect(Permission.POST) { user ->
respondMain { respondMain {
menuTemplate {
this.user = user
active = MenuTemplate.Tab.BOARD
}
content { content {
h1 { +"Create post" } h1 { +"Create post" }
div("post-edit-container") { div("post-edit-container") {

View file

@ -33,10 +33,6 @@ fun Route.room() {
val list = RoomRepository.all() val list = RoomRepository.all()
respondMain { respondMain {
menuTemplate {
this.user = user
active = MenuTemplate.Tab.ROOM
}
content { content {
insert(TableTemplate()) { insert(TableTemplate()) {
searchValue = search searchValue = search
@ -113,10 +109,6 @@ fun Route.room() {
val roomId = call.parameters["id"]?.toLongOrNull() ?: return@get val roomId = call.parameters["id"]?.toLongOrNull() ?: return@get
val editRoom = RoomRepository.get(roomId) ?: return@get val editRoom = RoomRepository.get(roomId) ?: return@get
respondMain { respondMain {
menuTemplate {
this.user = user
active = MenuTemplate.Tab.ROOM
}
content { content {
h1 { +"Edit room" } h1 { +"Edit room" }
form(method = FormMethod.post) { form(method = FormMethod.post) {
@ -276,10 +268,6 @@ fun Route.room() {
get("/room/new") { get("/room/new") {
authenticateOrRedirect(Permission.ROOM) { user -> authenticateOrRedirect(Permission.ROOM) { user ->
respondMain { respondMain {
menuTemplate {
this.user = user
active = MenuTemplate.Tab.ROOM
}
content { content {
h1 { +"Create room" } h1 { +"Create room" }
form(method = FormMethod.post) { form(method = FormMethod.post) {

View file

@ -90,10 +90,6 @@ fun Route.track() {
val search = call.parameters["search"] ?: "" val search = call.parameters["search"] ?: ""
val list = TrackRepository.all() val list = TrackRepository.all()
respondMain { respondMain {
menuTemplate {
this.user = user
active = MenuTemplate.Tab.WORK_GROUP
}
content { content {
insert(TableTemplate()) { insert(TableTemplate()) {
searchValue = search searchValue = search
@ -150,10 +146,6 @@ fun Route.track() {
val trackId = call.parameters["id"]?.toLongOrNull() ?: return@get val trackId = call.parameters["id"]?.toLongOrNull() ?: return@get
val editTrack = TrackRepository.get(trackId) ?: return@get val editTrack = TrackRepository.get(trackId) ?: return@get
respondMain { respondMain {
menuTemplate {
this.user = user
active = MenuTemplate.Tab.WORK_GROUP
}
content { content {
h1 { +"Edit track" } h1 { +"Edit track" }
form(method = FormMethod.post) { form(method = FormMethod.post) {
@ -219,10 +211,6 @@ fun Route.track() {
get("/track/new") { get("/track/new") {
authenticateOrRedirect(Permission.WORK_GROUP) { user -> authenticateOrRedirect(Permission.WORK_GROUP) { user ->
respondMain { respondMain {
menuTemplate {
this.user = user
active = MenuTemplate.Tab.WORK_GROUP
}
content { content {
h1 { +"Create track" } h1 { +"Create track" }
form(method = FormMethod.post) { form(method = FormMethod.post) {

View file

@ -33,10 +33,6 @@ fun Route.user() {
val search = call.parameters["search"] ?: "" val search = call.parameters["search"] ?: ""
val list = UserRepository.all() val list = UserRepository.all()
respondMain { respondMain {
menuTemplate {
this.user = user
active = MenuTemplate.Tab.USER
}
content { content {
insert(TableTemplate()) { insert(TableTemplate()) {
searchValue = search searchValue = search
@ -92,10 +88,6 @@ fun Route.user() {
val userId = call.parameters["id"]?.toLongOrNull() ?: return@get val userId = call.parameters["id"]?.toLongOrNull() ?: return@get
val editUser = UserRepository.get(userId) ?: return@get val editUser = UserRepository.get(userId) ?: return@get
respondMain { respondMain {
menuTemplate {
this.user = user
active = MenuTemplate.Tab.USER
}
content { content {
h1 { +"Edit user" } h1 { +"Edit user" }
form(method = FormMethod.post) { form(method = FormMethod.post) {
@ -185,10 +177,6 @@ fun Route.user() {
get("/user/new") { get("/user/new") {
authenticateOrRedirect(Permission.USER) { user -> authenticateOrRedirect(Permission.USER) { user ->
respondMain { respondMain {
menuTemplate {
this.user = user
active = MenuTemplate.Tab.USER
}
content { content {
h1 { +"Create user" } h1 { +"Create user" }
form(method = FormMethod.post) { form(method = FormMethod.post) {

View file

@ -29,10 +29,6 @@ fun Route.workGroup() {
val search = call.parameters["search"] ?: "" val search = call.parameters["search"] ?: ""
val list = WorkGroupRepository.all() val list = WorkGroupRepository.all()
respondMain { respondMain {
menuTemplate {
this.user = user
active = MenuTemplate.Tab.WORK_GROUP
}
content { content {
insert(TableTemplate()) { insert(TableTemplate()) {
searchValue = search searchValue = search
@ -166,10 +162,6 @@ fun Route.workGroup() {
} }
respondMain { respondMain {
menuTemplate {
this.user = user
active = MenuTemplate.Tab.WORK_GROUP
}
content { content {
h1 { +"Edit work group" } h1 { +"Edit work group" }
form(method = FormMethod.post) { form(method = FormMethod.post) {
@ -529,10 +521,6 @@ fun Route.workGroup() {
authenticateOrRedirect(Permission.WORK_GROUP) { user -> authenticateOrRedirect(Permission.WORK_GROUP) { user ->
val tracks = TrackRepository.all() val tracks = TrackRepository.all()
respondMain { respondMain {
menuTemplate {
this.user = user
active = MenuTemplate.Tab.WORK_GROUP
}
content { content {
h1 { +"Create work group" } h1 { +"Create work group" }
form(method = FormMethod.post) { form(method = FormMethod.post) {

View file

@ -1,21 +1,28 @@
package de.kif.backend.view package de.kif.backend.view
import de.kif.backend.PortalSession
import de.kif.backend.Resources 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.ApplicationCall
import io.ktor.application.call import io.ktor.application.call
import io.ktor.html.* import io.ktor.html.*
import io.ktor.request.path import io.ktor.request.path
import io.ktor.request.uri
import io.ktor.response.respondRedirect import io.ktor.response.respondRedirect
import io.ktor.sessions.get
import io.ktor.sessions.sessions
import io.ktor.util.pipeline.PipelineContext import io.ktor.util.pipeline.PipelineContext
import kotlinx.html.* import kotlinx.html.*
class MainTemplate( class MainTemplate(
private val theme: Theme, private val theme: Theme,
private val url: String,
private val user: User?,
private val noMenu: Boolean, private val noMenu: Boolean,
private val stretch: Boolean private val stretch: Boolean
) : Template<HTML> { ) : Template<HTML> {
val content = Placeholder<HtmlBlockTag>() val content = Placeholder<HtmlBlockTag>()
val menuTemplate = TemplatePlaceholder<MenuTemplate>()
override fun HTML.apply() { override fun HTML.apply() {
head { head {
@ -56,7 +63,7 @@ class MainTemplate(
} }
body { body {
if (!noMenu) { if (!noMenu) {
insert(MenuTemplate(), menuTemplate) insert(MenuTemplate(url, user)) {}
} }
val containerClasses = if (stretch) "container-full" else "container" val containerClasses = if (stretch) "container-full" else "container"
@ -88,12 +95,16 @@ class MainTemplate(
} }
enum class Theme { enum class Theme {
LIGHT, DARK, PRINCESS LIGHT, DARK, PRINCESS;
}
private fun String?.toTheme() = this?.let { str -> companion object {
Theme.values().find { str == it.name } private val loopup = values().toList().associateBy { it.name }
} ?: Theme.LIGHT
fun lookup(name: String?): Theme {
return loopup[(name ?: return LIGHT).toUpperCase()] ?: LIGHT
}
}
}
suspend fun PipelineContext<Unit, ApplicationCall>.respondMain( suspend fun PipelineContext<Unit, ApplicationCall>.respondMain(
noMenu: Boolean = false, noMenu: Boolean = false,
@ -101,11 +112,13 @@ suspend fun PipelineContext<Unit, ApplicationCall>.respondMain(
body: MainTemplate.() -> Unit body: MainTemplate.() -> Unit
) { ) {
val param = call.request.queryParameters["theme"] val param = call.request.queryParameters["theme"]
val url = call.request.uri.substring(1)
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (param != null) { if (param != null) {
call.response.cookies.append( call.response.cookies.append(
name = "theme", name = "theme",
value = param.toTheme().name, value = Theme.lookup(param).name,
maxAge = Int.MAX_VALUE, maxAge = Int.MAX_VALUE,
path = "/" path = "/"
) )
@ -113,7 +126,9 @@ suspend fun PipelineContext<Unit, ApplicationCall>.respondMain(
} else { } else {
call.respondHtmlTemplate( call.respondHtmlTemplate(
MainTemplate( MainTemplate(
call.request.cookies["theme"].toTheme(), Theme.lookup(call.request.cookies["theme"]),
url,
user,
noMenu, noMenu,
stretch stretch
), ),

View file

@ -5,19 +5,21 @@ import de.kif.common.model.User
import io.ktor.html.Template import io.ktor.html.Template
import kotlinx.html.* import kotlinx.html.*
class MenuTemplate() : Template<FlowContent> { class MenuTemplate(
private val url: String,
var active: Tab = Tab.BOARD private val user: User?
var user: User? = null ) : Template<FlowContent> {
override fun FlowContent.apply() { override fun FlowContent.apply() {
val tab = Tab.lookup(url)
nav("menu") { nav("menu") {
div("container") { div("container") {
div("menu-left") { div("menu-left") {
a("/", classes = if (active == Tab.BOARD) "active" else null) { a("/", classes = if (tab == null) "active" else null) {
+"Dashboard" +"News"
} }
a("/calendar", classes = if (active == Tab.CALENDAR) "active" else null) { a("/calendar", classes = if (tab == Tab.CALENDAR) "active" else null) {
+"Calendar" +"Calendar"
} }
} }
@ -28,26 +30,26 @@ class MenuTemplate() : Template<FlowContent> {
val user = user val user = user
div("menu-content") { div("menu-content") {
if (user == null) { if (user == null) {
a("/account", classes = if (active == Tab.ACCOUNT) "active" else null) { a("/account", classes = if (tab == Tab.LOGIN) "active" else null) {
+"Login" +"Login"
} }
} else { } else {
if (user.checkPermission(Permission.WORK_GROUP)) { 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" +"Work groups"
} }
} }
if (user.checkPermission(Permission.ROOM)) { 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" +"Rooms"
} }
} }
if (user.checkPermission(Permission.USER)) { 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" +"Users"
} }
} }
a("/account", classes = if (active == Tab.ACCOUNT) "active" else null) { a("/account", classes = if (tab == Tab.ACCOUNT) "active" else null) {
+user.username +user.username
} }
} }
@ -58,6 +60,14 @@ class MenuTemplate() : Template<FlowContent> {
} }
enum class Tab { 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)]
}
}
} }
} }