Compare commits

...

No commits in common. "master" and "dummy" have entirely different histories.

168 changed files with 90 additions and 18653 deletions

11
.gitignore vendored
View file

@ -1,11 +0,0 @@
.gradle/
.idea/
build/
web/
.sessions/
data/
*.swp
*.swo
*.db

7
.htaccess Executable file
View file

@ -0,0 +1,7 @@
Redirect /anmeldung https://tix.kif.rocks/470/
Redirect /wiki https://wiki.kif.rocks/wiki/KIF470:Hauptseite
Redirect /kulturtag https://wiki.kif.rocks/wiki/KIF470:Kulturtag
Redirect /cfd https://wiki.kif.rocks/wiki/KIF470:Call_for_Decoration
Redirect /twitter https://twitter.com/kif470
Redirect /teilnehmerliste https://tix.kif.rocks/470/page/oeffentliche-anmeldungen/
Redirect /engel https://engel.470.kif.rocks/

View file

@ -1,5 +0,0 @@
MIT License
Copyright (c) 2019 Lars Westermann
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

14
src/jsMain/resources/images/logo.svg → Logo.svg Normal file → Executable file

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

BIN
Montserrat-Bold.ttf Executable file

Binary file not shown.

View file

@ -1,16 +0,0 @@
# portal
Webportal for everything and stuff
## Usage
The server can be started directly via:
```bash
./gradlew run
```
Or create a shadow jar:
```bash
./gradlew jar
java -jar build/libs/portal.jar
```

View file

@ -1,223 +0,0 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import java.text.SimpleDateFormat
buildscript {
repositories {
jcenter()
maven {
url "https://plugins.gradle.org/m2/"
}
}
}
plugins {
id 'kotlin-multiplatform' version '1.3.31'
id 'kotlinx-serialization' version '1.3.31'
id "org.kravemir.gradle.sass" version "1.2.2"
id "com.github.johnrengelman.shadow" version "5.0.0"
}
group "de.kif"
version "0.1.0"
repositories {
mavenCentral()
jcenter()
maven { url 'https://jitpack.io' }
maven { url "https://dl.bintray.com/kotlin/ktor" }
maven { url "https://dl.bintray.com/jetbrains/markdown" }
maven { url "https://kotlin.bintray.com/kotlinx" }
maven { url "https://kotlin.bintray.com/kotlin-js-wrappers" }
maven { url "https://dl.bintray.com/soywiz/soywiz" }
}
def ktor_version = '1.1.5'
def serialization_version = '0.11.0'
def observable_version = '0.9.3'
def klockVersion = "1.4.0"
kotlin {
jvm() {
compilations.all {
kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs += [
"-Xuse-experimental=io.ktor.util.KtorExperimentalAPI"
]
}
}
}
js() {
compilations.all {
kotlinOptions {
moduleKind = "umd"
sourceMap = true
metaInfo = true
}
}
}
sourceSets {
commonMain {
dependencies {
implementation kotlin('stdlib-common')
implementation "de.westermann:KObserve-metadata:$observable_version"
implementation "com.soywiz:klock-metadata:$klockVersion"
implementation "com.soywiz:klock-locale-metadata:$klockVersion"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:$serialization_version"
}
}
commonTest {
dependencies {
implementation kotlin('test-common')
implementation kotlin('test-annotations-common')
}
}
jvmMain {
dependencies {
implementation kotlin('stdlib-jdk8')
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:$serialization_version"
implementation "io.ktor:ktor-server-netty:$ktor_version"
implementation "io.ktor:ktor-auth:$ktor_version"
implementation "io.ktor:ktor-server-sessions:$ktor_version"
implementation "io.ktor:ktor-jackson:$ktor_version"
implementation 'org.jetbrains:kotlin-css-jvm:1.0.0-pre.70-kotlin-1.3.21'
implementation "io.ktor:ktor-html-builder:$ktor_version"
implementation "io.ktor:ktor-client-apache:$ktor_version"
implementation 'org.xerial:sqlite-jdbc:3.25.2'
api 'mysql:mysql-connector-java:8.0.16'
api 'org.mariadb.jdbc:mariadb-java-client:1.1.7'
implementation 'org.jetbrains.exposed:exposed:0.12.2'
implementation 'net.sf.biweekly:biweekly:0.6.3'
implementation 'org.mindrot:jbcrypt:0.4'
implementation "de.westermann:KObserve-jvm:$observable_version"
implementation "com.soywiz:klock-jvm:$klockVersion"
implementation "com.soywiz:klock-locale-jvm:$klockVersion"
implementation 'com.github.uchuhimo:konf:master-SNAPSHOT'
implementation 'com.vladsch.flexmark:flexmark-all:0.42.10'
api 'io.github.microutils:kotlin-logging:1.6.23'
api 'ch.qos.logback:logback-classic:1.2.3'
api 'org.fusesource.jansi:jansi:1.8'
}
}
jvmTest {
dependencies {
implementation kotlin('test')
implementation kotlin('test-junit')
}
}
jsMain {
dependencies {
implementation kotlin('stdlib-js')
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:$serialization_version"
implementation "de.westermann:KObserve-js:$observable_version"
implementation "com.soywiz:klock-js:$klockVersion"
implementation "com.soywiz:klock-locale-js:$klockVersion"
}
}
jsTest {
dependencies {
implementation kotlin('test-js')
}
}
}
}
sass {
main {
srcDir = file("$projectDir/src/jsMain/resources/style")
outDir = file("$buildDir/processedResources/js/main/style")
exclude = "**/*.css"
}
}
def webFolder = new File(project.buildDir, "../web")
task populateWebFolder(dependsOn: [jsMainClasses, sass]) {
doLast {
copy {
from kotlin.targets.js.compilations.main.output
from kotlin.sourceSets.jsMain.resources.srcDirs
kotlin.targets.js.compilations.test.runtimeDependencyFiles.files.each {
if (it.exists() && !it.isDirectory()) {
from zipTree(it.absolutePath).matching {
include '*.js'
exclude '*.meta.js'
}
}
}
into webFolder
}
}
}
jsJar.dependsOn(populateWebFolder)
def mainClassName = 'de.kif.backend.MainKt'
task run(type: JavaExec, dependsOn: [jvmMainClasses, jsJar]) {
main = mainClassName
classpath {
[
kotlin.targets.jvm.compilations.main.output.allOutputs.files,
configurations.jvmRuntimeClasspath,
]
}
args = []
standardInput = System.in
}
clean.doFirst {
delete webFolder
delete ".sessions"
delete "data"
}
jsJar {
from webFolder
}
static String buildTime() {
def format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z")
format.setTimeZone(TimeZone.getTimeZone("UTC"))
return format.format(new Date())
}
task jar(type: ShadowJar, dependsOn: [assemble]) {
//minimize()
from(webFolder) {
into "web"
includeEmptyDirs false
exclude "*.js.map", "*.meta.js", "**/*.scss", "**/_*.css", "**/*.kjsm", "*.MF"
}
from kotlin.targets.jvm.compilations.main.runtimeDependencyFiles
from kotlin.targets.jvm.compilations.main.output
exclude "**/INDEX.LIST", "**/*.SF", "**/*.RSA"
archiveBaseName.set rootProject.name
archiveClassifier.set null
archiveVersion.set null
manifest {
attributes 'Main-Class': mainClassName
attributes "Build-Time": buildTime()
attributes "Build-Version": project.version
attributes "Build-Tools": "gradle-${project.getGradle().getGradleVersion()}, groovy-${GroovySystem.getVersion()}, java-${System.getProperty('java.version')}"
attributes "Build-System": "${System.getProperty("os.name")} '${System.getProperty("os.version")}' (${System.getProperty("os.arch")})"
}
}

View file

@ -1,19 +0,0 @@
* kif.ifsr.de geht zu dashboard
* kleine Kacheln mit Infos
* Zetplan wie Congress
* Click auf Eintrag gibt weitere Informationen
* Drag'n'Drop für angemeldete Menschen, sonst nicht.
* Beamerview
* Registrierte Benutzer:
* Helfer, Orga, Admins
* Anwednungsfälle
* Zeitplan bearbeiten
* Dashboard bearbeiten
* News hinzufügen/bearbeiten
* Räume hinzufügen
* AKS importieren
* AKs bearbeiten
* Account erstellen
* Personen eintragen/importieren
* Konstraints eintragen

View file

@ -1 +0,0 @@
kotlin.code.style=official

Binary file not shown.

View file

@ -1,6 +0,0 @@
#Tue Feb 05 15:31:59 CET 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip

172
gradlew vendored
View file

@ -1,172 +0,0 @@
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

84
gradlew.bat vendored
View file

@ -1,84 +0,0 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

77
index.html Executable file
View file

@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>47.0 KIF</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
@font-face {
font-family: Montserrat;
src: url('Montserrat-Bold.ttf')
}
body {
font-family: Montserrat, sans-serif;
font-size: 20pt;
max-width: 400px;
margin: auto;
background: #fff;
}
body > div {
width: 100%;
}
nav {
border-radius: 30px;
width: 77%;
padding: 4%;
background: rgb(177, 225, 28);
margin-top: -7%;
margin-left: auto;
margin-right: auto;
}
nav div {
background: #fff;
border-radius: 18px;
}
nav a {
display: block;
text-decoration: none;
color: rgb(177, 225, 28);
text-align: center;
border-top: solid 10px rgb(177, 225, 28);
padding: 10px 0;
}
nav a:hover {
background-color: rgb(177, 225, 28);
color: white;
outline: none;
}
nav a:first-child {
border: none;
}
</style>
</head>
<body>
<div>
<img src="Logo.svg">
</div>
<nav>
<div>
<a href="anmeldung">Anmeldung</a>
<a href="teilnehmerliste">Teilnehmerliste</a>
<a href="wiki">Wiki</a>
<a href="kulturtag">Kulturtag</a>
<a href="cfd">Call for Decoration</a>
<a href="twitter">Twitter</a>
<a href="engel">Engelsystem</a>
</div>
</nav>
</body>
</html>

View file

@ -1,26 +0,0 @@
[server]
host = "localhost"
port = 8080
prefix = "/plan"
debug = false
[schedule]
reference = "2019-06-13"
offset = 7200000
wall_start = 0
[reso]
day = 2
time = 900
[general]
wiki_url = "https://wiki.kif.rocks/w/index.php?title=KIF470:Arbeitskreise&action=raw"
[twitter]
timeline = "https://twitter.com/kiforbiter?ref_src=twsrc%5Etfw"
[database]
type = "sqlite"
url = ""
username = ""
password = ""

View file

@ -1,13 +0,0 @@
pluginManagement {
resolutionStrategy {
eachPlugin {
if (requested.id.id == "kotlin-multiplatform") {
useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:${requested.version}")
}
if (requested.id.id == "kotlinx-serialization") {
useModule("org.jetbrains.kotlin:kotlin-serialization:${requested.version}")
}
}
}
}
rootProject.name = 'portal'

View file

@ -1,67 +0,0 @@
package de.kif.common
import de.kif.common.model.Model
import de.westermann.kobserve.event.EventHandler
abstract class CachedRepository<T : Model>(val repository: Repository<T>) : Repository<T> {
override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>()
private var cache: Map<Long, T> = emptyMap()
private var cacheComplete: Boolean = false
override suspend fun get(id: Long): T? {
return if (id in cache) {
cache[id]
} else {
val element = repository.get(id)
if (element != null) {
cache = cache + (id to element)
}
element
}
}
override suspend fun create(model: T): Long {
return repository.create(model)
}
override suspend fun update(model: T) {
repository.update(model)
}
override suspend fun delete(id: Long) {
repository.delete(id)
}
override suspend fun all(): List<T> {
return if (cacheComplete) {
cache.values.toList()
} else {
val all = repository.all()
cache = all.associateBy { it.id!! }
cacheComplete = true
all
}
}
init {
repository.onCreate {
cacheComplete = false
cache = cache - it
}
repository.onUpdate {
cacheComplete = false
cache = cache - it
}
repository.onDelete {
cache = cache - it
}
}
}

View file

@ -1,3 +0,0 @@
package de.kif.common
const val CALENDAR_GRID_WIDTH = 15

View file

@ -1,210 +0,0 @@
package de.kif.common
import de.kif.common.model.ConstraintType
import de.kif.common.model.Room
import de.kif.common.model.Schedule
import kotlinx.serialization.Serializable
@Serializable
data class ConstraintError(
val reason: String = ""
)
@Serializable
data class ConstraintMap(
val map: List<Pair<Long, List<ConstraintError>>>
)
fun checkConstraints(
check: List<Schedule>,
against: List<Schedule>,
rooms: List<Room>,
resoDay: Int,
resoTime: Int
): ConstraintMap {
val map = mutableMapOf<Long, List<ConstraintError>>()
val roomMap = rooms.associateBy { it.id }
for (schedule in check) {
if (schedule.id == null) continue
val errors = mutableListOf<ConstraintError>()
if (schedule.workGroup.projector && !schedule.room.projector) {
errors += ConstraintError("Work group requires projector, but room does not have one!")
}
if (schedule.workGroup.internet && !schedule.room.internet) {
errors += ConstraintError("Work group requires internet, but room does not have one!")
}
if (schedule.workGroup.whiteboard && !schedule.room.whiteboard) {
errors += ConstraintError("Work group requires whiteboard, but room does not have one!")
}
if (schedule.workGroup.blackboard && !schedule.room.blackboard) {
errors += ConstraintError("Work group requires blackboard, but room does not have one!")
}
if (schedule.workGroup.accessible && !schedule.room.accessible) {
errors += ConstraintError("Work group requires accessible, but room does not have one!")
}
val blocked = schedule.room.blocked.map {
it.checkBlock(
schedule.day,
schedule.time,
schedule.time + schedule.workGroup.length
)
}.any { it }
if (blocked) {
errors += ConstraintError("The room ${schedule.room.name} is blocked!")
}
val start = schedule.getAbsoluteStartTime()
val end = schedule.getAbsoluteEndTime()
if (schedule.workGroup.resolution) {
val resoDeadline = resoDay * 24 * 60 + resoTime
if (end > resoDeadline) {
errors += ConstraintError("The work group is ${end - resoDeadline} minutes after resolution deadline")
}
}
if (schedule.workGroup.interested > schedule.room.places) {
errors += ConstraintError("The work group has more interested then the room has places")
}
for (leader in schedule.workGroup.leader) {
for (s in against) {
if (
schedule != s &&
schedule.day == s.day &&
leader in s.workGroup.leader &&
start < s.getAbsoluteEndTime() &&
s.getAbsoluteStartTime() < end
) {
errors += ConstraintError("Work group with leader $leader cannot be at same time with ${s.workGroup.name}!")
}
}
}
for (s in against) {
if (
schedule != s &&
schedule.day == s.day &&
schedule.room.id == s.room.id &&
start < s.getAbsoluteEndTime() &&
s.getAbsoluteStartTime() < end
) {
errors += ConstraintError("Work group cannot be at same time with ${s.workGroup.name} at the same room ${schedule.room.name}!")
}
}
for ((type, constraints) in schedule.workGroup.constraints.groupBy { it.type }) {
when (type) {
ConstraintType.OnlyOnDay -> {
val onlyOnDay = constraints.map { it.day == schedule.day }
if (onlyOnDay.none { it }) {
val dayList = constraints.mapNotNull { it.day }.distinct().sorted()
errors += ConstraintError("Work group requires days $dayList, but is on ${schedule.day}!")
}
}
ConstraintType.NotOnDay -> {
val notOnDay = constraints.map { it.day != schedule.day }
if (notOnDay.none { it }) {
val dayList = constraints.mapNotNull { it.day }.distinct().sorted()
errors += ConstraintError("Work group requires not days $dayList, but is on ${schedule.day}!")
}
}
ConstraintType.OnlyBeforeTime -> {
for (it in constraints) {
if (it.time == null) continue
if (it.day == null) {
if (it.time < schedule.time) {
errors += ConstraintError("Work group requires before time ${it.time}, but is on ${schedule.time}!")
}
} else {
if (it.day == schedule.day && it.time < schedule.time) {
errors += ConstraintError("Work group requires before time ${it.time} on day ${it.day}, but is on ${schedule.time}!")
}
}
}
}
ConstraintType.OnlyAfterTime -> {
for (it in constraints) {
if (it.time == null) continue
if (it.day == null) {
if (it.time > schedule.time) {
errors += ConstraintError("Work group requires after time ${it.time}, but is on ${schedule.time}!")
}
} else {
if (it.day == schedule.day && it.time > schedule.time) {
errors += ConstraintError("Work group requires after time ${it.time} on day ${it.day}, but is on ${schedule.time}!")
}
}
}
}
ConstraintType.ExactTime -> {
for (it in constraints) {
if (it.time == null) continue
if (it.day == null) {
if (it.time != schedule.time) {
errors += ConstraintError("Work group requires exact time ${it.time}, but is on ${schedule.time}!")
}
} else {
if (it.day == schedule.day && it.time != schedule.time) {
errors += ConstraintError("Work group requires exact time ${it.time} on day ${it.day}, but is on ${schedule.time}!")
}
}
}
}
ConstraintType.NotAtSameTime -> {
for (constraint in constraints) {
for (s in against) {
if (
s.workGroup.id == constraint.workGroup &&
schedule.day == s.day &&
start < s.getAbsoluteEndTime() &&
s.getAbsoluteStartTime() < end
) {
errors += ConstraintError("Work group requires not same time with ${s.workGroup.name}!")
}
}
}
}
ConstraintType.OnlyAfterWorkGroup -> {
for (constraint in constraints) {
for (s in against) {
if (
s.workGroup.id == constraint.workGroup && (
s.day > schedule.day ||
s.day == schedule.day && s.getAbsoluteEndTime() > start
)
) {
errors += ConstraintError("Work group requires after ${s.workGroup.name}!")
}
}
}
}
ConstraintType.Room -> {
val roomBools = constraints.map { it.room == schedule.room.id }
if (roomBools.none { it }) {
val roomList = constraints.mapNotNull { it.room }.distinct().sorted().map {
"${(roomMap[it]?.name ?: "")}($it)"
}
errors += ConstraintError("Work group requires rooms $roomList, but is in room ${schedule.room.name}(${schedule.room.id})")
}
}
}
}
map[schedule.id] = errors
}
return ConstraintMap(map.map { it.toPair() })
}

View file

@ -1,59 +0,0 @@
package de.kif.common
import com.soywiz.klock.*
import com.soywiz.klock.locale.german
import kotlin.math.ceil
fun formatDateTime(unix: Long, timezone: Long) =
DateFormat("EEEE, d. MMMM y HH:mm")
.withLocale(KlockLocale.german)
.format(DateTimeTz.utc(DateTime(unix), TimezoneOffset(timezone.toDouble())))
fun formatDate(unix: Long, timezone: Long) =
DateFormat("EEEE, d. MMMM y")
.withLocale(KlockLocale.german)
.format(DateTimeTz.utc(DateTime(unix), TimezoneOffset(timezone.toDouble())))
fun formatDateWithoutYear(unix: Long, timezone: Long) =
DateFormat("EEEE, d. MMMM")
.withLocale(KlockLocale.german)
.format(DateTimeTz.utc(DateTime(unix), TimezoneOffset(timezone.toDouble())))
fun formatTime(unix: Long, timezone: Long) =
DateFormat("HH:mm")
.withLocale(KlockLocale.german)
.format(DateTimeTz.utc(DateTime(unix), TimezoneOffset(timezone.toDouble())))
fun formatTimeDiff(time: Long, now: Long, timezone: Long): String {
var dt = ceil(((time - now) / 1000) / 60.0).toLong()
val minutes = dt % 60
dt /= 60
val hours = dt % 24
dt /= 24
val days = dt
return when {
days > 1L -> "in $days Tagen"
days > 0L -> "morgen"
hours > 1L -> {
val nowHour = DateFormat("HH")
.withLocale(KlockLocale.german)
.format(DateTimeTz.utc(DateTime(time), TimezoneOffset(timezone.toDouble()))).toIntOrNull() ?: 0
val ht = DateFormat("HH:mm")
.withLocale(KlockLocale.german)
.format(DateTimeTz.utc(DateTime(time), TimezoneOffset(timezone.toDouble())))
if ((ht.substringBefore(":").toIntOrNull() ?: 0) < nowHour) {
"morgen"
} else "um $ht"
}
hours > 0L -> "in " + hours.toTimeString() + ":" + minutes.toTimeString()
minutes > 1L -> "in 00:" + minutes.toTimeString()
else -> "in > 1 Minute"
}
}
@Suppress("NOTHING_TO_INLINE")
private inline fun Number.toTimeString() = toString().padStart(2, '0')

View file

@ -1,47 +0,0 @@
package de.kif.common
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
@Serializable
data class MessageBox(
val timestamp: Long,
val signature: String,
val valid: Boolean,
val messages: List<Message>
)
@Serializable
data class Message(
val type: MessageType,
val repository: RepositoryType,
val id: Long
) {
companion object {
val empty = Message(
MessageType.UPDATE,
RepositoryType.ANNOUNCEMENT,
0L
)
}
}
enum class MessageType {
CREATE, UPDATE, DELETE
}
enum class RepositoryType {
ROOM, SCHEDULE, TRACK, USER, WORK_GROUP, POST, ANNOUNCEMENT
}
object Serialization {
private val jsonContext = SerializersModule {}
val json = Json(context = jsonContext)
fun <T> stringify(serializer: SerializationStrategy<T>, obj: T) = json.stringify(serializer, obj)
fun <T> parse(serializer: DeserializationStrategy<T>, str: String) = json.parse(serializer, str)
}

View file

@ -1,22 +0,0 @@
package de.kif.common
import de.kif.common.model.Model
import de.westermann.kobserve.event.EventHandler
interface Repository<T : Model> {
suspend fun get(id: Long): T?
suspend fun create(model: T): Long
suspend fun update(model: T)
suspend fun delete(id: Long)
suspend fun all(): List<T>
val onCreate: EventHandler<Long>
val onUpdate: EventHandler<Long>
val onDelete: EventHandler<Long>
}

View file

@ -1,120 +0,0 @@
package de.kif.common
import kotlinx.serialization.Serializable
@Serializable
data class SearchElement(
val fields: Map<String, String> = emptyMap(),
val flags: Map<String, Boolean> = emptyMap(),
val numbers: Map<String, Double> = emptyMap()
) {
fun stringify(): String {
return Serialization.stringify(serializer(), this)
}
companion object {
fun parse(data: String): SearchElement {
return Serialization.parse(serializer(), data)
}
}
}
object Search {
private val regex = """
((\w+)\s?[:=]\s?)?
((\[.*]|\+\w+|-\w+|!\w+)|
((\w+)\s?([<=>]+)\s?(\d+))|
(\w+|"(.*)"))
""".trimIndent().replace("\n", "").toRegex()
fun match(search: String, element: SearchElement): Boolean {
val matches = regex.findAll(search)
val fields = mutableMapOf<String, String>()
val flags = mutableMapOf<String, Boolean>()
val numbers = mutableMapOf<String, ClosedRange<Double>>()
for (match in matches) {
val name = match.groups[2]?.value ?: ""
val field = match.groups[10]?.value ?: match.groups[9]?.value
val flag = match.groups[4]?.value
val numberName = match.groups[6]?.value
val numberRelation = match.groups[7]?.value
val numberDigits = match.groups[8]?.value?.toDoubleOrNull()
if (flag != null) {
val h = flag.replace("[\\[\\]+!\\-]".toRegex(), "")
val b = ("-" !in flag && "!" !in flag)
flags[h] = b
} else if (numberName != null && numberRelation != null && numberDigits != null) {
when (numberRelation) {
"<" -> numbers[numberName] = Double.NEGATIVE_INFINITY..(numberDigits - Double.MIN_VALUE)
"<=" -> numbers[numberName] = Double.NEGATIVE_INFINITY..numberDigits
"==" -> numbers[numberName] = numberDigits..numberDigits
">" -> numbers[numberName] = (numberDigits + Double.MIN_VALUE)..Double.POSITIVE_INFINITY
">=" -> numbers[numberName] = numberDigits..Double.POSITIVE_INFINITY
}
} else if (field != null) {
val old = fields[name]
fields[name] = if (old == null) field else "$old $field"
}
}
val fieldRadio = if (fields.isEmpty()) 1.0 else fields.count { (searchKey, searchValue) ->
for ((elementKey, elementValue) in element.fields) {
if (elementKey.contains(searchKey, true) && elementValue.contains(searchValue, true)) {
return@count true
}
}
for ((elementKey, elementValue) in element.flags) {
if (elementValue && elementKey.contains(searchValue, true)) {
return@count true
}
}
for ((elementKey, _) in element.numbers) {
if (elementKey.contains(searchValue, true)) {
return@count true
}
}
false
} / fields.size.toDouble()
for ((searchKey, searchValue) in flags) {
for ((elementKey, elementValue) in element.flags) {
if (elementKey.contains(searchKey, true) && searchValue != elementValue)
return false
}
}
for ((searchKey, searchValue) in numbers) {
for ((elementKey, elementValue) in element.numbers) {
if (elementKey.contains(searchKey, true) && elementValue !in searchValue)
return false
}
}
//println("$fieldRadio (${fieldRadio >= 0.5}) for $element")
return fieldRadio >= 0.5
}
}
/*
fun main() {
val element = SearchElement(
mapOf(
"name" to "lorem"
), mapOf(
"beamer" to true,
"room" to true,
"day" to false
), mapOf(
"places" to 500.0
)
)
println(Search.match("""search text data:"text to search" name = hans""", element))
println(Search.match("""lorem [beamer] places >= 100 +room -day""", element))
}
*/

View file

