aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/pl
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/kotlin/pl')
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/KVManager.kt18
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/form/Form.kt19
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/form/FormControl.kt18
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/form/FormPanel.kt19
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/form/upload/Upload.kt304
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/form/upload/UploadInput.kt270
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/utils/Utils.kt22
7 files changed, 668 insertions, 2 deletions
diff --git a/src/main/kotlin/pl/treksoft/kvision/KVManager.kt b/src/main/kotlin/pl/treksoft/kvision/KVManager.kt
index 121f0ab7..ee7b2d43 100644
--- a/src/main/kotlin/pl/treksoft/kvision/KVManager.kt
+++ b/src/main/kotlin/pl/treksoft/kvision/KVManager.kt
@@ -44,7 +44,7 @@ external fun require(name: String): dynamic
/**
* Internal singleton object which initializes and configures KVision framework.
*/
-@Suppress("EmptyCatchBlock")
+@Suppress("EmptyCatchBlock", "TooGenericExceptionCaught")
internal object KVManager {
internal const val AJAX_REQUEST_DELAY = 300
internal const val KVNULL = "#kvnull"
@@ -118,6 +118,22 @@ internal object KVManager {
require("element-resize-event")
} catch (e: Throwable) {
}
+ private val bootstrapFileinputCss = try {
+ require("bootstrap-fileinput/css/fileinput.min.css")
+ } catch (e: Throwable) {
+ }
+ private val bootstrapFileinputCssFa = try {
+ require("bootstrap-fileinput/themes/explorer-fa/theme.min.css")
+ } catch (e: Throwable) {
+ }
+ private val bootstrapFileinput = try {
+ require("bootstrap-fileinput")
+ } catch (e: Throwable) {
+ }
+ private val bootstrapFileinputFa = try {
+ require("bootstrap-fileinput/themes/explorer-fa/theme.min.js")
+ } catch (e: Throwable) {
+ }
private val resizable = require("jquery-resizable-dom")
internal val fecha = require("fecha")
diff --git a/src/main/kotlin/pl/treksoft/kvision/form/Form.kt b/src/main/kotlin/pl/treksoft/kvision/form/Form.kt
index 4ce21ec6..65161303 100644
--- a/src/main/kotlin/pl/treksoft/kvision/form/Form.kt
+++ b/src/main/kotlin/pl/treksoft/kvision/form/Form.kt
@@ -21,6 +21,7 @@
*/
package pl.treksoft.kvision.form
+import org.w3c.files.File
import kotlin.js.Date
import kotlin.js.Json
import kotlin.reflect.KProperty1
@@ -42,6 +43,7 @@ internal data class FieldParams<in F : FormControl>(
* @param panel optional instance of [FormPanel]
* @param modelFactory function transforming a Map<String, Any?> to a data model of class K
*/
+@Suppress("TooManyFunctions")
class Form<K>(private val panel: FormPanel<K>? = null, private val modelFactory: (Map<String, Any?>) -> K) {
internal val fields: MutableMap<String, FormControl> = mutableMapOf()
@@ -128,6 +130,23 @@ class Form<K>(private val panel: FormPanel<K>? = null, private val modelFactory:
}
/**
+ * Adds a files control to the form.
+ * @param key key identifier of the control
+ * @param control the files form control
+ * @param required determines if the control is required
+ * @param validatorMessage optional function returning validation message
+ * @param validator optional validation function
+ * @return current form
+ */
+ fun <C : FilesFormControl> add(
+ key: KProperty1<K, List<File>?>, control: C, required: Boolean = false,
+ validatorMessage: ((C) -> String?)? = null,
+ validator: ((C) -> Boolean?)? = null
+ ): Form<K> {
+ return addInternal(key, control, required, validatorMessage, validator)
+ }
+
+ /**
* Removes a control from the form.
* @param key key identifier of the control
* @return current form
diff --git a/src/main/kotlin/pl/treksoft/kvision/form/FormControl.kt b/src/main/kotlin/pl/treksoft/kvision/form/FormControl.kt
index 091f4162..be27ac7a 100644
--- a/src/main/kotlin/pl/treksoft/kvision/form/FormControl.kt
+++ b/src/main/kotlin/pl/treksoft/kvision/form/FormControl.kt
@@ -21,6 +21,7 @@
*/
package pl.treksoft.kvision.form
+import org.w3c.files.File
import pl.treksoft.kvision.core.Component
import kotlin.js.Date
@@ -202,3 +203,20 @@ interface DateFormControl : FormControl {
override fun getValueAsString(): String? = value?.toString()
}
+
+/**
+ * Base interface of a form control with a list of files value.
+ */
+interface FilesFormControl : FormControl {
+ /**
+ * List of files value.
+ */
+ var value: List<File>?
+
+ override fun getValue(): List<File>? = value
+ override fun setValue(v: Any?) {
+ if (v == null) value = null
+ }
+
+ override fun getValueAsString(): String? = value?.joinToString { it.name }
+}
diff --git a/src/main/kotlin/pl/treksoft/kvision/form/FormPanel.kt b/src/main/kotlin/pl/treksoft/kvision/form/FormPanel.kt
index 2a274684..3eb2a0ca 100644
--- a/src/main/kotlin/pl/treksoft/kvision/form/FormPanel.kt
+++ b/src/main/kotlin/pl/treksoft/kvision/form/FormPanel.kt
@@ -22,6 +22,7 @@
package pl.treksoft.kvision.form
import com.github.snabbdom.VNode
+import org.w3c.files.File
import pl.treksoft.kvision.core.Container
import pl.treksoft.kvision.core.StringBoolPair
import pl.treksoft.kvision.core.StringPair
@@ -81,6 +82,7 @@ enum class FormTarget(internal val target: String) {
* @param classes set of CSS class names
* @param modelFactory function transforming a Map<String, Any?> to a data model of class K
*/
+@Suppress("TooManyFunctions")
open class FormPanel<K>(
method: FormMethod? = null, action: String? = null, enctype: FormEnctype? = null,
private val type: FormType? = null, classes: Set<String> = setOf(),
@@ -288,6 +290,23 @@ open class FormPanel<K>(
}
/**
+ * Adds a files control to the form panel.
+ * @param key key identifier of the control
+ * @param control the files form control
+ * @param required determines if the control is required
+ * @param validatorMessage optional function returning validation message
+ * @param validator optional validation function
+ * @return current form panel
+ */
+ open fun <C : FilesFormControl> add(
+ key: KProperty1<K, List<File>?>, control: C, required: Boolean = false,
+ validatorMessage: ((C) -> String?)? = null,
+ validator: ((C) -> Boolean?)? = null
+ ): FormPanel<K> {
+ return addInternal(key, control, required, validatorMessage, validator)
+ }
+
+ /**
* Removes a control from the form panel.
* @param key key identifier of the control
* @return current form panel
diff --git a/src/main/kotlin/pl/treksoft/kvision/form/upload/Upload.kt b/src/main/kotlin/pl/treksoft/kvision/form/upload/Upload.kt
new file mode 100644
index 00000000..e6b397eb
--- /dev/null
+++ b/src/main/kotlin/pl/treksoft/kvision/form/upload/Upload.kt
@@ -0,0 +1,304 @@
+/*
+ * Copyright (c) 2018. Robert Jaros
+ */
+package pl.treksoft.kvision.form.upload
+
+import pl.treksoft.kvision.core.Container
+import pl.treksoft.kvision.core.StringBoolPair
+import pl.treksoft.kvision.core.Widget
+import pl.treksoft.kvision.form.FieldLabel
+import pl.treksoft.kvision.form.FilesFormControl
+import pl.treksoft.kvision.form.HelpBlock
+import pl.treksoft.kvision.panel.SimplePanel
+import pl.treksoft.kvision.utils.SnOn
+
+/**
+ * The form field file upload component.
+ *
+ * @constructor
+ * @param uploadUrl the optional URL for the upload processing action
+ * @param multiple determines if multiple file upload is supported
+ * @param label label text bound to the input element
+ * @param rich determines if [label] can contain HTML code
+ */
+@Suppress("TooManyFunctions")
+open class Upload(
+ uploadUrl: String? = null, multiple: Boolean = false, label: String? = null,
+ rich: Boolean = false
+) : SimplePanel(setOf("form-group")), FilesFormControl {
+
+ /**
+ * File input value.
+ */
+ override var value
+ get() = input.value
+ set(value) {
+ input.value = value
+ }
+ /**
+ * The optional URL for the upload processing action.
+ * If not set the upload button action will default to form submission.
+ */
+ var uploadUrl
+ get() = input.uploadUrl
+ set(value) {
+ input.uploadUrl = value
+ }
+ /**
+ * Determines if multiple file upload is supported.
+ */
+ var multiple
+ get() = input.multiple
+ set(value) {
+ input.multiple = value
+ }
+ /**
+ * The extra data that will be passed as data to the AJAX server call via POST.
+ */
+ var uploadExtraData
+ get() = input.uploadExtraData
+ set(value) {
+ input.uploadExtraData = value
+ }
+ /**
+ * Determines if the explorer theme is used.
+ */
+ var explorerTheme
+ get() = input.explorerTheme
+ set(value) {
+ input.explorerTheme = value
+ }
+ /**
+ * Determines if the input selection is required.
+ */
+ var required
+ get() = input.required
+ set(value) {
+ input.required = value
+ }
+ /**
+ * Determines if the caption is shown.
+ */
+ var showCaption
+ get() = input.showCaption
+ set(value) {
+ input.showCaption = value
+ }
+ /**
+ * Determines if the preview is shown.
+ */
+ var showPreview
+ get() = input.showPreview
+ set(value) {
+ input.showPreview = value
+ }
+ /**
+ * Determines if the remove button is shown.
+ */
+ var showRemove
+ get() = input.showRemove
+ set(value) {
+ input.showRemove = value
+ }
+ /**
+ * Determines if the upload button is shown.
+ */
+ var showUpload
+ get() = input.showUpload
+ set(value) {
+ input.showUpload = value
+ }
+ /**
+ * Determines if the cancel button is shown.
+ */
+ var showCancel
+ get() = input.showCancel
+ set(value) {
+ input.showCancel = value
+ }
+ /**
+ * Determines if the file browse button is shown.
+ */
+ var showBrowse
+ get() = input.showBrowse
+ set(value) {
+ input.showBrowse = value
+ }
+ /**
+ * Determines if the click on the preview zone opens file browse window.
+ */
+ var browseOnZoneClick
+ get() = input.browseOnZoneClick
+ set(value) {
+ input.browseOnZoneClick = value
+ }
+ /**
+ * Determines if the iconic preview is prefered.
+ */
+ var preferIconicPreview
+ get() = input.preferIconicPreview
+ set(value) {
+ input.preferIconicPreview = value
+ }
+ /**
+ * Allowed file types.
+ */
+ var allowedFileTypes
+ get() = input.allowedFileTypes
+ set(value) {
+ input.allowedFileTypes = value
+ }
+ /**
+ * Allowed file extensions.
+ */
+ var allowedFileExtensions
+ get() = input.allowedFileExtensions
+ set(value) {
+ input.allowedFileExtensions = value
+ }
+ /**
+ * Determines if Drag&Drop zone is enabled.
+ */
+ var dropZoneEnabled
+ get() = input.dropZoneEnabled
+ set(value) {
+ input.dropZoneEnabled = value
+ }
+ /**
+ * The label text bound to the spinner input element.
+ */
+ var label
+ get() = flabel.content
+ set(value) {
+ flabel.content = value
+ }
+ /**
+ * Determines if [label] can contain HTML code.
+ */
+ var rich
+ get() = flabel.rich
+ set(value) {
+ flabel.rich = value
+ }
+
+ protected val idc = "kv_form_upload_$counter"
+ final override val input: UploadInput = UploadInput(uploadUrl, multiple)
+ .apply {
+ this.id = idc
+ this.name = name
+ }
+ final override val flabel: FieldLabel = FieldLabel(idc, label, rich)
+ final override val validationInfo: HelpBlock = HelpBlock().apply { visible = false }
+
+ init {
+ @Suppress("LeakingThis")
+ input.eventTarget = this
+ this.addInternal(flabel)
+ this.addInternal(input)
+ this.addInternal(validationInfo)
+ counter++
+ }
+
+ override fun getSnClass(): List<StringBoolPair> {
+ val cl = super.getSnClass().toMutableList()
+ if (validatorError != null) {
+ cl.add("has-error" to true)
+ }
+ return cl
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ override fun <T : Widget> setEventListener(block: SnOn<T>.() -> Unit): Widget {
+ input.setEventListener(block)
+ return this
+ }
+
+ override fun setEventListener(block: SnOn<Widget>.() -> Unit): Widget {
+ input.setEventListener(block)
+ return this
+ }
+
+ override fun removeEventListeners(): Widget {
+ input.removeEventListeners()
+ return this
+ }
+
+ override fun getValueAsString(): String? {
+ return input.getValueAsString()
+ }
+
+ /**
+ * Resets the file input control.
+ */
+ open fun resetInput() {
+ input.resetInput()
+ }
+
+ /**
+ * Clears the file input control (including the native input).
+ */
+ open fun clearInput() {
+ input.clearInput()
+ }
+
+ /**
+ * Trigger ajax upload (only for ajax mode).
+ */
+ open fun upload() {
+ input.upload()
+ }
+
+ /**
+ * Cancel an ongoing ajax upload (only for ajax mode).
+ */
+ open fun cancel() {
+ input.cancel()
+ }
+
+ /**
+ * Locks the file input (disabling all buttons except a cancel button).
+ */
+ open fun lock() {
+ input.lock()
+ }
+
+ /**
+ * Unlocks the file input.
+ */
+ open fun unlock() {
+ input.unlock()
+ }
+
+ override fun focus() {
+ input.focus()
+ }
+
+ override fun blur() {
+ input.blur()
+ }
+
+ companion object {
+ internal var counter = 0
+
+ /**
+ * DSL builder extension function.
+ *
+ * It takes the same parameters as the constructor of the built component.
+ */
+ fun Container.upload(
+ uploadUrl: String? = null,
+ multiple: Boolean = false,
+ label: String? = null,
+ rich: Boolean = false,
+ init: (Upload.() -> Unit)? = null
+ ): Upload {
+ val upload = Upload(uploadUrl, multiple, label, rich).apply {
+ init?.invoke(
+ this
+ )
+ }
+ this.add(upload)
+ return upload
+ }
+ }
+}
diff --git a/src/main/kotlin/pl/treksoft/kvision/form/upload/UploadInput.kt b/src/main/kotlin/pl/treksoft/kvision/form/upload/UploadInput.kt
new file mode 100644
index 00000000..b0c07c3e
--- /dev/null
+++ b/src/main/kotlin/pl/treksoft/kvision/form/upload/UploadInput.kt
@@ -0,0 +1,270 @@
+/*
+ * Copyright (c) 2018. Robert Jaros
+ */
+package pl.treksoft.kvision.form.upload
+
+import com.github.snabbdom.VNode
+import org.w3c.files.File
+import pl.treksoft.kvision.core.Container
+import pl.treksoft.kvision.core.StringBoolPair
+import pl.treksoft.kvision.core.StringPair
+import pl.treksoft.kvision.core.Widget
+import pl.treksoft.kvision.form.FormInput
+import pl.treksoft.kvision.form.InputSize
+import pl.treksoft.kvision.utils.obj
+
+/**
+ * The file upload component.
+ *
+ * @constructor
+ * @param uploadUrl the optional URL for the upload processing action
+ * @param multiple determines if multiple file upload is supported
+ * @param classes a set of CSS class names
+ */
+@Suppress("TooManyFunctions")
+open class UploadInput(uploadUrl: String? = null, multiple: Boolean = false, classes: Set<String> = setOf()) :
+ Widget(classes + "form-control"), FormInput {
+
+ /**
+ * File input value.
+ */
+ var value: List<File>?
+ get() = getValue()
+ set(value) {
+ if (value == null) resetInput()
+ }
+
+ /**
+ * The optional URL for the upload processing action.
+ * If not set the upload button action will default to form submission.
+ */
+ var uploadUrl: String? by refreshOnUpdate(uploadUrl, { refreshUploadInput() })
+ /**
+ * Determines if multiple file upload is supported.
+ */
+ var multiple: Boolean by refreshOnUpdate(multiple, { refresh(); refreshUploadInput() })
+ /**
+ * The extra data that will be passed as data to the AJAX server call via POST.
+ */
+ var uploadExtraData: ((String, Int) -> dynamic)? by refreshOnUpdate({ refreshUploadInput() })
+ /**
+ * Determines if the explorer theme is used.
+ */
+ var explorerTheme: Boolean by refreshOnUpdate(false, { refreshUploadInput() })
+ /**
+ * Determines if the input selection is required.
+ */
+ var required: Boolean by refreshOnUpdate(false, { refreshUploadInput() })
+ /**
+ * Determines if the caption is shown.
+ */
+ var showCaption: Boolean by refreshOnUpdate(true, { refreshUploadInput() })
+ /**
+ * Determines if the preview is shown.
+ */
+ var showPreview: Boolean by refreshOnUpdate(true, { refreshUploadInput() })
+ /**
+ * Determines if the remove button is shown.
+ */
+ var showRemove: Boolean by refreshOnUpdate(true, { refreshUploadInput() })
+ /**
+ * Determines if the upload button is shown.
+ */
+ var showUpload: Boolean by refreshOnUpdate(true, { refreshUploadInput() })
+ /**
+ * Determines if the cancel button is shown.
+ */
+ var showCancel: Boolean by refreshOnUpdate(true, { refreshUploadInput() })
+ /**
+ * Determines if the file browse button is shown.
+ */
+ var showBrowse: Boolean by refreshOnUpdate(true, { refreshUploadInput() })
+ /**
+ * Determines if the click on the preview zone opens file browse window.
+ */
+ var browseOnZoneClick: Boolean by refreshOnUpdate(true, { refreshUploadInput() })
+ /**
+ * Determines if the iconic preview is prefered.
+ */
+ var preferIconicPreview: Boolean by refreshOnUpdate(false, { refreshUploadInput() })
+ /**
+ * Allowed file types.
+ */
+ var allowedFileTypes: Set<String>? by refreshOnUpdate({ refreshUploadInput() })
+ /**
+ * Allowed file extensions.
+ */
+ var allowedFileExtensions: Set<String>? by refreshOnUpdate({ refreshUploadInput() })
+ /**
+ * Determines if Drag&Drop zone is enabled.
+ */
+ var dropZoneEnabled: Boolean by refreshOnUpdate(true, { refreshUploadInput() })
+ /**
+ * The name attribute of the generated HTML input element.
+ */
+ override var name: String? by refreshOnUpdate()
+ /**
+ * Determines if the field is disabled.
+ */
+ override var disabled by refreshOnUpdate(false, { refresh(); refreshUploadInput() })
+ /**
+ * The size of the input (currently not working)
+ */
+ override var size: InputSize? by refreshOnUpdate()
+
+ override fun render(): VNode {
+ return render("input")
+ }
+
+ override fun getSnClass(): List<StringBoolPair> {
+ val cl = super.getSnClass().toMutableList()
+ size?.let {
+ cl.add(it.className to true)
+ }
+ return cl
+ }
+
+ override fun getSnAttrs(): List<StringPair> {
+ val sn = super.getSnAttrs().toMutableList()
+ sn.add("type" to "file")
+ name?.let {
+ sn.add("name" to it)
+ }
+ if (multiple) {
+ sn.add("multiple" to "true")
+ }
+ if (disabled) {
+ sn.add("disabled" to "disabled")
+ }
+ return sn
+ }
+
+ private fun getValue(): List<File>? {
+ val v = getFiles()
+ return if (v.isNotEmpty()) v else null
+ }
+
+ override fun afterInsert(node: VNode) {
+ getElementJQueryD()?.fileinput(getSettingsObj())
+ }
+
+ override fun afterDestroy() {
+ getElementJQueryD()?.fileinput("destroy")
+ }
+
+ private fun refreshUploadInput() {
+ getElementJQueryD()?.fileinput("refresh", getSettingsObj())
+ }
+
+ /**
+ * Resets the file input control.
+ */
+ open fun resetInput() {
+ getElementJQueryD()?.fileinput("reset")
+ }
+
+ /**
+ * Clears the file input control (including the native input).
+ */
+ open fun clearInput() {
+ getElementJQueryD()?.fileinput("clear")
+ }
+
+ /**
+ * Trigger ajax upload (only for ajax mode).
+ */
+ open fun upload() {
+ getElementJQueryD()?.fileinput("upload")
+ }
+
+ /**
+ * Cancel an ongoing ajax upload (only for ajax mode).
+ */
+ open fun cancel() {
+ getElementJQueryD()?.fileinput("cancel")
+ }
+
+ /**
+ * Locks the file input (disabling all buttons except a cancel button).
+ */
+ open fun lock() {
+ getElementJQueryD()?.fileinput("lock")
+ }
+
+ /**
+ * Unlocks the file input.
+ */
+ open fun unlock() {
+ getElementJQueryD()?.fileinput("unlock")
+ }
+
+ private fun getFiles(): List<File> {
+ return (getElementJQueryD()?.fileinput("getFileStack") as Array<File>).toList()
+ }
+
+ /**
+ * Returns the value of the file input control as a String.
+ * @return value as a String
+ */
+ fun getValueAsString(): String? {
+ return value?.joinToString { it.name }
+ }
+
+ /**
+ * Makes the input element focused.
+ */
+ open fun focus() {
+ getElementJQuery()?.focus()
+ }
+
+ /**
+ * Makes the input element blur.
+ */
+ open fun blur() {
+ getElementJQuery()?.blur()
+ }
+
+ private fun getSettingsObj(): dynamic {
+ return obj {
+ this.uploadUrl = uploadUrl
+ this.uploadExtraData = uploadExtraData ?: undefined
+ this.theme = if (explorerTheme) "explorer-fa" else null
+ this.required = required
+ this.showCaption = showCaption
+ this.showPreview = showPreview
+ this.showRemove = showRemove
+ this.showUpload = showUpload
+ this.showCancel = showCancel
+ this.showBrowse = showBrowse
+ this.browseOnZoneClick = browseOnZoneClick
+ this.preferIconicPreview = preferIconicPreview
+ this.allowedFileTypes = allowedFileTypes?.toTypedArray()
+ this.allowedFileExtensions = allowedFileExtensions?.toTypedArray()
+ this.dropZoneEnabled = dropZoneEnabled
+ }
+ }
+
+ companion object {
+ internal var counter = 0
+
+ /**
+ * DSL builder extension function.
+ *
+ * It takes the same parameters as the constructor of the built component.
+ */
+ fun Container.uploadInput(
+ uploadUrl: String? = null,
+ multiple: Boolean = false,
+ classes: Set<String> = setOf(),
+ init: (UploadInput.() -> Unit)? = null
+ ): UploadInput {
+ val uploadInput = UploadInput(uploadUrl, multiple, classes).apply {
+ init?.invoke(
+ this
+ )
+ }
+ this.add(uploadInput)
+ return uploadInput
+ }
+ }
+}
diff --git a/src/main/kotlin/pl/treksoft/kvision/utils/Utils.kt b/src/main/kotlin/pl/treksoft/kvision/utils/Utils.kt
index d638f79e..ba9467c3 100644
--- a/src/main/kotlin/pl/treksoft/kvision/utils/Utils.kt
+++ b/src/main/kotlin/pl/treksoft/kvision/utils/Utils.kt
@@ -23,6 +23,9 @@
package pl.treksoft.kvision.utils
+import kotlinx.coroutines.experimental.suspendCancellableCoroutine
+import org.w3c.files.File
+import org.w3c.files.FileReader
import pl.treksoft.kvision.KVManager
import pl.treksoft.kvision.core.CssSize
import pl.treksoft.kvision.core.UNIT
@@ -96,7 +99,7 @@ val Int.mm: CssSize
/**
* Extension property to convert Int to CSS in units.
*/
-@Suppress("FunctionNaming")
+@Suppress("TopLevelPropertyNaming")
val Int.`in`: CssSize
get() {
return Pair(this, UNIT.`in`)
@@ -204,3 +207,20 @@ fun Date.toStringF(format: String = "YYYY-MM-DD HH:mm:ss"): String {
* @return true if the current browser is IE11
*/
fun isIE11(): Boolean = window.navigator.userAgent.matches("Trident\\/7\\.")
+
+/**
+ * Suspending extension function to get file content.
+ * @return file content
+ */
+@Suppress("EXPERIMENTAL_FEATURE_WARNING")
+suspend fun File.getContent() = suspendCancellableCoroutine<String> { cont ->
+ val reader = FileReader()
+ reader.onload = {
+ @Suppress("UnsafeCastFromDynamic")
+ cont.resume(reader.result)
+ }
+ reader.onerror = { e ->
+ cont.resumeWithException(Exception(e.type))
+ }
+ reader.readAsDataURL(this@getContent)
+}