diff options
Diffstat (limited to 'kvision-modules/kvision-richtext')
9 files changed, 524 insertions, 0 deletions
diff --git a/kvision-modules/kvision-richtext/build.gradle b/kvision-modules/kvision-richtext/build.gradle new file mode 100644 index 00000000..c00c4c27 --- /dev/null +++ b/kvision-modules/kvision-richtext/build.gradle @@ -0,0 +1,9 @@ +apply from: "../shared.gradle" + +kotlinFrontend { + + npm { + dependency("trix", "0.11.1") + } + +} diff --git a/kvision-modules/kvision-richtext/package.json.d/project.info b/kvision-modules/kvision-richtext/package.json.d/project.info new file mode 100644 index 00000000..856dc500 --- /dev/null +++ b/kvision-modules/kvision-richtext/package.json.d/project.info @@ -0,0 +1,3 @@ +{ + "description": "KVision RichText module" +} diff --git a/kvision-modules/kvision-richtext/src/main/kotlin/pl/treksoft/kvision/KVManagerRichText.kt b/kvision-modules/kvision-richtext/src/main/kotlin/pl/treksoft/kvision/KVManagerRichText.kt new file mode 100644 index 00000000..1ccd7a85 --- /dev/null +++ b/kvision-modules/kvision-richtext/src/main/kotlin/pl/treksoft/kvision/KVManagerRichText.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * + * 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. + */ +package pl.treksoft.kvision + +import pl.treksoft.kvision.i18n.I18n +import pl.treksoft.kvision.utils.obj +import kotlin.browser.window + +internal val KVManagerRichTextInit = KVManagerRichText.init() + +/** + * Internal singleton object which initializes and configures KVision RichText module. + */ +@Suppress("EmptyCatchBlock", "TooGenericExceptionCaught") +internal object KVManagerRichText { + fun init() {} + + private val trixCss = try { + require("trix/dist/trix.css") + } catch (e: Throwable) { + } + private val trix = try { + val trix = require("trix") + window.asDynamic().Trix = trix + trix.config.languages = obj {} + trix.config.languages["en"] = obj {} + for (key in js("Object").keys(trix.config.lang)) { + trix.config.languages["en"][key] = trix.config.lang[key] + } + val orig = trix.config.toolbar.getDefaultHTML + trix.config.toolbar.getDefaultHTML = { + val config = if (trix.config.languages[I18n.language] != undefined) { + trix.config.languages[I18n.language] + } else { + trix.config.languages["en"] + } + for (key in js("Object").keys(trix.config.lang)) { + trix.config.lang[key] = config[key] + } + orig() + } + require("./js/locales/trix/trix.pl.js") + } catch (e: Throwable) { + } +} diff --git a/kvision-modules/kvision-richtext/src/main/kotlin/pl/treksoft/kvision/form/text/RichText.kt b/kvision-modules/kvision-richtext/src/main/kotlin/pl/treksoft/kvision/form/text/RichText.kt new file mode 100644 index 00000000..22126797 --- /dev/null +++ b/kvision-modules/kvision-richtext/src/main/kotlin/pl/treksoft/kvision/form/text/RichText.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * + * 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. + */ +package pl.treksoft.kvision.form.text + +import pl.treksoft.kvision.core.Container + +/** + * Form field rich text component. + * + * @constructor + * @param value text input value + * @param name the name attribute of the generated HTML input element + * @param label label text bound to the input element + * @param rich determines if [label] can contain HTML code + */ +open class RichText( + value: String? = null, name: String? = null, + label: String? = null, rich: Boolean = false +) : AbstractText(label, rich) { + + /** + * Rich input control height. + */ + var inputHeight + get() = input.height + set(value) { + input.height = value + } + + final override val input: RichTextInput = RichTextInput(value).apply { + this.id = idc + this.name = name + } + + init { + @Suppress("LeakingThis") + input.eventTarget = this + this.addInternal(input) + this.addInternal(validationInfo) + } + + companion object { + /** + * DSL builder extension function. + * + * It takes the same parameters as the constructor of the built component. + */ + fun Container.richText( + value: String? = null, + name: String? = null, + label: String? = null, + rich: Boolean = false, + init: (RichText.() -> Unit)? = null + ): RichText { + val richText = RichText(value, name, label, rich).apply { init?.invoke(this) } + this.add(richText) + return richText + } + } +} diff --git a/kvision-modules/kvision-richtext/src/main/kotlin/pl/treksoft/kvision/form/text/RichTextInput.kt b/kvision-modules/kvision-richtext/src/main/kotlin/pl/treksoft/kvision/form/text/RichTextInput.kt new file mode 100644 index 00000000..961c27cd --- /dev/null +++ b/kvision-modules/kvision-richtext/src/main/kotlin/pl/treksoft/kvision/form/text/RichTextInput.kt @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * + * 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. + */ +package pl.treksoft.kvision.form.text + +import com.github.snabbdom.VNode +import pl.treksoft.jquery.jQuery +import pl.treksoft.kvision.KVManagerRichText +import pl.treksoft.kvision.core.Container +import pl.treksoft.kvision.core.StringPair +import kotlin.browser.document + +/** + * Basic rich text component. + * + * @constructor + * @param value text input value + * @param classes a set of CSS class names + */ +open class RichTextInput(value: String? = null, classes: Set<String> = setOf()) : + AbstractTextInput(value, classes + "form-control" + "trix-control") { + private var trixId: String? = null + + override fun render(): VNode { + return render("trix-editor") + } + + override fun getSnAttrs(): List<StringPair> { + val sn = super.getSnAttrs().toMutableList() + placeholder?.let { + sn.add("placeholder" to translate(it)) + } + name?.let { + sn.add("name" to it) + } + autofocus?.let { + if (it) { + sn.add("autofocus" to "autofocus") + } + } + if (disabled) { + sn.add("disabled" to "disabled") + } + return sn + } + + @Suppress("UnsafeCastFromDynamic", "ComplexMethod") + override fun afterInsert(node: VNode) { + if (this.disabled || this.readonly == true) { + this.getElementJQuery()?.removeAttr("contenteditable") + } else { + this.getElementJQuery()?.on("trix-change", { _, _ -> + if (trixId != null) { + val v = document.getElementById("trix-input-$trixId")?.let { jQuery(it).`val`() as String? } + value = if (v != null && v.isNotEmpty()) { + v + } else { + null + } + val event = org.w3c.dom.events.Event("change") + this.getElement()?.dispatchEvent(event) + } + }) + } + this.getElementJQuery()?.on("trix-initialize", { _, _ -> + trixId = this.getElementJQuery()?.attr("trix-id") + if (trixId != null) { + value?.let { + if (this.getElement().asDynamic().editor != undefined) { + this.getElement().asDynamic().editor.loadHTML(it) + } + } + } + }) + this.getElementJQuery()?.on("trix-file-accept", { e, _ -> e.preventDefault() }) + } + + override fun afterDestroy() { + document.getElementById("trix-input-$trixId")?.let { jQuery(it).remove() } + document.getElementById("trix-toolbar-$trixId")?.let { jQuery(it).remove() } + trixId = null + } + + @Suppress("UnsafeCastFromDynamic") + override fun refreshState() { + val v = document.getElementById("trix-input-$trixId")?.let { jQuery(it).`val`() as String? } + if (value != v) { + val editor = this.getElement().asDynamic().editor + if (editor != undefined) { + value?.let { + editor.loadHTML(it) + } ?: editor.loadHTML("") + } + } + } + + override fun changeValue() { + // disabled parent class functionality + } + + companion object { + /** + * DSL builder extension function. + * + * It takes the same parameters as the constructor of the built component. + */ + fun Container.richTextInput( + value: String? = null, classes: Set<String> = setOf(), init: (RichTextInput.() -> Unit)? = null + ): RichTextInput { + val richTextInput = RichTextInput(value, classes).apply { init?.invoke(this) } + this.add(richTextInput) + return richTextInput + } + } +} diff --git a/kvision-modules/kvision-richtext/src/main/resources/js/locales/trix/trix.pl.js b/kvision-modules/kvision-richtext/src/main/resources/js/locales/trix/trix.pl.js new file mode 100644 index 00000000..84dbd886 --- /dev/null +++ b/kvision-modules/kvision-richtext/src/main/resources/js/locales/trix/trix.pl.js @@ -0,0 +1,28 @@ +(function($){ + window.Trix.config.languages["pl"] = { + GB:"GB", + KB:"KB", + MB:"MB", + PB:"PB", + TB:"TB", + bold:"Pogrubienie", + bullets:"Wypunktowanie", + byte:"Bajt", + bytes:"Bajty", + captionPlaceholder:"Dodaj tytuł…", + code:"Kod źródłowy", + heading1:"Nagłówek", + indent:"Zwiększ poziom", + italic:"Pochylenie", + link:"Link", + numbers:"Numerowanie", + outdent:"Zmniejsz poziom", + quote:"Cytat", + redo:"Ponów", + remove:"Usuń", + strike:"Przekreślenie", + undo:"Cofnij", + unlink:"Usuń link", + urlPlaceholder:"Wprowadź adres URL…" + } +}(jQuery)); diff --git a/kvision-modules/kvision-richtext/src/test/kotlin/test/pl/treksoft/kvision/TestUtil.kt b/kvision-modules/kvision-richtext/src/test/kotlin/test/pl/treksoft/kvision/TestUtil.kt new file mode 100644 index 00000000..1da1fe1a --- /dev/null +++ b/kvision-modules/kvision-richtext/src/test/kotlin/test/pl/treksoft/kvision/TestUtil.kt @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * + * 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. + */ +package test.pl.treksoft.kvision + +import org.w3c.dom.Element +import pl.treksoft.jquery.jQuery +import pl.treksoft.kvision.core.Widget +import pl.treksoft.kvision.panel.Root +import kotlin.browser.document +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +interface TestSpec { + fun beforeTest() + + fun afterTest() + + fun run(code: () -> Unit) { + beforeTest() + code() + afterTest() + } +} + +interface SimpleSpec : TestSpec { + + override fun beforeTest() { + } + + override fun afterTest() { + } + +} + +interface DomSpec : TestSpec { + + override fun beforeTest() { + val fixture = "<div style=\"display: none\" id=\"pretest\">" + + "<div id=\"test\"></div></div>" + document.body?.insertAdjacentHTML("afterbegin", fixture) + } + + override fun afterTest() { + val div = document.getElementById("pretest") + div?.let { jQuery(it).remove() } + jQuery(`object` = ".modal-backdrop").remove() + } + + fun assertEqualsHtml(expected: String?, actual: String?, message: String?) { + if (expected != null && actual != null) { + val exp = jQuery(html = expected) + val act = jQuery(html = actual) + val result = exp[0]?.isEqualNode(act[0]) + if (result == true) { + assertTrue(result == true, message) + } else { + assertEquals(expected, actual, message) + } + } else { + assertEquals(expected, actual, message) + } + } +} + +interface WSpec : DomSpec { + + fun runW(code: (widget: Widget, element: Element?) -> Unit) { + run { + val root = Root("test", true) + val widget = Widget() + widget.id = "test_id" + root.add(widget) + val element = document.getElementById("test_id") + code(widget, element) + } + } + +} + +external fun require(name: String): dynamic diff --git a/kvision-modules/kvision-richtext/src/test/kotlin/test/pl/treksoft/kvision/form/text/RichTextInputSpec.kt b/kvision-modules/kvision-richtext/src/test/kotlin/test/pl/treksoft/kvision/form/text/RichTextInputSpec.kt new file mode 100644 index 00000000..21b7dc39 --- /dev/null +++ b/kvision-modules/kvision-richtext/src/test/kotlin/test/pl/treksoft/kvision/form/text/RichTextInputSpec.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * + * 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. + */ +package test.pl.treksoft.kvision.form.text + +import pl.treksoft.jquery.jQuery +import pl.treksoft.kvision.form.text.RichTextInput +import pl.treksoft.kvision.panel.Root +import test.pl.treksoft.kvision.DomSpec +import kotlin.browser.document +import kotlin.test.Test +import kotlin.test.assertTrue + +class RichTextInputSpec : DomSpec { + + @Test + fun render() { + run { + val root = Root("test", true) + val hai = RichTextInput(value = "abc").apply { + placeholder = "place" + id = "idti" + } + root.add(hai) + val content = document.getElementById("test")?.let { jQuery(it).find("trix-editor")[0]?.outerHTML } + assertTrue( + content?.startsWith("<trix-editor") == true, + "Should render correct html area control" + ) + } + } + +}
\ No newline at end of file diff --git a/kvision-modules/kvision-richtext/src/test/kotlin/test/pl/treksoft/kvision/form/text/RichTextSpec.kt b/kvision-modules/kvision-richtext/src/test/kotlin/test/pl/treksoft/kvision/form/text/RichTextSpec.kt new file mode 100644 index 00000000..844b7e94 --- /dev/null +++ b/kvision-modules/kvision-richtext/src/test/kotlin/test/pl/treksoft/kvision/form/text/RichTextSpec.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * + * 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. + */ +package test.pl.treksoft.kvision.form.text + +import pl.treksoft.jquery.jQuery +import pl.treksoft.kvision.form.text.RichText +import pl.treksoft.kvision.panel.Root +import test.pl.treksoft.kvision.DomSpec +import kotlin.browser.document +import kotlin.test.Test +import kotlin.test.assertTrue + +class RichTextSpec : DomSpec { + + @Test + fun render() { + run { + val root = Root("test", true) + val hai = RichText(value = "abc", label = "Field").apply { + placeholder = "place" + id = "idti" + } + root.add(hai) + val iid = hai.input.id + val content = document.getElementById("test")?.let { jQuery(it).find("trix-editor")[0]?.outerHTML } + assertTrue( + content?.startsWith("<trix-editor") == true, + "Should render correct html area form control" + ) + val label = document.getElementById("test")?.let { jQuery(it).find("label")[0]?.outerHTML } + assertEqualsHtml( + "<label class=\"control-label\" for=\"$iid\">Field</label>", + label, + "Should render correct label for html area form control" + ) + } + } + +}
\ No newline at end of file |