@ -1,87 +0,0 @@
package de.kif.common.model
import kotlinx.serialization.Serializable
@Serializable
data class Color(
val red: Int,
val green: Int,
val blue: Int,
val alpha: Double = 1.0
) {
override fun toString(): String {
return if (alpha >= 1.0) {
val r = red.toString(16).padStart(2, '0')
val g = green.toString(16).padStart(2, '0')
val b = blue.toString(16).padStart(2, '0')
"#$r$g$b"
} else {
"rgba($red, $green, $blue, $alpha)"
}
}
fun calcRedDouble() = red / 255.0
fun calcGreenDouble() = green / 255.0
fun calcBlueDouble() = blue / 255.0
fun calcLuminance(): Double = 0.2126 * calcRedDouble() + 0.7152 * calcGreenDouble() + 0.0722 * calcBlueDouble()
fun calcTextColor(): Color = if (calcLuminance() < 0.7) WHITE else BLACK
companion object {
fun parse(color: String): Color {
val r: Int
val g: Int
val b: Int
val a: Double
if (color.startsWith("#")) {
if (color.length == 3) {
r = color.substring(1, 2).toInt(16)
g = color.substring(2, 3).toInt(16)
b = color.substring(3, 4).toInt(16)
} else {
r = color.substring(1, 3).toInt(16)
g = color.substring(3, 5).toInt(16)
b = color.substring(5, 7).toInt(16)
}
a = 1.0
} else {
val split = color.substringAfter("(").substringBefore(")").split(",")
r = split[0].toInt()
g = split[1].toInt()
b = split[2].toInt()
a = split.getOrNull(3)?.toDouble() ?: 1.0
}
return Color(r, g, b, a)
}
val WHITE = Color(255, 255, 255)
val BLACK = Color(51, 51, 51)
val default = listOf(
"red" to parse("#F44336"),
"pink" to parse("#E91E63"),
"purple" to parse("#9C27B0"),
"deep-purple" to parse("#673AB7"),
"indigo" to parse("#3F51B5"),
"blue" to parse("#1E88E5"),
"light blue" to parse("#03A9F4"),
"cyan" to parse("#00BCD4"),
"teal" to parse("#009688"),
"green" to parse("#43A047"),
"light-green" to parse("#7CB342"),
"lime" to parse("#C0CA33"),
"yellow" to parse("#FDD835"),
"amber" to parse("#FFB300"),
"orange" to parse("#FB8C00"),
"deep-orange" to parse("#F4511E"),
"brown" to parse("#795548"),
"gray" to parse("#9E9E9E"),
"blue gray" to parse("#607D8B")
)
}
}
fun String.parseColor() = Color.parse(this)

View file

@ -1,6 +0,0 @@
package de.kif.common.model
enum class Language(val code: String, val localeName: String) {
GERMAN("DE", "Deutsch"),
ENGLISH("EN", "English")
}

View file

@ -1,11 +0,0 @@
package de.kif.common.model
import de.kif.common.SearchElement
interface Model {
val id : Long?
fun createSearch(): SearchElement
val createdAt: Long
val updateAt: Long
}

View file

@ -1,10 +0,0 @@
package de.kif.common.model
enum class Permission(val germanInfo: String) {
USER("Nutzer"),
SCHEDULE("Scheduling"),
WORK_GROUP("Arbeitskreise"),
ROOM("Räume"),
POST("Beiträge"),
ADMIN("Admin")
}

View file

@ -1,68 +0,0 @@
package de.kif.common.model
import de.kif.common.SearchElement
import kotlinx.serialization.Serializable
import kotlin.random.Random
@Serializable
data class Post(
override val id: Long? = null,
val name: String = "",
val content: String = "",
val url: String = "",
val image: String? = null,
val pinned: Boolean = false,
val hideOnProjector: Boolean = false,
override val createdAt: Long = 0,
override val updateAt: Long = 0
) : Model {
override fun createSearch() = SearchElement(
mapOf(
"name" to name
)
)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as Post
if (id != other.id) return false
if (name != other.name) return false
if (content != other.content) return false
if (url != other.url) return false
if (image != other.image) return false
if (pinned != other.pinned) return false
if (hideOnProjector != other.hideOnProjector) return false
return true
}
fun equalsIgnoreId(other: Post): Boolean = copy(id = null) == other.copy(id = null)
override fun hashCode(): Int {
var result = id?.hashCode() ?: 0
result = 31 * result + name.hashCode()
result = 31 * result + content.hashCode()
result = 31 * result + url.hashCode()
result = 31 * result + (image?.hashCode() ?: 0)
result = 31 * result + pinned.hashCode()
result = 31 * result + hideOnProjector.hashCode()
return result
}
companion object {
private const val chars = "abcdefghijklmnopqrstuvwxyz"
private const val length = 32
fun generateUrl() = (0 until length).asSequence()
.map { Random.nextInt(chars.length) }
.map { chars[it] }
.map {
if (Random.nextBoolean()) it else it.toUpperCase()
}.joinToString("")
}
}

View file

@ -1,72 +0,0 @@
package de.kif.common.model
import de.kif.common.SearchElement
import kotlinx.serialization.Serializable
@Serializable
data class Room(
override val id: Long? = null,
val name: String = "",
val places: Int = 0,
val projector: Boolean = false,
val internet: Boolean = false,
val whiteboard: Boolean = false,
val blackboard: Boolean = false,
val accessible: Boolean = false,
val pool: Boolean = false,
val blocked : List<RoomBlock> = emptyList(),
override val createdAt: Long = 0,
override val updateAt: Long = 0
) : Model {
override fun createSearch() = SearchElement(
mapOf(
"name" to name
), mapOf(
"projector" to projector,
"internet" to internet,
"whiteboard" to whiteboard,
"blackboard" to blackboard,
"accessible" to accessible,
"pool" to pool
), mapOf(
"places" to places.toDouble()
)
)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as Room
if (id != other.id) return false
if (name != other.name) return false
if (places != other.places) return false
if (projector != other.projector) return false
if (internet != other.internet) return false
if (whiteboard != other.whiteboard) return false
if (blackboard != other.blackboard) return false
if (accessible != other.accessible) return false
if (pool != other.pool) return false
if (blocked != other.blocked) return false
return true
}
fun equalsIgnoreId(other: Room): Boolean = copy(id = null) == other.copy(id = null)
override fun hashCode(): Int {
var result = id?.hashCode() ?: 0
result = 31 * result + name.hashCode()
result = 31 * result + places
result = 31 * result + projector.hashCode()
result = 31 * result + internet.hashCode()
result = 31 * result + whiteboard.hashCode()
result = 31 * result + blackboard.hashCode()
result = 31 * result + accessible.hashCode()
result = 31 * result + pool.hashCode()
result = 31 * result + blocked.hashCode()
return result
}
}

View file

@ -1,19 +0,0 @@
package de.kif.common.model
import kotlinx.serialization.Serializable
@Serializable
data class RoomBlock(
val day: Int,
val start: Int? = null,
val end: Int? = null
) {
fun checkBlock(day: Int, start: Int, end: Int): Boolean {
if (this.day != day) return false
val s = this.start ?: Int.MIN_VALUE
val e = this.end ?: Int.MAX_VALUE
return s <= end && start < e
}
}

View file

@ -1,106 +0,0 @@
package de.kif.common.model
import de.kif.common.SearchElement
import kotlinx.serialization.Serializable
@Serializable
data class Schedule(
override val id: Long?,
val workGroup: WorkGroup,
val room: Room,
val day: Int,
val time: Int,
val lockRoom: Boolean = false,
val lockTime: Boolean = false,
override val createdAt: Long = 0,
override val updateAt: Long = 0
) : Model {
override fun createSearch() = SearchElement(
mapOf(
"workgroup" to workGroup.name,
"room" to room.name,
"track" to (workGroup.track?.name ?: ""),
"language" to workGroup.language.localeName
), mapOf(), mapOf(
"day" to day.toDouble()
)
)
fun getAbsoluteStartTime(): Int = day * 60 * 24 + time
fun getAbsoluteEndTime(): Int = getAbsoluteStartTime() + workGroup.length
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as Schedule
if (id != other.id) return false
if (workGroup != other.workGroup) return false
if (room != other.room) return false
if (day != other.day) return false
if (time != other.time) return false
if (lockRoom != other.lockRoom) return false
if (lockTime != other.lockTime) return false
return true
}
fun equalsIgnoreId(other: Schedule): Boolean = copy(id = null) == other.copy(id = null)
override fun hashCode(): Int {
var result = id?.hashCode() ?: 0
result = 31 * result + workGroup.hashCode()
result = 31 * result + room.hashCode()
result = 31 * result + day
result = 31 * result + time
result = 31 * result + lockRoom.hashCode()
result = 31 * result + lockTime.hashCode()
return result
}
companion object {
fun timeOfDayToString(time: Int): String {
var day = time % (24 * 60)
if (day < 0) day += 24 * 60
val hour = day / 60
val minute = day % 60
return hour.toString().padStart(2, '0') + ":" + minute.toString().padStart(2, '0')
}
fun timeDifferenceToString(time: Long): String {
if (time < 0) {
return "running"
}
var remainder = time
val seconds = (remainder % 60).toString().padStart(2, '0')
remainder /= 60
val minutes = (remainder % 60).toString().padStart(2, '0')
remainder /= 60
val hours = (remainder % 24).toString().padStart(2, '0')
remainder /= 24
val days = remainder
if (days > 1) {
return "$days days"
}
if (days > 0) {
return "1 day"
}
if (hours == "00") {
return "$minutes:$seconds"
}
return "$hours:$minutes:$seconds"
}
}
}

View file

@ -1,42 +0,0 @@
package de.kif.common.model
import de.kif.common.SearchElement
import kotlinx.serialization.Serializable
@Serializable
data class Track(
override val id: Long? = null,
val name: String = "",
val color: Color = Color.WHITE,
override val createdAt: Long = 0,
override val updateAt: Long = 0
) : Model {
override fun createSearch() = SearchElement(
mapOf(
"name" to name
)
)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as Track
if (id != other.id) return false
if (name != other.name) return false
if (color != other.color) return false
return true
}
fun equalsIgnoreId(other: Track): Boolean = copy(id = null) == other.copy(id = null)
override fun hashCode(): Int {
var result = id?.hashCode() ?: 0
result = 31 * result + name.hashCode()
result = 31 * result + color.hashCode()
return result
}
}

View file

@ -1,49 +0,0 @@
package de.kif.common.model
import de.kif.common.SearchElement
import kotlinx.serialization.Serializable
@Serializable
data class User(
override val id: Long? = null,
val username: String = "",
val password: String = "",
val permissions: Set<Permission> = emptySet(),
override val createdAt: Long = 0,
override val updateAt: Long = 0
) : Model {
fun checkPermission(permission: Permission): Boolean {
return permission in permissions || Permission.ADMIN in permissions
}
override fun createSearch() = SearchElement(
mapOf(
"username" to username
)
)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as User
if (id != other.id) return false
if (username != other.username) return false
if (password != other.password) return false
if (permissions != other.permissions) return false
return true
}
fun equalsIgnoreId(other: User): Boolean = copy(id = null) == other.copy(id = null)
override fun hashCode(): Int {
var result = id?.hashCode() ?: 0
result = 31 * result + username.hashCode()
result = 31 * result + password.hashCode()
result = 31 * result + permissions.hashCode()
return result
}
}

View file

@ -1,86 +0,0 @@
package de.kif.common.model
import de.kif.common.SearchElement
import kotlinx.serialization.Serializable
@Serializable
data class WorkGroup(
override val id: Long? = null,
val name: String = "",
val description: String = "",
val interested: Int = 0,
val track: Track? = null,
val projector: Boolean = false,
val resolution: Boolean = false,
val internet: Boolean = false,
val whiteboard: Boolean = false,
val blackboard: Boolean = false,
val accessible: Boolean = false,
val length: Int = 0,
val language: Language = Language.GERMAN,
val leader: List<String> = emptyList(),
val constraints: List<WorkGroupConstraint> = emptyList(),
override val createdAt: Long = 0,
override val updateAt: Long = 0
) : Model {
override fun createSearch() = SearchElement(
mapOf(
"name" to name,
"track" to (track?.name ?: ""),
"language" to language.localeName
), mapOf(
"projector" to projector,
"resolution" to resolution
), mapOf(
"interested" to interested.toDouble(),
"length" to length.toDouble()
)
)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as WorkGroup
if (id != other.id) return false
if (name != other.name) return false
if (description != other.description) return false
if (interested != other.interested) return false
if (track != other.track) return false
if (projector != other.projector) return false
if (resolution != other.resolution) return false
if (internet != other.internet) return false
if (whiteboard != other.whiteboard) return false
if (blackboard != other.blackboard) return false
if (accessible != other.accessible) return false
if (length != other.length) return false
if (language != other.language) return false
if (leader != other.leader) return false
if (constraints != other.constraints) return false
return true
}
fun equalsIgnoreId(other: WorkGroup): Boolean = copy(id = null) == other.copy(id = null)
override fun hashCode(): Int {
var result = id?.hashCode() ?: 0
result = 31 * result + name.hashCode()
result = 31 * result + description.hashCode()
result = 31 * result + interested
result = 31 * result + (track?.hashCode() ?: 0)
result = 31 * result + projector.hashCode()
result = 31 * result + resolution.hashCode()
result = 31 * result + internet.hashCode()
result = 31 * result + whiteboard.hashCode()
result = 31 * result + blackboard.hashCode()
result = 31 * result + accessible.hashCode()
result = 31 * result + length
result = 31 * result + language.hashCode()
result = 31 * result + leader.hashCode()
result = 31 * result + constraints.hashCode()
return result
}
}

View file

@ -1,55 +0,0 @@
package de.kif.common.model
import kotlinx.serialization.Serializable
@Serializable
data class WorkGroupConstraint(
val type: ConstraintType,
val day: Int? = null,
val time: Int? = null,
val workGroup: Long? = null,
val room: Long? = null
)
enum class ConstraintType {
/**
* Requires day, permits time, workGroup and room.
*/
OnlyOnDay,
/**
* Requires day, permits time, workGroup and room.
*/
NotOnDay,
/**
* Requires time, optionally allows day, permits workGroup and room.
*/
OnlyAfterTime,
/**
* Requires time, optionally allows day, permits workGroup and room.
*/
OnlyBeforeTime,
/**
* Requires time, optionally allows day, permits workGroup and room.
*/
ExactTime,
/**
* Requires workGroup, permits day, time and room.
*/
NotAtSameTime,
/**
* Requires workGroup, permits day, time and room.
*/
OnlyAfterWorkGroup,
/**
* Requires room, permits day, time and workGroup
*/
Room
}

View file

@ -1,179 +0,0 @@
package de.kif.frontend
import de.kif.common.MessageBox
import de.kif.common.MessageType
import de.kif.common.RepositoryType
import de.kif.common.Serialization
import de.kif.frontend.repository.*
import de.westermann.kwebview.clearInterval
import de.westermann.kwebview.createHtmlView
import de.westermann.kwebview.interval
import kotlinx.serialization.DynamicObjectParser
import org.w3c.dom.EventSource
import org.w3c.dom.MessageEvent
import org.w3c.dom.events.EventListener
import org.w3c.dom.get
import org.w3c.xhr.XMLHttpRequest
import kotlin.browser.document
import kotlin.browser.window
class PushServiceClient {
private val prefix = js("prefix")
private val pollingUrl = "$prefix/api/updates"
private val eventUrl = "$prefix/api/events"
private val body = document.body ?: createHtmlView()
private val parser = DynamicObjectParser()
private var timestamp = body.dataset["timestamp"]?.toLongOrNull() ?: 0L
private val signature = body.dataset["signature"] ?: ""
private var intervalId: Int? = null
private var errorTimeout = ERROR_TIMEOUT
private fun reload() {
val id = intervalId ?: return
clearInterval(id)
intervalId = null
println("reload")
window.location.reload()
}
private fun onMessage(messageBox: MessageBox) {
body.classList.remove("offline")
errorTimeout = ERROR_TIMEOUT
if (messageBox.valid && signature == messageBox.signature) {
timestamp = messageBox.timestamp
for (message in messageBox.messages) {
for (handler in messageHandlers) {
if (handler.repository == message.repository) {
when (message.type) {
MessageType.CREATE -> handler.onCreate(message.id)
MessageType.UPDATE -> handler.onUpdate(message.id)
MessageType.DELETE -> handler.onDelete(message.id)
}
}
}
}
} else {
reload()
}
}
private fun onError(code: Int): Boolean {
if (errorTimeout > 0) {
errorTimeout--
return false
}
if (!body.classList.contains("offline")) {
console.log("Offline reason: $code")
}
body.classList.add("offline")
return true
}
private fun request() {
val xmlHttpRequest = XMLHttpRequest()
xmlHttpRequest.onreadystatechange = {
try {
if (xmlHttpRequest.readyState == 4.toShort()) {
if (xmlHttpRequest.status == 200.toShort() || xmlHttpRequest.status == 304.toShort()) {
val json = JSON.parse<JsonResponse>(xmlHttpRequest.responseText)
if (json.OK) {
val message = parser.parse(json.data, MessageBox.serializer())
onMessage(message)
} else {
onError(-1)
}
} else {
onError(xmlHttpRequest.status.toInt())
}
}
Unit
} catch (e: Exception) {
console.error(e)
onError(-2)
}
}
xmlHttpRequest.open("GET", "$pollingUrl?timestamp=$timestamp", true)
xmlHttpRequest.overrideMimeType("application/json")
xmlHttpRequest.send()
}
private fun initEventSource() {
val eventSource = EventSource(eventUrl)
var timeout = 3
eventSource.addEventListener("update", EventListener {
val event = it as? MessageEvent ?: return@EventListener
val message = event.data as? String ?: return@EventListener
onMessage(Serialization.parse(MessageBox.serializer(), message))
})
eventSource.addEventListener("ping", EventListener {
timeout = 3
val event = it as? MessageEvent ?: return@EventListener
val s = event.data as? String ?: return@EventListener
if (s != signature) {
reload()
}
})
intervalId = interval(500) {
timeout -= 1
if (timeout <= 0) {
if (onError(-1)) {
val id = intervalId
if (id != null) {
clearInterval(id)
intervalId = null
}
intervalId = interval(500) {
request()
}
}
}
}
}
private val messageHandlers: List<MessageHandler> = listOf(
RoomRepository.handler,
ScheduleRepository.handler,
TrackRepository.handler,
UserRepository.handler,
WorkGroupRepository.handler,
PostRepository.handler,
AnnouncementRepository.handler
)
init {
try {
initEventSource()
} catch (e: Exception) {
console.log("Cannot connect to event source, use polling fallback!")
val id = intervalId
if (id != null) {
clearInterval(id)
intervalId = null
}
intervalId = interval(500) {
request()
}
}
}
companion object {
private const val ERROR_TIMEOUT = 2
}
}
abstract class MessageHandler(val repository: RepositoryType) {
abstract fun onCreate(id: Long)
abstract fun onUpdate(id: Long)
abstract fun onDelete(id: Long)
}

View file

@ -1,13 +0,0 @@
package de.kif.frontend
import kotlin.coroutines.Continuation
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.startCoroutine
fun launch(context: CoroutineContext = EmptyCoroutineContext, block: suspend () -> Unit) =
block.startCoroutine(Continuation(context) { result ->
result.onFailure { exception ->
console.error(exception)
}
})

View file

@ -1,97 +0,0 @@
package de.kif.frontend
import de.kif.frontend.views.board.initBoard
import de.kif.frontend.views.calendar.initCalendar
import de.kif.frontend.views.initAnnouncement
import de.kif.frontend.views.initRoomConstraints
import de.kif.frontend.views.initWorkGroupConstraints
import de.kif.frontend.views.overview.initOverviewMain
import de.kif.frontend.views.overview.initPostEdit
import de.kif.frontend.views.overview.initPosts
import de.kif.frontend.views.table.initTableLayout
import de.westermann.kwebview.View
import de.westermann.kwebview.components.init
import de.westermann.kwebview.iterator
import org.w3c.dom.get
import kotlin.browser.document
import kotlin.browser.window
var timezoneOffset = 0L
fun main() = init {
PushServiceClient()
timezoneOffset = document.body?.dataset?.get("timezone")?.toLongOrNull() ?: 0L
if (document.getElementsByClassName("calendar").length > 0) {
initCalendar()
}
if (document.getElementsByClassName("table-layout").length > 0) {
initTableLayout()
}
if (document.getElementsByClassName("work-group-constraints").length > 0) {
initWorkGroupConstraints()
}
if (document.getElementsByClassName("room-constraints").length > 0) {
initRoomConstraints()
}
if (document.getElementsByClassName("overview-main").length > 0) {
initOverviewMain()
}
if (document.getElementsByClassName("post").length > 0) {
initPosts()
}
if (document.getElementsByClassName("post-edit-right").length > 0) {
initPostEdit()
}
if (document.getElementsByClassName("board").length > 0) {
initBoard()
}
if (document.getElementsByClassName("announcement").length > 0) {
initAnnouncement()
}
for (btn in document.getElementsByClassName("btn-danger").iterator()) {
View.wrap(btn).onClick {
val result = window.confirm("Wollen Sie wirklich löschen?")
if (!result) {
it.stopPropagation()
it.preventDefault()
}
}
}
/*
val url = window.location.pathname
if ("brett" in url || "wand" in url) {
ScheduleRepository.onCreate {
window.location.reload()
}
ScheduleRepository.onUpdate {
window.location.reload()
}
ScheduleRepository.onDelete {
window.location.reload()
}
RoomRepository.onCreate {
window.location.reload()
}
RoomRepository.onUpdate {
window.location.reload()
}
RoomRepository.onDelete {
window.location.reload()
}
WorkGroupRepository.onCreate {
window.location.reload()
}
WorkGroupRepository.onUpdate {
window.location.reload()
}
WorkGroupRepository.onDelete {
window.location.reload()
}
}
*/
}

View file

@ -1,132 +0,0 @@
package de.kif.frontend.repository
import de.kif.common.Repository
import de.kif.common.model.Model
import org.w3c.xhr.XMLHttpRequest
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
import kotlin.js.Promise
interface JsonResponse {
val OK: Boolean
val data: dynamic
}
suspend fun repositoryGet(
url: String
): dynamic {
val promise = Promise<JsonResponse> { resolve, reject ->
val xhttp = XMLHttpRequest()
xhttp.onreadystatechange = {
if (xhttp.readyState == 4.toShort()) {
if (xhttp.status == 200.toShort() || xhttp.status == 304.toShort()) {
resolve(JSON.parse(xhttp.responseText))
} else {
reject(NoSuchElementException("${xhttp.status}: ${xhttp.statusText}"))
}
}
}
xhttp.open("GET", url, true)
xhttp.send()
}
try {
val d = promise.await()
return if (d.OK) {
d.data
} else {
null
}
} catch (e: NoSuchElementException) {
console.error(e)
return null
}
}
suspend fun repositoryPost(
url: String,
data: String? = null
): dynamic {
val promise = Promise<JsonResponse> { resolve, reject ->
val xhttp = XMLHttpRequest()
xhttp.onreadystatechange = {
if (xhttp.readyState == 4.toShort()) {
if (xhttp.status == 200.toShort() || xhttp.status == 304.toShort()) {
resolve(JSON.parse(xhttp.responseText))
} else {
reject(NoSuchElementException("${xhttp.status}: ${xhttp.statusText}"))
}
}
}
xhttp.open("POST", url, true)
xhttp.setRequestHeader("Content-type", "application/json");
xhttp.send(data)
}
try {
val d = promise.await()
return if (d.OK) {
d.data
} else {
null
}
} catch (e: NoSuchElementException) {
console.error(e)
return null
}
}
suspend fun repositoryRawGet(
url: String
): String {
val promise = Promise<String> { resolve, reject ->
val xhttp = XMLHttpRequest()
xhttp.onreadystatechange = {
if (xhttp.readyState == 4.toShort()) {
if (xhttp.status == 200.toShort() || xhttp.status == 304.toShort()) {
resolve(xhttp.responseText)
} else {
reject(NoSuchElementException("${xhttp.status}: ${xhttp.statusText}"))
}
}
}
xhttp.open("GET", url, true)
xhttp.send()
}
return promise.await()
}
suspend fun <T> Promise<T>.await(): T = suspendCoroutine { cont ->
then({ cont.resume(it) }, { cont.resumeWithException(it) })
}
class RepositoryDelegate<T : Model>(
private val repository: Repository<T>,
private val id: Long
) {
private var backing: T? = null
suspend fun get(): T {
if (backing == null) {
backing = repository.get(id) ?: throw NoSuchElementException()
}
return backing!!
}
fun set(value: T) {
backing = value
}
}

View file

@ -1,34 +0,0 @@
package de.kif.frontend.repository
import de.kif.common.RepositoryType
import de.kif.frontend.MessageHandler
import de.westermann.kobserve.event.EventHandler
import kotlinx.serialization.DynamicObjectParser
object AnnouncementRepository {
private val prefix = js("prefix")
val onUpdate = EventHandler<Unit>()
private val parser = DynamicObjectParser()
suspend fun getAnnouncement(): String {
val json = repositoryGet("$prefix/api/announcement") ?: return ""
return json as String
}
suspend fun setAnnouncement(value: String){
return repositoryPost("$prefix/api/announcement", value)
?: throw IllegalStateException("Cannot set announcement!")
}
val handler = object : MessageHandler(RepositoryType.ANNOUNCEMENT) {
override fun onCreate(id: Long) {}
override fun onUpdate(id: Long) = onUpdate.emit(Unit)
override fun onDelete(id: Long) {}
}
}

View file

@ -1,62 +0,0 @@
package de.kif.frontend.repository
import de.kif.common.Repository
import de.kif.common.RepositoryType
import de.kif.common.Serialization
import de.kif.common.model.Post
import de.kif.frontend.MessageHandler
import de.westermann.kobserve.event.EventHandler
import kotlinx.serialization.DynamicObjectParser
import kotlinx.serialization.list
object PostRepository : Repository<Post> {
val prefix = js("prefix")
override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>()
private val parser = DynamicObjectParser()
override suspend fun get(id: Long): Post? {
val json = repositoryGet("$prefix/api/post/$id") ?: return null
return parser.parse(json, Post.serializer())
}
override suspend fun create(model: Post): Long {
return repositoryPost("$prefix/api/posts", Serialization.stringify(Post.serializer(), model))
?: throw IllegalStateException("Cannot create model!")
}
override suspend fun update(model: Post) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
repositoryPost("$prefix/api/post/${model.id}", Serialization.stringify(Post.serializer(), model))
}
override suspend fun delete(id: Long) {
repositoryPost("$prefix/api/post/$id/delete")
}
override suspend fun all(): List<Post> {
val json = repositoryGet("$prefix/api/posts") ?: return emptyList()
return parser.parse(json, Post.serializer().list)
}
suspend fun htmlByUrl(url: String): String {
return repositoryRawGet("$prefix/api/p/$url")
}
suspend fun render(data: String): String {
return repositoryPost("$prefix/api/render", data)
}
val handler = object : MessageHandler(RepositoryType.POST) {
override fun onCreate(id: Long) = onCreate.emit(id)
override fun onUpdate(id: Long) = onUpdate.emit(id)
override fun onDelete(id: Long) = onDelete.emit(id)
}
}

View file

@ -1,54 +0,0 @@
package de.kif.frontend.repository
import de.kif.common.Repository
import de.kif.common.RepositoryType
import de.kif.common.Serialization
import de.kif.common.model.Room
import de.kif.frontend.MessageHandler
import de.westermann.kobserve.event.EventHandler
import kotlinx.serialization.DynamicObjectParser
import kotlinx.serialization.list
object RoomRepository : Repository<Room> {
val prefix = js("prefix")
override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>()
private val parser = DynamicObjectParser()
override suspend fun get(id: Long): Room? {
val json = repositoryGet("$prefix/api/room/$id") ?: return null
return parser.parse(json, Room.serializer())
}
override suspend fun create(model: Room): Long {
return repositoryPost("$prefix/api/rooms", Serialization.stringify(Room.serializer(), model))
?: throw IllegalStateException("Cannot create model!")
}
override suspend fun update(model: Room) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
repositoryPost("$prefix/api/room/${model.id}", Serialization.stringify(Room.serializer(), model))
}
override suspend fun delete(id: Long) {
repositoryPost("$prefix/api/room/$id/delete")
}
override suspend fun all(): List<Room> {
val json = repositoryGet("$prefix/api/rooms") ?: return emptyList()
return parser.parse(json, Room.serializer().list)
}
val handler = object : MessageHandler(RepositoryType.ROOM) {
override fun onCreate(id: Long) = onCreate.emit(id)
override fun onUpdate(id: Long) = onUpdate.emit(id)
override fun onDelete(id: Long) = onDelete.emit(id)
}
}

View file

@ -1,70 +0,0 @@
package de.kif.frontend.repository
import de.kif.common.ConstraintMap
import de.kif.common.Repository
import de.kif.common.RepositoryType
import de.kif.common.Serialization
import de.kif.common.model.Schedule
import de.kif.frontend.MessageHandler
import de.westermann.kobserve.event.EventHandler
import kotlinx.serialization.DynamicObjectParser
import kotlinx.serialization.list
object ScheduleRepository : Repository<Schedule> {
val prefix = js("prefix")
override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>()
private val parser = DynamicObjectParser()
override suspend fun get(id: Long): Schedule? {
val json = repositoryGet("$prefix/api/schedule/$id") ?: return null
return parser.parse(json, Schedule.serializer())
}
suspend fun getUpcoming(count: Int = 8): List<Schedule> {
val json = repositoryGet("$prefix/api/schedules/upcoming?count=$count") ?: return emptyList()
return parser.parse(json, Schedule.serializer().list)
}
override suspend fun create(model: Schedule): Long {
return repositoryPost("$prefix/api/schedules", Serialization.stringify(Schedule.serializer(), model))
?: throw IllegalStateException("Cannot create model!")
}
override suspend fun update(model: Schedule) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
repositoryPost("$prefix/api/schedule/${model.id}", Serialization.stringify(Schedule.serializer(), model))
}
override suspend fun delete(id: Long) {
repositoryPost("$prefix/api/schedule/$id/delete")
}
override suspend fun all(): List<Schedule> {
val json = repositoryGet("$prefix/api/schedules") ?: return emptyList()
return parser.parse(json, Schedule.serializer().list)
}
val handler = object : MessageHandler(RepositoryType.SCHEDULE) {
override fun onCreate(id: Long) = onCreate.emit(id)
override fun onUpdate(id: Long) = onUpdate.emit(id)
override fun onDelete(id: Long) = onDelete.emit(id)
}
suspend fun checkConstraints(): ConstraintMap {
val json = repositoryGet("$prefix/api/constraints")
return parser.parse(json, ConstraintMap.serializer())
}
suspend fun checkConstraintsFor(schedule: Schedule): ConstraintMap {
val json = repositoryGet("$prefix/api/constraint/${schedule.id}")
return parser.parse(json, ConstraintMap.serializer())
}
}

View file

@ -1,54 +0,0 @@
package de.kif.frontend.repository
import de.kif.common.Repository
import de.kif.common.RepositoryType
import de.kif.common.Serialization
import de.kif.common.model.Track
import de.kif.frontend.MessageHandler
import de.westermann.kobserve.event.EventHandler
import kotlinx.serialization.DynamicObjectParser
import kotlinx.serialization.list
object TrackRepository : Repository<Track> {
val prefix = js("prefix")
override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>()
private val parser = DynamicObjectParser()
override suspend fun get(id: Long): Track? {
val json = repositoryGet("$prefix/api/track/$id") ?: return null
return parser.parse(json, Track.serializer())
}
override suspend fun create(model: Track): Long {
return repositoryPost("$prefix/api/tracks", Serialization.stringify(Track.serializer(), model))
?: throw IllegalStateException("Cannot create model!")
}
override suspend fun update(model: Track) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
repositoryPost("$prefix/api/track/${model.id}", Serialization.stringify(Track.serializer(), model))
}
override suspend fun delete(id: Long) {
repositoryPost("$prefix/api/track/$id/delete")
}
override suspend fun all(): List<Track> {
val json = repositoryGet("$prefix/api/tracks") ?: return emptyList()
return parser.parse(json, Track.serializer().list)
}
val handler = object : MessageHandler(RepositoryType.TRACK) {
override fun onCreate(id: Long) = onCreate.emit(id)
override fun onUpdate(id: Long) = onUpdate.emit(id)
override fun onDelete(id: Long) = onDelete.emit(id)
}
}

View file

@ -1,54 +0,0 @@
package de.kif.frontend.repository
import de.kif.common.Repository
import de.kif.common.RepositoryType
import de.kif.common.Serialization
import de.kif.common.model.User
import de.kif.frontend.MessageHandler
import de.westermann.kobserve.event.EventHandler
import kotlinx.serialization.DynamicObjectParser
import kotlinx.serialization.list
object UserRepository : Repository<User> {
val prefix = js("prefix")
override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>()
private val parser = DynamicObjectParser()
override suspend fun get(id: Long): User? {
val json = repositoryGet("$prefix/api/user/$id") ?: return null
return parser.parse(json, User.serializer())
}
override suspend fun create(model: User): Long {
return repositoryPost("$prefix/api/users", Serialization.stringify(User.serializer(), model))
?: throw IllegalStateException("Cannot create model!")
}
override suspend fun update(model: User) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
repositoryPost("$prefix/api/user/${model.id}", Serialization.stringify(User.serializer(), model))
}
override suspend fun delete(id: Long) {
repositoryPost("$prefix/api/user/$id/delete")
}
override suspend fun all(): List<User> {
val json = repositoryGet("$prefix/api/users") ?: return emptyList()
return parser.parse(json, User.serializer().list)
}
val handler = object : MessageHandler(RepositoryType.USER) {
override fun onCreate(id: Long) = onCreate.emit(id)
override fun onUpdate(id: Long) = onUpdate.emit(id)
override fun onDelete(id: Long) = onDelete.emit(id)
}
}

View file

@ -1,54 +0,0 @@
package de.kif.frontend.repository
import de.kif.common.Repository
import de.kif.common.RepositoryType
import de.kif.common.Serialization
import de.kif.common.model.WorkGroup
import de.kif.frontend.MessageHandler
import de.westermann.kobserve.event.EventHandler
import kotlinx.serialization.DynamicObjectParser
import kotlinx.serialization.list
object WorkGroupRepository : Repository<WorkGroup> {
val prefix = js("prefix")
override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>()
private val parser = DynamicObjectParser()
override suspend fun get(id: Long): WorkGroup? {
val json = repositoryGet("$prefix/api/workgroup/$id") ?: return null
return parser.parse(json, WorkGroup.serializer())
}
override suspend fun create(model: WorkGroup): Long {
return repositoryPost("$prefix/api/workgroups", Serialization.stringify(WorkGroup.serializer(), model))
?: throw IllegalStateException("Cannot create model!")
}
override suspend fun update(model: WorkGroup) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
repositoryPost("$prefix/api/workgroup/${model.id}", Serialization.stringify(WorkGroup.serializer(), model))
}
override suspend fun delete(id: Long) {
repositoryPost("$prefix/api/workgroup/$id/delete")
}
override suspend fun all(): List<WorkGroup> {
val json = repositoryGet("$prefix/api/workgroups") ?: return emptyList()
return parser.parse(json, WorkGroup.serializer().list)
}
val handler = object : MessageHandler(RepositoryType.WORK_GROUP) {
override fun onCreate(id: Long) = onCreate.emit(id)
override fun onUpdate(id: Long) = onUpdate.emit(id)
override fun onDelete(id: Long) = onDelete.emit(id)
}
}

View file

@ -1,21 +0,0 @@
package de.kif.frontend.views
import de.kif.frontend.launch
import de.kif.frontend.repository.AnnouncementRepository
import org.w3c.dom.HTMLElement
import org.w3c.dom.get
import kotlin.browser.document
fun initAnnouncement() {
val announcement = document.getElementsByClassName("announcement")[0] as? HTMLElement ?: return
val span = announcement.children[0] as? HTMLElement ?: return
AnnouncementRepository.onUpdate {
launch {
val text = AnnouncementRepository.getAnnouncement()
announcement.classList.toggle("announcement-blank", text.isBlank())
span.textContent = text
}
}
}

View file

@ -1,84 +0,0 @@
package de.kif.frontend.views
import de.kif.common.formatDate
import de.kif.frontend.timezoneOffset
import de.westermann.kwebview.View
import de.westermann.kwebview.components.InputType
import de.westermann.kwebview.components.InputView
import de.westermann.kwebview.components.TextView
import de.westermann.kwebview.createHtmlView
import de.westermann.kwebview.iterator
import org.w3c.dom.*
import kotlin.browser.document
fun initRoomConstraints() {
var index = 10000
val constraints =
document.getElementsByClassName("room-constraints")[0] as HTMLElement
val addButton =
View.wrap(document.getElementsByClassName("room-constraints-add")[0] as HTMLElement)
val referenceDate = constraints.dataset["reference"]?.toLongOrNull() ?: 0L
fun updateDateView(inputGroup: HTMLElement, day: Int) {
val date = referenceDate + (day * 1000 * 60 * 60 * 24)
val dateName = formatDate(date, timezoneOffset)
inputGroup.dataset["hint"] = dateName
}
addButton.onClick {
constraints.appendChild(View.wrap(createHtmlView<HTMLDivElement>()) {
classList += "input-group"
html.appendChild(TextView("Gesperrt").apply {
classList += "form-btn"
onClick { this@wrap.html.remove() }
}.html)
html.appendChild(InputView(InputType.NUMBER).apply {
classList += "form-control"
html.name = "constraint-room-day-${index}"
placeholder = "Tag"
min = -1337.0
max = 1337.0
updateDateView(this@wrap.html, value.toIntOrNull() ?: 0)
valueProperty.onChange {
updateDateView(this@wrap.html, value.toIntOrNull() ?: 0)
}
}.html)
html.appendChild(InputView(InputType.TEXT).apply {
classList += "form-control"
html.name = "constraint-room-start-${index}"
placeholder = "Start"
}.html)
html.appendChild(InputView(InputType.TEXT).apply {
classList += "form-control"
html.name = "constraint-room-end-${index++}"
placeholder = "Ende"
}.html)
}.html)
}
for (child in constraints.children.iterator()) {
if (child.classList.contains("input-group")) {
val span = child.firstElementChild as HTMLElement
span.addEventListener("click", org.w3c.dom.events.EventListener {
println("click")
child.remove()
})
for (e in child.children.iterator()) {
if (e is HTMLInputElement && e.name.contains("-day-")) {
val input = InputView.wrap(InputType.NUMBER, e)
updateDateView(child, input.value.toIntOrNull() ?: 0)
input.valueProperty.onChange {
updateDateView(child, input.value.toIntOrNull() ?: 0)
}
}
}
}
}
}

View file

@ -1,303 +0,0 @@
package de.kif.frontend.views
import de.kif.common.formatDate
import de.kif.frontend.launch
import de.kif.frontend.repository.RoomRepository
import de.kif.frontend.repository.WorkGroupRepository
import de.kif.frontend.timezoneOffset
import de.westermann.kobserve.event.EventListener
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
import kotlin.dom.clear
fun initWorkGroupConstraints() {
var index = 10000
val constraints =
document.getElementsByClassName("work-group-constraints")[0] as HTMLElement
val addButton =
View.wrap(document.getElementsByClassName("work-group-constraints-add")[0] as HTMLElement)
val addList =
ListView.wrap<View>(document.getElementsByClassName("work-group-constraints-add-list")[0] as HTMLElement)
val referenceDate = constraints.dataset["reference"]?.toLongOrNull() ?: 0L
addButton.onClick {
addList.classList += "active"
var listener: EventListener<*>? = null
async {
listener = Body.onClick.reference {
addList.classList -= "active"
listener?.detach()
}
}
}
fun updateDateView(inputGroup: HTMLElement, day: Int) {
val date = referenceDate + (day * 1000 * 60 * 60 * 24)
val dateName = formatDate(date, timezoneOffset)
inputGroup.dataset["hint"] = dateName
}
addList.textView("Nur an Tag x") {
onClick {
constraints.appendChild(View.wrap(createHtmlView<HTMLDivElement>()) {
classList += "input-group"
html.appendChild(TextView("An Tag").apply {
classList += "form-btn"
onClick { this@wrap.html.remove() }
}.html)
html.appendChild(InputView(InputType.NUMBER).apply {
classList += "form-control"
html.name = "constraint-only-on-day-${index++}"
min = -1337.0
max = 1337.0
placeholder = "Tag"
updateDateView(this@wrap.html, value.toIntOrNull() ?: 0)
valueProperty.onChange {
updateDateView(this@wrap.html, value.toIntOrNull() ?: 0)
}
}.html)
}.html)
}
}
addList.textView("Nicht an Tag x") {
onClick {
constraints.appendChild(View.wrap(createHtmlView<HTMLDivElement>()) {
classList += "input-group"
html.appendChild(TextView("Nicht an Tag").apply {
classList += "form-btn"
onClick { this@wrap.html.remove() }
}.html)
html.appendChild(InputView(InputType.NUMBER).apply {
classList += "form-control"
html.name = "constraint-not-on-day-${index++}"
min = -1337.0
max = 1337.0
placeholder = "Tag"
updateDateView(this@wrap.html, value.toIntOrNull() ?: 0)
valueProperty.onChange {
updateDateView(this@wrap.html, value.toIntOrNull() ?: 0)
}
}.html)
}.html)
}
}
addList.textView("Wenn an Tag x, dann vor Zeit t") {
onClick {
constraints.appendChild(View.wrap(createHtmlView<HTMLDivElement>()) {
classList += "input-group"
html.appendChild(TextView("Vor Zeit").apply {
classList += "form-btn"
onClick { this@wrap.html.remove() }
}.html)
html.appendChild(InputView(InputType.NUMBER).apply {
classList += "form-control"
html.name = "constraint-only-before-time-day-${index}"
placeholder = "Tag (optional)"
min = -1337.0
max = 1337.0
updateDateView(this@wrap.html, value.toIntOrNull() ?: 0)
valueProperty.onChange {
updateDateView(this@wrap.html, value.toIntOrNull() ?: 0)
}
}.html)
html.appendChild(InputView(InputType.TEXT).apply {
classList += "form-control"
html.name = "constraint-only-before-time-${index++}"
placeholder = "HH:MM | Min"
}.html)
}.html)
}
}
addList.textView("Wenn an Tag x, dann ab Zeit t") {
onClick {
constraints.appendChild(View.wrap(createHtmlView<HTMLDivElement>()) {
classList += "input-group"
html.appendChild(TextView("Nach Zeit").apply {
classList += "form-btn"
onClick { this@wrap.html.remove() }
}.html)
html.appendChild(InputView(InputType.NUMBER).apply {
classList += "form-control"
html.name = "constraint-only-after-time-day-${index}"
placeholder = "Tag (optional)"
min = -1337.0
max = 1337.0
updateDateView(this@wrap.html, value.toIntOrNull() ?: 0)
valueProperty.onChange {
updateDateView(this@wrap.html, value.toIntOrNull() ?: 0)
}
}.html)
html.appendChild(InputView(InputType.TEXT).apply {
classList += "form-control"
html.name = "constraint-only-after-time-${index++}"
placeholder = "HH:MM | Min"
}.html)
}.html)
}
}
addList.textView("Wenn an Tag x, dann Zeitpunkt t") {
onClick {
constraints.appendChild(View.wrap(createHtmlView<HTMLDivElement>()) {
classList += "input-group"
html.appendChild(TextView("Zeitpunkt").apply {
classList += "form-btn"
onClick { this@wrap.html.remove() }
}.html)
html.appendChild(InputView(InputType.NUMBER).apply {
classList += "form-control"
html.name = "constraint-exact-time-day-${index}"
placeholder = "Tag (optional)"
min = -1337.0
max = 1337.0
updateDateView(this@wrap.html, value.toIntOrNull() ?: 0)
valueProperty.onChange {
updateDateView(this@wrap.html, value.toIntOrNull() ?: 0)
}
}.html)
html.appendChild(InputView(InputType.TEXT).apply {
classList += "form-control"
html.name = "constraint-exact-time-${index++}"
placeholder = "HH:MM | Min"
}.html)
}.html)
}
}
addList.textView("Nicht zur selben Zeit wie AK x") {
onClick {
constraints.appendChild(View.wrap(createHtmlView<HTMLDivElement>()) {
classList += "input-group"
html.appendChild(TextView("Nicht parallel").apply {
classList += "form-btn"
onClick { this@wrap.html.remove() }
}.html)
val select = createHtmlView<HTMLSelectElement>()
select.classList.add("form-control")
select.name = "constraint-not-at-same-time-${index++}"
launch {
val all = WorkGroupRepository.all()
val id = (select.options[select.selectedIndex] as? HTMLOptionElement)?.value
for (wg in all) {
val option = createHtmlView<HTMLOptionElement>()
option.value = wg.id.toString()
option.textContent = wg.name
if (option.value == id) {
option.selected = true
}
select.appendChild(option)
}
}
html.appendChild(select)
}.html)
}
}
addList.textView("Nachdem AK x stattgefunden hat") {
onClick {
constraints.appendChild(View.wrap(createHtmlView<HTMLDivElement>()) {
classList += "input-group"
html.appendChild(TextView("Nach AK").apply {
classList += "form-btn"
onClick { this@wrap.html.remove() }
}.html)
val select = createHtmlView<HTMLSelectElement>()
select.classList.add("form-control")
select.name = "constraint-only-after-work-group-${index++}"
launch {
val all = WorkGroupRepository.all()
val id = (select.options[select.selectedIndex] as? HTMLOptionElement)?.value
for (wg in all) {
val option = createHtmlView<HTMLOptionElement>()
option.value = wg.id.toString()
option.textContent = wg.name
if (option.value == id) {
option.selected = true
}
select.appendChild(option)
}
}
html.appendChild(select)
}.html)
}
}
addList.textView("In Raum x") {
onClick {
constraints.appendChild(View.wrap(createHtmlView<HTMLDivElement>()) {
classList += "input-group"
html.appendChild(TextView("Raum").apply {
classList += "form-btn"
onClick { this@wrap.html.remove() }
}.html)
val select = createHtmlView<HTMLSelectElement>()
select.classList.add("form-control")
select.name = "constraint-room-${index++}"
val id = (select.options[select.selectedIndex] as? HTMLOptionElement)?.value
launch {
val all = RoomRepository.all()
select.clear()
for (room in all) {
val option = createHtmlView<HTMLOptionElement>()
option.value = room.id.toString()
option.textContent = room.name
if (option.value == id) {
option.selected = true
}
select.appendChild(option)
}
}
html.appendChild(select)
}.html)
}
}
for (child in constraints.children.iterator()) {
if (child.classList.contains("input-group")) {
val span = child.firstElementChild as HTMLElement
span.addEventListener("click", org.w3c.dom.events.EventListener {
child.remove()
})
for (e in child.children.iterator()) {
if (e is HTMLInputElement && e.name.contains("-day-")) {
val input = InputView.wrap(InputType.NUMBER, e)
updateDateView(child, input.value.toIntOrNull() ?: 0)
input.valueProperty.onChange {
updateDateView(child, input.value.toIntOrNull() ?: 0)
}
}
}
}
}
}

View file

@ -1,74 +0,0 @@
package de.kif.frontend.views.board
import de.kif.common.formatDate
import de.kif.common.formatTime
import de.kif.frontend.launch
import de.kif.frontend.repository.ScheduleRepository
import de.kif.frontend.timezoneOffset
import de.kif.frontend.views.overview.getByClassOrCreate
import de.westermann.kwebview.interval
import de.westermann.kwebview.iterator
import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLSpanElement
import org.w3c.dom.get
import kotlin.browser.document
import kotlin.dom.clear
import kotlin.js.Date
fun initBoard() {
val dateContainer = document.getElementsByClassName("board-header-date")[0] as HTMLElement
val timeView = dateContainer.getByClassOrCreate<HTMLSpanElement>("board-header-date-time")
val dateView = dateContainer.getByClassOrCreate<HTMLSpanElement>("board-header-date-date")
val initTime = Date.now().toLong()
val referenceInitTime = dateContainer.dataset["now"]?.toLongOrNull() ?: initTime
val diff = initTime - referenceInitTime
val boardRunning = document.getElementsByClassName("board-running")[0] as HTMLElement
val scheduleList = mutableListOf<BoardSchedule>()
val runningReferenceTime = boardRunning.dataset["reference"]?.toLongOrNull() ?: 0L
fun update() {
launch {
scheduleList.clear()
val list = ScheduleRepository.getUpcoming()
boardRunning.clear()
val now = Date.now().toLong() + diff
for (s in list) {
val v = BoardSchedule.create(s, runningReferenceTime, now)
scheduleList += v
boardRunning.appendChild(v.html)
}
}
}
for (bs in boardRunning.getElementsByClassName("board-schedule").iterator()) {
scheduleList += BoardSchedule(bs).also {
it.onRemove {
update()
}
}
}
interval(1000) {
val now = Date.now().toLong() + diff
timeView.textContent = formatTime(now, timezoneOffset)
dateView.textContent = formatDate(now, timezoneOffset)
scheduleList.forEach { it.updateTime(now) }
}
ScheduleRepository.onCreate {
update()
}
ScheduleRepository.onUpdate {
update()
}
ScheduleRepository.onDelete {
update()
}
}

View file

@ -1,68 +0,0 @@
package de.kif.frontend.views.board
import de.kif.common.formatTimeDiff
import de.kif.common.model.Schedule
import de.kif.frontend.timezoneOffset
import de.kif.frontend.views.overview.getByClassOrCreate
import de.westermann.kobserve.event.EventHandler
import de.westermann.kwebview.View
import de.westermann.kwebview.createHtmlView
import org.w3c.dom.HTMLDivElement
import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLSpanElement
import org.w3c.dom.get
class BoardSchedule(
view: HTMLElement,
startTime: Long = 0L,
endTime: Long = 0L
) : View(view) {
val colorViewContainer = view.getByClassOrCreate<HTMLDivElement>("board-schedule-color")
val colorView = colorViewContainer.getByClassOrCreate<HTMLSpanElement>("bsc")
val timeView = view.getByClassOrCreate<HTMLDivElement>("board-schedule-time")
val nameView = view.getByClassOrCreate<HTMLDivElement>("board-schedule-name")
val roomView = view.getByClassOrCreate<HTMLDivElement>("board-schedule-room")
val clockViewContainer = view.getByClassOrCreate<HTMLDivElement>("board-schedule-clock")
val clockView = clockViewContainer.getByClassOrCreate<HTMLElement>("material-icons", "i").also {
it.textContent = "alarm"
}
private val startTime: Long = timeView.dataset["startTime"]?.toLongOrNull() ?: startTime
private val endTime: Long = timeView.dataset["endTime"]?.toLongOrNull() ?: endTime
val onRemove = EventHandler<Unit>()
fun updateTime(now: Long) {
timeView.textContent = when {
startTime >= now -> "Start ${formatTimeDiff(startTime, now, timezoneOffset)}"
endTime >= now -> "Ende ${formatTimeDiff(endTime, now, timezoneOffset)}"
else -> {
onRemove.emit(Unit)
"---"
}
}
classList["board-schedule-running"] = now in startTime..endTime
}
companion object {
fun create(schedule: Schedule, referenceTime: Long, now: Long): BoardSchedule {
val startTime = ((schedule.getAbsoluteStartTime() * 60 * 1000) + referenceTime)
val endTime = ((schedule.getAbsoluteEndTime() * 60 * 1000) + referenceTime)
val entry = BoardSchedule(createHtmlView(), startTime, endTime)
if (schedule.workGroup.track?.color != null) {
entry.colorView.style.backgroundColor = schedule.workGroup.track.color.toString()
}
entry.nameView.textContent = schedule.workGroup.name
entry.roomView.textContent = schedule.room.name
entry.updateTime(now)
return entry
}
}
}

View file

