Add drag and drop creation of schedule

This commit is contained in:
Lars Westermann 2019-05-17 22:39:42 +02:00
parent b7d6476a70
commit 84f8e71239
Signed by: lars.westermann
GPG key ID: 9D417FA5BB9D5E1D
38 changed files with 1277 additions and 548 deletions

View file

@ -1,6 +1,7 @@
package de.kif.frontend
import de.kif.frontend.views.initCalendar
import de.kif.frontend.views.calendar.initCalendar
import de.kif.frontend.views.initTableLayout
import de.westermann.kwebview.components.init
import kotlin.browser.document
@ -10,4 +11,7 @@ fun main() = init {
if (document.getElementsByClassName("calendar").length > 0) {
initCalendar()
}
if (document.getElementsByClassName("table-layout").length > 0) {
initTableLayout()
}
}

View file

@ -23,7 +23,7 @@ object RoomRepository : Repository<Room> {
}
override suspend fun create(model: Room): Long {
return repositoryPost("/api/rooms", Message.json.stringify(Room.serializer(), model))?.toLong()
return repositoryPost("/api/rooms", Message.json.stringify(Room.serializer(), model))
?: throw IllegalStateException("Cannot create model!")
}

View file

@ -23,7 +23,7 @@ object ScheduleRepository : Repository<Schedule> {
}
override suspend fun create(model: Schedule): Long {
return repositoryPost("/api/schedules", Message.json.stringify(Schedule.serializer(), model))?.toLong()
return repositoryPost("/api/schedules", Message.json.stringify(Schedule.serializer(), model))
?: throw IllegalStateException("Cannot create model!")
}

View file

@ -23,7 +23,7 @@ object TrackRepository : Repository<Track> {
}
override suspend fun create(model: Track): Long {
return repositoryPost("/api/tracks", Message.json.stringify(Track.serializer(), model))?.toLong()
return repositoryPost("/api/tracks", Message.json.stringify(Track.serializer(), model))
?: throw IllegalStateException("Cannot create model!")
}

View file

@ -23,7 +23,7 @@ object UserRepository : Repository<User> {
}
override suspend fun create(model: User): Long {
return repositoryPost("/api/users", Message.json.stringify(User.serializer(), model))?.toLong()
return repositoryPost("/api/users", Message.json.stringify(User.serializer(), model))
?: throw IllegalStateException("Cannot create model!")
}

View file

@ -23,7 +23,7 @@ object WorkGroupRepository : Repository<WorkGroup> {
}
override suspend fun create(model: WorkGroup): Long {
return repositoryPost("/api/workgroups", Message.json.stringify(WorkGroup.serializer(), model))?.toLong()
return repositoryPost("/api/workgroups", Message.json.stringify(WorkGroup.serializer(), model))
?: throw IllegalStateException("Cannot create model!")
}

View file

