diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..60413a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.gradle/ +.idea/ +build/ +web/ + +*.swp +*.swo + diff --git a/LICENSE b/LICENSE index 00d9714..17a5346 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ MIT License -Copyright (c) +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. diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..067aa8b --- /dev/null +++ b/build.gradle @@ -0,0 +1,133 @@ +buildscript { + repositories { + jcenter() + + maven { + url "https://plugins.gradle.org/m2/" + } + } +} + +plugins { + id 'kotlin-multiplatform' version '1.3.20' + id 'kotlinx-serialization' version '1.3.20' + id "org.kravemir.gradle.sass" version "1.2.2" +} + +group "de.kif" +version "0.1.0" + +repositories { + jcenter() + maven { url "http://dl.bintray.com/kotlin/ktor" } + maven { url "https://kotlin.bintray.com/kotlinx" } + mavenCentral() +} +def ktor_version = '1.1.2' +def serialization_version = '0.10.0' + +kotlin { + jvm() + js() { + compilations.all { + kotlinOptions { + moduleKind = "umd" + sourceMap = true + metaInfo = true + } + } + } + sourceSets { + commonMain { + dependencies { + implementation kotlin('stdlib-common') + + 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-websockets:$ktor_version" + + implementation "io.ktor:ktor-html-builder:$ktor_version" + + api 'io.github.microutils:kotlin-logging:1.5.4' + 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:0.8.0" + } + } + jsTest { + dependencies { + implementation kotlin('test-js') + } + } + } +} + +sass{ + main { + srcDir = file("$projectDir/src/jsMain/resources/style") + outDir = file("$buildDir/processedResources/js/main/style") + } +} + + +def webFolder = new File(project.buildDir, "../web") +def jsCompilations = kotlin.targets.js.compilations + +task populateWebFolder(dependsOn: [jsMainClasses, sass]) { + doLast { + copy { + from jsCompilations.main.output + from kotlin.sourceSets.jsMain.resources.srcDirs + jsCompilations.test.runtimeDependencyFiles.each { + if (it.exists() && !it.isDirectory()) { + from zipTree(it.absolutePath).matching { include '*.js' } + } + } + into webFolder + } + } +} + +jsJar.dependsOn(populateWebFolder) + +task run(type: JavaExec, dependsOn: [jvmMainClasses, jsJar]) { + main = "de.kif.backend.MainKt" + classpath { [ + kotlin.targets.jvm.compilations.main.output.allOutputs.files, + configurations.jvmRuntimeClasspath, + ] } + args = [] +} + +clean.doFirst { + delete webFolder +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..29e08e8 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +kotlin.code.style=official \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..87b738c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..7cae24e --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..af6708f --- /dev/null +++ b/gradlew @@ -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" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..0f8d593 --- /dev/null +++ b/gradlew.bat @@ -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 diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..0c780ba --- /dev/null +++ b/settings.gradle @@ -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' diff --git a/src/jsMain/kotlin/de/kif/frontend/main.kt b/src/jsMain/kotlin/de/kif/frontend/main.kt new file mode 100644 index 0000000..d881a6e --- /dev/null +++ b/src/jsMain/kotlin/de/kif/frontend/main.kt @@ -0,0 +1,9 @@ +package de.kif.frontend + +import de.westermann.kwebview.components.h1 +import de.westermann.kwebview.components.init + +fun main() = init { + clear() + h1("Test") +} diff --git a/src/jsMain/kotlin/de/westermann/kwebview/AttributeDelegate.kt b/src/jsMain/kotlin/de/westermann/kwebview/AttributeDelegate.kt new file mode 100644 index 0000000..2c8a3e7 --- /dev/null +++ b/src/jsMain/kotlin/de/westermann/kwebview/AttributeDelegate.kt @@ -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()) + } + } +} \ No newline at end of file diff --git a/src/jsMain/kotlin/de/westermann/kwebview/ClassDelegate.kt b/src/jsMain/kotlin/de/westermann/kwebview/ClassDelegate.kt new file mode 100644 index 0000000..e61e796 --- /dev/null +++ b/src/jsMain/kotlin/de/westermann/kwebview/ClassDelegate.kt @@ -0,0 +1,49 @@ +package de.westermann.kwebview + +import de.westermann.kobserve.Property +import de.westermann.kobserve.basic.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 + + operator fun getValue(container: View, property: KProperty<*>): Property { + 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 + } + } +} \ No newline at end of file diff --git a/src/jsMain/kotlin/de/westermann/kwebview/ClassList.kt b/src/jsMain/kotlin/de/westermann/kwebview/ClassList.kt new file mode 100644 index 0000000..4921668 --- /dev/null +++ b/src/jsMain/kotlin/de/westermann/kwebview/ClassList.kt @@ -0,0 +1,119 @@ +package de.westermann.kwebview + +import de.westermann.kobserve.ListenerReference +import de.westermann.kobserve.Property +import de.westermann.kobserve.ReadOnlyProperty +import org.w3c.dom.DOMTokenList + +/** + * Represents the css classes of an html element. + * + * @author lars + */ +class ClassList( + private val list: DOMTokenList +) : Iterable { + + private val bound: MutableMap = mutableMapOf() + + + /** + * Add css class. + */ + fun add(clazz: String) { + if (clazz in bound) { + val p = bound[clazz] ?: return + if (p.property is Property) { + 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) { + 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) { + 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 unbind(clazz: String) { + if (clazz !in bound) { + throw IllegalArgumentException("Class is not bound!") + } + + bound[clazz]?.reference?.remove() + bound -= clazz + } + + override fun iterator(): Iterator { + return toString().split(" +".toRegex()).iterator() + } + + override fun toString(): String = list.value + + private data class Bound( + val property: ReadOnlyProperty, + val reference: ListenerReference? + ) +} \ No newline at end of file diff --git a/src/jsMain/kotlin/de/westermann/kwebview/DataSet.kt b/src/jsMain/kotlin/de/westermann/kwebview/DataSet.kt new file mode 100644 index 0000000..5103d75 --- /dev/null +++ b/src/jsMain/kotlin/de/westermann/kwebview/DataSet.kt @@ -0,0 +1,128 @@ +package de.westermann.kwebview + +import de.westermann.kobserve.ListenerReference +import de.westermann.kobserve.Property +import de.westermann.kobserve.ReadOnlyProperty +import org.w3c.dom.DOMStringMap +import org.w3c.dom.get +import org.w3c.dom.set + +/** + * Represents the css classes of an html element. + * + * @author lars + */ +class DataSet( + private val map: DOMStringMap +) { + + private val bound: MutableMap = mutableMapOf() + + /** + * Add css class. + */ + operator fun plusAssign(entry: Pair) { + 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) { + if (key in bound) { + throw IllegalArgumentException("Class is already bound!") + } + + bound[key] = Bound(key, null, property) + } + + fun bind(key: String, property: ReadOnlyProperty) { + if (key in bound) { + throw IllegalArgumentException("Class is already bound!") + } + + bound[key] = Bound(key, property, null) + } + + fun unbind(key: String) { + if (key !in bound) { + throw IllegalArgumentException("Class is not bound!") + } + + bound[key]?.reference?.remove() + bound -= key + } + + private inner class Bound( + val key: String, + val propertyNullable: ReadOnlyProperty?, + val property: ReadOnlyProperty? + ) { + + var reference: ListenerReference? = 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 + } + + } + } +} \ No newline at end of file diff --git a/src/jsMain/kotlin/de/westermann/kwebview/Dimension.kt b/src/jsMain/kotlin/de/westermann/kwebview/Dimension.kt new file mode 100644 index 0000000..72c39fb --- /dev/null +++ b/src/jsMain/kotlin/de/westermann/kwebview/Dimension.kt @@ -0,0 +1,62 @@ +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 edges: Set + 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) + } +} \ No newline at end of file diff --git a/src/jsMain/kotlin/de/westermann/kwebview/KWebViewDsl.kt b/src/jsMain/kotlin/de/westermann/kwebview/KWebViewDsl.kt new file mode 100644 index 0000000..43502bb --- /dev/null +++ b/src/jsMain/kotlin/de/westermann/kwebview/KWebViewDsl.kt @@ -0,0 +1,4 @@ +package de.westermann.kwebview + +@DslMarker +annotation class KWebViewDsl diff --git a/src/jsMain/kotlin/de/westermann/kwebview/Point.kt b/src/jsMain/kotlin/de/westermann/kwebview/Point.kt new file mode 100644 index 0000000..2d41d0f --- /dev/null +++ b/src/jsMain/kotlin/de/westermann/kwebview/Point.kt @@ -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() +} diff --git a/src/jsMain/kotlin/de/westermann/kwebview/View.kt b/src/jsMain/kotlin/de/westermann/kwebview/View.kt new file mode 100644 index 0000000..7090b93 --- /dev/null +++ b/src/jsMain/kotlin/de/westermann/kwebview/View.kt @@ -0,0 +1,124 @@ +package de.westermann.kwebview + +import de.westermann.kobserve.EventHandler +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() + + 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() + val onDblClick = EventHandler() + val onContext = EventHandler() + + val onMouseDown = EventHandler() + val onMouseMove = EventHandler() + val onMouseUp = EventHandler() + val onMouseEnter = EventHandler() + val onMouseLeave = EventHandler() + + val onWheel = EventHandler() + + val onKeyDown = EventHandler() + val onKeyPress = EventHandler() + val onKeyUp = EventHandler() + + val onFocus = EventHandler() + val onBlur = EventHandler() + + 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") + } +} diff --git a/src/jsMain/kotlin/de/westermann/kwebview/ViewCollection.kt b/src/jsMain/kotlin/de/westermann/kwebview/ViewCollection.kt new file mode 100644 index 0000000..44e4d43 --- /dev/null +++ b/src/jsMain/kotlin/de/westermann/kwebview/ViewCollection.kt @@ -0,0 +1,69 @@ +package de.westermann.kwebview + +import org.w3c.dom.HTMLElement +import kotlin.dom.clear + +/** + * @author lars + */ +abstract class ViewCollection(view: HTMLElement = createHtmlView()) : View(view), Iterable { + + private val children: MutableList = mutableListOf() + + 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 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) + + val isEmpty: Boolean + get() = children.isEmpty() + + fun clear() { + children.clear() + html.clear() + } + + override fun iterator(): Iterator = children.iterator() + + val size: Int + get() = children.size + + operator fun contains(view: V) = children.contains(view) + + operator fun V.unaryPlus() { + append(this) + } +} diff --git a/src/jsMain/kotlin/de/westermann/kwebview/ViewForLabel.kt b/src/jsMain/kotlin/de/westermann/kwebview/ViewForLabel.kt new file mode 100644 index 0000000..88330e8 --- /dev/null +++ b/src/jsMain/kotlin/de/westermann/kwebview/ViewForLabel.kt @@ -0,0 +1,57 @@ +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(createHtmlView()) { + 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) + } + } +} diff --git a/src/jsMain/kotlin/de/westermann/kwebview/components/Body.kt b/src/jsMain/kotlin/de/westermann/kwebview/components/Body.kt new file mode 100644 index 0000000..766467d --- /dev/null +++ b/src/jsMain/kotlin/de/westermann/kwebview/components/Body.kt @@ -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(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) + } + } + } +} \ No newline at end of file diff --git a/src/jsMain/kotlin/de/westermann/kwebview/components/BoxView.kt b/src/jsMain/kotlin/de/westermann/kwebview/components/BoxView.kt new file mode 100644 index 0000000..983a187 --- /dev/null +++ b/src/jsMain/kotlin/de/westermann/kwebview/components/BoxView.kt @@ -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(createHtmlView()) { + override val html = super.html as HTMLDivElement +} + +@KWebViewDsl +fun ViewCollection.boxView(vararg classes: String, init: BoxView.() -> Unit = {}): BoxView { + val view = BoxView() + for (c in classes) { + view.classList += c + } + append(view) + init(view) + return view +} diff --git a/src/jsMain/kotlin/de/westermann/kwebview/components/Button.kt b/src/jsMain/kotlin/de/westermann/kwebview/components/Button.kt new file mode 100644 index 0000000..5094ab9 --- /dev/null +++ b/src/jsMain/kotlin/de/westermann/kwebview/components/Button.kt @@ -0,0 +1,53 @@ +package de.westermann.kwebview.components + +import de.westermann.kobserve.Property +import de.westermann.kobserve.ReadOnlyProperty +import de.westermann.kobserve.basic.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() : ViewCollection(createHtmlView()) { + + constructor(text: String) : this() { + this.text = text + } + + override val html = super.html as HTMLButtonElement + + fun bind(property: ReadOnlyProperty) { + textProperty.bind(property) + } + + fun unbind() { + textProperty.unbind() + } + + var text: String + get() = html.textContent ?: "" + set(value) { + html.textContent = value + textProperty.invalidate() + } + + val textProperty: Property = property(this::text) +} + +@KWebViewDsl +fun ViewCollection.button(text: String = "", init: Button.() -> Unit = {}) = + Button(text).also(this::append).also(init) + +@KWebViewDsl +fun ViewCollection.button(text: ReadOnlyProperty, init: Button.() -> Unit = {}) = + Button(text.value).also(this::append).also { it.bind(text) }.also(init) + +@KWebViewDsl +fun ViewCollection.button(init: Button.() -> Unit = {}) = + Button().also(this::append).also(init) diff --git a/src/jsMain/kotlin/de/westermann/kwebview/components/Checkbox.kt b/src/jsMain/kotlin/de/westermann/kwebview/components/Checkbox.kt new file mode 100644 index 0000000..a4aaf4f --- /dev/null +++ b/src/jsMain/kotlin/de/westermann/kwebview/components/Checkbox.kt @@ -0,0 +1,68 @@ +package de.westermann.kwebview.components + +import de.westermann.kobserve.Property +import de.westermann.kobserve.ReadOnlyProperty +import de.westermann.kobserve.ValidationProperty +import de.westermann.kobserve.basic.property +import de.westermann.kwebview.* +import org.w3c.dom.events.Event +import org.w3c.dom.events.EventListener + +class Checkbox( + initValue: Boolean = false +) : ViewForLabel() { + + fun bind(property: ReadOnlyProperty) { + checkedProperty.bind(property) + readonly = true + } + + fun bind(property: Property) { + checkedProperty.bindBidirectional(property) + } + + fun unbind() { + checkedProperty.unbind() + } + + var checked: Boolean + get() = html.checked + set(value) { + html.checked = value + checkedProperty.invalidate() + } + + val checkedProperty: Property = 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.checkbox(value: Boolean = false, init: Checkbox.() -> Unit = {}) = + Checkbox(value).also(this::append).also(init) + +@KWebViewDsl +fun ViewCollection.checkbox(value: ReadOnlyProperty, init: Checkbox.() -> Unit = {}) = + Checkbox(value.value).also(this::append).also { it.bind(value) }.also(init) + +@KWebViewDsl +fun ViewCollection.checkbox(value: Property, init: Checkbox.() -> Unit = {}) = + Checkbox(value.value).also(this::append).also { it.bind(value) }.also(init) diff --git a/src/jsMain/kotlin/de/westermann/kwebview/components/FilterList.kt b/src/jsMain/kotlin/de/westermann/kwebview/components/FilterList.kt new file mode 100644 index 0000000..3da687a --- /dev/null +++ b/src/jsMain/kotlin/de/westermann/kwebview/components/FilterList.kt @@ -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( + val property: ReadOnlyProperty, + val filter: Filter +) : ViewCollection(createHtmlView()) { + + private val content: MutableMap = 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) { + view.onClick { + property.value = element + } + } + content[element] = view + } + + clear() + + for (element in list) { + append(content[element]!!) + } + } + + init { + update() + + property.onChange { + update() + } + } +} + +interface Filter { + fun filter(partial: T): List + fun render(element: T): V + + val useCache: Boolean +} + +class StringFilter( + private val dataSet: List +) : Filter { + override fun filter(partial: String): List { + 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 +) : Filter { + 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 +) : Filter { + override fun filter(partial: String) = filter.invoke(partial) + + override fun render(element: String) = TextView(element) + + override val useCache = false +} + + +fun ViewCollection>.filterList(property: ReadOnlyProperty, filter: Filter, init: FilterList.() -> Unit = {}) = + FilterList(property, filter).also(this::append).also(init) \ No newline at end of file diff --git a/src/jsMain/kotlin/de/westermann/kwebview/components/Heading.kt b/src/jsMain/kotlin/de/westermann/kwebview/components/Heading.kt new file mode 100644 index 0000000..7808b0f --- /dev/null +++ b/src/jsMain/kotlin/de/westermann/kwebview/components/Heading.kt @@ -0,0 +1,96 @@ +package de.westermann.kwebview.components + +import de.westermann.kobserve.Property +import de.westermann.kobserve.ReadOnlyProperty +import de.westermann.kobserve.basic.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(type.tagName)) { + + override val html = super.html as HTMLHeadingElement + + fun bind(property: ReadOnlyProperty) { + textProperty.bind(property) + } + + fun unbind() { + textProperty.unbind() + } + + var text: String + get() = html.textContent ?: "" + set(value) { + html.textContent = value + textProperty.invalidate() + } + + val textProperty: Property = 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.h1(text: String = "", init: Heading.() -> Unit = {}) = + Heading(Heading.Type.H1, text).also(this::append).also(init) + +@KWebViewDsl +fun ViewCollection.h1(text: ReadOnlyProperty, init: Heading.() -> Unit = {}) = + Heading(Heading.Type.H1, text.value).also(this::append).also { it.bind(text) }.also(init) + +@KWebViewDsl +fun ViewCollection.h2(text: String = "", init: Heading.() -> Unit = {}) = + Heading(Heading.Type.H2, text).also(this::append).also(init) + +@KWebViewDsl +fun ViewCollection.h2(text: ReadOnlyProperty, init: Heading.() -> Unit = {}) = + Heading(Heading.Type.H2, text.value).also(this::append).also { it.bind(text) }.also(init) + +@KWebViewDsl +fun ViewCollection.h3(text: String = "", init: Heading.() -> Unit = {}) = + Heading(Heading.Type.H3, text).also(this::append).also(init) + +@KWebViewDsl +fun ViewCollection.h3(text: ReadOnlyProperty, init: Heading.() -> Unit = {}) = + Heading(Heading.Type.H3, text.value).also(this::append).also { it.bind(text) }.also(init) + +@KWebViewDsl +fun ViewCollection.h4(text: String = "", init: Heading.() -> Unit = {}) = + Heading(Heading.Type.H4, text).also(this::append).also(init) + +@KWebViewDsl +fun ViewCollection.h4(text: ReadOnlyProperty, init: Heading.() -> Unit = {}) = + Heading(Heading.Type.H4, text.value).also(this::append).also { it.bind(text) }.also(init) + +@KWebViewDsl +fun ViewCollection.h5(text: String = "", init: Heading.() -> Unit = {}) = + Heading(Heading.Type.H5, text).also(this::append).also(init) + +@KWebViewDsl +fun ViewCollection.h5(text: ReadOnlyProperty, init: Heading.() -> Unit = {}) = + Heading(Heading.Type.H5, text.value).also(this::append).also { it.bind(text) }.also(init) + +@KWebViewDsl +fun ViewCollection.h6(text: String = "", init: Heading.() -> Unit = {}) = + Heading(Heading.Type.H6, text).also(this::append).also(init) + +@KWebViewDsl +fun ViewCollection.h6(text: ReadOnlyProperty, init: Heading.() -> Unit = {}) = + Heading(Heading.Type.H6, text.value).also(this::append).also { it.bind(text) }.also(init) diff --git a/src/jsMain/kotlin/de/westermann/kwebview/components/ImageView.kt b/src/jsMain/kotlin/de/westermann/kwebview/components/ImageView.kt new file mode 100644 index 0000000..c1c1f6b --- /dev/null +++ b/src/jsMain/kotlin/de/westermann/kwebview/components/ImageView.kt @@ -0,0 +1,46 @@ +package de.westermann.kwebview.components + +import de.westermann.kobserve.Property +import de.westermann.kobserve.ReadOnlyProperty +import de.westermann.kobserve.basic.property +import de.westermann.kwebview.* +import org.w3c.dom.HTMLImageElement + +class ImageView( + src: String +) : View(createHtmlView("img")) { + + override val html = super.html as HTMLImageElement + + fun bind(property: ReadOnlyProperty) { + sourceProperty.bind(property) + } + + fun unbind() { + sourceProperty.unbind() + } + + var source: String + get() = html.src + set(value) { + html.src = value + sourceProperty.invalidate() + } + + val sourceProperty: Property = property(this::source) + + + var alt by AttributeDelegate("alt") + + init { + source = src + } +} + +@KWebViewDsl +fun ViewCollection.imageView(src: String = "", init: ImageView.() -> Unit = {}) = + ImageView(src).also(this::append).also(init) + +@KWebViewDsl +fun ViewCollection.imageView(src: ReadOnlyProperty, init: ImageView.() -> Unit = {}) = + ImageView(src.value).also(this::append).also { it.bind(src) }.also(init) diff --git a/src/jsMain/kotlin/de/westermann/kwebview/components/InputView.kt b/src/jsMain/kotlin/de/westermann/kwebview/components/InputView.kt new file mode 100644 index 0000000..b7c5685 --- /dev/null +++ b/src/jsMain/kotlin/de/westermann/kwebview/components/InputView.kt @@ -0,0 +1,154 @@ +package de.westermann.kwebview.components + +import de.westermann.kobserve.Property +import de.westermann.kobserve.ReadOnlyProperty +import de.westermann.kobserve.ValidationProperty +import de.westermann.kobserve.basic.property +import de.westermann.kobserve.not +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 = "" +) : ViewForLabel() { + + fun bind(property: ReadOnlyProperty) { + valueProperty.bind(property) + readonly = true + } + + fun bind(property: Property) { + valueProperty.bindBidirectional(property) + } + + fun bind(property: ValidationProperty) { + 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 = property(this::value) + + var placeholder: String + get() = html.placeholder + set(value) { + html.placeholder = value + placeholderProperty.invalidate() + } + + val placeholderProperty: Property = 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) + } +} + +enum class InputType(val html: String) { + TEXT("text"), + NUMBER("number"), + PASSWORD("password"); + + companion object { + fun find(html: String): InputType? = values().find { it.html == html } + } +} + +@KWebViewDsl +fun ViewCollection.inputView(text: String = "", init: InputView.() -> Unit = {}) = + InputView(InputType.TEXT, text).also(this::append).also(init) + +@KWebViewDsl +fun ViewCollection.inputView(text: ReadOnlyProperty, init: InputView.() -> Unit = {}) = + InputView(InputType.TEXT, text.value).also(this::append).also { it.bind(text) }.also(init) + +@KWebViewDsl +fun ViewCollection.inputView(text: Property, init: InputView.() -> Unit = {}) = + InputView(InputType.TEXT, text.value).also(this::append).also { it.bind(text) }.also(init) + +@KWebViewDsl +fun ViewCollection.inputView(text: ValidationProperty, init: InputView.() -> Unit = {}) = + InputView(InputType.TEXT, text.value).also(this::append).also { it.bind(text) }.also(init) + + +@KWebViewDsl +fun ViewCollection.inputView(type: InputType = InputType.TEXT, text: String = "", init: InputView.() -> Unit = {}) = + InputView(type, text).also(this::append).also(init) + +@KWebViewDsl +fun ViewCollection.inputView(type: InputType = InputType.TEXT, text: ReadOnlyProperty, init: InputView.() -> Unit = {}) = + InputView(type, text.value).also(this::append).also { it.bind(text) }.also(init) + +@KWebViewDsl +fun ViewCollection.inputView(type: InputType = InputType.TEXT, text: Property, init: InputView.() -> Unit = {}) = + InputView(type, text.value).also(this::append).also { it.bind(text) }.also(init) + +@KWebViewDsl +fun ViewCollection.inputView(type: InputType = InputType.TEXT, text: ValidationProperty, init: InputView.() -> Unit = {}) = + InputView(type, text.value).also(this::append).also { it.bind(text) }.also(init) diff --git a/src/jsMain/kotlin/de/westermann/kwebview/components/Label.kt b/src/jsMain/kotlin/de/westermann/kwebview/components/Label.kt new file mode 100644 index 0000000..c2985ea --- /dev/null +++ b/src/jsMain/kotlin/de/westermann/kwebview/components/Label.kt @@ -0,0 +1,51 @@ +package de.westermann.kwebview.components + +import de.westermann.kobserve.Property +import de.westermann.kobserve.ReadOnlyProperty +import de.westermann.kobserve.basic.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()) { + + override val html = super.html as HTMLLabelElement + + fun bind(property: ReadOnlyProperty) { + textProperty.bind(property) + } + + fun unbind() { + textProperty.unbind() + } + + var text: String + get() = html.textContent ?: "" + set(value) { + html.textContent = value + textProperty.invalidate() + } + + val textProperty: Property = property(this::text) + + init { + text = value + + inputElement.setLabel(this) + } +} + +@KWebViewDsl +fun ViewCollection.label(inputElement: ViewForLabel, text: String = "", init: Label.() -> Unit = {}) = + Label(inputElement, text).also(this::append).also(init) + +@KWebViewDsl +fun ViewCollection.label(inputElement: ViewForLabel, text: ReadOnlyProperty, init: Label.() -> Unit = {}) = + Label(inputElement, text.value).also(this::append).also { it.bind(text) }.also(init) diff --git a/src/jsMain/kotlin/de/westermann/kwebview/components/Link.kt b/src/jsMain/kotlin/de/westermann/kwebview/components/Link.kt new file mode 100644 index 0000000..014a6c9 --- /dev/null +++ b/src/jsMain/kotlin/de/westermann/kwebview/components/Link.kt @@ -0,0 +1,44 @@ +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(target: String) : ViewCollection(createHtmlView("a")) { + + 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 + } + + init { + this.target = target + } +} + +@KWebViewDsl +fun ViewCollection.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 +} diff --git a/src/jsMain/kotlin/de/westermann/kwebview/components/OptionView.kt b/src/jsMain/kotlin/de/westermann/kwebview/components/OptionView.kt new file mode 100644 index 0000000..64a5bc1 --- /dev/null +++ b/src/jsMain/kotlin/de/westermann/kwebview/components/OptionView.kt @@ -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(val value: T) : View(createHtmlView()) { + + 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 + } + +} \ No newline at end of file diff --git a/src/jsMain/kotlin/de/westermann/kwebview/components/SelectView.kt b/src/jsMain/kotlin/de/westermann/kwebview/components/SelectView.kt new file mode 100644 index 0000000..d1fa0f1 --- /dev/null +++ b/src/jsMain/kotlin/de/westermann/kwebview/components/SelectView.kt @@ -0,0 +1,112 @@ +package de.westermann.kwebview.components + +import de.westermann.kobserve.Property +import de.westermann.kobserve.ReadOnlyProperty +import de.westermann.kobserve.basic.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( + dataSet: List, + private val initValue: T, + val transform: (T) -> String = { it.toString() } +) : ViewCollection>(createHtmlView()) { + + override val html = super.html as HTMLSelectElement + + fun bind(property: ReadOnlyProperty) { + valueProperty.bind(property) + readonly = true + } + + fun bind(property: Property) { + valueProperty.bindBidirectional(property) + } + + fun unbind() { + valueProperty.unbind() + } + + var dataSet: List = 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 ViewCollection>.selectView(dataSet: List, initValue: T, transform: (T) -> String = { it.toString() }, init: SelectView.() -> Unit = {}) = + SelectView(dataSet, initValue, transform).also(this::append).also(init) + +@KWebViewDsl +fun ViewCollection>.selectView(dataSet: List, property: ReadOnlyProperty, transform: (T) -> String = { it.toString() }, init: SelectView.() -> Unit = {}) = + SelectView(dataSet, property.value, transform).apply { bind(property) }.also(this::append).also(init) + +@KWebViewDsl +fun ViewCollection>.selectView(dataSet: List, property: Property, transform: (T) -> String = { it.toString() }, init: SelectView.() -> Unit = {}) = + SelectView(dataSet, property.value, transform).apply { bind(property) }.also(this::append).also(init) + + +@KWebViewDsl +inline fun > ViewCollection>.selectView(initValue: T, noinline transform: (T) -> String = { it.toString() }, init: SelectView.() -> Unit = {}) = + SelectView(enumValues().toList(), initValue, transform).also(this::append).also(init) + +@KWebViewDsl +inline fun > ViewCollection>.selectView(property: ReadOnlyProperty, noinline transform: (T) -> String = { it.toString() }, init: SelectView.() -> Unit = {}) = + SelectView(enumValues().toList(), property.value, transform).apply { bind(property) }.also(this::append).also(init) + +@KWebViewDsl +inline fun > ViewCollection>.selectView(property: Property, noinline transform: (T) -> String = { it.toString() }, init: SelectView.() -> Unit = {}) = + SelectView(enumValues().toList(), property.value, transform).apply { bind(property) }.also(this::append).also(init) diff --git a/src/jsMain/kotlin/de/westermann/kwebview/components/Table.kt b/src/jsMain/kotlin/de/westermann/kwebview/components/Table.kt new file mode 100644 index 0000000..5346344 --- /dev/null +++ b/src/jsMain/kotlin/de/westermann/kwebview/components/Table.kt @@ -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(createHtmlView()) { + override val html = super.html as HTMLTableElement +} + +@KWebViewDsl +fun ViewCollection.table(vararg classes: String, init: Table.() -> Unit = {}): Table { + val view = Table() + for (c in classes) { + view.classList += c + } + append(view) + init(view) + return view +} diff --git a/src/jsMain/kotlin/de/westermann/kwebview/components/TableCaption.kt b/src/jsMain/kotlin/de/westermann/kwebview/components/TableCaption.kt new file mode 100644 index 0000000..ad3830f --- /dev/null +++ b/src/jsMain/kotlin/de/westermann/kwebview/components/TableCaption.kt @@ -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(createHtmlView("caption")) { + override val html = super.html as HTMLTableCaptionElement +} + +@KWebViewDsl +fun ViewCollection.caption(init: TableCaption.() -> Unit = {}): TableCaption { + val view = TableCaption() + append(view) + init(view) + return view +} diff --git a/src/jsMain/kotlin/de/westermann/kwebview/components/TableCell.kt b/src/jsMain/kotlin/de/westermann/kwebview/components/TableCell.kt new file mode 100644 index 0000000..de7c3df --- /dev/null +++ b/src/jsMain/kotlin/de/westermann/kwebview/components/TableCell.kt @@ -0,0 +1,41 @@ +package de.westermann.kwebview.components + +import de.westermann.kwebview.* +import org.w3c.dom.HTMLTableCellElement + +class TableCell(val isHead: Boolean) : + ViewCollection(createHtmlView(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.cell(colSpan: Int? = null, init: TableCell.() -> Unit = {}): TableCell { + val view = TableCell(false) + view.colSpan = colSpan + append(view) + init(view) + return view +} + +@KWebViewDsl +fun ViewCollection.head(colSpan: Int? = null, init: TableCell.() -> Unit = {}): TableCell { + val view = TableCell(true) + view.colSpan = colSpan + append(view) + init(view) + return view +} diff --git a/src/jsMain/kotlin/de/westermann/kwebview/components/TableRow.kt b/src/jsMain/kotlin/de/westermann/kwebview/components/TableRow.kt new file mode 100644 index 0000000..8aab596 --- /dev/null +++ b/src/jsMain/kotlin/de/westermann/kwebview/components/TableRow.kt @@ -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(createHtmlView("tr")) { + override val html = super.html as HTMLTableRowElement +} + +@KWebViewDsl +fun ViewCollection.row(vararg classes: String, init: TableRow.() -> Unit = {}): TableRow { + val view = TableRow() + for (c in classes) { + view.classList += c + } + append(view) + init(view) + return view +} diff --git a/src/jsMain/kotlin/de/westermann/kwebview/components/TableSection.kt b/src/jsMain/kotlin/de/westermann/kwebview/components/TableSection.kt new file mode 100644 index 0000000..2bb9fbd --- /dev/null +++ b/src/jsMain/kotlin/de/westermann/kwebview/components/TableSection.kt @@ -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(createHtmlView(type.tagName)) { + override val html = super.html as HTMLTableSectionElement + + enum class Type(val tagName: String) { + THEAD("thead"), + TBODY("tbody"), + TFOOT("tfoot") + } +} + +@KWebViewDsl +fun ViewCollection.thead(init: TableSection.() -> Unit = {}): TableSection { + val view = TableSection(TableSection.Type.THEAD) + append(view) + init(view) + return view +} + +@KWebViewDsl +fun ViewCollection.tbody(init: TableSection.() -> Unit = {}): TableSection { + val view = TableSection(TableSection.Type.TBODY) + append(view) + init(view) + return view +} + +@KWebViewDsl +fun ViewCollection.tfoot(init: TableSection.() -> Unit = {}): TableSection { + val view = TableSection(TableSection.Type.TFOOT) + append(view) + init(view) + return view +} diff --git a/src/jsMain/kotlin/de/westermann/kwebview/components/TextView.kt b/src/jsMain/kotlin/de/westermann/kwebview/components/TextView.kt new file mode 100644 index 0000000..9228091 --- /dev/null +++ b/src/jsMain/kotlin/de/westermann/kwebview/components/TextView.kt @@ -0,0 +1,51 @@ +package de.westermann.kwebview.components + +import de.westermann.kobserve.Property +import de.westermann.kobserve.ReadOnlyProperty +import de.westermann.kobserve.basic.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 + +/** + * Represents a html span element. + * + * @author lars + */ +class TextView( + value: String = "" +) : View(createHtmlView()) { + + override val html = super.html as HTMLSpanElement + + fun bind(property: ReadOnlyProperty) { + textProperty.bind(property) + } + + fun unbind() { + textProperty.unbind() + } + + var text: String + get() = html.textContent ?: "" + set(value) { + html.textContent = value + textProperty.invalidate() + } + + val textProperty: Property = property(this::text) + + init { + text = value + } +} + +@KWebViewDsl +fun ViewCollection.textView(text: String = "", init: TextView.() -> Unit = {}) = + TextView(text).also(this::append).also(init) + +@KWebViewDsl +fun ViewCollection.textView(text: ReadOnlyProperty, init: TextView.() -> Unit = {}) = + TextView(text.value).also(this::append).also { it.bind(text) }.also(init) diff --git a/src/jsMain/kotlin/de/westermann/kwebview/extensions.kt b/src/jsMain/kotlin/de/westermann/kwebview/extensions.kt new file mode 100644 index 0000000..598b6bd --- /dev/null +++ b/src/jsMain/kotlin/de/westermann/kwebview/extensions.kt @@ -0,0 +1,79 @@ +package de.westermann.kwebview + +import de.westermann.kobserve.EventHandler +import org.w3c.dom.DOMRect +import org.w3c.dom.HTMLElement +import org.w3c.dom.events.Event +import org.w3c.dom.events.EventListener +import org.w3c.dom.events.MouseEvent +import kotlin.browser.document +import kotlin.browser.window + +inline fun 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" + } + } + return document.createElement(tagName) as V +} + +fun String.toDashCase() = replace("([a-z])([A-Z])".toRegex(), "$1-$2").toLowerCase() + +inline fun EventHandler.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 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) { + if (timeout < 1) throw IllegalArgumentException("Timeout must be greater than 0!") + window.setTimeout(block, timeout) +} + +fun interval(timeout: Int, block: () -> Unit): Int { + if (timeout < 1) throw IllegalArgumentException("Timeout must be greater than 0!") + return window.setInterval(block, timeout) +} + +fun clearInterval(id: Int) { + window.clearInterval(id) +} diff --git a/src/jsMain/kotlin/de/westermann/kwebview/i18n.kt b/src/jsMain/kotlin/de/westermann/kwebview/i18n.kt new file mode 100644 index 0000000..84b5b69 --- /dev/null +++ b/src/jsMain/kotlin/de/westermann/kwebview/i18n.kt @@ -0,0 +1,147 @@ +package de.westermann.kwebview + +import kotlin.browser.window + +@Suppress("ClassName") +object i18n { + private val data: MutableMap = mutableMapOf() + + private var fallbackLocale: Locale? = null + private var locale: Locale? = null + + fun register(id: String, name: String, path: String, fallback: Boolean = false) { + val locale = Locale(id, name, path, fallback) + + if (fallback) { + if (fallbackLocale != null) { + throw IllegalStateException("Fallback locale is already set!") + } + + fallbackLocale = locale + } + + data[id] = locale + + window.fetch(path).then { + it.json() + }.then { + locale.json = it + locale.isLoaded = true + }.catch { + throw it + } + } + + val isReady: Boolean + get() = data.values.all { it.isLoaded } + + fun load(id: String, block: () -> Unit) { + fun ready() { + if (isReady) { + locale = data[id] + block() + } else { + async(50) { ready() } + } + } + ready() + } + + private fun findKey(locale: Locale, key: String): dynamic { + val keys = key.split(".") + + var result = locale.json + for (k in keys) { + if (result.hasOwnProperty(k) as Boolean) { + result = result[k] + } else { + return undefined + } + } + + return result + } + + private fun findKey(key: String): dynamic { + var result: dynamic = undefined + + if (locale != null) { + result = findKey(locale!!, key) + } + + if (result == undefined) { + if (fallbackLocale != null) { + result = findKey(fallbackLocale!!, key) + } + } + + if (result == undefined) { + throw InternationalizationError("Cannot find key '$key'!") + } else { + return result + } + } + + private fun replace(str: String, arguments: List>): String { + val unnamed = arguments.filter { it.first == null }.map { it.second } + val named = arguments.mapNotNull { it.first?.to(it.second) } + + var s = str + + for ((key, replacement) in named) { + s = s.replace("{$key}", replacement?.toString() ?: "null") + } + + for (replacement in unnamed) { + if (s.contains("{}")) { + s = s.replaceFirst("{}", replacement?.toString() ?: "null") + } + } + + return s + } + + fun t(key: String, arguments: List>): String { + return replace(findKey(key).toString(), arguments) + } + + fun t(count: Number, key: String, arguments: List>): String { + val json = findKey(key) + if (count == 0 && json.hasOwnProperty("zero") as Boolean) { + return replace(json.zero.toString(), arguments) + } else if (count == 1 && json.hasOwnProperty("one") as Boolean) { + return replace(json.one.toString(), arguments) + } + + return if (json.hasOwnProperty("many") as Boolean) + replace(json.many.toString(), arguments) + else { + replace(json.toString(), arguments) + } + + } + + private class Locale( + val id: String, + val name: String, + val path: String, + val fallback: Boolean + ) { + var isLoaded = false + var json = js("{}") + } +} + +class InternationalizationError(message: String? = null) : Error(message) + +fun t(key: String) = i18n.t(key, emptyList()) + +fun t(key: String, vararg arguments: Any?) = i18n.t(key, arguments.map { null to it }) + +fun t(key: String, vararg arguments: Pair) = i18n.t(key, arguments.asList()) + +fun t(count: Number, key: String) = i18n.t(count, key, emptyList()) + +fun t(count: Number, key: String, vararg arguments: Any?) = i18n.t(count, key, arguments.map { null to it }) + +fun t(count: Number, key: String, vararg arguments: Pair) = i18n.t(count, key, arguments.asList()) diff --git a/src/jsMain/resources/require.min.js b/src/jsMain/resources/require.min.js new file mode 100644 index 0000000..0dcbf0e --- /dev/null +++ b/src/jsMain/resources/require.min.js @@ -0,0 +1,2 @@ + +var requirejs,require,define;!function(global,setTimeout){function commentReplace(e,t){return t||""}function isFunction(e){return"[object Function]"===ostring.call(e)}function isArray(e){return"[object Array]"===ostring.call(e)}function each(e,t){if(e){var i;for(i=0;i-1&&(!e[i]||!t(e[i],i,e));i-=1);}}function hasProp(e,t){return hasOwn.call(e,t)}function getOwn(e,t){return hasProp(e,t)&&e[t]}function eachProp(e,t){var i;for(i in e)if(hasProp(e,i)&&t(e[i],i))break}function mixin(e,t,i,r){return t&&eachProp(t,function(t,n){!i&&hasProp(e,n)||(!r||"object"!=typeof t||!t||isArray(t)||isFunction(t)||t instanceof RegExp?e[n]=t:(e[n]||(e[n]={}),mixin(e[n],t,i,r)))}),e}function bind(e,t){return function(){return t.apply(e,arguments)}}function scripts(){return document.getElementsByTagName("script")}function defaultOnError(e){throw e}function getGlobal(e){if(!e)return e;var t=global;return each(e.split("."),function(e){t=t[e]}),t}function makeError(e,t,i,r){var n=new Error(t+"\nhttp://requirejs.org/docs/errors.html#"+e);return n.requireType=e,n.requireModules=r,i&&(n.originalError=i),n}function newContext(e){function t(e){var t,i;for(t=0;t0&&(e.splice(t-1,2),t-=2)}}function i(e,i,r){var n,o,a,s,u,c,d,p,f,l,h=i&&i.split("/"),m=y.map,g=m&&m["*"];if(e&&(c=(e=e.split("/")).length-1,y.nodeIdCompat&&jsSuffixRegExp.test(e[c])&&(e[c]=e[c].replace(jsSuffixRegExp,"")),"."===e[0].charAt(0)&&h&&(e=h.slice(0,h.length-1).concat(e)),t(e),e=e.join("/")),r&&m&&(h||g)){e:for(a=(o=e.split("/")).length;a>0;a-=1){if(u=o.slice(0,a).join("/"),h)for(s=h.length;s>0;s-=1)if((n=getOwn(m,h.slice(0,s).join("/")))&&(n=getOwn(n,u))){d=n,p=a;break e}!f&&g&&getOwn(g,u)&&(f=getOwn(g,u),l=a)}!d&&f&&(d=f,p=l),d&&(o.splice(0,p,d),e=o.join("/"))}return getOwn(y.pkgs,e)||e}function r(e){isBrowser&&each(scripts(),function(t){if(t.getAttribute("data-requiremodule")===e&&t.getAttribute("data-requirecontext")===q.contextName)return t.parentNode.removeChild(t),!0})}function n(e){var t=getOwn(y.paths,e);if(t&&isArray(t)&&t.length>1)return t.shift(),q.require.undef(e),q.makeRequire(null,{skipMap:!0})([e]),!0}function o(e){var t,i=e?e.indexOf("!"):-1;return i>-1&&(t=e.substring(0,i),e=e.substring(i+1,e.length)),[t,e]}function a(e,t,r,n){var a,s,u,c,d=null,p=t?t.name:null,f=e,l=!0,h="";return e||(l=!1,e="_@r"+(T+=1)),c=o(e),d=c[0],e=c[1],d&&(d=i(d,p,n),s=getOwn(j,d)),e&&(d?h=r?e:s&&s.normalize?s.normalize(e,function(e){return i(e,p,n)}):-1===e.indexOf("!")?i(e,p,n):e:(d=(c=o(h=i(e,p,n)))[0],h=c[1],r=!0,a=q.nameToUrl(h))),u=!d||s||r?"":"_unnormalized"+(A+=1),{prefix:d,name:h,parentMap:t,unnormalized:!!u,url:a,originalName:f,isDefine:l,id:(d?d+"!"+h:h)+u}}function s(e){var t=e.id,i=getOwn(S,t);return i||(i=S[t]=new q.Module(e)),i}function u(e,t,i){var r=e.id,n=getOwn(S,r);!hasProp(j,r)||n&&!n.defineEmitComplete?(n=s(e)).error&&"error"===t?i(n.error):n.on(t,i):"defined"===t&&i(j[r])}function c(e,t){var i=e.requireModules,r=!1;t?t(e):(each(i,function(t){var i=getOwn(S,t);i&&(i.error=e,i.events.error&&(r=!0,i.emit("error",e)))}),r||req.onError(e))}function d(){globalDefQueue.length&&(each(globalDefQueue,function(e){var t=e[0];"string"==typeof t&&(q.defQueueMap[t]=!0),O.push(e)}),globalDefQueue=[])}function p(e){delete S[e],delete k[e]}function f(e,t,i){var r=e.map.id;e.error?e.emit("error",e.error):(t[r]=!0,each(e.depMaps,function(r,n){var o=r.id,a=getOwn(S,o);!a||e.depMatched[n]||i[o]||(getOwn(t,o)?(e.defineDep(n,j[o]),e.check()):f(a,t,i))}),i[r]=!0)}function l(){var e,t,i=1e3*y.waitSeconds,o=i&&q.startTime+i<(new Date).getTime(),a=[],s=[],u=!1,d=!0;if(!x){if(x=!0,eachProp(k,function(e){var i=e.map,c=i.id;if(e.enabled&&(i.isDefine||s.push(e),!e.error))if(!e.inited&&o)n(c)?(t=!0,u=!0):(a.push(c),r(c));else if(!e.inited&&e.fetched&&i.isDefine&&(u=!0,!i.prefix))return d=!1}),o&&a.length)return e=makeError("timeout","Load timeout for modules: "+a,null,a),e.contextName=q.contextName,c(e);d&&each(s,function(e){f(e,{},{})}),o&&!t||!u||!isBrowser&&!isWebWorker||w||(w=setTimeout(function(){w=0,l()},50)),x=!1}}function h(e){hasProp(j,e[0])||s(a(e[0],null,!0)).init(e[1],e[2])}function m(e,t,i,r){e.detachEvent&&!isOpera?r&&e.detachEvent(r,t):e.removeEventListener(i,t,!1)}function g(e){var t=e.currentTarget||e.srcElement;return m(t,q.onScriptLoad,"load","onreadystatechange"),m(t,q.onScriptError,"error"),{node:t,id:t&&t.getAttribute("data-requiremodule")}}function v(){var e;for(d();O.length;){if(null===(e=O.shift())[0])return c(makeError("mismatch","Mismatched anonymous define() module: "+e[e.length-1]));h(e)}q.defQueueMap={}}var x,b,q,E,w,y={waitSeconds:7,baseUrl:"./",paths:{},bundles:{},pkgs:{},shim:{},config:{}},S={},k={},M={},O=[],j={},P={},R={},T=1,A=1;return E={require:function(e){return e.require?e.require:e.require=q.makeRequire(e.map)},exports:function(e){if(e.usingExports=!0,e.map.isDefine)return e.exports?j[e.map.id]=e.exports:e.exports=j[e.map.id]={}},module:function(e){return e.module?e.module:e.module={id:e.map.id,uri:e.map.url,config:function(){return getOwn(y.config,e.map.id)||{}},exports:e.exports||(e.exports={})}}},b=function(e){this.events=getOwn(M,e.id)||{},this.map=e,this.shim=getOwn(y.shim,e.id),this.depExports=[],this.depMaps=[],this.depMatched=[],this.pluginMaps={},this.depCount=0},b.prototype={init:function(e,t,i,r){r=r||{},this.inited||(this.factory=t,i?this.on("error",i):this.events.error&&(i=bind(this,function(e){this.emit("error",e)})),this.depMaps=e&&e.slice(0),this.errback=i,this.inited=!0,this.ignore=r.ignore,r.enabled||this.enabled?this.enable():this.check())},defineDep:function(e,t){this.depMatched[e]||(this.depMatched[e]=!0,this.depCount-=1,this.depExports[e]=t)},fetch:function(){if(!this.fetched){this.fetched=!0,q.startTime=(new Date).getTime();var e=this.map;if(!this.shim)return e.prefix?this.callPlugin():this.load();q.makeRequire(this.map,{enableBuildCallback:!0})(this.shim.deps||[],bind(this,function(){return e.prefix?this.callPlugin():this.load()}))}},load:function(){var e=this.map.url;P[e]||(P[e]=!0,q.load(this.map.id,e))},check:function(){if(this.enabled&&!this.enabling){var e,t,i=this.map.id,r=this.depExports,n=this.exports,o=this.factory;if(this.inited){if(this.error)this.emit("error",this.error);else if(!this.defining){if(this.defining=!0,this.depCount<1&&!this.defined){if(isFunction(o)){if(this.events.error&&this.map.isDefine||req.onError!==defaultOnError)try{n=q.execCb(i,o,r,n)}catch(t){e=t}else n=q.execCb(i,o,r,n);if(this.map.isDefine&&void 0===n&&((t=this.module)?n=t.exports:this.usingExports&&(n=this.exports)),e)return e.requireMap=this.map,e.requireModules=this.map.isDefine?[this.map.id]:null,e.requireType=this.map.isDefine?"define":"require",c(this.error=e)}else n=o;if(this.exports=n,this.map.isDefine&&!this.ignore&&(j[i]=n,req.onResourceLoad)){var a=[];each(this.depMaps,function(e){a.push(e.normalizedMap||e)}),req.onResourceLoad(q,this.map,a)}p(i),this.defined=!0}this.defining=!1,this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete=!0)}}else hasProp(q.defQueueMap,i)||this.fetch()}},callPlugin:function(){var e=this.map,t=e.id,r=a(e.prefix);this.depMaps.push(r),u(r,"defined",bind(this,function(r){var n,o,d,f=getOwn(R,this.map.id),l=this.map.name,h=this.map.parentMap?this.map.parentMap.name:null,m=q.makeRequire(e.parentMap,{enableBuildCallback:!0});return this.map.unnormalized?(r.normalize&&(l=r.normalize(l,function(e){return i(e,h,!0)})||""),o=a(e.prefix+"!"+l,this.map.parentMap,!0),u(o,"defined",bind(this,function(e){this.map.normalizedMap=o,this.init([],function(){return e},null,{enabled:!0,ignore:!0})})),void((d=getOwn(S,o.id))&&(this.depMaps.push(o),this.events.error&&d.on("error",bind(this,function(e){this.emit("error",e)})),d.enable()))):f?(this.map.url=q.nameToUrl(f),void this.load()):((n=bind(this,function(e){this.init([],function(){return e},null,{enabled:!0})})).error=bind(this,function(e){this.inited=!0,this.error=e,e.requireModules=[t],eachProp(S,function(e){0===e.map.id.indexOf(t+"_unnormalized")&&p(e.map.id)}),c(e)}),n.fromText=bind(this,function(i,r){var o=e.name,u=a(o),d=useInteractive;r&&(i=r),d&&(useInteractive=!1),s(u),hasProp(y.config,t)&&(y.config[o]=y.config[t]);try{req.exec(i)}catch(e){return c(makeError("fromtexteval","fromText eval for "+t+" failed: "+e,e,[t]))}d&&(useInteractive=!0),this.depMaps.push(u),q.completeLoad(o),m([o],n)}),void r.load(e.name,m,n,y))})),q.enable(r,this),this.pluginMaps[r.id]=r},enable:function(){k[this.map.id]=this,this.enabled=!0,this.enabling=!0,each(this.depMaps,bind(this,function(e,t){var i,r,n;if("string"==typeof e){if(e=a(e,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap),this.depMaps[t]=e,n=getOwn(E,e.id))return void(this.depExports[t]=n(this));this.depCount+=1,u(e,"defined",bind(this,function(e){this.undefed||(this.defineDep(t,e),this.check())})),this.errback?u(e,"error",bind(this,this.errback)):this.events.error&&u(e,"error",bind(this,function(e){this.emit("error",e)}))}i=e.id,r=S[i],hasProp(E,i)||!r||r.enabled||q.enable(e,this)})),eachProp(this.pluginMaps,bind(this,function(e){var t=getOwn(S,e.id);t&&!t.enabled&&q.enable(e,this)})),this.enabling=!1,this.check()},on:function(e,t){var i=this.events[e];i||(i=this.events[e]=[]),i.push(t)},emit:function(e,t){each(this.events[e],function(e){e(t)}),"error"===e&&delete this.events[e]}},q={config:y,contextName:e,registry:S,defined:j,urlFetched:P,defQueue:O,defQueueMap:{},Module:b,makeModuleMap:a,nextTick:req.nextTick,onError:c,configure:function(e){if(e.baseUrl&&"/"!==e.baseUrl.charAt(e.baseUrl.length-1)&&(e.baseUrl+="/"),"string"==typeof e.urlArgs){var t=e.urlArgs;e.urlArgs=function(e,i){return(-1===i.indexOf("?")?"?":"&")+t}}var i=y.shim,r={paths:!0,bundles:!0,config:!0,map:!0};eachProp(e,function(e,t){r[t]?(y[t]||(y[t]={}),mixin(y[t],e,!0,!0)):y[t]=e}),e.bundles&&eachProp(e.bundles,function(e,t){each(e,function(e){e!==t&&(R[e]=t)})}),e.shim&&(eachProp(e.shim,function(e,t){isArray(e)&&(e={deps:e}),!e.exports&&!e.init||e.exportsFn||(e.exportsFn=q.makeShimExports(e)),i[t]=e}),y.shim=i),e.packages&&each(e.packages,function(e){var t;t=(e="string"==typeof e?{name:e}:e).name,e.location&&(y.paths[t]=e.location),y.pkgs[t]=e.name+"/"+(e.main||"main").replace(currDirRegExp,"").replace(jsSuffixRegExp,"")}),eachProp(S,function(e,t){e.inited||e.map.unnormalized||(e.map=a(t,null,!0))}),(e.deps||e.callback)&&q.require(e.deps||[],e.callback)},makeShimExports:function(e){return function(){var t;return e.init&&(t=e.init.apply(global,arguments)),t||e.exports&&getGlobal(e.exports)}},makeRequire:function(t,n){function o(i,r,u){var d,p,f;return n.enableBuildCallback&&r&&isFunction(r)&&(r.__requireJsBuild=!0),"string"==typeof i?isFunction(r)?c(makeError("requireargs","Invalid require call"),u):t&&hasProp(E,i)?E[i](S[t.id]):req.get?req.get(q,i,t,o):(p=a(i,t,!1,!0),d=p.id,hasProp(j,d)?j[d]:c(makeError("notloaded",'Module name "'+d+'" has not been loaded yet for context: '+e+(t?"":". Use require([])")))):(v(),q.nextTick(function(){v(),(f=s(a(null,t))).skipMap=n.skipMap,f.init(i,r,u,{enabled:!0}),l()}),o)}return n=n||{},mixin(o,{isBrowser:isBrowser,toUrl:function(e){var r,n=e.lastIndexOf("."),o=e.split("/")[0],a="."===o||".."===o;return-1!==n&&(!a||n>1)&&(r=e.substring(n,e.length),e=e.substring(0,n)),q.nameToUrl(i(e,t&&t.id,!0),r,!0)},defined:function(e){return hasProp(j,a(e,t,!1,!0).id)},specified:function(e){return e=a(e,t,!1,!0).id,hasProp(j,e)||hasProp(S,e)}}),t||(o.undef=function(e){d();var i=a(e,t,!0),n=getOwn(S,e);n.undefed=!0,r(e),delete j[e],delete P[i.url],delete M[e],eachReverse(O,function(t,i){t[0]===e&&O.splice(i,1)}),delete q.defQueueMap[e],n&&(n.events.defined&&(M[e]=n.events),p(e))}),o},enable:function(e){getOwn(S,e.id)&&s(e).enable()},completeLoad:function(e){var t,i,r,o=getOwn(y.shim,e)||{},a=o.exports;for(d();O.length;){if(null===(i=O.shift())[0]){if(i[0]=e,t)break;t=!0}else i[0]===e&&(t=!0);h(i)}if(q.defQueueMap={},r=getOwn(S,e),!t&&!hasProp(j,e)&&r&&!r.inited){if(!(!y.enforceDefine||a&&getGlobal(a)))return n(e)?void 0:c(makeError("nodefine","No define call for "+e,null,[e]));h([e,o.deps||[],o.exportsFn])}l()},nameToUrl:function(e,t,i){var r,n,o,a,s,u,c,d=getOwn(y.pkgs,e);if(d&&(e=d),c=getOwn(R,e))return q.nameToUrl(c,t,i);if(req.jsExtRegExp.test(e))s=e+(t||"");else{for(r=y.paths,o=(n=e.split("/")).length;o>0;o-=1)if(a=n.slice(0,o).join("/"),u=getOwn(r,a)){isArray(u)&&(u=u[0]),n.splice(0,o,u);break}s=n.join("/"),s=("/"===(s+=t||(/^data\:|^blob\:|\?/.test(s)||i?"":".js")).charAt(0)||s.match(/^[\w\+\.\-]+:/)?"":y.baseUrl)+s}return y.urlArgs&&!/^blob\:/.test(s)?s+y.urlArgs(e,s):s},load:function(e,t){req.load(q,e,t)},execCb:function(e,t,i,r){return t.apply(r,i)},onScriptLoad:function(e){if("load"===e.type||readyRegExp.test((e.currentTarget||e.srcElement).readyState)){interactiveScript=null;var t=g(e);q.completeLoad(t.id)}},onScriptError:function(e){var t=g(e);if(!n(t.id)){var i=[];return eachProp(S,function(e,r){0!==r.indexOf("_@r")&&each(e.depMaps,function(e){if(e.id===t.id)return i.push(r),!0})}),c(makeError("scripterror",'Script error for "'+t.id+(i.length?'", needed by: '+i.join(", "):'"'),e,[t.id]))}}},q.require=q.makeRequire(),q}function getInteractiveScript(){return interactiveScript&&"interactive"===interactiveScript.readyState?interactiveScript:(eachReverse(scripts(),function(e){if("interactive"===e.readyState)return interactiveScript=e}),interactiveScript)}var req,s,head,baseElement,dataMain,src,interactiveScript,currentlyAddingScript,mainScript,subPath,version="2.3.4",commentRegExp=/\/\*[\s\S]*?\*\/|([^:"'=]|^)\/\/.*$/gm,cjsRequireRegExp=/[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g,jsSuffixRegExp=/\.js$/,currDirRegExp=/^\.\//,op=Object.prototype,ostring=op.toString,hasOwn=op.hasOwnProperty,isBrowser=!("undefined"==typeof window||"undefined"==typeof navigator||!window.document),isWebWorker=!isBrowser&&"undefined"!=typeof importScripts,readyRegExp=isBrowser&&"PLAYSTATION 3"===navigator.platform?/^complete$/:/^(complete|loaded)$/,defContextName="_",isOpera="undefined"!=typeof opera&&"[object Opera]"===opera.toString(),contexts={},cfg={},globalDefQueue=[],useInteractive=!1;if(void 0===define){if(void 0!==requirejs){if(isFunction(requirejs))return;cfg=requirejs,requirejs=void 0}void 0===require||isFunction(require)||(cfg=require,require=void 0),req=requirejs=function(e,t,i,r){var n,o,a=defContextName;return isArray(e)||"string"==typeof e||(o=e,isArray(t)?(e=t,t=i,i=r):e=[]),o&&o.context&&(a=o.context),(n=getOwn(contexts,a))||(n=contexts[a]=req.s.newContext(a)),o&&n.configure(o),n.require(e,t,i)},req.config=function(e){return req(e)},req.nextTick=void 0!==setTimeout?function(e){setTimeout(e,4)}:function(e){e()},require||(require=req),req.version=version,req.jsExtRegExp=/^\/|:|\?|\.js$/,req.isBrowser=isBrowser,s=req.s={contexts:contexts,newContext:newContext},req({}),each(["toUrl","undef","defined","specified"],function(e){req[e]=function(){var t=contexts[defContextName];return t.require[e].apply(t,arguments)}}),isBrowser&&(head=s.head=document.getElementsByTagName("head")[0],(baseElement=document.getElementsByTagName("base")[0])&&(head=s.head=baseElement.parentNode)),req.onError=defaultOnError,req.createNode=function(e,t,i){var r=e.xhtml?document.createElementNS("http://www.w3.org/1999/xhtml","html:script"):document.createElement("script");return r.type=e.scriptType||"text/javascript",r.charset="utf-8",r.async=!0,r},req.load=function(e,t,i){var r,n=e&&e.config||{};if(isBrowser)return(r=req.createNode(n,t,i)).setAttribute("data-requirecontext",e.contextName),r.setAttribute("data-requiremodule",t),!r.attachEvent||r.attachEvent.toString&&r.attachEvent.toString().indexOf("[native code")<0||isOpera?(r.addEventListener("load",e.onScriptLoad,!1),r.addEventListener("error",e.onScriptError,!1)):(useInteractive=!0,r.attachEvent("onreadystatechange",e.onScriptLoad)),r.src=i,n.onNodeCreated&&n.onNodeCreated(r,n,t,i),currentlyAddingScript=r,baseElement?head.insertBefore(r,baseElement):head.appendChild(r),currentlyAddingScript=null,r;if(isWebWorker)try{setTimeout(function(){},0),importScripts(i),e.completeLoad(t)}catch(r){e.onError(makeError("importscripts","importScripts failed for "+t+" at "+i,r,[t]))}},isBrowser&&!cfg.skipDataMain&&eachReverse(scripts(),function(e){if(head||(head=e.parentNode),dataMain=e.getAttribute("data-main"))return mainScript=dataMain,cfg.baseUrl||-1!==mainScript.indexOf("!")||(src=mainScript.split("/"),mainScript=src.pop(),subPath=src.length?src.join("/")+"/":"./",cfg.baseUrl=subPath),mainScript=mainScript.replace(jsSuffixRegExp,""),req.jsExtRegExp.test(mainScript)&&(mainScript=dataMain),cfg.deps=cfg.deps?cfg.deps.concat(mainScript):[mainScript],!0}),define=function(e,t,i){var r,n;"string"!=typeof e&&(i=t,t=e,e=null),isArray(t)||(i=t,t=null),!t&&isFunction(i)&&(t=[],i.length&&(i.toString().replace(commentRegExp,commentReplace).replace(cjsRequireRegExp,function(e,i){t.push(i)}),t=(1===i.length?["require"]:["require","exports","module"]).concat(t))),useInteractive&&(r=currentlyAddingScript||getInteractiveScript())&&(e||(e=r.getAttribute("data-requiremodule")),n=contexts[r.getAttribute("data-requirecontext")]),n?(n.defQueue.push([e,t,i]),n.defQueueMap[e]=!0):globalDefQueue.push([e,t,i])},define.amd={jQuery:!0},req.exec=function(text){return eval(text)},req(cfg)}}(this,"undefined"==typeof setTimeout?void 0:setTimeout); diff --git a/src/jsMain/resources/style/style.scss b/src/jsMain/resources/style/style.scss new file mode 100644 index 0000000..6edd8f2 --- /dev/null +++ b/src/jsMain/resources/style/style.scss @@ -0,0 +1,5 @@ +$primary-background-color: yellow; + +body { + background: $primary-background-color; +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/de/kif/backend/main.kt b/src/jvmMain/kotlin/de/kif/backend/main.kt new file mode 100644 index 0000000..7d1b8c4 --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/main.kt @@ -0,0 +1,58 @@ +package de.kif.backend + +import io.ktor.application.call +import io.ktor.html.respondHtml +import io.ktor.http.content.files +import io.ktor.http.content.static +import io.ktor.routing.get +import io.ktor.routing.routing +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import kotlinx.html.* +import java.io.File + + +fun main() { + embeddedServer(Netty, port = 8080, host = "127.0.0.1") { + val currentDir = File(".").absoluteFile.canonicalFile + + val webDir = listOf( + "web", + "../web" + ).map { + File(currentDir, it).canonicalFile + }.firstOrNull { it.isDirectory }?.absoluteFile ?: error("Can't find 'web' folder!") + + val modules = webDir.list().toList().filter { + it.endsWith(".js") && + !it.endsWith(".meta.js") && + !it.endsWith("-test.js") && + it != "require.min.js" + }.joinToString(", ") { "'${it.take(it.length - 3)}'" } + + environment.log.info("Web directory: $webDir") + + routing { + get("/") { + call.respondHtml { + head { + title("KIF Portal") + + link(href = "/static/style/style.css", type = "text/css", rel = "stylesheet") + + script(src = "/static/require.min.js") {} + + script { + +"require.config({baseUrl: '/static'});\n" + +("require([$modules]);\n") + } + } + body {} + } + } + static("/static") { + files(webDir) + } + } + }.start(wait = true) +} diff --git a/src/jvmMain/resources/logback.xml b/src/jvmMain/resources/logback.xml new file mode 100644 index 0000000..57a363c --- /dev/null +++ b/src/jvmMain/resources/logback.xml @@ -0,0 +1,15 @@ + + + + + + true + + %d{HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{20}) - %msg %n + + + + + + +