@ -1,308 +0,0 @@
package de.kif.frontend.views.calendar
import de.kif.frontend.launch
import de.kif.frontend.repository.RoomRepository
import de.kif.frontend.repository.ScheduleRepository
import de.westermann.kwebview.*
import de.westermann.robots.website.toolkit.view.TouchEvent
import org.w3c.dom.*
import org.w3c.dom.events.Event
import org.w3c.dom.events.EventListener
import org.w3c.dom.events.MouseEvent
import org.w3c.dom.events.WheelEvent
import kotlin.browser.document
import kotlin.browser.window
import kotlin.js.Date
import kotlin.math.max
import kotlin.math.min
class Calendar(calendar: HTMLElement) : View(calendar) {
var autoScroll = true
val calendarTable = calendar.getElementsByClassName("calendar-table")[0] as HTMLElement
private val calendarTableHeader = calendar.getElementsByClassName("calendar-header")[0] as HTMLElement
val showAllWorkGroupsBtn =
(document.getElementById("calendar-all-work-groups") as? HTMLElement)?.let { wrap(it) }
private val htmlBody = document.body ?: createHtmlView()
val day = (calendarTable.dataset["day"]?.toIntOrNull() ?: -1)
val reloadOnFinish = (calendarTable.dataset["reload"]?.toBoolean() ?: false)
val hideEmpty = (calendarTable.dataset["hideEmpty"]?.toBoolean() ?: false)
val referenceDate = (calendarTable.dataset["reference"]?.toLongOrNull() ?: -1L)
val nowDate = (calendarTable.dataset["now"]?.toLongOrNull() ?: -1L)
val timeDifference = (Date.now().toLong() - nowDate)
fun scrollVerticalBy(pixel: Double, scrollBehavior: ScrollBehavior = ScrollBehavior.SMOOTH) {
scrollAllVerticalBy(pixel, scrollBehavior)
}
fun scrollHorizontalBy(pixel: Double, scrollBehavior: ScrollBehavior = ScrollBehavior.SMOOTH) {
scrollAllHorizontalBy(pixel, scrollBehavior)
}
fun scrollVerticalTo(pixel: Double, scrollBehavior: ScrollBehavior = ScrollBehavior.SMOOTH) {
scrollAllVerticalTo(pixel, scrollBehavior)
}
fun scrollHorizontalTo(pixel: Double, scrollBehavior: ScrollBehavior = ScrollBehavior.SMOOTH) {
scrollAllHorizontalTo(pixel, scrollBehavior)
}
val editable = calendar.dataset["editable"]?.toBoolean() ?: false
val body = CalendarBody(this, calendar.getElementsByClassName("calendar-body")[0] as HTMLElement)
val orientation: Orientation = if (document.getElementsByClassName("time-to-room").length > 0) {
Orientation.TIME_TO_ROOM
} else {
Orientation.ROOM_TO_TIME
}
private var autoCheck: Boolean = false
fun autoCheck() {
if (autoCheck) checkConstraints()
}
private fun checkConstraints() {
launch {
val errors = ScheduleRepository.checkConstraints()
for ((s, l) in errors.map) {
for (entry in body.calendarEntries) {
if (entry.scheduleId == s) {
entry.setError(l)
}
}
}
}
}
private fun scroll(horizontal: Double, vertical: Double, event: Event? = null) {
var horizontalScroll = horizontal
var verticalScroll = vertical
if (verticalScroll > 0) {
val x = html.offsetTop - htmlBody.scrollTop
if (x > 0) {
val bodyScroll = min(x, verticalScroll)
verticalScroll = 0.0
htmlBody.scrollBy(ScrollToOptions(0.0, bodyScroll, ScrollBehavior.INSTANT))
} else {
if (calendarTable.scrollTop + calendarTable.clientHeight + 5 >= calendarTable.scrollHeight) {
htmlBody.scrollBy(ScrollToOptions(0.0, verticalScroll, ScrollBehavior.INSTANT))
} else if (x < 0) {
htmlBody.scrollBy(ScrollToOptions(0.0, x, ScrollBehavior.INSTANT))
}
}
} else if (verticalScroll < 0) {
val x = html.offsetTop - htmlBody.scrollTop
if (x < 0) {
val bodyScroll = max(x, verticalScroll)
verticalScroll = 0.0
htmlBody.scrollBy(ScrollToOptions(0.0, bodyScroll, ScrollBehavior.INSTANT))
} else {
if (calendarTable.scrollTop == 0.0) {
htmlBody.scrollBy(ScrollToOptions(0.0, verticalScroll, ScrollBehavior.INSTANT))
} else if (x > 0) {
htmlBody.scrollBy(ScrollToOptions(0.0, x, ScrollBehavior.INSTANT))
}
}
}
scrollHorizontalBy(horizontalScroll, ScrollBehavior.INSTANT)
scrollVerticalBy(verticalScroll, ScrollBehavior.INSTANT)
event?.preventDefault()
event?.stopPropagation()
}
private var visibleRooms: Set<Long> = emptySet()
fun isRoomHidden(roomId: Long): Boolean {
return hideEmpty && roomId !in visibleRooms
}
fun updateVisibility() {
visibleRooms = body.calendarCells.asSequence().filter { it.isNotEmpty() }.map { it.roomId }.toSet()
body.updateVisibility()
for (element in calendarTableHeader.children) {
val id = element.dataset["room"]?.toLongOrNull() ?: continue
element.dataset["hidden"] = isRoomHidden(id).toString()
}
}
init {
scroll += calendarTable
if (editable) {
CalendarEdit(this, calendar.querySelector(".calendar-edit") as HTMLElement)
}
val checkConstraintsBtn =
(document.getElementById("calendar-check-constraints") as? HTMLElement)?.let { wrap(it) }
if (checkConstraintsBtn != null) {
checkConstraintsBtn.onClick.addListener {
checkConstraints()
}
}
val autoCheckConstraintsBtn =
(document.getElementById("calendar-auto-check-constraints") as? HTMLElement)?.let { wrap(it) }
if (autoCheckConstraintsBtn != null) {
autoCheckConstraintsBtn.onClick.addListener {
if (autoCheck) {
for (entry in body.calendarEntries) {
entry.setError(emptyList())
}
autoCheck = false
} else {
autoCheck = true
autoCheck()
}
autoCheckConstraintsBtn.classList["btn-primary"] = autoCheck
}
}
document.addEventListener("wheel", EventListener {
val event = it as? WheelEvent ?: return@EventListener
autoScroll = false
val multiplier = when (event.deltaMode) {
1 -> 16.0
2 -> window.innerHeight.toDouble()
else -> 1.0
}
scroll(event.deltaX * multiplier, event.deltaY * multiplier, event)
})
var mousePoint: Point? = null
document.addEventListener("mousedown", EventListener {
val event = it as? MouseEvent ?: return@EventListener
mousePoint = event.toPoint()
})
document.addEventListener("mouseup", EventListener {
mousePoint = null
})
document.addEventListener("mousemove", EventListener {
val event = it as? MouseEvent ?: return@EventListener
autoScroll = false
val mp = mousePoint ?: return@EventListener
val p = event.toPoint()
scroll(mp.x - p.x, mp.y - p.y, event)
mousePoint = p
})
document.addEventListener("touchstart", EventListener {
val event = it as? TouchEvent ?: return@EventListener
mousePoint = event.toPoint()
})
document.addEventListener("touchend", EventListener {
mousePoint = null
})
document.addEventListener("touchmove", EventListener {
val event = it as? TouchEvent ?: return@EventListener
autoScroll = false
val mp = mousePoint ?: return@EventListener
val p = event.toPoint() ?: return@EventListener
scroll(mp.x - p.x, mp.y - p.y, event)
mousePoint = p
})
document.addEventListener("scroll", EventListener {
autoScroll = false
})
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()
}
}
}
}
enum class Orientation {
/**
* Columns contains time
* Rows contains rooms
*
* Like the old kif tool
*/
TIME_TO_ROOM,
/**
* Columns contains rooms
* Rows contains time
*
* Like the congress schedule
*/
ROOM_TO_TIME
}
companion object {
private var scroll = listOf<HTMLElement>()
private fun scrollAllVerticalBy(pixel: Double, scrollBehavior: ScrollBehavior = ScrollBehavior.SMOOTH) {
for (calendarTable in scroll) {
calendarTable.scrollTop = calendarTable.scrollTop + pixel
//calendarTable.scrollBy(ScrollToOptions(0.0, pixel, scrollBehavior))
}
}
private fun scrollAllHorizontalBy(pixel: Double, scrollBehavior: ScrollBehavior = ScrollBehavior.SMOOTH) {
for (calendarTable in scroll) {
calendarTable.scrollLeft = calendarTable.scrollLeft + pixel
//calendarTable.scrollBy(ScrollToOptions(pixel, 0.0, scrollBehavior))
}
}
private fun scrollAllVerticalTo(pixel: Double, scrollBehavior: ScrollBehavior = ScrollBehavior.SMOOTH) {
for (calendarTable in scroll) {
calendarTable.scrollTop = pixel
//calendarTable.scrollTo(ScrollToOptions(0.0, pixel, scrollBehavior))
}
}
private fun scrollAllHorizontalTo(pixel: Double, scrollBehavior: ScrollBehavior = ScrollBehavior.SMOOTH) {
for (calendarTable in scroll) {
calendarTable.scrollLeft = pixel
//calendarTable.scrollTo(ScrollToOptions(pixel, 0.0, scrollBehavior))
}
}
}
}
fun initCalendar() {
document.getElementsByClassName("calendar").iterator().forEach { Calendar(it) }
}

View file

@ -1,232 +0,0 @@
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.async
import de.westermann.kwebview.interval
import de.westermann.kwebview.iterator
import org.w3c.dom.HTMLElement
import org.w3c.dom.INSTANT
import org.w3c.dom.SMOOTH
import org.w3c.dom.ScrollBehavior
import kotlin.browser.window
import kotlin.js.Date
import kotlin.math.max
import kotlin.math.min
fun getYearOfDate(date: Date): Int {
return ((Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) - Date.UTC(
date.getFullYear(),
0,
0
)) / 24 / 60 / 60 / 1000).toInt()
}
class CalendarBody(val calendar: Calendar, view: HTMLElement) : ViewCollection<CalendarRow>(view) {
val editable = calendar.editable
val day = calendar.day
val calendarCells: List<CalendarCell>
get() = iterator().asSequence().flatten().toList()
val calendarEntries: List<CalendarEntry>
get() = calendarCells.asSequence().flatten().toList()
var maxTime = 0
var minTime = 0
fun updateVisibility() {
for (c in children) {
c.updateVisibility()
}
}
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
minTime = min
maxTime = max
min = calendarBodies.map { it.minTime }.min() ?: min
max = calendarBodies.map { it.maxTime }.max() ?: max
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))
}
calendar.updateVisibility()
}
fun update(scroll: ScrollBehavior) {
val now = Date.now().toLong() - calendar.timeDifference
val refDay = getYearOfDate(Date(calendar.referenceDate))
val nowDay = getYearOfDate(Date(now))
val d = nowDay - refDay
val diff = (day - d)
val date = Date(now)
val currentTime = date.getHours() * 60 + date.getMinutes() + (diff * 60 * 24)
val rowTime = (currentTime / 15) * 15
var activeRow: CalendarRow? = null
var largestRowTime = Int.MIN_VALUE
for (row in this) {
largestRowTime = max(largestRowTime, row.time)
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}"
activeRow = row
} else {
for (str in row.classList) {
if ("now" in str) {
row.classList -= str
}
}
}
}
if (largestRowTime < currentTime && calendar.reloadOnFinish) {
window.location.reload()
}
if (calendar.autoScroll && activeRow != null) {
if (calendar.orientation == Calendar.Orientation.ROOM_TO_TIME) {
calendar.scrollVerticalTo((activeRow.offsetTop - 150).toDouble(), scroll)
} else {
calendar.scrollHorizontalTo((activeRow.offsetLeft - 100).toDouble(), scroll)
}
}
for (entry in calendarEntries) {
entry.updateTime(diff, currentTime)
}
}
init {
calendarBodies += this
wrapContent {
CalendarRow(this, it)
}
ScheduleRepository.onCreate {
launch {
val schedule = ScheduleRepository.get(it) ?: return@launch
updateRows(schedule.time, schedule.workGroup.length)
CalendarEntry.create(this, schedule)
calendar.updateVisibility()
}
}
ScheduleRepository.onUpdate {
launch {
val schedule = ScheduleRepository.get(it) ?: return@launch
updateRows(schedule.time, schedule.workGroup.length)
var found = false
for (entry in calendarEntries) {
if (entry.scheduleId == it) {
entry.load(schedule)
found = true
}
}
if (!found) {
CalendarEntry.create(this, schedule)
}
updateRows()
}
}
ScheduleRepository.onDelete {
for (entry in calendarEntries) {
if (entry.scheduleId == it) {
entry.html.remove()
launch {
updateRows()
}
}
}
}
async {
update(ScrollBehavior.INSTANT)
}
interval(1000) {
update(ScrollBehavior.SMOOTH)
}
}
companion object {
val calendarBodies: MutableList<CalendarBody> = mutableListOf()
}
}

View file

@ -1,53 +0,0 @@
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.set
class CalendarCell(val row: CalendarRow, view: HTMLElement) : ViewCollection<CalendarEntry>(view) {
val day = row.day
val time = row.time
val roomId = dataset["room"]?.toLongOrNull() ?: 0
var hiddenString by dataset.property("hidden")
var hidden: Boolean
get() = hiddenString?.toBoolean() ?: false
set(value) {
hiddenString = value.toString()
}
private lateinit var room: Room
suspend fun getRoom(): Room {
if (this::room.isInitialized) {
return room
}
room = RoomRepository.get(roomId) ?: throw NoSuchElementException()
return room
}
fun updateVisibility() {
hidden = row.calendar.calendar.isRoomHidden(roomId)
}
init {
wrapContent("calendar-entry") {
CalendarEntry(row.calendar, it)
}
}
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,133 +0,0 @@
package de.kif.frontend.views.calendar
import de.kif.common.Search
import de.kif.common.model.Schedule
import de.kif.common.model.WorkGroup
import de.kif.frontend.launch
import de.kif.frontend.repository.ScheduleRepository
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.InputType
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
import kotlin.browser.document
class CalendarEdit(
private val calendar: Calendar, view: HTMLElement
) : View(view) {
private val toggleEditButton =
Button.wrap(document.getElementById("calendar-edit-button") as HTMLButtonElement)
val search =
InputView.wrap(InputType.SEARCH, view.querySelector(".calendar-edit-search input") as HTMLInputElement)
val listView = ListView.wrap<CalendarWorkGroup>(
view.querySelector(".calendar-edit-list") as HTMLElement
)
private var showAll = false
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)
}
var scheduled: Map<WorkGroup, List<Schedule>> = emptyMap()
private fun load() {
if (loaded) return
loaded = true
launch {
scheduled = ScheduleRepository.all().groupBy { it.workGroup }
for (workGroup in WorkGroupRepository.all()) {
workGroupList += CalendarWorkGroup(calendar, this, workGroup).also {
it.isScheduled = !showAll && workGroup in scheduled
}
}
}
}
fun update() {
for (wk in workGroupList) {
wk.isScheduled = !showAll && wk.workGroup in scheduled
}
}
init {
toggleEditButton.onClick {
calendar.classList.toggle("edit")
if (!loaded) {
load()
}
}
calendar.showAllWorkGroupsBtn?.onClick?.addListener {
showAll = !showAll
calendar.showAllWorkGroupsBtn.classList.toggle("btn-primary", showAll)
update()
}
onWheel {
it.stopPropagation()
}
WorkGroupRepository.onCreate {
if (loaded) {
launch {
workGroupList += CalendarWorkGroup(
calendar,
this,
WorkGroupRepository.get(it)!!
)
}
}
}
ScheduleRepository.onCreate { id ->
launch {
val schedule = ScheduleRepository.get(id) ?: return@launch
val schedules = scheduled[schedule.workGroup] ?: emptyList()
scheduled += schedule.workGroup to schedules + schedule
workGroupList.firstOrNull { it.workGroup.id == schedule.workGroup.id }?.isScheduled =
!showAll && schedule.workGroup in scheduled
}
}
ScheduleRepository.onDelete { id ->
val schedule = scheduled.values.flatten().firstOrNull { it.id == id } ?: return@onDelete
val schedules = scheduled[schedule.workGroup] ?: emptyList()
val new = schedules - schedule
if (new.isEmpty())
scheduled -= schedule.workGroup
else
scheduled += schedule.workGroup to schedules - schedule
workGroupList.firstOrNull { it.workGroup.id == schedule.workGroup.id }?.isScheduled =
!showAll && schedule.workGroup in scheduled
}
listView.listFactory(sortedList)
}
}

View file

@ -1,308 +0,0 @@
package de.kif.frontend.views.calendar
import de.kif.common.CALENDAR_GRID_WIDTH
import de.kif.common.ConstraintError
import de.kif.common.model.Schedule
import de.kif.common.model.WorkGroup
import de.kif.frontend.launch
import de.kif.frontend.repository.RepositoryDelegate
import de.kif.frontend.repository.ScheduleRepository
import de.kif.frontend.views.overview.getByClassOrCreate
import de.westermann.kwebview.*
import de.westermann.kwebview.components.Body
import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLSpanElement
import org.w3c.dom.events.MouseEvent
import kotlin.browser.document
import kotlin.browser.window
import kotlin.js.Date
class CalendarEntry(private val calendar: CalendarBody, view: HTMLElement) : View(view) {
private lateinit var mouseDelta: Point
private var ignoreEditHover = false
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 startTime = dataset["time"]?.toIntOrNull() ?: 0
var length = dataset["length"]?.toIntOrNull() ?: 0
private val finishedProperty = dataset.property("finished")
var finished: Boolean
get() = finishedProperty.value == "true"
set(value) {
finishedProperty.value = value.toString()
}
var pending by classList.property("pending")
private var error by classList.property("error")
private var nextScroll = 0.0
private val editable: Boolean
get() = calendar.editable
private var moveLockRoom: Long? = null
private var moveLockTime: Int? = null
private val nameView = view.getByClassOrCreate<HTMLSpanElement>("calendar-entry-name")
private fun onMove(event: MouseEvent) {
val position = event.toPoint() - mouseDelta
if (ignoreEditHover) {
for (element in document.elementsFromPoint(position.x, position.y)) {
if (element.classList.contains("calendar-edit")) {
return
}
}
ignoreEditHover = false
}
val cell = calendar.calendarCells.find {
position in it.dimension
}
if (cell != null) {
if (moveLockRoom != null && cell.roomId != moveLockRoom) {
return
}
if (moveLockTime != null && cell.time != moveLockTime) {
return
}
cell += this
if (newCell == null) {
style {
left = "0"
top = "0.1rem"
}
}
async {
val now = Date.now()
if (now <= nextScroll) {
return@async
}
val width = calendar.calendar.calendarTable.clientWidth
val height = window.innerHeight
val rect = html.getBoundingClientRect()
if (rect.left < 0.0) {
nextScroll = now + 500.0
calendar.calendar.scrollHorizontalBy(rect.left - 80.0)
} else if (rect.right > width) {
nextScroll = now + 0.500
calendar.calendar.scrollHorizontalBy(rect.right - width + 50.0)
}
if (rect.top < 20.0) {
nextScroll = now + 500.0
calendar.calendar.scrollVerticalBy(rect.top - 50.0)
} else if (rect.bottom > height - 20.0) {
nextScroll = now + 500.0
calendar.calendar.scrollVerticalBy(rect.bottom - height + 50.0)
}
}
launch {
calendarTools?.setName(cell.getRoom(), cell.time)
}
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,
lockRoom = false,
lockTime = false
)
)
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() {
classList += "drag"
listeners = listOf(
Body.onMouseMove.reference(this::onMove),
Body.onMouseUp.reference(this::onFinishMove),
Body.onMouseLeave.reference(this::onFinishMove)
)
}
private val calendarTools = if (editable) CalendarTools(this) else null
private val calendarErrors = if (editable) CalendarErrors() else null
init {
onMouseDown { event ->
val isValidTarget = event.target == html || event.target == nameView
if (!editable || !isValidTarget || "pending" in classList) {
event.stopPropagation()
return@onMouseDown
}
launch {
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
moveLockRoom = if (s.lockRoom) s.room.id else null
moveLockTime = if (s.lockTime) s.time else null
startDrag()
}
event.preventDefault()
event.stopPropagation()
}
if (calendarTools != null) {
html.appendChild(calendarTools.html)
launch {
val s = schedule.get()
calendarTools.update(s)
}
}
if (calendarErrors != null) {
html.appendChild(calendarErrors.html)
}
}
fun load(schedule: Schedule) {
pending = false
if (schedule.id != null) scheduleId = schedule.id
this.schedule.set(schedule)
html.removeAttribute("style")
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)
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 {
it.day == schedule.day && it.time == time && it.roomId == schedule.room.id
}
if (cell != null && cell.html != html.parentElement) {
cell += this
}
calendar.calendar.autoCheck()
}
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()
}
}
nameView.textContent = workGroup.name
}
fun setError(errors: List<ConstraintError>) {
error = errors.isNotEmpty()
calendarErrors?.setErrors(errors)
}
fun updateTime(diff: Int, currentTime: Int) {
finished = (diff < 0 || diff == 0 && startTime + length < currentTime)
}
companion object {
fun create(calendar: CalendarBody, schedule: Schedule): CalendarEntry {
val entry = CalendarEntry(calendar, createHtmlView())
entry.load(schedule)
return entry
}
fun create(calendar: CalendarBody, workGroup: WorkGroup): CalendarEntry {
val entry = CalendarEntry(calendar, createHtmlView())
entry.load(workGroup)
entry.mouseDelta = Point.ZERO
entry.ignoreEditHover = true
entry.startDrag()
return entry
}
}
}

View file

@ -1,17 +0,0 @@
package de.kif.frontend.views.calendar
import de.kif.common.ConstraintError
import de.westermann.kwebview.ViewCollection
import de.westermann.kwebview.components.TextView
import de.westermann.kwebview.components.textView
class CalendarErrors() : ViewCollection<TextView>() {
fun setErrors(errors: List<ConstraintError>) {
clear()
for (error in errors) {
textView(error.reason)
}
}
}

View file

@ -1,69 +0,0 @@
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.*
class CalendarRow(val calendar: CalendarBody, view: HTMLElement) : ViewCollection<CalendarCell>(view) {
val day = calendar.day
val time = dataset["time"]?.toIntOrNull() ?: 0
fun updateVisibility() {
for (c in children) {
c.updateVisibility()
}
}
init {
wrapContent {
if (it.dataset["room"] != null)
CalendarCell(this, it)
else null
}
RoomRepository.onCreate {
+CalendarCell.create(this, it)
calendar.calendar.updateVisibility()
}
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)
val rooms = RoomRepository.all()
for (room in rooms) {
if (room.id != null) {
row += CalendarCell.create(row, room.id)
}
}
return row
}
}
}

View file

@ -1,116 +0,0 @@
package de.kif.frontend.views.calendar
import de.kif.common.model.Room
import de.kif.common.model.Schedule
import de.kif.frontend.launch
import de.kif.frontend.repository.ScheduleRepository
import de.westermann.kwebview.View
import de.westermann.kwebview.ViewCollection
import de.westermann.kwebview.components.*
class CalendarTools(entry: CalendarEntry) : ViewCollection<View>() {
fun setName(room: Room, time: Int) {
nameView.text = room.name + " - " + Schedule.timeOfDayToString(time)
}
lateinit var schedule: Schedule
fun update(schedule: Schedule) {
this.schedule = schedule
setName(schedule.room, schedule.time)
lockRoomButton.classList["disabled"] = !schedule.lockRoom
lockTimeButton.classList["disabled"] = !schedule.lockTime
}
private lateinit var nameView: TextView
private lateinit var lockRoomButton: IconView
private lateinit var lockTimeButton: IconView
init {
boxView {
nameView = textView { }
lockRoomButton = iconView(MaterialIcon.GPS_FIXED) {
title = "Lock room"
onClick {
entry.pending = true
launch {
val s = entry.schedule.get()
ScheduleRepository.update(s.copy(lockRoom = "disabled" in this.classList))
}
}
}
lockTimeButton = iconView(MaterialIcon.ALARM) {
title = "Lock time slot"
onClick {
entry.pending = true
launch {
val s = entry.schedule.get()
ScheduleRepository.update(s.copy(lockTime = "disabled" in this.classList))
}
}
}
}
boxView {
textView("-10") {
title = "Schedule 10 minutes earlier"
onClick {
if (schedule.lockTime) return@onClick
entry.pending = true
launch {
val s = entry.schedule.get()
ScheduleRepository.update(s.copy(time = s.time - 10))
}
}
}
textView("-5") {
title = "Schedule 5 minutes earlier"
onClick {
if (schedule.lockTime) return@onClick
entry.pending = true
launch {
val s = entry.schedule.get()
ScheduleRepository.update(s.copy(time = s.time - 5))
}
}
}
textView("+5") {
title = "Schedule 5 minutes later"
onClick {
if (schedule.lockTime) return@onClick
entry.pending = true
launch {
val s = entry.schedule.get()
ScheduleRepository.update(s.copy(time = s.time + 5))
}
}
}
textView("+10") {
title = "Schedule 10 minutes later"
onClick {
if (schedule.lockTime) return@onClick
entry.pending = true
launch {
val s = entry.schedule.get()
ScheduleRepository.update(s.copy(time = s.time + 10))
}
}
}
iconView(MaterialIcon.DELETE) {
title = "Delete"
onClick {
entry.pending = true
launch {
ScheduleRepository.delete(entry.scheduleId)
}
}
}
}
}
}

View file

@ -1,69 +0,0 @@
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()
}
}
}
fun remove() {
html.remove()
}
var isScheduled: Boolean by classList.property("scheduled")
fun update() {
launch {
val wk = WorkGroupRepository.get(workGroup.id ?: return@launch) ?: return@launch
load(wk)
}
}
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.body, workGroup)
}
}
}

View file

@ -1,116 +0,0 @@
package de.kif.frontend.views.overview
import de.kif.frontend.launch
import de.kif.frontend.repository.PostRepository
import de.westermann.kobserve.event.subscribe
import de.westermann.kwebview.iterator
import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.HTMLTextAreaElement
import org.w3c.dom.events.EventListener
import org.w3c.dom.get
import org.w3c.files.File
import org.w3c.files.FileReader
import org.w3c.files.get
import kotlin.browser.document
import kotlin.dom.clear
private fun sortOverviewPosts(container: HTMLElement) {
val list = container.children.iterator().asSequence().toList()
val sorted = list.sortedWith(compareBy(
{ if (it.dataset["pinned"]?.toBoolean() == true) 0 else 1 },
{ -(it.dataset["id"]?.toLong() ?: -1) }
))
container.clear()
for (element in sorted) {
container.appendChild(element)
}
}
fun initOverviewMain() {
val main = document.getElementsByClassName("overview-main")[0] as HTMLElement
PostRepository.onCreate {
val post = PostView.create(it)
post.classList += "overview-post"
main.appendChild(post.html)
sortOverviewPosts(main)
}
subscribe<PostChangeEvent> {
sortOverviewPosts(main)
}
}
fun initPosts() {
val postList = document.getElementsByClassName("post")
for (post in postList) {
PostView(post)
}
}
fun initPostEdit() {
// Content preview
val textArea = document.getElementById("content") as HTMLTextAreaElement
val preview = document.getElementsByClassName("post-edit-right")[0] as HTMLElement
textArea.addEventListener("change", EventListener {
launch {
preview.innerHTML = PostRepository.render(textArea.value)
}
})
textArea.addEventListener("keyup", EventListener {
launch {
preview.innerHTML = PostRepository.render(textArea.value)
}
})
launch {
preview.innerHTML = PostRepository.render(textArea.value)
}
// Image preview
val imageView = document.getElementsByClassName("post-edit-image")[0] as HTMLElement
val uploadButton = document.getElementById("image") as HTMLInputElement
val deleteSwitch = document.getElementById("image-delete") as? HTMLInputElement
var file: File? = null
val original = imageView.style.backgroundImage
fun updateImage() {
val deleteState = deleteSwitch?.checked == true
val f = file
when {
deleteState -> {
imageView.removeAttribute("style")
uploadButton.value = ""
file = null
}
f == null -> imageView.style.backgroundImage = original
else -> {
val reader = FileReader()
reader.onload = {
val dataUrl = it.target.asDynamic().result as String
imageView.style.backgroundImage = "url(\"$dataUrl\")"
Unit
}
reader.readAsDataURL(f)
}
}
}
uploadButton.addEventListener("change", EventListener {
val files = uploadButton.files ?: return@EventListener
file = files[0]
updateImage()
})
deleteSwitch?.addEventListener("change", EventListener {
updateImage()
})
}