@ -1,359 +0,0 @@
package de.kif.frontend.views
import de.kif.common.CALENDAR_GRID_WIDTH
import de.kif.common.model.Room
import de.kif.common.model.Schedule
import de.kif.frontend.iterator
import de.kif.frontend.launch
import de.kif.frontend.repository.RepositoryDelegate
import de.kif.frontend.repository.RoomRepository
import de.kif.frontend.repository.ScheduleRepository
import de.westermann.kwebview.*
import de.westermann.kwebview.components.Body
import org.w3c.dom.HTMLAnchorElement
import org.w3c.dom.HTMLElement
import org.w3c.dom.events.EventListener
import org.w3c.dom.events.MouseEvent
import org.w3c.dom.get
import kotlin.browser.document
import kotlin.dom.appendText
import kotlin.dom.isText
class CalendarTools(entry: CalendarEntry, view: HTMLElement) : View(view) {
init {
var linkM10: HTMLAnchorElement? = null
var linkM5: HTMLAnchorElement? = null
var linkReset: HTMLAnchorElement? = null
var linkP5: HTMLAnchorElement? = null
var linkP10: HTMLAnchorElement? = null
var linkDel: HTMLAnchorElement? = null
for (element in html.children) {
when {
element.classList.contains("calendar-tools-m10") -> linkM10 = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-m5") -> linkM5 = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-reset") -> linkReset = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-p5") -> linkP5 = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-p10") -> linkP10 = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-del") -> linkDel = element as? HTMLAnchorElement
}
}
linkM10 = linkM10 ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-m10")
link.textContent = "-10"
html.appendChild(link)
link
}
linkM10.removeAttribute("href")
linkM10.addEventListener("click", EventListener {
entry.pending = true
launch {
val s = entry.schedule.get()
ScheduleRepository.update(s.copy(time = s.time - 10))
}
})
linkM5 = linkM5 ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-m5")
link.textContent = "-5"
html.appendChild(link)
link
}
linkM5.removeAttribute("href")
linkM5.addEventListener("click", EventListener {
entry.pending = true
launch {
val s = entry.schedule.get()
ScheduleRepository.update(s.copy(time = s.time - 5))
}
})
linkReset = linkReset ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-reset")
link.textContent = "reset"
html.appendChild(link)
link
}
linkReset.removeAttribute("href")
linkReset.addEventListener("click", EventListener {
entry.pending = true
launch {
val s = entry.schedule.get()
ScheduleRepository.update(s.copy(time = s.time / CALENDAR_GRID_WIDTH * CALENDAR_GRID_WIDTH))
}
})
linkP5 = linkP5 ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-p5")
link.textContent = "+5"
html.appendChild(link)
link
}
linkP5.removeAttribute("href")
linkP5.addEventListener("click", EventListener {
entry.pending = true
launch {
val s = entry.schedule.get()
ScheduleRepository.update(s.copy(time = s.time + 5))
}
})
linkP10 = linkP10 ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-p10")
link.textContent = "+10"
html.appendChild(link)
link
}
linkP10.removeAttribute("href")
linkP10.addEventListener("click", EventListener {
entry.pending = true
launch {
val s = entry.schedule.get()
ScheduleRepository.update(s.copy(time = s.time + 10))
}
})
linkDel = linkDel ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-del")
link.textContent = "del"
html.appendChild(link)
link
}
linkDel.removeAttribute("href")
linkDel.addEventListener("click", EventListener {
entry.pending = true
launch {
ScheduleRepository.delete(entry.scheduleId)
}
})
}
}
class CalendarEntry(view: HTMLElement) : View(view) {
private lateinit var mouseDelta: Point
private var newCell: CalendarCell? = null
private var language by dataset.property("language")
val scheduleId = dataset["id"]?.toLongOrNull() ?: 0
val schedule = RepositoryDelegate(ScheduleRepository, scheduleId)
var pending by classList.property("pending")
private fun onMove(event: MouseEvent) {
val position = event.toPoint() - mouseDelta
val cell = calendarCells.find {
position in it.dimension
}
if (cell != null) {
cell += this
if (newCell == null) {
style {
left = "0"
top = "0.1rem"
}
}
newCell = cell
}
event.preventDefault()
event.stopPropagation()
}
private fun onFinishMove(event: MouseEvent) {
classList -= "drag"
newCell?.let { cell ->
launch {
val newTime = cell.time
val newRoom = cell.getRoom()
pending = true
val s = schedule.get().copy(room = newRoom, time = newTime)
ScheduleRepository.update(s)
}
}
newCell = null
for (it in listeners) {
it.detach()
}
listeners = emptyList()
event.preventDefault()
event.stopPropagation()
}
private var listeners: List<de.westermann.kobserve.event.EventListener<*>> = emptyList()
init {
onMouseDown { event ->
if (event.target != html || "pending" in classList) {
event.stopPropagation()
return@onMouseDown
}
launch {
classList += "drag"
val s = schedule.get()
val time = s.time / CALENDAR_GRID_WIDTH * CALENDAR_GRID_WIDTH
val p = calendarCells.find {
it.day == s.day && it.time == time && it.roomId == s.room.id
}?.dimension?.center ?: dimension.center
mouseDelta = event.toPoint() - p
listeners = listOf(
Body.onMouseMove.reference(this::onMove),
Body.onMouseUp.reference(this::onFinishMove),
Body.onMouseLeave.reference(this::onFinishMove)
)
}
event.preventDefault()
event.stopPropagation()
}
var calendarTools: CalendarTools? = null
for (item in html.children) {
if (item.classList.contains("calendar-tools")) {
calendarTools = CalendarTools(this, item)
break
}
}
if (calendarTools == null) {
calendarTools = CalendarTools(this, createHtmlView())
html.appendChild(calendarTools.html)
}
}
fun load(schedule: Schedule) {
pending = false
language = schedule.workGroup.language.code
this.schedule.set(schedule)
style {
val size = schedule.workGroup.length / CALENDAR_GRID_WIDTH.toDouble()
val pos = (schedule.time % CALENDAR_GRID_WIDTH) / CALENDAR_GRID_WIDTH.toDouble()
val ps = "${pos * 100}%"
val sz = "${size * 100}%"
left = ps
top = "calc($ps + 0.1rem)"
width = sz
height = "calc($sz - 0.2rem)"
if (schedule.workGroup.track?.color != null) {
backgroundColor = schedule.workGroup.track.color.toString()
color = schedule.workGroup.track.color.calcTextColor().toString()
}
}
for (element in html.childNodes) {
if (element.isText) {
html.removeChild(element)
}
}
html.appendText(schedule.workGroup.name)
val time = schedule.time / CALENDAR_GRID_WIDTH * CALENDAR_GRID_WIDTH
val cell = calendarCells.find {
it.day == schedule.day && it.time == time && it.roomId == schedule.room.id
}
if (cell != null && cell.html != html.parentElement) {
cell += this
}
}
companion object {
fun create(schedule: Schedule): CalendarEntry {
val entry = CalendarEntry(createHtmlView())
entry.load(schedule)
return entry
}
}
}
class CalendarCell(view: HTMLElement) : ViewCollection<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
suspend fun getRoom(): Room {
if (this::room.isInitialized) {
return room
}
room = RoomRepository.get(roomId) ?: throw NoSuchElementException()
return room
}
}
var calendarEntries: List<CalendarEntry> = emptyList()
var calendarCells: List<CalendarCell> = emptyList()
fun initCalendar() {
calendarEntries = document.getElementsByClassName("calendar-entry")
.iterator().asSequence().map(::CalendarEntry).toList()
calendarCells = document.getElementsByClassName("calendar-cell")
.iterator().asSequence().filter { it.dataset["time"] != null }.map(::CalendarCell).toList()
ScheduleRepository.onCreate {
launch {
val schedule = ScheduleRepository.get(it) ?: throw NoSuchElementException()
CalendarEntry.create(schedule)
}
}
ScheduleRepository.onUpdate {
launch {
val schedule = ScheduleRepository.get(it) ?: throw NoSuchElementException()
var found = false
for (entry in calendarEntries) {
if (entry.scheduleId == it) {
entry.load(schedule)
found = true
}
}
if (!found) {
CalendarEntry.create(schedule)
}
}
}
ScheduleRepository.onDelete {
for (entry in calendarEntries) {
if (entry.scheduleId == it) {
entry.html.remove()
}
}
}
}

