Compare commits
No commits in common. "dummy" and "master" have entirely different histories.
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
.gradle/
|
||||||
|
.idea/
|
||||||
|
build/
|
||||||
|
web/
|
||||||
|
.sessions/
|
||||||
|
data/
|
||||||
|
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
*.db
|
|
@ -1,7 +0,0 @@
|
||||||
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/
|
|
5
LICENSE
Normal file
5
LICENSE
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
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.
|
Binary file not shown.
16
README.md
Normal file
16
README.md
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# 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
|
||||||
|
```
|
223
build.gradle
Normal file
223
build.gradle
Normal file
|
@ -0,0 +1,223 @@
|
||||||
|
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")})"
|
||||||
|
}
|
||||||
|
}
|
19
concept.md
Normal file
19
concept.md
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
* 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
|
1
gradle.properties
Normal file
1
gradle.properties
Normal file
|
@ -0,0 +1 @@
|
||||||
|
kotlin.code.style=official
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
#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
Normal file
172
gradlew
vendored
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
#!/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
Normal file
84
gradlew.bat
vendored
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
@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
77
index.html
|
@ -1,77 +0,0 @@
|
||||||
<!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>
|
|
26
portal.toml
Normal file
26
portal.toml
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
[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 = ""
|
13
settings.gradle
Normal file
13
settings.gradle
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
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'
|
67
src/commonMain/kotlin/de/kif/common/CachedRepository.kt
Normal file
67
src/commonMain/kotlin/de/kif/common/CachedRepository.kt
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
src/commonMain/kotlin/de/kif/common/Constants.kt
Normal file
3
src/commonMain/kotlin/de/kif/common/Constants.kt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
package de.kif.common
|
||||||
|
|
||||||
|
const val CALENDAR_GRID_WIDTH = 15
|
210
src/commonMain/kotlin/de/kif/common/ConstraintChecking.kt
Normal file
210
src/commonMain/kotlin/de/kif/common/ConstraintChecking.kt
Normal file
|
@ -0,0 +1,210 @@
|
||||||
|
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() })
|
||||||
|
}
|
59
src/commonMain/kotlin/de/kif/common/DateFormat.kt
Normal file
59
src/commonMain/kotlin/de/kif/common/DateFormat.kt
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
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')
|
47
src/commonMain/kotlin/de/kif/common/Message.kt
Normal file
47
src/commonMain/kotlin/de/kif/common/Message.kt
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
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)
|
||||||
|
}
|
22
src/commonMain/kotlin/de/kif/common/Repository.kt
Normal file
22
src/commonMain/kotlin/de/kif/common/Repository.kt
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
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>
|
||||||
|
}
|
120
src/commonMain/kotlin/de/kif/common/Search.kt
Normal file
120
src/commonMain/kotlin/de/kif/common/Search.kt
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
*/
|
87
src/commonMain/kotlin/de/kif/common/model/Color.kt
Normal file
87
src/commonMain/kotlin/de/kif/common/model/Color.kt
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
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)
|
6
src/commonMain/kotlin/de/kif/common/model/Language.kt
Normal file
6
src/commonMain/kotlin/de/kif/common/model/Language.kt
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
package de.kif.common.model
|
||||||
|
|
||||||
|
enum class Language(val code: String, val localeName: String) {
|
||||||
|
GERMAN("DE", "Deutsch"),
|
||||||
|
ENGLISH("EN", "English")
|
||||||
|
}
|
11
src/commonMain/kotlin/de/kif/common/model/Model.kt
Normal file
11
src/commonMain/kotlin/de/kif/common/model/Model.kt
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package de.kif.common.model
|
||||||
|
|
||||||
|
import de.kif.common.SearchElement
|
||||||
|
|
||||||
|
interface Model {
|
||||||
|
val id : Long?
|
||||||
|
fun createSearch(): SearchElement
|
||||||
|
|
||||||
|
val createdAt: Long
|
||||||
|
val updateAt: Long
|
||||||
|
}
|
10
src/commonMain/kotlin/de/kif/common/model/Permission.kt
Normal file
10
src/commonMain/kotlin/de/kif/common/model/Permission.kt
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
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")
|
||||||
|
}
|
68
src/commonMain/kotlin/de/kif/common/model/Post.kt
Normal file
68
src/commonMain/kotlin/de/kif/common/model/Post.kt
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
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("")
|
||||||
|
}
|
||||||
|
}
|
72
src/commonMain/kotlin/de/kif/common/model/Room.kt
Normal file
72
src/commonMain/kotlin/de/kif/common/model/Room.kt
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
19
src/commonMain/kotlin/de/kif/common/model/RoomBlock.kt
Normal file
19
src/commonMain/kotlin/de/kif/common/model/RoomBlock.kt
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
106
src/commonMain/kotlin/de/kif/common/model/Schedule.kt
Normal file
106
src/commonMain/kotlin/de/kif/common/model/Schedule.kt
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
42
src/commonMain/kotlin/de/kif/common/model/Track.kt
Normal file
42
src/commonMain/kotlin/de/kif/common/model/Track.kt
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
49
src/commonMain/kotlin/de/kif/common/model/User.kt
Normal file
49
src/commonMain/kotlin/de/kif/common/model/User.kt
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
86
src/commonMain/kotlin/de/kif/common/model/WorkGroup.kt
Normal file
86
src/commonMain/kotlin/de/kif/common/model/WorkGroup.kt
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
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
|
||||||
|
}
|
179
src/jsMain/kotlin/de/kif/frontend/PushServiceClient.kt
Normal file
179
src/jsMain/kotlin/de/kif/frontend/PushServiceClient.kt
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
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)
|
||||||
|
}
|
13
src/jsMain/kotlin/de/kif/frontend/extensions.kt
Normal file
13
src/jsMain/kotlin/de/kif/frontend/extensions.kt
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
97
src/jsMain/kotlin/de/kif/frontend/main.kt
Normal file
97
src/jsMain/kotlin/de/kif/frontend/main.kt
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
132
src/jsMain/kotlin/de/kif/frontend/repository/Ajax.kt
Normal file
132
src/jsMain/kotlin/de/kif/frontend/repository/Ajax.kt
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
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) {}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
21
src/jsMain/kotlin/de/kif/frontend/views/Announcements.kt
Normal file
21
src/jsMain/kotlin/de/kif/frontend/views/Announcements.kt
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
84
src/jsMain/kotlin/de/kif/frontend/views/RoomConstraints.kt
Normal file
84
src/jsMain/kotlin/de/kif/frontend/views/RoomConstraints.kt
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
303
src/jsMain/kotlin/de/kif/frontend/views/WorkGroupConstraints.kt
Normal file
303
src/jsMain/kotlin/de/kif/frontend/views/WorkGroupConstraints.kt
Normal file
|
@ -0,0 +1,303 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
74
src/jsMain/kotlin/de/kif/frontend/views/board/Board.kt
Normal file
74
src/jsMain/kotlin/de/kif/frontend/views/board/Board.kt
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
308
src/jsMain/kotlin/de/kif/frontend/views/calendar/Calendar.kt
Normal file
308
src/jsMain/kotlin/de/kif/frontend/views/calendar/Calendar.kt
Normal file
|
@ -0,0 +1,308 @@
|
||||||
|
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) }
|
||||||
|
}
|
232
src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarBody.kt
Normal file
232
src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarBody.kt
Normal file
|
@ -0,0 +1,232 @@
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
133
src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarEdit.kt
Normal file
133
src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarEdit.kt
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,308 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
116
src/jsMain/kotlin/de/kif/frontend/views/overview/OverviewMain.kt
Normal file
116
src/jsMain/kotlin/de/kif/frontend/views/overview/OverviewMain.kt
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
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()
|
||||||
|
})
|
||||||
|
}
|
100
src/jsMain/kotlin/de/kif/frontend/views/overview/PostView.kt
Normal file
100
src/jsMain/kotlin/de/kif/frontend/views/overview/PostView.kt
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
41
src/jsMain/kotlin/de/kif/frontend/views/table/TableLayout.kt
Normal file
41
src/jsMain/kotlin/de/kif/frontend/views/table/TableLayout.kt
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
75
src/jsMain/kotlin/de/kif/frontend/views/table/TableLine.kt
Normal file
75
src/jsMain/kotlin/de/kif/frontend/views/table/TableLine.kt
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
49
src/jsMain/kotlin/de/westermann/kwebview/ClassDelegate.kt
Normal file
49
src/jsMain/kotlin/de/westermann/kwebview/ClassDelegate.kt
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
144
src/jsMain/kotlin/de/westermann/kwebview/ClassList.kt
Normal file
144
src/jsMain/kotlin/de/westermann/kwebview/ClassList.kt
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
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>?
|
||||||
|
)
|
||||||
|
}
|
141
src/jsMain/kotlin/de/westermann/kwebview/DataSet.kt
Normal file
141
src/jsMain/kotlin/de/westermann/kwebview/DataSet.kt
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
65
src/jsMain/kotlin/de/westermann/kwebview/Dimension.kt
Normal file
65
src/jsMain/kotlin/de/westermann/kwebview/Dimension.kt
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
4
src/jsMain/kotlin/de/westermann/kwebview/KWebViewDsl.kt
Normal file
4
src/jsMain/kotlin/de/westermann/kwebview/KWebViewDsl.kt
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
package de.westermann.kwebview
|
||||||
|
|
||||||
|
@DslMarker
|
||||||
|
annotation class KWebViewDsl
|
50
src/jsMain/kotlin/de/westermann/kwebview/Point.kt
Normal file
50
src/jsMain/kotlin/de/westermann/kwebview/Point.kt
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
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()
|
||||||
|
}
|
39
src/jsMain/kotlin/de/westermann/kwebview/TouchPolyfill.kt
Normal file
39
src/jsMain/kotlin/de/westermann/kwebview/TouchPolyfill.kt
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
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 }
|
149
src/jsMain/kotlin/de/westermann/kwebview/View.kt
Normal file
149
src/jsMain/kotlin/de/westermann/kwebview/View.kt
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
110
src/jsMain/kotlin/de/westermann/kwebview/ViewCollection.kt
Normal file
110
src/jsMain/kotlin/de/westermann/kwebview/ViewCollection.kt
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
59
src/jsMain/kotlin/de/westermann/kwebview/ViewForLabel.kt
Normal file
59
src/jsMain/kotlin/de/westermann/kwebview/ViewForLabel.kt
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
42
src/jsMain/kotlin/de/westermann/kwebview/components/Body.kt
Normal file
42
src/jsMain/kotlin/de/westermann/kwebview/components/Body.kt
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
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)
|
|
@ -0,0 +1,69 @@
|
||||||
|
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)
|
|
@ -0,0 +1,105 @@
|
||||||
|
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)
|
|
@ -0,0 +1,96 @@
|
||||||
|
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)
|
15
src/jsMain/kotlin/de/westermann/kwebview/components/Icon.kt
Normal file
15
src/jsMain/kotlin/de/westermann/kwebview/components/Icon.kt
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
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)
|
|
@ -0,0 +1,46 @@
|
||||||
|
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)
|
176
src/jsMain/kotlin/de/westermann/kwebview/components/InputView.kt
Normal file
176
src/jsMain/kotlin/de/westermann/kwebview/components/InputView.kt
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
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)
|
51
src/jsMain/kotlin/de/westermann/kwebview/components/Label.kt
Normal file
51
src/jsMain/kotlin/de/westermann/kwebview/components/Label.kt
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
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)
|
48
src/jsMain/kotlin/de/westermann/kwebview/components/Link.kt
Normal file
48
src/jsMain/kotlin/de/westermann/kwebview/components/Link.kt
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
package de.westermann.kwebview.components
|
||||||
|
|
||||||
|
import de.westermann.kwebview.KWebViewDsl
|
||||||
|
import de.westermann.kwebview.View
|
||||||
|
import de.westermann.kwebview.ViewCollection
|
||||||
|
import de.westermann.kwebview.createHtmlView
|
||||||
|
import org.w3c.dom.HTMLDivElement
|
||||||
|
import org.w3c.dom.HTMLElement
|
||||||
|
|
||||||
|
class ListView<T : View>(view: HTMLElement = createHtmlView()) : ViewCollection<T>(view) {
|
||||||
|
override val html = super.html as HTMLDivElement
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun <T : View> wrap(view: HTMLElement) = ListView<T>(view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@KWebViewDsl
|
||||||
|
fun <T : View> ViewCollection<in ListView<T>>.listView(
|
||||||
|
vararg classes: String,
|
||||||
|
init: ListView<T>.() -> Unit = {}
|
||||||
|
): ListView<T> {
|
||||||
|
val view = ListView<T>()
|
||||||
|
for (c in classes) {
|
||||||
|
view.classList += c
|
||||||
|
}
|
||||||
|
append(view)
|
||||||
|
init(view)
|
||||||
|
return view
|
||||||
|
}
|
|
@ -0,0 +1,948 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,112 @@
|
||||||
|
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)
|
22
src/jsMain/kotlin/de/westermann/kwebview/components/Table.kt
Normal file
22
src/jsMain/kotlin/de/westermann/kwebview/components/Table.kt
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
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)
|
213
src/jsMain/kotlin/de/westermann/kwebview/extensions.kt
Normal file
213
src/jsMain/kotlin/de/westermann/kwebview/extensions.kt
Normal file
|
@ -0,0 +1,213 @@
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
package de.westermann.kwebview.extra
|
||||||
|
|
||||||
|
import de.westermann.kobserve.list.ObservableReadOnlyList
|
||||||
|
import de.westermann.kwebview.View
|
||||||
|
import de.westermann.kwebview.ViewCollection
|
||||||
|
import de.westermann.kwebview.async
|
||||||
|
|
||||||
|
fun <T, V : View> ViewCollection<in V>.listFactory(
|
||||||
|
list: ObservableReadOnlyList<T>,
|
||||||
|
factory: (T) -> V,
|
||||||
|
animateAdd: Int? = null,
|
||||||
|
animateRemove: Int? = null
|
||||||
|
) {
|
||||||
|
for (element in list) {
|
||||||
|
+factory(element)
|
||||||
|
}
|
||||||
|
list.onAdd { (index, element) ->
|
||||||
|
val view = factory(element)
|
||||||
|
add(index, view)
|
||||||
|
|
||||||
|
if (animateAdd != null) {
|
||||||
|
classList += "animate-add"
|
||||||
|
view.classList += "active"
|
||||||
|
|
||||||
|
async(animateAdd) {
|
||||||
|
classList -= "animate-add"
|
||||||
|
view.classList -= "active"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list.onRemove { (index) ->
|
||||||
|
@Suppress("UNCHECKED_CAST") val view = this[index] as V
|
||||||
|
|
||||||
|
if (animateRemove == null) {
|
||||||
|
remove(view)
|
||||||
|
} else {
|
||||||
|
classList += "animate-remove"
|
||||||
|
view.classList += "active"
|
||||||
|
|
||||||
|
async(animateRemove) {
|
||||||
|
classList -= "animate-remove"
|
||||||
|
view.classList -= "active"
|
||||||
|
remove(view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list.onUpdate { (oldIndex, newIndex, element) ->
|
||||||
|
removeAt(oldIndex)
|
||||||
|
add(newIndex, factory(element))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <V : View> ViewCollection<in V>.listFactory(
|
||||||
|
list: ObservableReadOnlyList<V>,
|
||||||
|
animateAdd: Int? = null,
|
||||||
|
animateRemove: Int? = null
|
||||||
|
) = listFactory(
|
||||||
|
list,
|
||||||
|
{ it },
|
||||||
|
animateAdd,
|
||||||
|
animateRemove
|
||||||
|
)
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue