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 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()
}

View file

@ -6,31 +6,6 @@ import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
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) =
block.startCoroutine(Continuation(context) { result ->
result.onFailure { exception ->

View file

@ -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

View file

@ -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

View file

@ -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<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 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<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.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<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
private lateinit var room: Room
@ -22,7 +27,13 @@ class CalendarCell(view: HTMLElement) : ViewCollection<CalendarEntry>(view) {
return room
}
init {
(view.getElementsByClassName("calendar-link")[0] as? HTMLElement)?.remove()
companion object {
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
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)

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 {
CalendarEntry.create(calendar, workGroup)
CalendarEntry.create(calendar.body, workGroup)
}
}
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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<String> {
private val bound: MutableMap<String, Bound> = 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<Boolean>,
val reference: EventListener<Unit>?
val property: ReadOnlyProperty<Boolean>,
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> {
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) {
children += view

View file

@ -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<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 {
var tagName: String
if (tag != null) {

View file

@ -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;