View file

@ -0,0 +1,32 @@
package de.kif.frontend.views
import de.kif.common.Search
import de.kif.common.SearchElement
import de.kif.frontend.iterator
import de.westermann.kwebview.components.InputView
import org.w3c.dom.HTMLFormElement
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.HTMLTableElement
import org.w3c.dom.get
import kotlin.browser.document
fun initTableLayout() {
val form = document.getElementsByClassName("table-layout-search")[0] as HTMLFormElement
form.onsubmit = { false }
val table = document.getElementsByClassName("table-layout-table")[0] as HTMLTableElement
val list = table.getElementsByTagName("tr").iterator().asSequence().filter {
it.dataset.get("search") != null
}.associateWith {
SearchElement.parse(it.dataset.get("search")!!)
}
val input = form.getElementsByTagName("input")[0] as HTMLInputElement
val search = InputView.wrap(input)
search.valueProperty.onChange {
for ((row, s) in list) {
row.style.display = if (Search.match(search.value, s)) "table-row" else "none"
}
}
}

View file

@ -0,0 +1,65 @@
package de.kif.frontend.views.calendar
import de.kif.frontend.iterator
import de.kif.frontend.launch
import de.kif.frontend.repository.ScheduleRepository
import de.westermann.kwebview.View
import org.w3c.dom.HTMLElement
import org.w3c.dom.get
import kotlin.browser.document
class Calendar(calendar: HTMLElement): View(calendar) {
var calendarEntries: List<CalendarEntry> = emptyList()
var calendarCells: List<CalendarCell> = emptyList()
val day: Int
init {
val editable = calendar.dataset["editable"]?.toBoolean() ?: false
day = calendar.dataset["day"]?.toIntOrNull() ?: -1
calendarEntries = document.getElementsByClassName("calendar-entry")
.iterator().asSequence().map{ CalendarEntry(this, it) }.onEach { it.editable = editable }.toList()
calendarCells = document.getElementsByClassName("calendar-cell")
.iterator().asSequence().filter { it.dataset["time"] != null }.map(::CalendarCell).toList()
if (editable) {
CalendarEdit(this, calendar.querySelector(".calendar-edit") as HTMLElement)
}
ScheduleRepository.onCreate {
launch {
val schedule = ScheduleRepository.get(it) ?: throw NoSuchElementException()
calendarEntries += CalendarEntry.create(this, schedule).also { it.editable = editable }
}
}
ScheduleRepository.onUpdate {
launch {
val schedule = ScheduleRepository.get(it) ?: throw NoSuchElementException()
var found = false
for (entry in calendarEntries) {
if (entry.scheduleId == it) {
entry.load(schedule)
found = true
}
}
if (!found) {
calendarEntries += CalendarEntry.create(this, schedule).also { it.editable = editable }
}
}
}
ScheduleRepository.onDelete {
for (entry in calendarEntries) {
if (entry.scheduleId == it) {
entry.html.remove()
}
}
}
}
}
fun initCalendar() {
Calendar(document.getElementsByClassName("calendar")[0] as? HTMLElement ?: return)
}

View file

@ -0,0 +1,28 @@
package de.kif.frontend.views.calendar
import de.kif.common.model.Room
import de.kif.frontend.repository.RoomRepository
import de.westermann.kwebview.ViewCollection
import org.w3c.dom.HTMLElement
import org.w3c.dom.get
class CalendarCell(view: HTMLElement) : ViewCollection<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
suspend fun getRoom(): Room {
if (this::room.isInitialized) {
return room
}
room = RoomRepository.get(roomId) ?: throw NoSuchElementException()
return room
}
init {
(view.getElementsByClassName("calendar-link")[0] as? HTMLElement)?.remove()
}
}

View file