View file

@ -1,100 +0,0 @@
package de.kif.frontend.views.overview
import de.kif.common.formatDateTime
import de.kif.frontend.launch
import de.kif.frontend.repository.PostRepository
import de.kif.frontend.timezoneOffset
import de.westermann.kobserve.event.emit
import de.westermann.kwebview.View
import de.westermann.kwebview.components.Link
import de.westermann.kwebview.createHtmlView
import org.w3c.dom.HTMLElement
import org.w3c.dom.get
import org.w3c.dom.set
class PostView(
view: HTMLElement
) : View(view) {
val postId = dataset["id"]?.toLongOrNull() ?: -1
var pinned: Boolean
get() = dataset["pinned"] == "true"
set(value) {
dataset["pinned"] = value.toString()
}
private val nameView: Link
private val contentView: HTMLElement
private val footerView: HTMLElement
private val imageView: HTMLElement
private fun reload() {
launch {
val p = PostRepository.get(postId) ?: return@launch
classList["post-no-image"] = p.image == null
nameView.text = p.name
nameView.target = "/p/${p.url}"
pinned = p.pinned
if (p.image == null) {
imageView.removeAttribute("style")
} else {
imageView.style.backgroundImage = "url(\"/images/${p.image}\")"
}
contentView.innerHTML = PostRepository.htmlByUrl(p.url)
footerView.innerText = formatDateTime(p.createdAt, timezoneOffset)
emit(PostChangeEvent(postId))
}
}
init {
nameView = Link.wrap(view.getByClassOrCreate("post-name"))
val postColumn = view.getByClassOrCreate<HTMLElement>("post-column")
val postColumnLeft = postColumn.getByClassOrCreate<HTMLElement>("post-column-left")
val postColumnRight = postColumn.getByClassOrCreate<HTMLElement>("post-column-right")
imageView = postColumnLeft.getByClassOrCreate("post-image", "figure")
contentView = postColumnRight.getByClassOrCreate("post-content")
footerView = postColumnRight.getByClassOrCreate("post-footer")
PostRepository.onUpdate {
if (it == postId) {
reload()
}
}
PostRepository.onDelete {
html.remove()
}
}
companion object {
fun create(postId: Long): PostView {
val div = createHtmlView<HTMLElement>()
div.classList.add("post")
div.dataset["id"] = postId.toString()
return PostView(div).also(PostView::reload)
}
}
}
data class PostChangeEvent(val id: Long)
inline fun <reified T : HTMLElement> HTMLElement.getByClassOrCreate(name: String, newTagName: String? = null): T {
val v = this.getElementsByClassName(name)[0] as? T
if (v != null) {
return v
}
val h = createHtmlView<T>(newTagName)
h.classList.add(name)
this.appendChild(h)
return h
}

View file

@ -1,77 +0,0 @@
package de.kif.frontend.views.table
import de.kif.common.SearchElement
import de.kif.frontend.launch
import de.kif.frontend.repository.RepositoryDelegate
import de.kif.frontend.repository.RoomRepository
import de.westermann.kwebview.components.TextView
import de.westermann.kwebview.iterator
import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLSpanElement
import org.w3c.dom.get
class RoomTableLine(view: HTMLElement) : TableLine(view) {
var lineId = dataset["id"]?.toLongOrNull() ?: -1
private val room =
RepositoryDelegate(RoomRepository, lineId)
private val spanRoomName: TextView
private val spanRoomPlaces: TextView
private val spanRoomProjector: TextView
override var searchElement: SearchElement = super.searchElement
init {
val spans = view.getElementsByTagName("span").iterator().asSequence().toList()
spanRoomName =
TextView.wrap(spans.first { it.dataset["editType"] == "room-name" } as HTMLSpanElement)
spanRoomPlaces =
TextView.wrap(spans.first { it.dataset["editType"] == "room-places" } as HTMLSpanElement)
spanRoomProjector =
TextView.wrap(spans.first { it.dataset["editType"] == "room-projector" } as HTMLSpanElement)
setupEditable(spanRoomName) {
launch {
val wg = room.get()
if (wg.name != it) {
RoomRepository.update(wg.copy(name = it))
}
}
}
setupEditable(spanRoomPlaces, "\\d+".toRegex()) {
val number = it.toIntOrNull() ?: return@setupEditable
launch {
val wg = room.get()
if (wg.places != number) {
RoomRepository.update(wg.copy(places = number))
}
}
}
setupBoolean(spanRoomProjector) {
launch {
val wg = room.get()
RoomRepository.update(wg.copy(projector = !wg.projector))
}
}
RoomRepository.onUpdate {
if (it != lineId) return@onUpdate
launch {
val wg = RoomRepository.get(it) ?: return@launch
room.set(wg)
searchElement = wg.createSearch()
spanRoomName.text = wg.name
spanRoomPlaces.text = wg.places.toString()
spanRoomProjector.text = wg.projector.toString()
}
}
}
}

View file

@ -1,41 +0,0 @@
package de.kif.frontend.views.table
import de.kif.frontend.launch
import de.kif.frontend.repository.TrackRepository
import de.westermann.kwebview.components.InputType
import de.westermann.kwebview.components.InputView
import de.westermann.kwebview.iterator
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
launch {
val tracks = TrackRepository.all()
val list = table.getElementsByTagName("tr").iterator().asSequence().filter {
it.dataset["search"] != null
}.map {
when (it.dataset["edit"]) {
"workgroup" -> WorkGroupTableLine(it, tracks)
"room" -> RoomTableLine(it)
else -> TableLine(it)
}
}.toList()
val input = form.getElementsByTagName("input")[0] as HTMLInputElement
val search = InputView.wrap(InputType.SEARCH, input)
search.valueProperty.onChange {
for (row in list) {
row.search(search.value)
}
}
}
}

View file

@ -1,75 +0,0 @@
package de.kif.frontend.views.table
import de.kif.common.Search
import de.kif.common.SearchElement
import de.westermann.kwebview.View
import de.westermann.kwebview.components.ListView
import de.westermann.kwebview.components.TextView
import de.westermann.kwebview.components.textView
import org.w3c.dom.HTMLElement
open class TableLine(line: HTMLElement) : View(line) {
open val searchElement: SearchElement = SearchElement.parse(dataset["search"]!!)
fun search(value: String) {
style.display = if (Search.match(value, searchElement)) "table-row" else "none"
}
protected fun setupEditable(view: TextView, regex: Regex = ".*".toRegex(), onSave: (String) -> Unit) {
view.contentEditable = true
view.onKeyDown {
if (it.keyCode == 13) {
it.preventDefault()
view.blur()
return@onKeyDown
}
}
view.onKeyUp {
view.classList["error"] = !regex.matches(view.text)
}
view.onBlur {
view.classList["error"] = !regex.matches(view.text)
if (!view.classList["error"]) {
onSave(view.text)
}
}
}
protected fun setupBoolean(view: TextView, onSave: () -> Unit) {
view.classList += "no-select"
view.tabIndex = 0
view.onDblClick {
onSave()
}
}
protected fun <T : Any> setupList(view: TextView, list: List<T?>, transform: (T) -> String, onSave: (T?) -> Unit) {
view.classList += "no-select"
view.tabIndex = 0
val listView = ListView<TextView>()
listView.classList += "table-select-box"
for (elem in list) {
val text = if (elem == null) "" else transform(elem)
listView.textView(text) {
onClick {
onSave(elem)
view.blur()
}
}
}
view.onFocus {
view.html.appendChild(listView.html)
}
view.onBlur {
listView.html.remove()
}
}
}

View file

@ -1,109 +0,0 @@
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.launch
import de.kif.frontend.repository.RepositoryDelegate
import de.kif.frontend.repository.TrackRepository
import de.kif.frontend.repository.WorkGroupRepository
import de.westermann.kwebview.components.TextView
import de.westermann.kwebview.iterator
import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLSpanElement
import org.w3c.dom.get
class WorkGroupTableLine(view: HTMLElement, tracks: List<Track>) : TableLine(view) {
private var lineId = dataset["id"]?.toLongOrNull() ?: -1
private val workGroup =
RepositoryDelegate(WorkGroupRepository, lineId)
private val spanWorkGroupName: TextView
private val spanWorkGroupLength: TextView
private val spanWorkGroupInterested: TextView
private val spanWorkGroupTrack: TextView
private val spanWorkGroupResolution: TextView
override var searchElement: SearchElement = super.searchElement
init {
val spans = view.getElementsByTagName("span").iterator().asSequence().toList()
spanWorkGroupName =
TextView.wrap(spans.first { it.dataset["editType"] == "workgroup-name" } as HTMLSpanElement)
spanWorkGroupLength =
TextView.wrap(spans.first { it.dataset["editType"] == "workgroup-length" } as HTMLSpanElement)
spanWorkGroupInterested =
TextView.wrap(spans.first { it.dataset["editType"] == "workgroup-interested" } as HTMLSpanElement)
spanWorkGroupTrack =
TextView.wrap(spans.first { it.dataset["editType"] == "workgroup-track" } as HTMLSpanElement)
spanWorkGroupResolution =
TextView.wrap(spans.first { it.dataset["editType"] == "workgroup-resolution" } as HTMLSpanElement)
setupEditable(spanWorkGroupName) {
launch {
val wg = workGroup.get()
if (wg.name != it) {
WorkGroupRepository.update(wg.copy(name = it))
}
}
}
setupEditable(spanWorkGroupLength, "\\d+".toRegex()) {
val number = it.toIntOrNull() ?: return@setupEditable
launch {
val wg = workGroup.get()
if (wg.length != number) {
WorkGroupRepository.update(wg.copy(length = number))
}
}
}
setupEditable(spanWorkGroupInterested, "\\d+".toRegex()) {
val number = it.toIntOrNull() ?: return@setupEditable
launch {
val wg = workGroup.get()
if (wg.interested != number) {
WorkGroupRepository.update(wg.copy(interested = number))
}
}
}
setupBoolean(spanWorkGroupResolution) {
launch {
val wg = workGroup.get()
WorkGroupRepository.update(wg.copy(resolution = !wg.resolution))
}
}
launch {
val list = listOf<Track?>(null) + tracks
setupList(spanWorkGroupTrack, list, { it.name }) {
launch x@{
val wg = workGroup.get()
if (wg.track == it) return@x
WorkGroupRepository.update(wg.copy(track = it))
}
}
}
WorkGroupRepository.onUpdate {
if (it != lineId) return@onUpdate
launch {
val wg = WorkGroupRepository.get(it) ?: return@launch
workGroup.set(wg)
searchElement = wg.createSearch()
spanWorkGroupName.text = wg.name
spanWorkGroupLength.text = wg.length.toString()
spanWorkGroupInterested.text = wg.interested.toString()
spanWorkGroupTrack.text = wg.track?.name ?: ""
spanWorkGroupResolution.text = wg.resolution.toString()
}
}
}
}

View file

@ -1,25 +0,0 @@
package de.westermann.kwebview
import kotlin.reflect.KProperty
/**
* Delegate to easily access html attributes.
*
* @author lars
*/
class AttributeDelegate(
private val paramName: String? = null
) {
private fun getParamName(property: KProperty<*>): String = paramName ?: property.name.toLowerCase()
operator fun getValue(container: View, property: KProperty<*>) = container.html.getAttribute(getParamName(property))
operator fun setValue(container: View, property: KProperty<*>, value: String?) {
if (value == null) {
container.html.removeAttribute(getParamName(property))
} else {
container.html.setAttribute(getParamName(property), value.toString())
}
}
}

View file

@ -1,49 +0,0 @@
package de.westermann.kwebview
import de.westermann.kobserve.Property
import de.westermann.kobserve.property.property
import kotlin.reflect.KProperty
/**
* Delegate to easily set css classes as boolean attributes.
*
* @author lars
*/
class ClassDelegate(
className: String? = null
) {
private lateinit var container: View
private lateinit var paramName: String
private lateinit var classProperty: Property<Boolean>
operator fun getValue(container: View, property: KProperty<*>): Property<Boolean> {
if (!this::container.isInitialized) {
this.container = container
}
if (!this::paramName.isInitialized) {
var name = property.name.toDashCase()
if (name.endsWith("-property")) {
name = name.replace("-property", "")
}
paramName = name
}
if (!this::classProperty.isInitialized) {
classProperty = property(container.html.classList.contains(paramName))
classProperty.onChange {
container.html.classList.toggle(paramName, classProperty.value)
}
}
return classProperty
}
init {
if (className != null) {
this.paramName = className
}
}
}

View file

@ -1,144 +0,0 @@
package de.westermann.kwebview
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.set
/**
* Represents the css classes of an html element.
*
* @author lars
*/
class ClassList(
private val list: DOMTokenList
) : Iterable<String> {
private val bound: MutableMap<String, Bound> = mutableMapOf()
/**
* Add css class.
*/
fun add(clazz: String) {
if (clazz in bound) {
val p = bound[clazz] ?: return
if (p.property is Property<Boolean>) {
p.property.value = true
} else {
throw IllegalStateException("The given class is bound and cannot be modified manually!")
}
} else {
list.add(clazz)
}
}
/**
* Add css class.
*/
operator fun plusAssign(clazz: String) = add(clazz)
/**
* Add css class.
*/
fun remove(clazz: String) {
if (clazz in bound) {
val p = bound[clazz] ?: return
if (p.property is Property<Boolean>) {
p.property.value = false
} else {
throw IllegalStateException("The given class is bound and cannot be modified manually!")
}
} else {
list.remove(clazz)
}
}
/**
* Remove css class.
*/
operator fun minusAssign(clazz: String) = remove(clazz)
/**
* Check if css class exits.
*/
operator fun get(clazz: String): Boolean = list.contains(clazz)
/**
* Check if css class exits.
*/
operator fun contains(clazz: String): Boolean = list.contains(clazz)
/**
* Set css class present.
*/
operator fun set(clazz: String, present: Boolean) =
if (present) {
add(clazz)
} else {
remove(clazz)
}
/**
* Toggle css class.
*/
fun toggle(clazz: String, force: Boolean? = null) = set(clazz, force ?: !contains(clazz))
fun bind(clazz: String, property: ReadOnlyProperty<Boolean>) {
if (clazz in bound) {
throw IllegalArgumentException("Class is already bound!")
}
set(clazz, property.value)
bound[clazz] = Bound(property,
property.onChange.reference {
list.toggle(clazz, property.value)
}
)
}
fun property(clazz: String): Property<Boolean> {
if (clazz in bound) {
throw IllegalArgumentException("Class is already bound!")
}
val property = property(get(clazz))
bound[clazz] = Bound(property,
property.onChange.reference {
list.toggle(clazz, property.value)
}
)
return property
}
fun unbind(clazz: String) {
if (clazz !in bound) {
throw IllegalArgumentException("Class is not bound!")
}
bound[clazz]?.reference?.detach()
bound -= clazz
}
override fun iterator(): Iterator<String> {
return toString().split(" +".toRegex()).iterator()
}
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>?
)
}

View file

@ -1,141 +0,0 @@
package de.westermann.kwebview
import de.westermann.kobserve.Property
import de.westermann.kobserve.ReadOnlyProperty
import de.westermann.kobserve.event.EventListener
import org.w3c.dom.DOMStringMap
import org.w3c.dom.get
import org.w3c.dom.set
import kotlin.collections.set
/**
* Represents the css classes of an html element.
*
* @author lars
*/
class DataSet(
private val map: DOMStringMap
) {
private val bound: MutableMap<String, Bound> = mutableMapOf()
/**
* Add css class.
*/
operator fun plusAssign(entry: Pair<String, String>) {
if (entry.first in bound) {
bound[entry.first]?.set(entry.second)
} else {
map[entry.first] = entry.second
}
}
/**
* Remove css class.
*/
operator fun minusAssign(key: String) {
if (key in bound) {
bound[key]?.set(null)
} else {
delete(map, key)
}
}
/**
* Check if css class exits.
*/
operator fun get(key: String): String? = map[key]
/**
* Set css class present.
*/
operator fun set(key: String, value: String?) =
if (value == null) {
this -= key
} else {
this += key to value
}
fun bind(key: String, property: ReadOnlyProperty<String>) {
if (key in bound) {
throw IllegalArgumentException("Class is already bound!")
}
bound[key] = Bound(key, null, property)
}
fun bind(key: String, property: ReadOnlyProperty<String?>) {
if (key in bound) {
throw IllegalArgumentException("Class is already bound!")
}
bound[key] = Bound(key, property, null)
}
fun property(key: String): Property<String?> {
if (key in bound) {
return bound[key]?.propertyNullable as? Property<String?> ?: throw IllegalArgumentException("Class is already bound!")
}
val property = de.westermann.kobserve.property.property(get(key))
bound[key] = Bound(key, property, null)
return property
}
fun unbind(key: String) {
if (key !in bound) {
throw IllegalArgumentException("Class is not bound!")
}
bound[key]?.reference?.detach()
bound -= key
}
private inner class Bound(
val key: String,
val propertyNullable: ReadOnlyProperty<String?>?,
val property: ReadOnlyProperty<String>?
) {
var reference: EventListener<Unit>? = null
fun set(value: String?) {
if (propertyNullable != null && propertyNullable is Property) {
propertyNullable.value = value
} else if (property != null && property is Property && value != null) {
property.value = value
} else {
throw IllegalStateException("The given class is bound and cannot be modified manually!")
}
}
init {
if (propertyNullable != null) {
reference = propertyNullable.onChange.reference {
val value = propertyNullable.value
if (value == null) {
delete(map, key)
} else {
map[key] = value
}
}
val value = propertyNullable.value
if (value == null) {
delete(map, key)
} else {
map[key] = value
}
} else if (property != null) {
reference = property.onChange.reference {
map[key] = property.value
}
map[key] = property.value
}
}
}
}

View file

@ -1,65 +0,0 @@
package de.westermann.kwebview
import kotlin.math.abs
import kotlin.math.min
/**
* @author lars
*/
data class Dimension(
val left: Double,
val top: Double,
val width: Double = 0.0,
val height: Double = 0.0
) {
constructor(position: Point, size: Point = Point.ZERO) : this(position.x, position.y, size.x, size.y)
val position: Point
get() = Point(left, top)
val size: Point
get() = Point(width, height)
val right: Double
get() = left + width
val bottom: Double
get() = top + height
val center: Point
get() = Point(left + width / 2.0, top + height / 2.0)
val edges: Set<Point>
get() = setOf(
Point(left, top),
Point(right, top),
Point(left, bottom),
Point(right, bottom)
)
val normalized: Dimension
get() {
val l = min(left, right)
val t = min(top, bottom)
return Dimension(l, t, abs(width), abs(width))
}
operator fun contains(other: Dimension): Boolean = !(other.left > right ||
other.right < left ||
other.top > bottom ||
other.bottom < top)
operator fun contains(other: Point): Boolean {
val n = normalized
return (n.left <= other.x && (n.left + width) >= other.x)
&& (n.top <= other.y && (n.top + height) >= other.y)
}
operator fun plus(point: Point) = copy(left + point.x, top + point.y)
companion object {
val ZERO = Dimension(0.0, 0.0)
}
}

View file

@ -1,4 +0,0 @@
package de.westermann.kwebview
@DslMarker
annotation class KWebViewDsl

View file

@ -1,50 +0,0 @@
package de.westermann.kwebview
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sqrt
/**
* @author lars
*/
data class Point(
val x: Double,
val y: Double
) {
constructor(x: Int, y: Int) : this(x.toDouble(), y.toDouble())
operator fun plus(number: Int) = Point(x + number, y + number)
operator fun plus(number: Double) = Point(x + number, y + number)
operator fun plus(point: Point) = Point(x + point.x, y + point.y)
operator fun minus(number: Int) = Point(x - number, y - number)
operator fun minus(number: Double) = Point(x - number, y - number)
operator fun minus(point: Point) = Point(x - point.x, y - point.y)
operator fun times(number: Int) = Point(x * number, y * number)
operator fun times(number: Double) = Point(x * number, y * number)
operator fun times(point: Point) = Point(x * point.x, y * point.y)
operator fun div(number: Int) = Point(x / number, y / number)
operator fun div(number: Double) = Point(x / number, y / number)
operator fun div(point: Point) = Point(x / point.x, y / point.y)
operator fun unaryMinus(): Point = Point(-x, -y)
fun min(): Double = min(x, y)
fun max(): Double = max(x, y)
val isZero: Boolean
get() = x == 0.0 && y == 0.0
companion object {
val ZERO = Point(0.0, 0.0)
}
val asPx: String
get() = "${x}px, ${y}px"
fun distance(): Double = sqrt(x * x + y * y)
infix fun distance(other: Point) = (this - other).distance()
}

View file

@ -1,39 +0,0 @@
package de.westermann.robots.website.toolkit.view
import org.w3c.dom.events.EventTarget
import org.w3c.dom.events.UIEvent
/**
* @author lars
*/
open external class TouchEvent(type: String) : UIEvent {
open val changedTouches: TouchList
open val targetTouches: TouchList
open val touches: TouchList
open val ctrlKey: Boolean
open val shiftKey: Boolean
open val altKey: Boolean
open val metaKey: Boolean
fun getModifierState(keyArg: String): Boolean
}
open external class TouchList() {
open val length: Int
open fun item(index: Int): Touch?
}
open external class Touch() {
open val identifier: Int
open val target: EventTarget
open val screenX: Int
open val screenY: Int
open val clientX: Int
open val clientY: Int
open val pageX: Int
open val pageY: Int
}
operator fun TouchList.get(index: Int) = item(index)
fun TouchList.all(): List<Touch> = (0..length).map { item(it) }.filterNotNull()
fun TouchList.find(identifier: Int): Touch? = all().find { it.identifier == identifier }

View file

@ -1,149 +0,0 @@
package de.westermann.kwebview
import de.westermann.kobserve.event.EventHandler
import org.w3c.dom.DragEvent
import org.w3c.dom.HTMLElement
import org.w3c.dom.css.CSSStyleDeclaration
import org.w3c.dom.events.FocusEvent
import org.w3c.dom.events.KeyboardEvent
import org.w3c.dom.events.MouseEvent
import org.w3c.dom.events.WheelEvent
abstract class View(view: HTMLElement = createHtmlView()) {
open val html: HTMLElement = view.also { view ->
this::class.simpleName?.let { name ->
view.classList.add(name.toDashCase())
}
}
val classList = ClassList(view.classList)
val dataset = DataSet(view.dataset)
var id by AttributeDelegate()
val clientLeft: Int
get() = html.clientLeft
val clientTop: Int
get() = html.clientTop
val clientWidth: Int
get() = html.clientWidth
val clientHeight: Int
get() = html.clientHeight
val offsetLeft: Int
get() = html.offsetLeft
val offsetTop: Int
get() = html.offsetTop
val offsetWidth: Int
get() = html.offsetWidth
val offsetHeight: Int
get() = html.offsetHeight
val offsetLeftTotal: Int
get() {
var element: HTMLElement? = html
var offset = 0
while (element != null) {
offset += element.offsetLeft
element = element.offsetParent as? HTMLElement
}
return offset
}
val offsetTopTotal: Int
get() {
var element: HTMLElement? = html
var offset = 0
while (element != null) {
offset += element.offsetTop
element = element.offsetParent as? HTMLElement
}
return offset
}
val dimension: Dimension
get() = html.getBoundingClientRect().toDimension()
val point: Point
get() = dimension.position
var title by AttributeDelegate()
val style = view.style
fun style(block: CSSStyleDeclaration.() -> Unit) {
block(style)
}
fun focus() {
html.focus()
}
fun blur() {
html.blur()
}
fun click() {
html.click()
}
val onClick = EventHandler<MouseEvent>()
val onDblClick = EventHandler<MouseEvent>()
val onContext = EventHandler<MouseEvent>()
val onMouseDown = EventHandler<MouseEvent>()
val onMouseMove = EventHandler<MouseEvent>()
val onMouseUp = EventHandler<MouseEvent>()
val onMouseEnter = EventHandler<MouseEvent>()
val onMouseLeave = EventHandler<MouseEvent>()
val onWheel = EventHandler<WheelEvent>()
val onKeyDown = EventHandler<KeyboardEvent>()
val onKeyPress = EventHandler<KeyboardEvent>()
val onKeyUp = EventHandler<KeyboardEvent>()
val onFocus = EventHandler<FocusEvent>()
val onBlur = EventHandler<FocusEvent>()
val onDragStart = EventHandler<DragEvent>()
val onDrag = EventHandler<DragEvent>()
val onDragEnter = EventHandler<DragEvent>()
val onDragLeave = EventHandler<DragEvent>()
val onDragOver = EventHandler<DragEvent>()
val onDrop = EventHandler<DragEvent>()
val onDragEnd = EventHandler<DragEvent>()
init {
onClick.bind(view, "click")
onDblClick.bind(view, "dblclick")
onContext.bind(view, "contextmenu")
onMouseDown.bind(view, "mousedown")
onMouseMove.bind(view, "mousemove")
onMouseUp.bind(view, "mouseup")
onMouseEnter.bind(view, "mouseenter")
onMouseLeave.bind(view, "mouseleave")
onWheel.bind(view, "wheel")
onKeyDown.bind(view, "keydown")
onKeyPress.bind(view, "keypress")
onKeyUp.bind(view, "keyup")
onFocus.bind(view, "focus")
onBlur.bind(view, "blur")
onDragStart.bind(view, "dragstart")
onDrag.bind(view, "drag")
onDragEnter.bind(view, "dragenter")
onDragLeave.bind(view, "dragleave")
onDragOver.bind(view, "dragover")
onDrop.bind(view, "drop")
onDragEnd.bind(view, "dragend")
}
companion object {
fun wrap(htmlElement: HTMLElement, init: View.() -> Unit = {}) = object : View(htmlElement) {}.also(init)
}
}