@ -0,0 +1,76 @@
package de.kif.frontend.views.calendar
import de.kif.common.Search
import de.kif.frontend.launch
import de.kif.frontend.repository.WorkGroupRepository
import de.westermann.kobserve.list.filterObservable
import de.westermann.kobserve.list.observableListOf
import de.westermann.kobserve.list.sortObservable
import de.westermann.kwebview.View
import de.westermann.kwebview.components.Button
import de.westermann.kwebview.components.InputView
import de.westermann.kwebview.components.ListView
import de.westermann.kwebview.extra.listFactory
import org.w3c.dom.HTMLButtonElement
import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLInputElement
class CalendarEdit(
private val calendar: Calendar, view: HTMLElement
) : View(view) {
private val toggleEditButton =
Button.wrap(view.querySelector(".calendar-edit-top button") as HTMLButtonElement)
val search =
InputView.wrap(view.querySelector(".calendar-edit-search input") as HTMLInputElement)
val listView = ListView.wrap<CalendarWorkGroup>(
view.querySelector(".calendar-edit-list") as HTMLElement
)
private var loaded = false
val workGroupList = observableListOf<CalendarWorkGroup>()
private val sortedList = workGroupList.sortObservable(compareBy {
it.workGroup.name
}).filterObservable(search.valueProperty) { entry, search ->
val s = entry.workGroup.createSearch()
Search.match(search, s)
}
private fun load() {
if (loaded) return
loaded = true
launch {
for (workGroup in WorkGroupRepository.all()) {
workGroupList += CalendarWorkGroup(calendar, this, workGroup)
}
}
}
init {
toggleEditButton.onClick {
calendar.classList.toggle("edit")
if (!loaded) {
load()
}
}
WorkGroupRepository.onCreate {
if (loaded) {
launch {
workGroupList += CalendarWorkGroup(
calendar,
this,
WorkGroupRepository.get(it)!!
)
}
}
}
listView.listFactory(sortedList)
}
}

View file

@ -0,0 +1,222 @@
package de.kif.frontend.views.calendar
import de.kif.common.CALENDAR_GRID_WIDTH
import de.kif.common.model.Schedule
import de.kif.common.model.WorkGroup
import de.kif.frontend.iterator
import de.kif.frontend.launch
import de.kif.frontend.repository.RepositoryDelegate
import de.kif.frontend.repository.ScheduleRepository
import de.westermann.kwebview.Point
import de.westermann.kwebview.View
import de.westermann.kwebview.components.Body
import de.westermann.kwebview.createHtmlView
import de.westermann.kwebview.toPoint
import org.w3c.dom.HTMLElement
import org.w3c.dom.events.MouseEvent
import kotlin.dom.appendText
import kotlin.dom.isText
class CalendarEntry(private val calendar: Calendar, view: HTMLElement) : View(view) {
private lateinit var mouseDelta: Point
private var newCell: CalendarCell? = null
private var language by dataset.property("language")
var scheduleId = dataset["id"]?.toLongOrNull() ?: -1
val schedule =
RepositoryDelegate(ScheduleRepository, scheduleId)
private lateinit var workGroup: WorkGroup
var pending by classList.property("pending")
var editable: Boolean = false
private fun onMove(event: MouseEvent) {
val position = event.toPoint() - mouseDelta
val cell = calendar.calendarCells.find {
position in it.dimension
}
if (cell != null) {
cell += this
if (newCell == null) {
style {
left = "0"
top = "0.1rem"
}
}
newCell = cell
}
event.preventDefault()
event.stopPropagation()
}
private fun onFinishMove(event: MouseEvent) {
classList -= "drag"
newCell?.let { cell ->
launch {
val newTime = cell.time
val newRoom = cell.getRoom()
pending = true
if (scheduleId < 0) {
ScheduleRepository.create(
Schedule(
null,
workGroup,
newRoom,
calendar.day,
newTime
)
)
html.remove()
} else {
ScheduleRepository.update(schedule.get().copy(room = newRoom, time = newTime))
}
}
}
newCell = null
for (it in listeners) {
it.detach()
}
listeners = emptyList()
event.preventDefault()
event.stopPropagation()
}
private var listeners: List<de.westermann.kobserve.event.EventListener<*>> = emptyList()
fun startDrag() {
listeners = listOf(
Body.onMouseMove.reference(this::onMove),
Body.onMouseUp.reference(this::onFinishMove),
Body.onMouseLeave.reference(this::onFinishMove)
)
}
init {
onMouseDown { event ->
if (!editable || event.target != html || "pending" in classList) {
event.stopPropagation()
return@onMouseDown
}
launch {
classList += "drag"
val s = schedule.get()
val time = s.time / CALENDAR_GRID_WIDTH * CALENDAR_GRID_WIDTH
val p = calendar.calendarCells.find {
it.day == s.day && it.time == time && it.roomId == s.room.id
}?.dimension?.center ?: dimension.center
mouseDelta = event.toPoint() - p
startDrag()
}
event.preventDefault()
event.stopPropagation()
}
var calendarTools: CalendarTools? = null
for (item in html.children) {
if (item.classList.contains("calendar-tools")) {
calendarTools = CalendarTools(this, item)
break
}
}
if (calendarTools == null) {
calendarTools = CalendarTools(this, createHtmlView())
html.appendChild(calendarTools.html)
}
}
fun load(schedule: Schedule) {
pending = false
if (schedule.id != null) scheduleId = schedule.id
this.schedule.set(schedule)
style {
val pos = (schedule.time % CALENDAR_GRID_WIDTH) / CALENDAR_GRID_WIDTH.toDouble()
val ps = "${pos * 100}%"
left = ps
top = "calc($ps + 0.1rem)"
}
load(schedule.workGroup)
val time = schedule.time / CALENDAR_GRID_WIDTH * CALENDAR_GRID_WIDTH
val cell = calendar.calendarCells.find {
it.day == schedule.day && it.time == time && it.roomId == schedule.room.id
}
if (cell != null && cell.html != html.parentElement) {
cell += this
}
}
fun load(workGroup: WorkGroup) {
pending = false
language = workGroup.language.code
this.workGroup = workGroup
style {
val size = workGroup.length / CALENDAR_GRID_WIDTH.toDouble()
val sz = "${size * 100}%"
width = sz
height = "calc($sz - 0.2rem)"
if (workGroup.track?.color != null) {
backgroundColor = workGroup.track.color.toString()
color = workGroup.track.color.calcTextColor().toString()
}
}
for (element in html.childNodes) {
if (element.isText) {
html.removeChild(element)
}
}
html.appendText(workGroup.name)
}
companion object {
fun create(calendar: Calendar, schedule: Schedule): CalendarEntry {
val entry = CalendarEntry(calendar, createHtmlView())
entry.load(schedule)
return entry
}
fun create(calendar: Calendar, workGroup: WorkGroup): CalendarEntry {
val entry = CalendarEntry(calendar, createHtmlView())
entry.load(workGroup)
entry.mouseDelta = Point.ZERO
entry.startDrag()
return entry
}
}
}

View file

@ -0,0 +1,129 @@
package de.kif.frontend.views.calendar
import de.kif.common.CALENDAR_GRID_WIDTH
import de.kif.frontend.iterator
import de.kif.frontend.launch
import de.kif.frontend.repository.ScheduleRepository
import de.westermann.kwebview.View
import de.westermann.kwebview.createHtmlView
import org.w3c.dom.HTMLAnchorElement
import org.w3c.dom.HTMLElement
import org.w3c.dom.events.EventListener
class CalendarTools(entry: CalendarEntry, view: HTMLElement) : View(view) {
init {
var linkM10: HTMLAnchorElement? = null
var linkM5: HTMLAnchorElement? = null
var linkReset: HTMLAnchorElement? = null
var linkP5: HTMLAnchorElement? = null
var linkP10: HTMLAnchorElement? = null
var linkDel: HTMLAnchorElement? = null
for (element in html.children) {
when {
element.classList.contains("calendar-tools-m10") -> linkM10 = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-m5") -> linkM5 = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-reset") -> linkReset = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-p5") -> linkP5 = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-p10") -> linkP10 = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-del") -> linkDel = element as? HTMLAnchorElement
}
}
linkM10 = linkM10 ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-m10")
link.textContent = "-10"
html.appendChild(link)
link
}
linkM10.removeAttribute("href")
linkM10.addEventListener("click", EventListener {
entry.pending = true
launch {
val s = entry.schedule.get()
ScheduleRepository.update(s.copy(time = s.time - 10))
}
})
linkM5 = linkM5 ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-m5")
link.textContent = "-5"
html.appendChild(link)
link
}
linkM5.removeAttribute("href")
linkM5.addEventListener("click", EventListener {
entry.pending = true
launch {
val s = entry.schedule.get()
ScheduleRepository.update(s.copy(time = s.time - 5))
}
})
linkReset = linkReset ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-reset")
link.textContent = "reset"
html.appendChild(link)
link
}
linkReset.removeAttribute("href")
linkReset.addEventListener("click", EventListener {
entry.pending = true
launch {
val s = entry.schedule.get()
ScheduleRepository.update(s.copy(time = s.time / CALENDAR_GRID_WIDTH * CALENDAR_GRID_WIDTH))
}
})
linkP5 = linkP5 ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-p5")
link.textContent = "+5"
html.appendChild(link)
link
}
linkP5.removeAttribute("href")
linkP5.addEventListener("click", EventListener {
entry.pending = true
launch {
val s = entry.schedule.get()
ScheduleRepository.update(s.copy(time = s.time + 5))
}
})
linkP10 = linkP10 ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-p10")
link.textContent = "+10"
html.appendChild(link)
link
}
linkP10.removeAttribute("href")
linkP10.addEventListener("click", EventListener {
entry.pending = true
launch {
val s = entry.schedule.get()
ScheduleRepository.update(s.copy(time = s.time + 10))
}
})
linkDel = linkDel ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-del")
link.textContent = "del"
html.appendChild(link)
link
}
linkDel.removeAttribute("href")
linkDel.addEventListener("click", EventListener {
entry.pending = true
launch {
ScheduleRepository.delete(entry.scheduleId)
}
})
}
}

View file