View file

@ -1,110 +0,0 @@
package de.westermann.kwebview
import org.w3c.dom.HTMLElement
import kotlin.dom.clear
/**
* @author lars
*/
abstract class ViewCollection<V : View>(view: HTMLElement = createHtmlView()) : View(view), Collection<V> {
protected val children: MutableList<V> = mutableListOf()
protected inline fun <reified T : HTMLElement> wrapContent(classes: String = "", transform: (T) -> V?) {
for (element in html.children.iterator()) {
val html = element as? T ?: continue
if (classes !in html.className) continue
children += transform(html) ?: continue
}
}
protected inline fun wrapContent(classes: String = "", transform: (HTMLElement) -> V?) = wrapContent<HTMLElement>(classes, transform)
fun append(view: V) {
children += view
html.appendChild(view.html)
}
operator fun plusAssign(view: V) = append(view)
fun prepand(view: V) {
children.add(0, view)
html.insertBefore(view.html, html.firstChild)
}
fun remove(view: V) {
if (children.contains(view)) {
children -= view
html.removeChild(view.html)
}
}
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)
append(view)
}
}
fun toBackground(view: V) {
if (view in children && children.indexOf(view) > 0) {
remove(view)
prepand(view)
}
}
fun first(): V = children.first()
fun last(): V = children.last()
operator fun minusAssign(view: V) = remove(view)
override fun isEmpty(): Boolean = children.isEmpty()
fun clear() {
children.clear()
html.clear()
}
override fun iterator(): Iterator<V> = children.iterator()
override val size: Int
get() = children.size
override fun contains(element: V) = children.contains(element)
override fun containsAll(elements: Collection<V>): Boolean = children.containsAll(elements)
operator fun V.unaryPlus() {
append(this)
}
}

View file

@ -1,59 +0,0 @@
package de.westermann.kwebview
import de.westermann.kwebview.components.Label
import org.w3c.dom.HTMLInputElement
import kotlin.math.abs
import kotlin.random.Random
abstract class ViewForLabel(
view: HTMLInputElement = createHtmlView()
) : View(view) {
override val html = super.html as HTMLInputElement
private var label: Label? = null
fun setLabel(label: Label) {
if (this.label != null) {
throw IllegalStateException("Label already set!")
}
this.label = label
val id = id
if (id?.isNotBlank() == true) {
label.html.htmlFor = id
} else {
val newId = this::class.simpleName?.toDashCase() + "-" + generateId()
this.id = newId
label.html.htmlFor = newId
}
}
private var requiredInternal by AttributeDelegate("required")
var required: Boolean
get() = requiredInternal != null
set(value) {
requiredInternal = if (value) "required" else null
}
private var readonlyInternal by AttributeDelegate("readonly")
var readonly: Boolean
get() = readonlyInternal != null
set(value) {
readonlyInternal = if (value) "readonly" else null
}
var tabindex by AttributeDelegate()
fun preventTabStop() {
tabindex = "-1"
}
companion object {
fun generateId(length: Int = 16): String {
var str = ""
while (str.length <= length) {
str += abs(Random.nextLong()).toString(36)
}
return str.take(length)
}
}
}

View file

@ -1,42 +0,0 @@
package de.westermann.kwebview.components
import de.westermann.kwebview.KWebViewDsl
import de.westermann.kwebview.View
import de.westermann.kwebview.ViewCollection
import de.westermann.kwebview.i18n
import org.w3c.dom.DocumentReadyState
import org.w3c.dom.HTMLBodyElement
import org.w3c.dom.LOADING
import kotlin.browser.document
import kotlin.browser.window
object Body : ViewCollection<View>(document.body
?: throw NullPointerException("Access to body before body was loaded")) {
override val html = super.html as HTMLBodyElement
}
@KWebViewDsl
fun init(language: String? = null, block: Body.() -> Unit) {
var done = if (language == null) 1 else 2
if (document.readyState == DocumentReadyState.LOADING) {
window.onload = {
done -= 1
if (done <= 0) {
block(Body)
}
}
} else {
done -= 1
if (done <= 0) {
block(Body)
}
}
if (language != null) {
i18n.load(language) {
done -= 1
if (done <= 0) {
block(Body)
}
}
}
}

View file

@ -1,22 +0,0 @@
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
class BoxView() : ViewCollection<View>(createHtmlView<HTMLDivElement>()) {
override val html = super.html as HTMLDivElement
}
@KWebViewDsl
fun ViewCollection<in BoxView>.boxView(vararg classes: String, init: BoxView.() -> Unit = {}): BoxView {
val view = BoxView()
for (c in classes) {
view.classList += c
}
append(view)
init(view)
return view
}

View file

@ -1,57 +0,0 @@
package de.westermann.kwebview.components
import de.westermann.kobserve.Property
import de.westermann.kobserve.ReadOnlyProperty
import de.westermann.kobserve.property.property
import de.westermann.kwebview.KWebViewDsl
import de.westermann.kwebview.View
import de.westermann.kwebview.ViewCollection
import de.westermann.kwebview.createHtmlView
import org.w3c.dom.HTMLButtonElement
/**
* Represents a html span element.
*
* @author lars
*/
class Button(view: HTMLButtonElement = createHtmlView()) : ViewCollection<View>(view) {
constructor(text: String) : this() {
this.text = text
}
override val html = super.html as HTMLButtonElement
fun bind(property: ReadOnlyProperty<String>) {
textProperty.bind(property)
}
fun unbind() {
textProperty.unbind()
}
var text: String
get() = html.textContent ?: ""
set(value) {
html.textContent = value
textProperty.invalidate()
}
val textProperty: Property<String> = property(this::text)
companion object {
fun wrap(view: HTMLButtonElement) = Button(view)
}
}
@KWebViewDsl
fun ViewCollection<in Button>.button(text: String = "", init: Button.() -> Unit = {}) =
Button(text).also(this::append).also(init)
@KWebViewDsl
fun ViewCollection<in Button>.button(text: ReadOnlyProperty<String>, init: Button.() -> Unit = {}) =
Button(text.value).also(this::append).also { it.bind(text) }.also(init)
@KWebViewDsl
fun ViewCollection<in Button>.button(init: Button.() -> Unit = {}) =
Button().also(this::append).also(init)

View file

@ -1,69 +0,0 @@
package de.westermann.kwebview.components
import de.westermann.kobserve.Property
import de.westermann.kobserve.ReadOnlyProperty
import de.westermann.kobserve.property.property
import de.westermann.kwebview.KWebViewDsl
import de.westermann.kwebview.ViewCollection
import de.westermann.kwebview.ViewForLabel
import org.w3c.dom.events.Event
import org.w3c.dom.events.EventListener
class Checkbox(
initValue: Boolean = false
) : ViewForLabel() {
fun bind(property: ReadOnlyProperty<Boolean>) {
checkedProperty.bind(property)
readonly = true
}
fun bind(property: Property<Boolean>) {
checkedProperty.bindBidirectional(property)
}
fun unbind() {
checkedProperty.unbind()
}
var checked: Boolean
get() = html.checked
set(value) {
html.checked = value
checkedProperty.invalidate()
}
val checkedProperty: Property<Boolean> = property(this::checked)
init {
checked = initValue
html.type = "checkbox"
var lastValue = checked
val changeListener = object : EventListener {
override fun handleEvent(event: Event) {
val value = checked
if (value != checkedProperty.value || value != lastValue) {
lastValue = value
checkedProperty.value = value
}
}
}
html.addEventListener("change", changeListener)
html.addEventListener("keyup", changeListener)
html.addEventListener("keypress", changeListener)
}
}
@KWebViewDsl
fun ViewCollection<in Checkbox>.checkbox(value: Boolean = false, init: Checkbox.() -> Unit = {}) =
Checkbox(value).also(this::append).also(init)
@KWebViewDsl
fun ViewCollection<in Checkbox>.checkbox(value: ReadOnlyProperty<Boolean>, init: Checkbox.() -> Unit = {}) =
Checkbox(value.value).also(this::append).also { it.bind(value) }.also(init)
@KWebViewDsl
fun ViewCollection<in Checkbox>.checkbox(value: Property<Boolean>, init: Checkbox.() -> Unit = {}) =
Checkbox(value.value).also(this::append).also { it.bind(value) }.also(init)

View file

@ -1,105 +0,0 @@
package de.westermann.kwebview.components
import de.westermann.kobserve.Property
import de.westermann.kobserve.ReadOnlyProperty
import de.westermann.kwebview.View
import de.westermann.kwebview.ViewCollection
import de.westermann.kwebview.createHtmlView
class FilterList<T, V : View>(
val property: ReadOnlyProperty<T>,
val filter: Filter<T, V>
) : ViewCollection<V>(createHtmlView()) {
private val content: MutableMap<T, V> = mutableMapOf()
fun update() {
val list = filter.filter(property.value)
var missing = list
for ((element, view) in content.entries) {
if (element in list) {
missing -= element
} else {
if (contains(view)) {
remove(view)
}
if (!filter.useCache) {
content -= element
}
}
}
for (element in missing) {
val view = filter.render(element)
append(view)
if (property is Property<T>) {
view.onClick {
property.value = element
}
}
content[element] = view
}
clear()
for (element in list) {
append(content[element]!!)
}
}
init {
update()
property.onChange {
update()
}
}
}
interface Filter<T, V : View> {
fun filter(partial: T): List<T>
fun render(element: T): V
val useCache: Boolean
}
class StringFilter(
private val dataSet: List<String>
) : Filter<String, TextView> {
override fun filter(partial: String): List<String> {
val lower = partial.trim().toLowerCase()
return dataSet.filter {
it.toLowerCase().contains(lower)
}.sortedBy { it.length + it.toLowerCase().indexOf(partial) * 2 }
}
override fun render(element: String) = TextView(element)
override val useCache = true
}
class StaticStringFilter(
private val dataSet: List<String>
) : Filter<String, TextView> {
override fun filter(partial: String) = dataSet
override fun render(element: String) = TextView(element)
override val useCache = true
}
class DynamicStringFilter(
private val filter: (partial: String) -> List<String>
) : Filter<String, TextView> {
override fun filter(partial: String) = filter.invoke(partial)
override fun render(element: String) = TextView(element)
override val useCache = false
}
fun <T, V : View> ViewCollection<in FilterList<T, V>>.filterList(property: ReadOnlyProperty<T>, filter: Filter<T, V>, init: FilterList<T, V>.() -> Unit = {}) =
FilterList(property, filter).also(this::append).also(init)

View file

@ -1,96 +0,0 @@
package de.westermann.kwebview.components
import de.westermann.kobserve.Property
import de.westermann.kobserve.ReadOnlyProperty
import de.westermann.kobserve.property.property
import de.westermann.kwebview.KWebViewDsl
import de.westermann.kwebview.View
import de.westermann.kwebview.ViewCollection
import de.westermann.kwebview.createHtmlView
import org.w3c.dom.HTMLHeadingElement
class Heading(
val type: Type,
value: String = ""
) : View(createHtmlView<HTMLHeadingElement>(type.tagName)) {
override val html = super.html as HTMLHeadingElement
fun bind(property: ReadOnlyProperty<String>) {
textProperty.bind(property)
}
fun unbind() {
textProperty.unbind()
}
var text: String
get() = html.textContent ?: ""
set(value) {
html.textContent = value
textProperty.invalidate()
}
val textProperty: Property<String> = property(this::text)
init {
text = value
}
enum class Type(val tagName: String) {
H1("h1"),
H2("h2"),
H3("h3"),
H4("h4"),
H5("h5"),
H6("h6")
}
}
@KWebViewDsl
fun ViewCollection<in Heading>.h1(text: String = "", init: Heading.() -> Unit = {}) =
Heading(Heading.Type.H1, text).also(this::append).also(init)
@KWebViewDsl
fun ViewCollection<in Heading>.h1(text: ReadOnlyProperty<String>, init: Heading.() -> Unit = {}) =
Heading(Heading.Type.H1, text.value).also(this::append).also { it.bind(text) }.also(init)
@KWebViewDsl
fun ViewCollection<in Heading>.h2(text: String = "", init: Heading.() -> Unit = {}) =
Heading(Heading.Type.H2, text).also(this::append).also(init)
@KWebViewDsl
fun ViewCollection<in Heading>.h2(text: ReadOnlyProperty<String>, init: Heading.() -> Unit = {}) =
Heading(Heading.Type.H2, text.value).also(this::append).also { it.bind(text) }.also(init)
@KWebViewDsl
fun ViewCollection<in Heading>.h3(text: String = "", init: Heading.() -> Unit = {}) =
Heading(Heading.Type.H3, text).also(this::append).also(init)
@KWebViewDsl
fun ViewCollection<in Heading>.h3(text: ReadOnlyProperty<String>, init: Heading.() -> Unit = {}) =
Heading(Heading.Type.H3, text.value).also(this::append).also { it.bind(text) }.also(init)
@KWebViewDsl
fun ViewCollection<in Heading>.h4(text: String = "", init: Heading.() -> Unit = {}) =
Heading(Heading.Type.H4, text).also(this::append).also(init)
@KWebViewDsl
fun ViewCollection<in Heading>.h4(text: ReadOnlyProperty<String>, init: Heading.() -> Unit = {}) =
Heading(Heading.Type.H4, text.value).also(this::append).also { it.bind(text) }.also(init)
@KWebViewDsl
fun ViewCollection<in Heading>.h5(text: String = "", init: Heading.() -> Unit = {}) =
Heading(Heading.Type.H5, text).also(this::append).also(init)
@KWebViewDsl
fun ViewCollection<in Heading>.h5(text: ReadOnlyProperty<String>, init: Heading.() -> Unit = {}) =
Heading(Heading.Type.H5, text.value).also(this::append).also { it.bind(text) }.also(init)
@KWebViewDsl
fun ViewCollection<in Heading>.h6(text: String = "", init: Heading.() -> Unit = {}) =
Heading(Heading.Type.H6, text).also(this::append).also(init)
@KWebViewDsl
fun ViewCollection<in Heading>.h6(text: ReadOnlyProperty<String>, init: Heading.() -> Unit = {}) =
Heading(Heading.Type.H6, text.value).also(this::append).also { it.bind(text) }.also(init)

View file

@ -1,15 +0,0 @@
package de.westermann.kwebview.components
import org.w3c.dom.Element
/**
* Base interface for all icons.
*
* @author lars
*/
interface Icon {
/**
* Dom element that represents an icon.
*/
val element: Element
}

View file

@ -1,53 +0,0 @@
package de.westermann.kwebview.components
import de.westermann.kobserve.Property
import de.westermann.kobserve.ReadOnlyProperty
import de.westermann.kobserve.property.property
import de.westermann.kwebview.KWebViewDsl
import de.westermann.kwebview.View
import de.westermann.kwebview.ViewCollection
import de.westermann.kwebview.createHtmlView
import org.w3c.dom.HTMLSpanElement
import kotlin.dom.clear
/**
* Represents all kinds of icon views.
*
* @author lars
*/
class IconView(icon: Icon?) : View(createHtmlView<HTMLSpanElement>()) {
override val html = super.html as HTMLSpanElement
fun bind(property: ReadOnlyProperty<Icon?>) {
iconProperty.bind(property)
}
fun unbind() {
iconProperty.unbind()
}
var icon: Icon? = null
set(value) {
field = value
html.clear()
value?.let {
html.appendChild(it.element)
}
iconProperty.invalidate()
}
val iconProperty: Property<Icon?> = property(this::icon)
init {
this.icon = icon
}
}
@KWebViewDsl
fun ViewCollection<in IconView>.iconView(icon: Icon? = null, init: IconView.() -> Unit = {}) =
IconView(icon).also(this::append).also(init)
@KWebViewDsl
fun ViewCollection<in IconView>.iconView(icon: ReadOnlyProperty<Icon?>, init: IconView.() -> Unit = {}) =
IconView(icon.value).also(this::append).also { it.bind(icon) }.also(init)

View file

@ -1,46 +0,0 @@
package de.westermann.kwebview.components
import de.westermann.kobserve.Property
import de.westermann.kobserve.ReadOnlyProperty
import de.westermann.kobserve.property.property
import de.westermann.kwebview.*
import org.w3c.dom.HTMLImageElement
class ImageView(
src: String
) : View(createHtmlView<HTMLImageElement>("img")) {
override val html = super.html as HTMLImageElement
fun bind(property: ReadOnlyProperty<String>) {
sourceProperty.bind(property)
}
fun unbind() {
sourceProperty.unbind()
}
var source: String
get() = html.src
set(value) {
html.src = value
sourceProperty.invalidate()
}
val sourceProperty: Property<String> = property(this::source)
var alt by AttributeDelegate("alt")
init {
source = src
}
}
@KWebViewDsl
fun ViewCollection<in ImageView>.imageView(src: String = "", init: ImageView.() -> Unit = {}) =
ImageView(src).also(this::append).also(init)
@KWebViewDsl
fun ViewCollection<in ImageView>.imageView(src: ReadOnlyProperty<String>, init: ImageView.() -> Unit = {}) =
ImageView(src.value).also(this::append).also { it.bind(src) }.also(init)

View file

@ -1,176 +0,0 @@
package de.westermann.kwebview.components
import de.westermann.kobserve.Property
import de.westermann.kobserve.ReadOnlyProperty
import de.westermann.kobserve.ValidationProperty
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
import org.w3c.dom.events.EventListener
import org.w3c.dom.events.KeyboardEvent
class InputView(
type: InputType,
initValue: String = "",
view: HTMLInputElement = createHtmlView()
) : ViewForLabel(view) {
fun bind(property: ReadOnlyProperty<String>) {
valueProperty.bind(property)
readonly = true
}
fun bind(property: Property<String>) {
valueProperty.bindBidirectional(property)
}
fun bind(property: ValidationProperty<String>) {
valueProperty.bindBidirectional(property)
invalidProperty.bind(!property.validProperty)
}
fun unbind() {
valueProperty.unbind()
if (invalidProperty.isBound) {
invalidProperty.unbind()
}
}
var value: String
get() = html.value
set(value) {
html.value = value
valueProperty.invalidate()
}
val valueProperty: Property<String> = property(this::value)
var placeholder: String
get() = html.placeholder
set(value) {
html.placeholder = value
placeholderProperty.invalidate()
}
val placeholderProperty: Property<String> = property(this::placeholder)
val invalidProperty by ClassDelegate("invalid")
var invalid by invalidProperty
private var typeInternal by AttributeDelegate("type")
var type: InputType?
get() = typeInternal?.let(InputType.Companion::find)
set(value) {
typeInternal = value?.html
}
private var minInternal by AttributeDelegate("min")
var min: Double?
get() = minInternal?.toDoubleOrNull()
set(value) {
minInternal = value?.toString()
}
private var maxInternal by AttributeDelegate("max")
var max: Double?
get() = maxInternal?.toDoubleOrNull()
set(value) {
maxInternal = value?.toString()
}
private var stepInternal by AttributeDelegate("step")
var step: Double?
get() = stepInternal?.toDoubleOrNull()
set(value) {
stepInternal = value?.toString()
}
init {
value = initValue
this.type = type
var lastValue = value
val changeListener = object : EventListener {
override fun handleEvent(event: Event) {
val value = value
if (value != valueProperty.value || value != lastValue) {
lastValue = value
valueProperty.value = value
}
(event as? KeyboardEvent)?.let { e ->
when (e.keyCode) {
13, 27 -> blur()
}
}
}
}
html.addEventListener("change", changeListener)
html.addEventListener("keyup", changeListener)
html.addEventListener("keypress", changeListener)
}
companion object {
fun wrap(type: InputType, view: HTMLInputElement) = InputView(type, view.value, view)
}
}
enum class InputType(val html: String) {
TEXT("text"),
NUMBER("number"),
PASSWORD("password"),
SEARCH("search");
companion object {
fun find(html: String): InputType? = values().find { it.html == html }
}
}
@KWebViewDsl
fun ViewCollection<in InputView>.inputView(text: String = "", init: InputView.() -> Unit = {}) =
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)
@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)
@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)
@KWebViewDsl
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)
@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)
@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)

View file

@ -1,51 +0,0 @@
package de.westermann.kwebview.components
import de.westermann.kobserve.Property
import de.westermann.kobserve.ReadOnlyProperty
import de.westermann.kobserve.property.property
import de.westermann.kwebview.*
import org.w3c.dom.HTMLLabelElement
/**
* Represents a html label element.
*
* @author lars
*/
class Label(
inputElement: ViewForLabel,
value: String = ""
) : View(createHtmlView<HTMLLabelElement>()) {
override val html = super.html as HTMLLabelElement
fun bind(property: ReadOnlyProperty<String>) {
textProperty.bind(property)
}
fun unbind() {
textProperty.unbind()
}
var text: String
get() = html.textContent ?: ""
set(value) {
html.textContent = value
textProperty.invalidate()
}
val textProperty: Property<String> = property(this::text)
init {
text = value
inputElement.setLabel(this)
}
}
@KWebViewDsl
fun ViewCollection<in Label>.label(inputElement: ViewForLabel, text: String = "", init: Label.() -> Unit = {}) =
Label(inputElement, text).also(this::append).also(init)
@KWebViewDsl
fun ViewCollection<in Label>.label(inputElement: ViewForLabel, text: ReadOnlyProperty<String>, init: Label.() -> Unit = {}) =
Label(inputElement, text.value).also(this::append).also { it.bind(text) }.also(init)

View file

@ -1,48 +0,0 @@
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.HTMLAnchorElement
/**
* Represents a html span element.
*
* @author lars
*/
class Link(view: HTMLAnchorElement = createHtmlView()) : View(view) {
constructor(target: String, view: HTMLAnchorElement = createHtmlView()): this(view) {
this.target = target
}
override val html = super.html as HTMLAnchorElement
var text: String?
get() = html.textContent
set(value) {
html.textContent = value
}
var target: String
get() = html.href
set(value) {
html.href = value
}
companion object {
fun wrap(view: HTMLAnchorElement) = Link(view)
}
}
@KWebViewDsl
fun ViewCollection<in Link>.link(target: String, text: String? = null, init: Link.() -> Unit = {}): Link {
val view = Link(target)
if (text != null) {
view.text = text
}
append(view)
init(view)
return view
}

View file

@ -1,30 +0,0 @@
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