@ -0,0 +1,56 @@
package de.kif.frontend.views.calendar
import de.kif.common.model.WorkGroup
import de.kif.frontend.launch
import de.kif.frontend.repository.WorkGroupRepository
import de.westermann.kwebview.View
class CalendarWorkGroup(
private val calendar: Calendar,
private val calendarEdit: CalendarEdit,
workGroup: WorkGroup
) : View() {
private var language by dataset.property("language")
lateinit var workGroup: WorkGroup
private set
private fun load(workGroup: WorkGroup) {
this.workGroup = workGroup
html.textContent = workGroup.name
language = workGroup.language.code
style {
if (workGroup.track?.color != null) {
backgroundColor = workGroup.track.color.toString()
color = workGroup.track.color.calcTextColor().toString()
}
}
}
init {
load(workGroup)
val references: MutableList<de.westermann.kobserve.event.EventListener<*>> = mutableListOf()
references += WorkGroupRepository.onUpdate.reference {
if (it == workGroup.id) {
launch {
load(WorkGroupRepository.get(it)!!)
}
}
}
references += WorkGroupRepository.onDelete.reference {
if (it == workGroup.id) {
calendarEdit.workGroupList -= this
}
}
onMouseDown {
CalendarEntry.create(calendar, workGroup)
}
}
}

View file