@ -1,948 +0,0 @@
package de.westermann.kwebview.components
import org.w3c.dom.Element
import kotlin.browser.document
/**
* List of material design icons.
*/
enum class MaterialIcon(private val ligature: String) : Icon {
ROTATION_3D("3d_rotation"),
AC_UNIT("ac_unit"),
ACCESS_ALARM("access_alarm "),
ACCESS_ALARMS("access_alarms"),
ACCESS_TIME("access_time"),
ACCESSIBILITY("accessibility"),
ACCESSIBLE("accessible"),
ACCOUNT_BALANCE("account_balance"),
ACCOUNT_BALANCE_WALLET("account_balance_wallet"),
ACCOUNT_BOX("account_box"),
ACCOUNT_CIRCLE("account_circle"),
ADB("adb"),
ADD("add"),
ADD_A_PHOTO("add_a_photo"),
ADD_ALARM("add_alarm"),
ADD_ALERT("add_alert"),
ADD_BOX("add_box"),
ADD_CIRCLE("add_circle"),
ADD_CIRCLE_OUTLINE("add_circle_outline"),
ADD_LOCATION("add_location "),
ADD_SHOPPING_CART("add_shopping_cart"),
ADD_TO_PHOTOS("add_to_photos"),
ADD_TO_QUEUE("add_to_queue "),
ADJUST("adjust"),
AIRLINE_SEAT_FLAT("airline_seat_flat"),
AIRLINE_SEAT_FLAT_ANGLED("airline_seat_flat_angled"),
AIRLINE_SEAT_INDIVIDUAL_SUITE("airline_seat_individual_suite"),
AIRLINE_SEAT_LEGROOM_EXTRA("airline_seat_legroom_extra"),
AIRLINE_SEAT_LEGROOM_NORMAL("airline_seat_legroom_normal"),
AIRLINE_SEAT_LEGROOM_REDUCED("airline_seat_legroom_reduced"),
AIRLINE_SEAT_RECLINE_EXTRA("airline_seat_recline_extra"),
AIRLINE_SEAT_RECLINE_NORMAL("airline_seat_recline_normal"),
AIRPLANEMODE_ACTIVE("airplanemode_active"),
AIRPLANEMODE_INACTIVE("airplanemode_inactive"),
AIRPLAY("airplay"),
AIRPORT_SHUTTLE("airport_shuttle"),
ALARM("alarm"),
ALARM_ADD("alarm_add"),
ALARM_OFF("alarm_off"),
ALARM_ON("alarm_on"),
ALBUM("album"),
ALL_INCLUSIVE("all_inclusive"),
ALL_OUT("all_out"),
ANDROID("android"),
ANNOUNCEMENT("announcement "),
APPS("apps"),
ARCHIVE("archive"),
ARROW_BACK("arrow_back"),
ARROW_DOWNWARD("arrow_downward"),
ARROW_DROP_DOWN("arrow_drop_down"),
ARROW_DROP_DOWN_CIRCLE("arrow_drop_down_circle"),
ARROW_DROP_UP("arrow_drop_up"),
ARROW_FORWARD("arrow_forward"),
ARROW_UPWARD("arrow_upward "),
ART_TRACK("art_track"),
ASPECT_RATIO("aspect_ratio "),
ASSESSMENT("assessment"),
ASSIGNMENT("assignment"),
ASSIGNMENT_IND("assignment_ind"),
ASSIGNMENT_LATE("assignment_late"),
ASSIGNMENT_RETURN("assignment_return"),
ASSIGNMENT_RETURNED("assignment_returned"),
ASSIGNMENT_TURNED_IN("assignment_turned_in"),
ASSISTANT("assistant"),
ASSISTANT_PHOTO("assistant_photo"),
ATTACH_FILE("attach_file"),
ATTACH_MONEY("attach_money "),
ATTACHMENT("attachment"),
AUDIOTRACK("audiotrack"),
AUTORENEW("autorenew"),
AV_TIMER("av_timer"),
BACKSPACE("backspace"),
BACKUP("backup"),
BATTERY_ALERT("battery_alert"),
BATTERY_CHARGING_FULL("battery_charging_full"),
BATTERY_FULL("battery_full "),
BATTERY_STD("battery_std"),
BATTERY_UNKNOWN("battery_unknown"),
BEACH_ACCESS("beach_access "),
BEENHERE("beenhere"),
BLOCK("block"),
BLUETOOTH("bluetooth"),
BLUETOOTH_AUDIO("bluetooth_audio"),
BLUETOOTH_CONNECTED("bluetooth_connected"),
BLUETOOTH_DISABLED("bluetooth_disabled"),
BLUETOOTH_SEARCHING("bluetooth_searching"),
BLUR_CIRCULAR("blur_circular"),
BLUR_LINEAR("blur_linear"),
BLUR_OFF("blur_off"),
BLUR_ON("blur_on"),
BOOK("book"),
BOOKMARK("bookmark"),
BOOKMARK_BORDER("bookmark_border"),
BORDER_ALL("border_all"),
BORDER_BOTTOM("border_bottom"),
BORDER_CLEAR("border_clear "),
BORDER_COLOR("border_color "),
BORDER_HORIZONTAL("border_horizontal"),
BORDER_INNER("border_inner "),
BORDER_LEFT("border_left"),
BORDER_OUTER("border_outer "),
BORDER_RIGHT("border_right "),
BORDER_STYLE("border_style "),
BORDER_TOP("border_top"),
BORDER_VERTICAL("border_vertical"),
BRANDING_WATERMARK("branding_watermark"),
BRIGHTNESS_1("brightness_1 "),
BRIGHTNESS_2("brightness_2 "),
BRIGHTNESS_3("brightness_3 "),
BRIGHTNESS_4("brightness_4 "),
BRIGHTNESS_5("brightness_5 "),
BRIGHTNESS_6("brightness_6 "),
BRIGHTNESS_7("brightness_7 "),
BRIGHTNESS_AUTO("brightness_auto"),
BRIGHTNESS_HIGH("brightness_high"),
BRIGHTNESS_LOW("brightness_low"),
BRIGHTNESS_MEDIUM("brightness_medium"),
BROKEN_IMAGE("broken_image "),
BRUSH("brush"),
BUBBLE_CHART("bubble_chart "),
BUG_REPORT("bug_report"),
BUILD("build"),
BURST_MODE("burst_mode"),
BUSINESS("business"),
BUSINESS_CENTER("business_center"),
CACHED("cached"),
CAKE("cake"),
CALL("call"),
CALL_END("call_end"),
CALL_MADE("call_made"),
CALL_MERGE("call_merge"),
CALL_MISSED("call_missed"),
CALL_MISSED_OUTGOING("call_missed_outgoing"),
CALL_RECEIVED("call_received"),
CALL_SPLIT("call_split"),
CALL_TO_ACTION("call_to_action"),
CAMERA("camera"),
CAMERA_ALT("camera_alt"),
CAMERA_ENHANCE("camera_enhance"),
CAMERA_FRONT("camera_front "),
CAMERA_REAR("camera_rear"),
CAMERA_ROLL("camera_roll"),
CANCEL("cancel"),
CARD_GIFTCARD("card_giftcard"),
CARD_MEMBERSHIP("card_membership"),
CARD_TRAVEL("card_travel"),
CASINO("casino"),
CAST("cast"),
CAST_CONNECTED("cast_connected"),
CENTER_FOCUS_STRONG("center_focus_strong"),
CENTER_FOCUS_WEAK("center_focus_weak"),
CHANGE_HISTORY("change_history"),
CHAT("chat"),
CHAT_BUBBLE("chat_bubble"),
CHAT_BUBBLE_OUTLINE("chat_bubble_outline"),
CHECK("checkUpdates"),
CHECK_BOX("check_box"),
CHECK_BOX_OUTLINE_BLANK("check_box_outline_blank"),
CHECK_CIRCLE("check_circle "),
CHEVRON_LEFT("chevron_left "),
CHEVRON_RIGHT("chevron_right"),
CHILD_CARE("child_care"),
CHILD_FRIENDLY("child_friendly"),
CHROME_RR_MODE("chrome_rr_mode"),
CLASS("class"),
CLEAR("clear"),
CLEAR_ALL("clear_all"),
CLOSE("close"),
CLOSED_CAPTION("closed_caption"),
CLOUD("cloud"),
CLOUD_CIRCLE("cloud_circle "),
CLOUD_DONE("cloud_done"),
CLOUD_DOWNLOAD("cloud_download"),
CLOUD_OFF("cloud_off"),
CLOUD_QUEUE("cloud_queue"),
CLOUD_UPLOAD("cloud_upload "),
CODE("code"),
COLLECTIONS("collections"),
COLLECTIONS_BOOKMARK("collections_bookmark"),
COLOR_LENS("color_lens"),
COLORIZE("colorize"),
COMMENT("comment"),
COMPARE("compare"),
COMPARE_ARROWS("compare_arrows"),
COMPUTER("computer"),
CONFIRMATION_NUMBER("confirmation_number"),
CONTACT_MAIL("contact_mail "),
CONTACT_PHONE("contact_phone"),
CONTACTS("contacts"),
CONTENT_COPY("content_copy "),
CONTENT_CUT("content_cut"),
CONTENT_PASTE("content_paste"),
CONTROL_POINT("control_point"),
CONTROL_POINT_DUPLICATE("control_point_duplicate"),
COPYRIGHT("copyright"),
CREATE("onCreate"),
CREATE_NEW_FOLDER("create_new_folder"),
CREDIT_CARD("credit_card"),
CROP("crop"),
CROP_16_9("crop_16_9"),
CROP_3_2("crop_3_2"),
CROP_5_4("crop_5_4"),
CROP_7_5("crop_7_5"),
CROP_DIN("crop_din"),
CROP_FREE("crop_free"),
CROP_LANDSCAPE("crop_landscape"),
CROP_ORIGINAL("crop_original"),
CROP_PORTRAIT("crop_portrait"),
CROP_ROTATE("crop_rotate"),
CROP_SQUARE("crop_square"),
DASHBOARD("dashboard"),
DATA_USAGE("data_usage"),
DATE_RANGE("date_range"),
DEHAZE("dehaze"),
DELETE("delete"),
DELETE_FOREVER("delete_forever"),
DELETE_SWEEP("delete_sweep "),
DESCRIPTION("description"),
DESKTOP_MAC("desktop_mac"),
DESKTOP_WINDOWS("desktop_windows"),
DETAILS("details"),
DEVELOPER_BOARD("developer_board"),
DEVELOPER_MODE("developer_mode"),
DEVICE_HUB("device_hub"),
DEVICES("devices"),
DEVICES_OTHER("devices_other"),
DIALER_SIP("dialer_sip"),
DIALPAD("dialpad"),
DIRECTIONS("directions"),
DIRECTIONS_BIKE("directions_bike"),
DIRECTIONS_BOAT("directions_boat"),
DIRECTIONS_BUS("directions_bus"),
DIRECTIONS_CAR("directions_car"),
DIRECTIONS_RAILWAY("directions_railway"),
DIRECTIONS_RUN("directions_run"),
DIRECTIONS_SUBWAY("directions_subway"),
DIRECTIONS_TRANSIT("directions_transit"),
DIRECTIONS_WALK("directions_walk"),
DISC_FULL("disc_full"),
DNS("dns"),
DO_NOT_DISTURB("do_not_disturb"),
DO_NOT_DISTURB_ALT("do_not_disturb_alt"),
DO_NOT_DISTURB_OFF("do_not_disturb_off"),
DO_NOT_DISTURB_ON("do_not_disturb_on"),
DOCK("dock"),
DOMAIN("domain"),
DONE("done"),
DONE_ALL("done_all"),
DONUT_LARGE("donut_large"),
DONUT_SMALL("donut_small"),
DRAFTS("drafts"),
DRAG_HANDLE("drag_handle"),
DRIVE_ETA("drive_eta"),
DVR("dvr"),
EDIT("edit"),
EDIT_LOCATION("edit_location"),
EJECT("eject"),
EMAIL("email"),
ENHANCED_ENCRYPTION("enhanced_encryption"),
EQUALIZER("equalizer"),
ERROR("error"),
ERROR_OUTLINE("error_outline"),
EURO_SYMBOL("euro_symbol"),
EV_STATION("ev_station"),
EVENT("model"),
EVENT_AVAILABLE("event_available"),
EVENT_BUSY("event_busy"),
EVENT_NOTE("event_note"),
EVENT_SEAT("event_seat"),
EXIT_TO_APP("exit_to_app"),
EXPAND_LESS("expand_less"),
EXPAND_MORE("expand_more"),
EXPLICIT("explicit"),
EXPLORE("explore"),
EXPOSURE("exposure"),
EXPOSURE_NEG_1("exposure_neg_1"),
EXPOSURE_NEG_2("exposure_neg_2"),
EXPOSURE_PLUS_1("exposure_plus_1"),
EXPOSURE_PLUS_2("exposure_plus_2"),
EXPOSURE_ZERO("exposure_zero"),
EXTENSION("extension"),
FACE("face"),
FAST_FORWARD("fast_forward "),
FAST_REWIND("fast_rewind"),
FAVORITE("favorite"),
FAVORITE_BORDER("favorite_border"),
FEATURED_PLAY_LIST("featured_play_list"),
FEATURED_VIDEO("featured_video"),
FACK("fack"),
FIBER_DVR("fiber_dvr"),
FIBER_MANUAL_RECORD("fiber_manual_record"),
FIBER_NEW("fiber_new"),
FIBER_PIN("fiber_pin"),
FIBER_SMART_RECORD("fiber_smart_record"),
FILE_DOWNLOAD("file_download"),
FILE_UPLOAD("file_upload"),
FILTER("filter"),
FILTER_1("filter_1"),
FILTER_2("filter_2"),
FILTER_3("filter_3"),
FILTER_4("filter_4"),
FILTER_5("filter_5"),
FILTER_6("filter_6"),
FILTER_7("filter_7"),
FILTER_8("filter_8"),
FILTER_9("filter_9"),
FILTER_9_PLUS("filter_9_plus"),
FILTER_B_AND_W("filter_b_and_w"),
FILTER_CENTER_FOCUS("filter_center_focus"),
FILTER_DRAMA("filter_drama "),
FILTER_FRAMES("filter_frames"),
FILTER_HDR("filter_hdr"),
FILTER_LIST("filter_list"),
FILTER_NONE("filter_none"),
FILTER_TILT_SHIFT("filter_tilt_shift"),
FILTER_VINTAGE("filter_vintage"),
FIND_IN_PAGE("find_in_page "),
FIND_REPLACE("find_replace "),
FINGERPRINT("fingerprint"),
FIRST_PAGE("first_page"),
FITNESS_CENTER("fitness_center"),
FLAG("flag"),
FLARE("flare"),
FLASH_AUTO("flash_auto"),
FLASH_OFF("flash_off"),
FLASH_ON("flash_on"),
FLIGHT("flight"),
FLIGHT_LAND("flight_land"),
FLIGHT_TAKEOFF("flight_takeoff"),
FLIP("flip"),
FLIP_TO_BACK("flip_to_back "),
FLIP_TO_FRONT("flip_to_front"),
FOLDER("folder"),
FOLDER_OPEN("folder_open"),
FOLDER_SHARED("folder_shared"),
FOLDER_SPECIAL("folder_special"),
FONT_DOWNLOAD("font_download"),
FORMAT_ALIGN_CENTER("format_align_center"),
FORMAT_ALIGN_JUSTIFY("format_align_justify"),
FORMAT_ALIGN_LEFT("format_align_left"),
FORMAT_ALIGN_RIGHT("format_align_right"),
FORMAT_BOLD("format_bold"),
FORMAT_CLEAR("format_clear "),
FORMAT_COLOR_FILL("format_color_fill"),
FORMAT_COLOR_RESET("format_color_reset"),
FORMAT_COLOR_TEXT("format_color_text"),
FORMAT_INDENT_DECREASE("format_indent_decrease"),
FORMAT_INDENT_INCREASE("format_indent_increase"),
FORMAT_ITALIC("format_italic"),
FORMAT_LINE_SPACING("format_line_spacing"),
FORMAT_LIST_BULLETED("format_list_bulleted"),
FORMAT_LIST_NUMBERED("format_list_numbered"),
FORMAT_PAINT("format_paint "),
FORMAT_QUOTE("format_quote "),
FORMAT_SHAPES("format_shapes"),
FORMAT_SIZE("format_size"),
FORMAT_STRIKETHROUGH("format_strikethrough"),
FORMAT_TEXTDIRECTION_L_TO_R("format_textdirection_l_to_r"),
FORMAT_TEXTDIRECTION_R_TO_L("format_textdirection_r_to_l"),
FORMAT_UNDERLINED("format_underlined"),
FORUM("forum"),
FORWARD("forward"),
FORWARD_10("forward_10"),
FORWARD_30("forward_30"),
FORWARD_5("forward_5"),
FREE_BREAKFAST("free_breakfast"),
FULLSCREEN("fullscreen"),
FULLSCREEN_EXIT("fullscreen_exit"),
FUNCTIONS("functions"),
G_TRANSLATE("g_translate"),
GAMEPAD("gamepad"),
GAMES("games"),
GAVEL("gavel"),
GESTURE("gesture"),
GET_APP("get_app"),
GIF("gif"),
GOLF_COURSE("golf_course"),
GPS_FIXED("gps_fixed"),
GPS_NOT_FIXED("gps_not_fixed"),
GPS_OFF("gps_off"),
GRADE("grade"),
GRADIENT("gradient"),
GRAIN("grain"),
GRAPHIC_EQ("graphic_eq"),
GRID_OFF("grid_off"),
GRID_ON("grid_on"),
GROUP("group"),
GROUP_ADD("group_add"),
GROUP_WORK("group_work"),
HD("hd"),
HDR_OFF("hdr_off"),
HDR_ON("hdr_on"),
HDR_STRONG("hdr_strong"),
HDR_WEAK("hdr_weak"),
HEADSET("headset"),
HEADSET_MIC("headset_mic"),
HEALING("healing"),
HEARING("hearing"),
HELP("help"),
HELP_OUTLINE("help_outline "),
HIGH_QUALITY("high_quality "),
HIGHLIGHT("highlight"),
HIGHLIGHT_OFF("highlight_off"),
HISTORY("history"),
HOME("home"),
HOT_TUB("hot_tub"),
HOTEL("hotel"),
HOURGLASS_EMPTY("hourglass_empty"),
HOURGLASS_FULL("hourglass_full"),
HTTP("http"),
HTTPS("https"),
IMAGE("image"),
IMAGE_ASPECT_RATIO("image_aspect_ratio"),
IMPORT_CONTACTS("import_contacts"),
IMPORT_EXPORT("import_export"),
IMPORTANT_DEVICES("important_devices"),
INBOX("inbox"),
INDETERMINATE_CHECK_BOX("indeterminate_check_box"),
INFO("info"),
INFO_OUTLINE("info_outline "),
INPUT("input"),
INSERT_CHART("insert_chart "),
INSERT_COMMENT("insert_comment"),
INSERT_DRIVE_FILE("insert_drive_file"),
INSERT_EMOTICON("insert_emoticon"),
INSERT_INVITATION("insert_invitation"),
INSERT_LINK("insert_link"),
INSERT_PHOTO("insert_photo "),
INVERT_COLORS("invert_colors"),
INVERT_COLORS_OFF("invert_colors_off"),
ISO("iso"),
KEYBOARD("keyboard"),
KEYBOARD_ARROW_DOWN("keyboard_arrow_down"),
KEYBOARD_ARROW_LEFT("keyboard_arrow_left"),
KEYBOARD_ARROW_RIGHT("keyboard_arrow_right"),
KEYBOARD_ARROW_UP("keyboard_arrow_up"),
KEYBOARD_BACKSPACE("keyboard_backspace"),
KEYBOARD_CAPSLOCK("keyboard_capslock"),
KEYBOARD_HIDE("keyboard_hide"),
KEYBOARD_RETURN("keyboard_return"),
KEYBOARD_TAB("keyboard_tab "),
KEYBOARD_VOICE("keyboard_voice"),
KITCHEN("kitchen"),
LABEL("label"),
LABEL_OUTLINE("label_outline"),
LANDSCAPE("landscape"),
LANGUAGE("language"),
LAPTOP("laptop"),
LAPTOP_CHROMEBOOK("laptop_chromebook"),
LAPTOP_MAC("laptop_mac"),
LAPTOP_WINDOWS("laptop_windows"),
LAST_PAGE("last_page"),
LAUNCH("launch"),
LAYERS("layers"),
LAYERS_CLEAR("layers_clear "),
LEAK_ADD("leak_add"),
LEAK_REMOVE("leak_remove"),
LENS("lens"),
LIBRARY_ADD("library_add"),
LIBRARY_BOOKS("library_books"),
LIBRARY_MUSIC("library_music"),
LIGHTBULB_OUTLINE("lightbulb_outline"),
LINE_STYLE("line_style"),
LINE_WEIGHT("line_weight"),
LINEAR_SCALE("linear_scale "),
LINK("link"),
LINKED_CAMERA("linked_camera"),
LIST("list"),
LIVE_HELP("live_help"),
LIVE_TV("live_tv"),
LOCAL_ACTIVITY("local_activity"),
LOCAL_AIRPORT("local_airport"),
LOCAL_ATM("local_atm"),
LOCAL_BAR("local_bar"),
LOCAL_CAFE("local_cafe"),
LOCAL_CAR_WASH("local_car_wash"),
LOCAL_CONVENIENCE_STORE("local_convenience_store"),
LOCAL_DINING("local_dining "),
LOCAL_DRINK("local_drink"),
LOCAL_FLORIST("local_florist"),
LOCAL_GAS_STATION("local_gas_station"),
LOCAL_GROCERY_STORE("local_grocery_store"),
LOCAL_HOSPITAL("local_hospital"),
LOCAL_HOTEL("local_hotel"),
LOCAL_LAUNDRY_SERVICE("local_laundry_service"),
LOCAL_LIBRARY("local_library"),
LOCAL_MALL("local_mall"),
LOCAL_MOVIES("local_movies "),
LOCAL_OFFER("local_offer"),
LOCAL_PARKING("local_parking"),
LOCAL_PHARMACY("local_pharmacy"),
LOCAL_PHONE("local_phone"),
LOCAL_PIZZA("local_pizza"),
LOCAL_PLAY("local_play"),
LOCAL_POST_OFFICE("local_post_office"),
LOCAL_PRINTSHOP("local_printshop"),
LOCAL_SEE("local_see"),
LOCAL_SHIPPING("local_shipping"),
LOCAL_TAXI("local_taxi"),
LOCATION_CITY("location_city"),
LOCATION_DISABLED("location_disabled"),
LOCATION_OFF("location_off "),
LOCATION_ON("location_on"),
LOCATION_SEARCHING("location_searching"),
LOCK("lock"),
LOCK_OPEN("lock_open"),
LOCK_OUTLINE("lock_outline "),
LOOKS("looks"),
LOOKS_3("looks_3"),
LOOKS_4("looks_4"),
LOOKS_5("looks_5"),
LOOKS_6("looks_6"),
LOOKS_ONE("looks_one"),
LOOKS_TWO("looks_two"),
LOOP("loop"),
LOUPE("loupe"),
LOW_PRIORITY("low_priority "),
LOYALTY("loyalty"),
MAIL("mail"),
MAIL_OUTLINE("mail_outline "),
MAP("map"),
MARKUNREAD("markunread"),
MARKUNREAD_MAILBOX("markunread_mailbox"),
MEMORY("memory"),
MENU("menu"),
MERGE_TYPE("merge_type"),
MESSAGE("message"),
MIC("mic"),
MIC_NONE("mic_none"),
MIC_OFF("mic_off"),
MMS("mms"),
MODE_COMMENT("mode_comment "),
MODE_EDIT("mode_edit"),
MONETIZATION_ON("monetization_on"),
MONEY_OFF("money_off"),
MONOCHROME_PHOTOS("monochrome_photos"),
MOOD("mood"),
MOOD_BAD("mood_bad"),
MORE("more"),
MORE_HORIZ("more_horiz"),
MORE_VERT("more_vert"),
MOTORCYCLE("motorcycle"),
MOUSE("mouse"),
MOVE_TO_INBOX("move_to_inbox"),
MOVIE("movie"),
MOVIE_CREATION("movie_creation"),
MOVIE_FILTER("movie_filter "),
MULTILINE_CHART("multiline_chart"),
MUSIC_NOTE("music_note"),
MUSIC_VIDEO("music_video"),
MY_LOCATION("my_location"),
NATURE("nature"),
NATURE_PEOPLE("nature_people"),
NAVIGATE_BEFORE("navigate_before"),
NAVIGATE_NEXT("navigate_next"),
NAVIGATION("navigationDrawer"),
NEAR_ME("near_me"),
NETWORK_CELL("network_cell "),
NETWORK_CHECK("network_check"),
NETWORK_LOCKED("network_locked"),
NETWORK_WIFI("network_wifi "),
NEW_RELEASES("new_releases "),
NEXT_WEEK("next_week"),
NFC("nfc"),
NO_ENCRYPTION("no_encryption"),
NO_SIM("no_sim"),
NOT_INTERESTED("not_interested"),
NOTE("note"),
NOTE_ADD("note_add"),
NOTIFICATIONS("notifications"),
NOTIFICATIONS_ACTIVE("notifications_active"),
NOTIFICATIONS_NONE("notifications_none"),
NOTIFICATIONS_OFF("notifications_off"),
NOTIFICATIONS_PAUSED("notifications_paused"),
OFFLINE_PIN("offline_pin"),
ONDEMAND_VIDEO("ondemand_video"),
OPACITY("opacity"),
OPEN_IN_BROWSER("open_in_browser"),
OPEN_IN_NEW("open_in_new"),
OPEN_WITH("open_with"),
PAGES("pages"),
PAGEVIEW("pageview"),
PALETTE("palette"),
PAN_TOOL("pan_tool"),
PANORAMA("panorama"),
PANORAMA_FISH_EYE("panorama_fish_eye"),
PANORAMA_HORIZONTAL("panorama_horizontal"),
PANORAMA_VERTICAL("panorama_vertical"),
PANORAMA_WIDE_ANGLE("panorama_wide_angle"),
PARTY_MODE("party_mode"),
PAUSE("pause"),
PAUSE_CIRCLE_FILLED("pause_circle_filled"),
PAUSE_CIRCLE_OUTLINE("pause_circle_outline"),
PAYMENT("payment"),
PEOPLE("people"),
PEOPLE_OUTLINE("people_outline"),
PERM_CAMERA_MIC("perm_camera_mic"),
PERM_CONTACT_CALENDAR("perm_contact_calendar"),
PERM_DATA_SETTING("perm_data_setting"),
PERM_DEVICE_INFORMATION("perm_device_information"),
PERM_IDENTITY("perm_identity"),
PERM_MEDIA("perm_media"),
PERM_PHONE_MSG("perm_phone_msg"),
PERM_SCAN_WIFI("perm_scan_wifi"),
PERSON("person"),
PERSON_ADD("person_add"),
PERSON_OUTLINE("person_outline"),
PERSON_PIN("person_pin"),
PERSON_PIN_CIRCLE("person_pin_circle"),
PERSONAL_VIDEO("personal_video"),
PETS("pets"),
PHONE("phone"),
PHONE_ANDROID("phone_android"),
PHONE_BLUETOOTH_SPEAKER("phone_bluetooth_speaker"),
PHONE_FORWARDED("phone_forwarded"),
PHONE_IN_TALK("phone_in_talk"),
PHONE_IPHONE("phone_iphone "),
PHONE_LOCKED("phone_locked "),
PHONE_MISSED("phone_missed "),
PHONE_PAUSED("phone_paused "),
PHONELINK("phonelink"),
PHONELINK_ERASE("phonelink_erase"),
PHONELINK_LOCK("phonelink_lock"),
PHONELINK_OFF("phonelink_off"),
PHONELINK_RING("phonelink_ring"),
PHONELINK_SETUP("phonelink_setup"),
PHOTO("photo"),
PHOTO_ALBUM("photo_album"),
PHOTO_CAMERA("photo_camera "),
PHOTO_FILTER("photo_filter "),
PHOTO_LIBRARY("photo_library"),
PHOTO_SIZE_SELECT_ACTUAL("photo_size_select_actual"),
PHOTO_SIZE_SELECT_LARGE("photo_size_select_large"),
PHOTO_SIZE_SELECT_SMALL("photo_size_select_small"),
PICTURE_AS_PDF("picture_as_pdf"),
PICTURE_IN_PICTURE("picture_in_picture"),
PICTURE_IN_PICTURE_ALT("picture_in_picture_alt"),
PIE_CHART("pie_chart"),
PIE_CHART_OUTLINED("pie_chart_outlined"),
PIN_DROP("pin_drop"),
PLACE("place"),
PLAY_ARROW("play_arrow"),
PLAY_CIRCLE_FILLED("play_circle_filled"),
PLAY_CIRCLE_OUTLINE("play_circle_outline"),
PLAY_FOR_WORK("play_for_work"),
PLAYLIST_ADD("playlist_add "),
PLAYLIST_ADD_CHECK("playlist_add_check"),
PLAYLIST_PLAY("playlist_play"),
PLUS_ONE("plus_one"),
POLL("poll"),
POLYMER("polymer"),
POOL("pool"),
PORTABLE_WIFI_OFF("portable_wifi_off"),
PORTRAIT("portrait"),
POWER("power"),
POWER_INPUT("power_input"),
POWER_SETTINGS_NEW("power_settings_new"),
PREGNANT_WOMAN("pregnant_woman"),
PRESENT_TO_ALL("present_to_all"),
PRINT("print"),
PRIORITY_HIGH("priority_high"),
PUBLIC("public"),
PUBLISH("publish"),
QUERY_BUILDER("query_builder"),
QUESTION_ANSWER("question_answer"),
QUEUE("queue"),
QUEUE_MUSIC("queue_music"),
QUEUE_PLAY_NEXT("queue_play_next"),
RADIO("radio"),
RADIO_BUTTON_CHECKED("radio_button_checked"),
RADIO_BUTTON_UNCHECKED("radio_button_unchecked"),
RATE_REVIEW("rate_review"),
RECEIPT("receipt"),
RECENT_ACTORS("recent_actors"),
RECORD_VOICE_OVER("record_voice_over"),
RM("rm"),
REDO("redo"),
REFRESH("refresh"),
REMOVE("remove"),
REMOVE_CIRCLE("remove_circle"),
REMOVE_CIRCLE_OUTLINE("remove_circle_outline"),
REMOVE_FROM_QUEUE("remove_from_queue"),
REMOVE_RED_EYE("remove_red_eye"),
REMOVE_SHOPPING_CART("remove_shopping_cart"),
REORDER("reorder"),
REPEAT("repeat"),
REPEAT_ONE("repeat_one"),
REPLAY("replay"),
REPLAY_10("replay_10"),
REPLAY_30("replay_30"),
REPLAY_5("replay_5"),
REPLY("reply"),
REPLY_ALL("reply_all"),
REPORT("report"),
REPORT_PROBLEM("report_problem"),
RESTAURANT("restaurant"),
RESTAURANT_MENU("restaurant_menu"),
RESTORE("restore"),
RESTORE_PAGE("restore_page "),
RING_VOLUME("ring_volume"),
ROOM("room"),
ROOM_SERVICE("room_service "),
ROTATE_90_DEGREES_CCW("rotate_90_degrees_ccw"),
ROTATE_LEFT("rotate_left"),
ROTATE_RIGHT("rotate_right "),
ROUNDED_CORNER("rounded_corner"),
ROUTER("router"),
ROWING("rowing"),
RSS_FEED("rss_feed"),
RV_HOOKUP("rv_hookup"),
SATELLITE("satellite"),
SAVE("save"),
SCANNER("scanner"),
SCHEDULE("schedule"),
SCHOOL("school"),
SCREEN_LOCK_LANDSCAPE("screen_lock_landscape"),
SCREEN_LOCK_PORTRAIT("screen_lock_portrait"),
SCREEN_LOCK_ROTATION("screen_lock_rotation"),
SCREEN_ROTATION("screen_rotation"),
SCREEN_SHARE("screen_share "),
SD_CARD("sd_card"),
SD_STORAGE("sd_storage"),
SEARCH("search"),
SECURITY("security"),
SELECT_ALL("select_all"),
SEND("send"),
SENTIMENT_DISSATISFIED("sentiment_dissatisfied"),
SENTIMENT_NEUTRAL("sentiment_neutral"),
SENTIMENT_SATISFIED("sentiment_satisfied"),
SENTIMENT_VERY_DISSATISFIED("sentiment_very_dissatisfied"),
SENTIMENT_VERY_SATISFIED("sentiment_very_satisfied"),
SETTINGS("settings"),
SETTINGS_APPLICATIONS("settings_applications"),
SETTINGS_BACKUP_RESTORE("settings_backup_restore"),
SETTINGS_BLUETOOTH("settings_bluetooth"),
SETTINGS_BRIGHTNESS("settings_brightness"),
SETTINGS_CELL("settings_cell"),
SETTINGS_ETHERNET("settings_ethernet"),
SETTINGS_INPUT_ANTENNA("settings_input_antenna"),
SETTINGS_INPUT_COMPONENT("settings_input_component"),
SETTINGS_INPUT_COMPOSITE("settings_input_composite"),
SETTINGS_INPUT_HDMI("settings_input_hdmi"),
SETTINGS_INPUT_SVIDEO("settings_input_svideo"),
SETTINGS_OVERSCAN("settings_overscan"),
SETTINGS_PHONE("settings_phone"),
SETTINGS_POWER("settings_power"),
SETTINGS_REMOTE("settings_remote"),
SETTINGS_SYSTEM_DAYDREAM("settings_system_daydream"),
SETTINGS_VOICE("settings_voice"),
SHARE("share"),
SHOP("shop"),
SHOP_TWO("shop_two"),
SHOPPING_BASKET("shopping_basket"),
SHOPPING_CART("shopping_cart"),
SHORT_TEXT("short_text"),
SHOW_CHART("show_chart"),
SHUFFLE("shuffle"),
SIGNAL_CELLULAR_4_BAR("signal_cellular_4_bar"),
SIGNAL_CELLULAR_CONNECTED_NO_INTERNET_4_BAR("signal_cellular_connected_no_internet_4_bar"),
SIGNAL_CELLULAR_NO_SIM("signal_cellular_no_sim"),
SIGNAL_CELLULAR_NULL("signal_cellular_null"),
SIGNAL_CELLULAR_OFF("signal_cellular_off"),
SIGNAL_WIFI_4_BAR("signal_wifi_4_bar"),
SIGNAL_WIFI_4_BAR_LOCK("signal_wifi_4_bar_lock"),
SIGNAL_WIFI_OFF("signal_wifi_off"),
SIM_CARD("sim_card"),
SIM_CARD_ALERT("sim_card_alert"),
SKIP_NEXT("skip_next"),
SKIP_PREVIOUS("skip_previous"),
SLIDESHOW("slideshow"),
SLOW_MOTION_VIDEO("slow_motion_video"),
SMARTPHONE("smartphone"),
SMOKE_FREE("smoke_free"),
SMOKING_ROOMS("smoking_rooms"),
SMS("sms"),
SMS_FAILED("sms_failed"),
SNOOZE("snooze"),
SORT("sort"),
SORT_BY_ALPHA("sort_by_alpha"),
SPA("spa"),
SPACE_BAR("space_bar"),
SPEAKER("speaker"),
SPEAKER_GROUP("speaker_group"),
SPEAKER_NOTES("speaker_notes"),
SPEAKER_NOTES_OFF("speaker_notes_off"),
SPEAKER_PHONE("speaker_phone"),
SPELLCHECK("spellcheck"),
STAR("star"),
STAR_BORDER("star_border"),
STAR_HALF("star_half"),
STARS("stars"),
STAY_CURRENT_LANDSCAPE("stay_current_landscape"),
STAY_CURRENT_PORTRAIT("stay_current_portrait"),
STAY_PRIMARY_LANDSCAPE("stay_primary_landscape"),
STAY_PRIMARY_PORTRAIT("stay_primary_portrait"),
STOP("stop"),
STOP_SCREEN_SHARE("stop_screen_share"),
STORAGE("storage"),
STORE("store"),
STORE_MALL_DIRECTORY("store_mall_directory"),
STRAIGHTEN("straighten"),
STREETVIEW("streetview"),
STRIKETHROUGH_S("strikethrough_s"),
STYLE("style"),
SUBDIRECTORY_ARROW_LEFT("subdirectory_arrow_left"),
SUBDIRECTORY_ARROW_RIGHT("subdirectory_arrow_right"),
SUBJECT("subject"),
SUBSCRIPTIONS("subscriptions"),
SUBTITLES("subtitles"),
SUBWAY("subway"),
SUPERVISOR_ACCOUNT("supervisor_account"),
SURROUND_SOUND("surround_sound"),
SWAP_CALLS("swap_calls"),
SWAP_HORIZ("swap_horiz"),
SWAP_VERT("swap_vert"),
SWAP_VERTICAL_CIRCLE("swap_vertical_circle"),
SWITCH_CAMERA("switch_camera"),
SWITCH_VIDEO("switch_video "),
SYNC("sync"),
SYNC_DISABLED("sync_disabled"),
SYNC_PROBLEM("sync_problem "),
SYSTEM_UPDATE("system_update"),
SYSTEM_UPDATE_ALT("system_update_alt"),
TAB("tab"),
TAB_UNSELECTED("tab_unselected"),
TABLET("tablet"),
TABLET_ANDROID("tablet_android"),
TABLET_MAC("tablet_mac"),
TAG_FACES("tag_faces"),
TAP_AND_PLAY("tap_and_play "),
TERRAIN("terrain"),
TEXT_FIELDS("text_fields"),
TEXT_FORMAT("text_format"),
TEXTSMS("textsms"),
TEXTURE("texture"),
THEATERS("theaters"),
THUMB_DOWN("thumb_down"),
THUMB_UP("thumb_up"),
THUMBS_UP_DOWN("thumbs_up_down"),
TIME_TO_LEAVE("time_to_leave"),
TIMELAPSE("timelapse"),
TIMELINE("timeline"),
TIMER("timer"),
TIMER_10("timer_10"),
TIMER_3("timer_3"),
TIMER_OFF("timer_off"),
TITLE("title"),
TOC("toc"),
TODAY("today"),
TOLL("toll"),
TONALITY("tonality"),
TOUCH_APP("touch_app"),
TOYS("toys"),
TRACK_CHANGES("track_changes"),
TRAFFIC("traffic"),
TRAIN("train"),
TRAM("tram"),
TRANSFER_WITHIN_A_STATION("transfer_within_a_station"),
TRANSFORM("transform"),
TRANSLATE("translate"),
TRENDING_DOWN("trending_down"),
TRENDING_FLAT("trending_flat"),
TRENDING_UP("trending_up"),
TUNE("tune"),
TURNED_IN("turned_in"),
TURNED_IN_NOT("turned_in_not"),
TV("tv"),
UNARCHIVE("unarchive"),
UNDO("undo"),
UNFOLD_LESS("unfold_less"),
UNFOLD_MORE("unfold_more"),
UPDATE("update"),
USB("usb"),
VERIFIED_USER("verified_user"),
VERTICAL_ALIGN_BOTTOM("vertical_align_bottom"),
VERTICAL_ALIGN_CENTER("vertical_align_center"),
VERTICAL_ALIGN_TOP("vertical_align_top"),
VIBRATION("vibration"),
VIDEO_CALL("video_call"),
VIDEO_LABEL("video_label"),
VIDEO_LIBRARY("video_library"),
VIDEOCAM("videocam"),
VIDEOCAM_OFF("videocam_off "),
VIDEOGAME_ASSET("videogame_asset"),
VIEW_AGENDA("view_agenda"),
VIEW_ARRAY("view_array"),
VIEW_CAROUSEL("view_carousel"),
VIEW_COLUMN("view_column"),
VIEW_COMFY("view_comfy"),
VIEW_COMPACT("view_compact "),
VIEW_DAY("view_day"),
VIEW_HEADLINE("view_headline"),
VIEW_LIST("view_list"),
VIEW_MODULE("view_module"),
VIEW_QUILT("view_quilt"),
VIEW_STREAM("view_stream"),
VIEW_WEEK("view_week"),
VIGNETTE("vignette"),
VISIBILITY("visibility"),
VISIBILITY_OFF("visibility_off"),
VOICE_CHAT("voice_chat"),
VOICEMAIL("voicemail"),
VOLUME_DOWN("volume_down"),
VOLUME_MUTE("volume_mute"),
VOLUME_OFF("volume_off"),
VOLUME_UP("volume_up"),
VPN_KEY("vpn_key"),
VPN_LOCK("vpn_lock"),
WALLPAPER("wallpaper"),
WARNING("warning"),
WATCH("watch"),
WATCH_LATER("watch_later"),
WB_AUTO("wb_auto"),
WB_CLOUDY("wb_cloudy"),
WB_INCANDESCENT("wb_incandescent"),
WB_IRIDESCENT("wb_iridescent"),
WB_SUNNY("wb_sunny"),
WC("wc"),
WEB("web"),
WEB_ASSET("web_asset"),
WEEKEND("weekend"),
WHATSHOT("whatshot"),
WIDGETS("widgets"),
WIFI("wifi"),
WIFI_LOCK("wifi_lock"),
WIFI_TETHERING("wifi_tethering"),
WORK("work"),
WRAP_TEXT("wrap_text"),
YOUTUBE_SEARCHED_FOR("youtube_searched_for"),
ZOOM_IN("zoom_in"),
ZOOM_OUT("zoom_out"),
ZOOM_OUT_MAP("zoom_out_map ");
override val element: Element
get() = document.createElement("i").apply {
classList.add("material-icons")
textContent = ligature
}
}

View file

@ -1,32 +0,0 @@
package de.westermann.kwebview.components
import de.westermann.kwebview.View
import de.westermann.kwebview.createHtmlView
import org.w3c.dom.HTMLOptionElement
class OptionView<T>(val value: T) : View(createHtmlView<HTMLOptionElement>()) {
override val html = super.html as HTMLOptionElement
var htmlValue: String
get() = html.value
set(value) {
html.value = value
}
var text: String
get() = html.text
set(value) {
html.text = value
}
val index: Int
get() = html.index
var selected: Boolean
get() = html.selected
set(value) {
html.selected = value
}
}

View file

@ -1,112 +0,0 @@
package de.westermann.kwebview.components
import de.westermann.kobserve.Property
import de.westermann.kobserve.ReadOnlyProperty
import de.westermann.kobserve.property.property
import de.westermann.kwebview.AttributeDelegate
import de.westermann.kwebview.KWebViewDsl
import de.westermann.kwebview.ViewCollection
import de.westermann.kwebview.createHtmlView
import org.w3c.dom.HTMLSelectElement
import org.w3c.dom.events.Event
import org.w3c.dom.events.EventListener
class SelectView<T : Any>(
dataSet: List<T>,
private val initValue: T,
val transform: (T) -> String = { it.toString() }
) : ViewCollection<OptionView<T>>(createHtmlView<HTMLSelectElement>()) {
override val html = super.html as HTMLSelectElement
fun bind(property: ReadOnlyProperty<T>) {
valueProperty.bind(property)
readonly = true
}
fun bind(property: Property<T>) {
valueProperty.bindBidirectional(property)
}
fun unbind() {
valueProperty.unbind()
}
var dataSet: List<T> = emptyList()
set(value) {
field = value
clear()
value.forEachIndexed { index, v ->
+OptionView(v).also { option ->
option.text = transform(v)
option.htmlValue = index.toString()
}
}
}
var index: Int
get() = html.selectedIndex
set(value) {
val invalidate = html.selectedIndex != value
html.selectedIndex = value
if (invalidate) {
valueProperty.invalidate()
}
}
var value: T
get() = dataSet.getOrNull(index) ?: initValue
set(value) {
index = dataSet.indexOf(value)
}
val valueProperty = property(this::value)
private var readonlyInternal by AttributeDelegate("readonly")
var readonly: Boolean
get() = readonlyInternal != null
set(value) {
readonlyInternal = if (value) "readonly" else null
}
var tabindex by AttributeDelegate()
fun preventTabStop() {
tabindex = "-1"
}
init {
this.dataSet = dataSet
this.value = initValue
html.addEventListener("change", object : EventListener {
override fun handleEvent(event: Event) {
valueProperty.invalidate()
}
})
}
}
@KWebViewDsl
fun <T : Any> ViewCollection<in SelectView<T>>.selectView(dataSet: List<T>, initValue: T, transform: (T) -> String = { it.toString() }, init: SelectView<T>.() -> Unit = {}) =
SelectView(dataSet, initValue, transform).also(this::append).also(init)
@KWebViewDsl
fun <T : Any> ViewCollection<in SelectView<T>>.selectView(dataSet: List<T>, property: ReadOnlyProperty<T>, transform: (T) -> String = { it.toString() }, init: SelectView<T>.() -> Unit = {}) =
SelectView(dataSet, property.value, transform).apply { bind(property) }.also(this::append).also(init)
@KWebViewDsl
fun <T : Any> ViewCollection<in SelectView<T>>.selectView(dataSet: List<T>, property: Property<T>, transform: (T) -> String = { it.toString() }, init: SelectView<T>.() -> Unit = {}) =
SelectView(dataSet, property.value, transform).apply { bind(property) }.also(this::append).also(init)
@KWebViewDsl
inline fun <reified T : Enum<T>> ViewCollection<in SelectView<T>>.selectView(initValue: T, noinline transform: (T) -> String = { it.toString() }, init: SelectView<T>.() -> Unit = {}) =
SelectView(enumValues<T>().toList(), initValue, transform).also(this::append).also(init)
@KWebViewDsl
inline fun <reified T : Enum<T>> ViewCollection<in SelectView<T>>.selectView(property: ReadOnlyProperty<T>, noinline transform: (T) -> String = { it.toString() }, init: SelectView<T>.() -> Unit = {}) =
SelectView(enumValues<T>().toList(), property.value, transform).apply { bind(property) }.also(this::append).also(init)
@KWebViewDsl
inline fun <reified T : Enum<T>> ViewCollection<in SelectView<T>>.selectView(property: Property<T>, noinline transform: (T) -> String = { it.toString() }, init: SelectView<T>.() -> Unit = {}) =
SelectView(enumValues<T>().toList(), property.value, transform).apply { bind(property) }.also(this::append).also(init)

View file

@ -1,22 +0,0 @@
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.HTMLTableElement
class Table() : ViewCollection<View>(createHtmlView<HTMLTableElement>()) {
override val html = super.html as HTMLTableElement
}
@KWebViewDsl
fun ViewCollection<in Table>.table(vararg classes: String, init: Table.() -> Unit = {}): Table {
val view = Table()
for (c in classes) {
view.classList += c
}
append(view)
init(view)
return view
}

View file

@ -1,19 +0,0 @@
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.HTMLTableCaptionElement
class TableCaption() : ViewCollection<View>(createHtmlView<HTMLTableCaptionElement>("caption")) {
override val html = super.html as HTMLTableCaptionElement
}
@KWebViewDsl
fun ViewCollection<in TableCaption>.caption(init: TableCaption.() -> Unit = {}): TableCaption {
val view = TableCaption()
append(view)
init(view)
return view
}

View file

@ -1,41 +0,0 @@
package de.westermann.kwebview.components
import de.westermann.kwebview.*
import org.w3c.dom.HTMLTableCellElement
class TableCell(val isHead: Boolean) :
ViewCollection<View>(createHtmlView<HTMLTableCellElement>(if (isHead) "th" else "td")) {
override val html = super.html as HTMLTableCellElement
private var colSpanInternal by AttributeDelegate("colspan")
var colSpan: Int?
get() = colSpanInternal?.toIntOrNull()
set(value) {
colSpanInternal = value?.toString()
}
private var rowSpanInternal by AttributeDelegate("rowspan")
var rowSpan: Int?
get() = rowSpanInternal?.toIntOrNull()
set(value) {
rowSpanInternal = value?.toString()
}
}
@KWebViewDsl
fun ViewCollection<in TableCell>.cell(colSpan: Int? = null, init: TableCell.() -> Unit = {}): TableCell {
val view = TableCell(false)
view.colSpan = colSpan
append(view)
init(view)
return view
}
@KWebViewDsl
fun ViewCollection<in TableCell>.head(colSpan: Int? = null, init: TableCell.() -> Unit = {}): TableCell {
val view = TableCell(true)
view.colSpan = colSpan
append(view)
init(view)
return view
}

View file

@ -1,21 +0,0 @@
package de.westermann.kwebview.components
import de.westermann.kwebview.KWebViewDsl
import de.westermann.kwebview.ViewCollection
import de.westermann.kwebview.createHtmlView
import org.w3c.dom.HTMLTableRowElement
class TableRow() : ViewCollection<TableCell>(createHtmlView<HTMLTableRowElement>("tr")) {
override val html = super.html as HTMLTableRowElement
}
@KWebViewDsl
fun ViewCollection<in TableRow>.row(vararg classes: String, init: TableRow.() -> Unit = {}): TableRow {
val view = TableRow()
for (c in classes) {
view.classList += c
}
append(view)
init(view)
return view
}

View file

@ -1,40 +0,0 @@
package de.westermann.kwebview.components
import de.westermann.kwebview.KWebViewDsl
import de.westermann.kwebview.ViewCollection
import de.westermann.kwebview.createHtmlView
import org.w3c.dom.HTMLTableSectionElement
class TableSection(val type: Type) : ViewCollection<TableRow>(createHtmlView<HTMLTableSectionElement>(type.tagName)) {
override val html = super.html as HTMLTableSectionElement
enum class Type(val tagName: String) {
THEAD("thead"),
TBODY("tbody"),
TFOOT("tfoot")
}
}
@KWebViewDsl
fun ViewCollection<in TableSection>.thead(init: TableSection.() -> Unit = {}): TableSection {
val view = TableSection(TableSection.Type.THEAD)
append(view)
init(view)
return view
}
@KWebViewDsl
fun ViewCollection<in TableSection>.tbody(init: TableSection.() -> Unit = {}): TableSection {
val view = TableSection(TableSection.Type.TBODY)
append(view)
init(view)
return view
}
@KWebViewDsl
fun ViewCollection<in TableSection>.tfoot(init: TableSection.() -> Unit = {}): TableSection {
val view = TableSection(TableSection.Type.TFOOT)
append(view)
init(view)
return view
}

View file

@ -1,66 +0,0 @@
package de.westermann.kwebview.components
import de.westermann.kobserve.Property
import de.westermann.kobserve.ReadOnlyProperty
import de.westermann.kobserve.property.property
import de.westermann.kwebview.*
import org.w3c.dom.HTMLSpanElement
/**
* Represents a html span element.
*
* @author lars
*/
class TextView(
value: String = "",
view: HTMLSpanElement = createHtmlView()
) : View(view) {
override val html = super.html as HTMLSpanElement
fun bind(property: ReadOnlyProperty<String>) {
textProperty.bind(property)
}
fun unbind() {
textProperty.unbind()
}
var text: String
get() = html.textContent ?: ""
set(value) {
html.textContent = value
textProperty.invalidate()
}
val textProperty: Property<String> = property(this::text)
var contentEditable: Boolean
get() = html.isContentEditable
set(value) {
html.contentEditable = value.toString()
}
private var internalTabIndex by AttributeDelegate("tabIndex")
var tabIndex: Int?
get() = internalTabIndex?.toIntOrNull()
set(value) {
internalTabIndex = value?.toString()
}
init {
text = value
}
companion object {
fun wrap(view: HTMLSpanElement) = TextView(view.textContent ?: "", view)
}
}
@KWebViewDsl
fun ViewCollection<in TextView>.textView(text: String = "", init: TextView.() -> Unit = {}) =
TextView(text).also(this::append).also(init)
@KWebViewDsl
fun ViewCollection<in TextView>.textView(text: ReadOnlyProperty<String>, init: TextView.() -> Unit = {}) =
TextView(text.value).also(this::append).also { it.bind(text) }.also(init)

View file

@ -1,213 +0,0 @@
package de.westermann.kwebview
import de.westermann.kobserve.event.EventHandler
import de.westermann.robots.website.toolkit.view.TouchEvent
import de.westermann.robots.website.toolkit.view.get
import org.w3c.dom.*
import org.w3c.dom.events.Event
import org.w3c.dom.events.EventListener
import org.w3c.dom.events.MouseEvent
import org.w3c.xhr.FormData
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) {
tagName = tag
} else {
tagName = V::class.js.name.toLowerCase().replace("html([a-z]*)element".toRegex(), "$1")
if (tagName.isBlank()) tagName = "div"
if (tagName == "anchor") tagName = "a"
}
return document.createElement(tagName) as V
}
fun String.toDashCase() = replace("([a-z])([A-Z])".toRegex(), "$1-$2").toLowerCase()
inline fun <reified T> EventHandler<T>.bind(element: HTMLElement, event: String) {
val listener = object : EventListener {
override fun handleEvent(event: Event) {
this@bind.emit(event as T)
}
}
var isAttached = false
val updateState = {
if (isEmpty() && isAttached) {
element.removeEventListener(event, listener)
isAttached = false
} else if (!isEmpty() && !isAttached) {
element.addEventListener(event, listener)
isAttached = true
}
}
onAttach = updateState
onDetach = updateState
updateState()
}
fun MouseEvent.toPoint(): Point = Point(clientX, clientY)
fun TouchEvent.toPoint(): Point? = touches[0]?.let { Point(it.clientX, it.clientY) }
fun DOMRect.toDimension(): Dimension = Dimension(x, y, width, height)
fun Number.format(digits: Int): String = this.asDynamic().toFixed(digits)
external fun delete(p: dynamic): Boolean = definedExternally
fun delete(thing: dynamic, key: String) {
delete(thing[key])
}
/**
* Apply current dom changes and recalculate all sizes. Executes the given block afterwards.
*
* @param timeout Optionally set a timeout for this call. Defaults to 1.
* @param block Callback
*/
fun async(timeout: Int = 1, block: () -> Unit): Int {
if (timeout < 1) throw IllegalArgumentException("Timeout must be greater than 0!")
return window.setTimeout(block, timeout)
}
fun interval(delay: Int, block: () -> Unit): Int {
if (delay < 1) throw IllegalArgumentException("Delay must be greater than 0!")
return window.setInterval(block, delay)
}
fun clearTimeout(id: Int) {
window.clearTimeout(id)
}
fun clearInterval(id: Int) {
window.clearInterval(id)
}
fun get(
url: String,
data: Map<String, String> = emptyMap(),
onError: (Int) -> Unit = {},
onSuccess: (String) -> Unit = {}
) {
val xhttp = XMLHttpRequest()
xhttp.onreadystatechange = {
if (xhttp.readyState == 4.toShort()) {
if (xhttp.status == 200.toShort() || xhttp.status == 304.toShort()) {
onSuccess(xhttp.responseText)
} else {
onError(xhttp.status.toInt())
}
}
}
xhttp.open("GET", url, true)
if (data.isNotEmpty()) {
xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
val formData = FormData()
for ((key, value) in data) {
formData.append(key, value)
}
xhttp.send(formData)
} else {
xhttp.send()
}
}
fun postForm(
url: String,
data: Map<String, String> = emptyMap(),
onError: (Int) -> Unit = {},
onSuccess: (String) -> Unit = {}
) {
val xhttp = XMLHttpRequest()
xhttp.onreadystatechange = {
if (xhttp.readyState == 4.toShort()) {
console.log(xhttp.status)
if (xhttp.status == 200.toShort() || xhttp.status == 304.toShort()) {
onSuccess(xhttp.responseText)
} else {
onError(xhttp.status.toInt())
}
}
}
xhttp.open("POST", url, true)
if (data.isNotEmpty()) {
xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
val formData = FormData()
for ((key, value) in data) {
formData.append(key, value)
}
xhttp.send(formData)
} else {
xhttp.send()
}
}
fun postJson(
url: String,
data: dynamic,
onError: (Int) -> Unit = {},
onSuccess: (String) -> Unit = {}
) {
val xhttp = XMLHttpRequest()
xhttp.onreadystatechange = {
if (xhttp.readyState == 4.toShort()) {
console.log(xhttp.status)
if (xhttp.status == 200.toShort() || xhttp.status == 304.toShort()) {
onSuccess(xhttp.responseText)
} else {
onError(xhttp.status.toInt())
}
}
}
xhttp.open("POST", url, true)
if (data.isNotEmpty()) {
xhttp.setRequestHeader("Content-type", "application/json");
xhttp.send(data)
} else {
xhttp.send()
}
}
fun jsonObject(block: (dynamic) -> Unit): dynamic {
val json = js("{}")
block(json)
return json
}
fun jsonArray(block: (dynamic) -> Unit): dynamic {
val json = js("[]")
block(json)
return json
}

Some files were not shown because too many files have changed in this diff Show more