@ -6,7 +6,7 @@ import kotlin.dom.clear
/**
* @author lars
*/
abstract class ViewCollection<V : View>(view: HTMLElement = createHtmlView()) : View(view), Iterable<V> {
abstract class ViewCollection<V : View>(view: HTMLElement = createHtmlView()) : View(view), Collection<V> {
private val children: MutableList<V> = mutableListOf()
@ -29,6 +29,36 @@ abstract class ViewCollection<V : View>(view: HTMLElement = createHtmlView()) :
}
}
fun replace(oldView: V, newView: V) {
if (children.contains(oldView)) {
children.add(children.indexOf(oldView), newView)
html.insertBefore(newView.html, oldView.html)
children -= oldView
html.removeChild(oldView.html)
}
}
fun add(view: V) = append(view)
fun add(index: Int, view: V) {
if (index >= size) {
append(view)
} else {
html.insertBefore(view.html, children[index].html)
children.add(index, view)
}
}
operator fun get(index: Int): V {
return children[index]
}
fun removeAt(index: Int) {
if (index in 0 until size) {
remove(children[index])
}
}
fun toForeground(view: V) {
if (view in children && children.indexOf(view) < children.size - 1) {
remove(view)
@ -48,8 +78,7 @@ abstract class ViewCollection<V : View>(view: HTMLElement = createHtmlView()) :
operator fun minusAssign(view: V) = remove(view)
val isEmpty: Boolean
get() = children.isEmpty()
override fun isEmpty(): Boolean = children.isEmpty()
fun clear() {
children.clear()
@ -58,16 +87,14 @@ abstract class ViewCollection<V : View>(view: HTMLElement = createHtmlView()) :
override fun iterator(): Iterator<V> = children.iterator()
val size: Int
override val size: Int
get() = children.size
operator fun contains(view: V) = children.contains(view)
override fun contains(element: V) = children.contains(element)
override fun containsAll(elements: Collection<V>): Boolean = children.containsAll(elements)
operator fun V.unaryPlus() {
append(this)
}
companion object {
fun <V : View> wrap(htmlElement: HTMLElement) = object : ViewCollection<V>(htmlElement) {}
}
}

View file

@ -5,7 +5,9 @@ import org.w3c.dom.HTMLInputElement
import kotlin.math.abs
import kotlin.random.Random
abstract class ViewForLabel : View(createHtmlView<HTMLInputElement>()) {
abstract class ViewForLabel(
view: HTMLInputElement = createHtmlView()
) : View(view) {
override val html = super.html as HTMLInputElement
private var label: Label? = null

View file

@ -14,7 +14,7 @@ import org.w3c.dom.HTMLButtonElement
*
* @author lars
*/
class Button() : ViewCollection<View>(createHtmlView<HTMLButtonElement>()) {
class Button(view: HTMLButtonElement = createHtmlView()) : ViewCollection<View>(view) {
constructor(text: String) : this() {
this.text = text
@ -38,6 +38,10 @@ class Button() : ViewCollection<View>(createHtmlView<HTMLButtonElement>()) {
}
val textProperty: Property<String> = property(this::text)
companion object {
fun wrap(view: HTMLButtonElement) = Button(view)
}
}
@KWebViewDsl

View file

@ -3,8 +3,8 @@ package de.westermann.kwebview.components
import de.westermann.kobserve.Property
import de.westermann.kobserve.ReadOnlyProperty
import de.westermann.kobserve.ValidationProperty
import de.westermann.kobserve.property.property
import de.westermann.kobserve.not
import de.westermann.kobserve.property.property
import de.westermann.kwebview.*
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.events.Event
@ -12,9 +12,10 @@ import org.w3c.dom.events.EventListener
import org.w3c.dom.events.KeyboardEvent
class InputView(
type: InputType,
initValue: String = ""
) : ViewForLabel() {
type: InputType,
initValue: String = "",
view: HTMLInputElement = createHtmlView()
) : ViewForLabel(view) {
fun bind(property: ReadOnlyProperty<String>) {
valueProperty.bind(property)
@ -108,12 +109,17 @@ class InputView(
html.addEventListener("keyup", changeListener)
html.addEventListener("keypress", changeListener)
}
companion object {
fun wrap(view: HTMLInputElement) = InputView(InputType.SEARCH, view.value, view)
}
}
enum class InputType(val html: String) {
TEXT("text"),
NUMBER("number"),
PASSWORD("password");
PASSWORD("password"),
SEARCH("search");
companion object {
fun find(html: String): InputType? = values().find { it.html == html }
@ -122,33 +128,49 @@ enum class InputType(val html: String) {
@KWebViewDsl
fun ViewCollection<in InputView>.inputView(text: String = "", init: InputView.() -> Unit = {}) =
InputView(InputType.TEXT, text).also(this::append).also(init)
InputView(InputType.TEXT, text).also(this::append).also(init)
@KWebViewDsl
fun ViewCollection<in InputView>.inputView(text: ReadOnlyProperty<String>, init: InputView.() -> Unit = {}) =
InputView(InputType.TEXT, text.value).also(this::append).also { it.bind(text) }.also(init)
InputView(InputType.TEXT, text.value).also(this::append).also { it.bind(text) }.also(init)
@KWebViewDsl
fun ViewCollection<in InputView>.inputView(text: Property<String>, init: InputView.() -> Unit = {}) =
InputView(InputType.TEXT, text.value).also(this::append).also { it.bind(text) }.also(init)
InputView(InputType.TEXT, text.value).also(this::append).also { it.bind(text) }.also(init)
@KWebViewDsl
fun ViewCollection<in InputView>.inputView(text: ValidationProperty<String>, init: InputView.() -> Unit = {}) =
InputView(InputType.TEXT, text.value).also(this::append).also { it.bind(text) }.also(init)
InputView(InputType.TEXT, text.value).also(this::append).also { it.bind(text) }.also(init)
@KWebViewDsl
fun ViewCollection<in InputView>.inputView(type: InputType = InputType.TEXT, text: String = "", init: InputView.() -> Unit = {}) =
InputView(type, text).also(this::append).also(init)
fun ViewCollection<in InputView>.inputView(
type: InputType = InputType.TEXT,
text: String = "",
init: InputView.() -> Unit = {}
) =
InputView(type, text).also(this::append).also(init)
@KWebViewDsl
fun ViewCollection<in InputView>.inputView(type: InputType = InputType.TEXT, text: ReadOnlyProperty<String>, init: InputView.() -> Unit = {}) =
InputView(type, text.value).also(this::append).also { it.bind(text) }.also(init)
fun ViewCollection<in InputView>.inputView(
type: InputType = InputType.TEXT,
text: ReadOnlyProperty<String>,
init: InputView.() -> Unit = {}
) =
InputView(type, text.value).also(this::append).also { it.bind(text) }.also(init)
@KWebViewDsl
fun ViewCollection<in InputView>.inputView(type: InputType = InputType.TEXT, text: Property<String>, init: InputView.() -> Unit = {}) =
InputView(type, text.value).also(this::append).also { it.bind(text) }.also(init)
fun ViewCollection<in InputView>.inputView(
type: InputType = InputType.TEXT,
text: Property<String>,
init: InputView.() -> Unit = {}
) =
InputView(type, text.value).also(this::append).also { it.bind(text) }.also(init)
@KWebViewDsl
fun ViewCollection<in InputView>.inputView(type: InputType = InputType.TEXT, text: ValidationProperty<String>, init: InputView.() -> Unit = {}) =
InputView(type, text.value).also(this::append).also { it.bind(text) }.also(init)
fun ViewCollection<in InputView>.inputView(
type: InputType = InputType.TEXT,
text: ValidationProperty<String>,
init: InputView.() -> Unit = {}
) =
InputView(type, text.value).also(this::append).also { it.bind(text) }.also(init)

View file

@ -0,0 +1,30 @@
package de.westermann.kwebview.components
import de.westermann.kwebview.KWebViewDsl
import de.westermann.kwebview.View
import de.westermann.kwebview.ViewCollection
import de.westermann.kwebview.createHtmlView
import org.w3c.dom.HTMLDivElement
import org.w3c.dom.HTMLElement
class ListView<T : View>(view: HTMLElement = createHtmlView()) : ViewCollection<T>(view) {
override val html = super.html as HTMLDivElement
companion object {
fun <T : View> wrap(view: HTMLElement) = ListView<T>(view)
}
}
@KWebViewDsl
fun <T : View> ViewCollection<in ListView<T>>.listView(
vararg classes: String,
init: ListView<T>.() -> Unit = {}
): ListView<T> {
val view = ListView<T>()
for (c in classes) {
view.classList += c
}
append(view)
init(view)
return view
}

View file

@ -0,0 +1,62 @@
package de.westermann.kwebview.extra
import de.westermann.kobserve.list.ObservableReadOnlyList
import de.westermann.kwebview.View
import de.westermann.kwebview.ViewCollection
import de.westermann.kwebview.async
fun <T, V : View> ViewCollection<in V>.listFactory(
list: ObservableReadOnlyList<T>,
factory: (T) -> V,
animateAdd: Int? = null,
animateRemove: Int? = null
) {
for (element in list) {
+factory(element)
}
list.onAdd { (index, element) ->
val view = factory(element)
add(index, view)
if (animateAdd != null) {
classList += "animate-add"
view.classList += "active"
async(animateAdd) {
classList -= "animate-add"
view.classList -= "active"
}
}
}
list.onRemove { (index) ->
@Suppress("UNCHECKED_CAST") val view = this[index] as V
if (animateRemove == null) {
remove(view)
} else {
classList += "animate-remove"
view.classList += "active"
async(animateRemove) {
classList -= "animate-remove"
view.classList -= "active"
remove(view)
}
}
}
list.onUpdate { (oldIndex, newIndex, element) ->
removeAt(oldIndex)
add(newIndex, factory(element))
}
}
fun <V : View> ViewCollection<in V>.listFactory(
list: ObservableReadOnlyList<V>,
animateAdd: Int? = null,
animateRemove: Int? = null
) = listFactory(
list,
{ it },
animateAdd,
animateRemove
)

View file

@ -220,28 +220,6 @@ a {
.table-layout-search {
float: left;
padding-bottom: 0.5rem !important;
input {
padding-right: 4rem;
}
.btn-search {
position: absolute;
top: 0;
height: 2.5rem;
line-height: 2.5rem;
right: -3px;
border-bottom-left-radius: 0;
border-top-left-radius: 0;
margin-top: 1px;
margin-right: 2px;
}
input:focus ~ .btn-search {
border-color: $primary-color;
border-width: 2px;
}
}
.table-layout-action {
@ -470,13 +448,106 @@ form {
width: 100%;
position: relative;
margin-top: 1rem;
}
& > div {
width: calc(100% - 6rem);
overflow-x: scroll;
overflow-y: visible;
margin-left: 6rem;
padding-bottom: 1rem;
.calendar-table {
float: left;
width: 100%;
overflow-x: scroll;
overflow-y: visible;
padding-bottom: 1rem;
position: relative;
transition: width $transitionTime;
}
.calendar-edit {
width: 16rem;
display: block;
position: absolute;
right: 0;
top: -3rem;
padding-top: 3rem;
overflow: hidden;
.calendar-edit-main {
position: relative;
left: 16rem;
opacity: 0;
transition: left $transitionTime, opacity$transitionTime;
}
}
.calendar-edit-top {
position: absolute;
right: 0;
top: 0;
}
.calendar-edit-search {
padding: 0.5rem;
}
.calendar-work-group {
position: relative;
display: block;
border-radius: 0.2rem;
line-height: 2rem;
font-size: 0.8rem;
white-space: nowrap;
text-overflow: ellipsis;
background-color: $primary-color;
color: $primary-text-color;
padding: 0 0.5rem;
margin: 0.5rem;
&::after {
content: attr(data-language);
bottom: 0;
right: 1rem;
opacity: 0.6;
position: absolute;
text-transform: uppercase;
}
&:hover .calendar-tools {
display: block;
}
&.drag {
box-shadow: 0 0.1rem 0.2rem rgba($text-primary-color, 0.8);
z-index: 2;
.calendar-tools {
display: none;
}
}
&.pending::before {
content: '';
display: block;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: $background-primary-color;
opacity: 0.6;
}
@include no-select()
}
.calendar[data-editable = "true"].edit {
.calendar-table {
width: calc(100% - 16rem);
border-right: solid 1px rgba($text-primary-color, 0.1);
}
.calendar-edit-main {
left: 0;
opacity: 1;
}
}
@ -507,6 +578,10 @@ form {
}
}
.calendar[data-editable=false] .calendar-tools {
display: none !important;
}
.calendar-entry {
position: absolute;
display: block;
@ -555,6 +630,8 @@ form {
background-color: $background-primary-color;
opacity: 0.6;
}
@include no-select()
}
.calendar-table-time-to-room {
@ -618,7 +695,6 @@ form {
}
.calendar-cell:first-child {
position: absolute;
width: 6rem;
left: 0;
text-align: center;
@ -663,13 +739,6 @@ form {
line-height: 2rem;
height: 1.3rem;
&:nth-child(4n + 2)::before {
content: '';
position: absolute;
width: calc(100% - 6rem);
border-top: solid 1px rgba($text-primary-color, 0.1);
}
.calendar-cell {
position: relative;
width: 12rem;
@ -677,6 +746,7 @@ form {
&::before {
content: '';
height: 100%;
width: 100%;
left: 0;
top: 0;
border-left: solid 1px rgba($text-primary-color, 0.1);
@ -694,18 +764,22 @@ form {
left: 0.1rem !important;
right: 0.1rem;
}
a {
position: relative;
}
}
.calendar-cell:first-child:not(:empty)::before {
width: 100%;
top: 0;
.calendar-cell:first-child::before {
border-left: none;
}
&:nth-child(4n + 2) .calendar-cell::before {
border-top: solid 1px rgba($text-primary-color, 0.1);
}
}
.calendar-cell:first-child {
position: absolute;
width: 6rem;
left: 0;
text-align: center;