diff --git a/Module.md b/Module.md index 211a994884..b3ee90cc66 100644 --- a/Module.md +++ b/Module.md @@ -109,6 +109,78 @@ Bootstrap navbar components. Bootstrap offcanvas component. +# Package io.kvision.material.button + +Material web button components (elevated, filled, filled tonal, outlined, text). + +# Package io.kvision.material.checkbox + +Material web checkbox component. + +# Package io.kvision.material.chips + +Material web chips components (chip set, chip). + +# Package io.kvision.material.dialog + +Material web dialog component. + +# Package io.kvision.material.divider + +Material web divider component. + +# Package io.kvision.material.fab + +Material web fab components (fab, branded fab). + +# Package io.kvision.material.icon + +Material web icon component. + +# Package io.kvision.material.iconbutton + +Material web icon button components (filled, filled tonal, outlined, standard). + +# Package io.kvision.material.list + +Material web list components (list, list item). + +# Package io.kvision.material.menu + +Material web menu components (menu, menu item, submenu). + +# Package io.kvision.material.progress + +Material web progress components (circular, linear). + +# Package io.kvision.material.radio + +Material web radio component. + +# Package io.kvision.material.ripple + +Material web ripple component. + +# Package io.kvision.material.select + +Material web select components (filled, outlined, select option). + +# Package io.kvision.material.slider + +Material web slider components (slider, range slider). + +# Package io.kvision.material.switch + +Material web switch component. + +# Package io.kvision.material.tabs + +Material web tabs components (tabs, primary tab, secondary tab). + +# Package io.kvision.material.textfield + +Material web text field component (filled, outlined). + # Package io.kvision.onsenui Onsen UI helper utility functions. diff --git a/build.gradle.kts b/build.gradle.kts index dd5498158f..f76050524b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -49,6 +49,7 @@ val jqueryVersion: String by project val leafletVersion: String by project val geojsonVersion: String by project val geojsonTypesVersion: String by project +val materialVersion: String by project val onsenuiVersion: String by project val paceProgressbarVersion: String by project val printjsVersion: String by project @@ -93,6 +94,7 @@ rootProject.plugins.withType + * + * 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 io.kvision.material + +@RequiresOptIn( + message = "This material API is experimental. It may be changed in the future without notice." +) +@Retention(AnnotationRetention.BINARY) +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +annotation class ExperimentalMaterialApi diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/MaterialModule.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/MaterialModule.kt new file mode 100644 index 0000000000..e3a1348b8f --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/MaterialModule.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material + +import io.kvision.ModuleInitializer +import io.kvision.require +import kotlinx.browser.document + +/** + * @author Maanrifa Bacar Ali + */ +object MaterialModule : ModuleInitializer { + + override fun initialize() { + require("@material/web/all.js") + val typescaleStyles = require("@material/web/typography/md-typescale-styles.js") + document.asDynamic().adoptedStyleSheets.push(typescaleStyles.styles.styleSheet) + } +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/button/MdButton.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/button/MdButton.kt new file mode 100644 index 0000000000..5fc99fc4fe --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/button/MdButton.kt @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.button + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.form.MdFormWidget +import io.kvision.material.slot.HasIconSlot +import io.kvision.material.util.addBool +import io.kvision.material.widget.LinkTarget +import io.kvision.material.widget.Slot +import io.kvision.core.AttributeSetBuilder +import io.kvision.core.Component +import io.kvision.snabbdom.VNode + +enum class ButtonType(internal val value: String) { + Button("button"), + Submit("submit"), + Reset("reset") +} + +/** + * Buttons help people initiate actions, from sending an email, to sharing a document, + * to liking a post. + * + * See https://material-web.dev/components/button/ + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +abstract class MdButton internal constructor( + tag: String, + text: String? = null, + disabled: Boolean = false, + href: String? = null, + target: LinkTarget? = null, + trailingIcon: Boolean = false, + type: ButtonType = ButtonType.Submit, + value: String? = null, + name: String? = null, + className: String? = null, + init: (MdButton.() -> Unit)? = null +) : MdFormWidget( + tag = tag, + disabled = disabled, + value = value, + name = name, + className = className +), HasIconSlot { + + /** + * Button text. + */ + var text: String? by refreshOnUpdate(text) + + /** + * The URL that the link button points to. + */ + var href by refreshOnUpdate(href) + + /** + * Where to display the linked href URL for a link button. + * Common options include _blank to open in a new tab. + */ + var target by refreshOnUpdate(target) + + /** + * Whether to render the icon at the inline end of the label rather than the inline start. + * Note: Link buttons cannot have trailing icons. + */ + var trailingIcon by refreshOnUpdate(trailingIcon) + + /** + * Button type. + */ + var type by refreshOnUpdate(type) + + init { + init?.let { this.it() } + } + + /////////////////////////////////////////////////////////////////////////// + // Rendering + /////////////////////////////////////////////////////////////////////////// + + override fun render(): VNode { + return renderWithTranslatableText(text) + } + + /////////////////////////////////////////////////////////////////////////// + // Attributes + /////////////////////////////////////////////////////////////////////////// + + override fun buildAttributeSet(attributeSetBuilder: AttributeSetBuilder) { + super.buildAttributeSet(attributeSetBuilder) + attributeSetBuilder.add("type", type.value) + + href?.let { + attributeSetBuilder.add("href", it) + } + + target?.let { + attributeSetBuilder.add("target", it.value) + } + + if (trailingIcon) { + attributeSetBuilder.addBool("trailing-icon") + } + } + + /////////////////////////////////////////////////////////////////////////// + // Slots + /////////////////////////////////////////////////////////////////////////// + + override fun icon(component: Component?) { + Slot.Icon(component) + } + + /////////////////////////////////////////////////////////////////////////// + // Events + /////////////////////////////////////////////////////////////////////////// + + override fun hasChangeEvent(): Boolean = false + + override fun hasInputEvent(): Boolean = false +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/button/MdElevatedButton.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/button/MdElevatedButton.kt new file mode 100644 index 0000000000..67b566a8e1 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/button/MdElevatedButton.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.button + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.widget.LinkTarget +import io.kvision.core.Container + +/** + * Elevated buttons are essentially filled tonal buttons with a shadow. To prevent shadow creep, + * only use them when absolutely necessary, such as when the button requires visual separation from + * a patterned background. + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdElevatedButton( + text: String? = null, + disabled: Boolean = false, + href: String? = null, + target: LinkTarget? = null, + trailingIcon: Boolean = false, + type: ButtonType = ButtonType.Submit, + value: String? = null, + name: String? = null, + className: String? = null, + init: (MdElevatedButton.() -> Unit)? = null +) : MdButton( + tag = "md-elevated-button", + text = text, + disabled = disabled, + href = href, + target = target, + trailingIcon = trailingIcon, + type = type, + value = value, + name = name, + className = className +) { + + init { + init?.let { this.it() } + } +} + +@ExperimentalMaterialApi +fun Container.elevatedButton( + text: String? = null, + disabled: Boolean = false, + href: String? = null, + target: LinkTarget? = null, + trailingIcon: Boolean = false, + type: ButtonType = ButtonType.Submit, + value: String? = null, + name: String? = null, + className: String? = null, + init: (MdElevatedButton.() -> Unit)? = null +) = MdElevatedButton( + text = text, + disabled = disabled, + href = href, + target = target, + trailingIcon = trailingIcon, + type = type, + value = value, + name = name, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/button/MdFilledButton.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/button/MdFilledButton.kt new file mode 100644 index 0000000000..5fc7380375 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/button/MdFilledButton.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.button + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.widget.LinkTarget +import io.kvision.core.Container + +/** + * Filled buttons have the most visual impact after the FAB, and should be used for important, + * final actions that complete a flow, like Save, Join now, or Confirm. + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdFilledButton( + text: String? = null, + disabled: Boolean = false, + href: String? = null, + target: LinkTarget? = null, + trailingIcon: Boolean = false, + type: ButtonType = ButtonType.Submit, + value: String? = null, + name: String? = null, + className: String? = null, + init: (MdFilledButton.() -> Unit)? = null +) : MdButton( + tag = "md-filled-button", + text = text, + disabled = disabled, + href = href, + target = target, + trailingIcon = trailingIcon, + type = type, + value = value, + name = name, + className = className +) { + + init { + init?.let { this.it() } + } +} + +@ExperimentalMaterialApi +fun Container.filledButton( + text: String? = null, + disabled: Boolean = false, + href: String? = null, + target: LinkTarget? = null, + trailingIcon: Boolean = false, + type: ButtonType = ButtonType.Submit, + value: String? = null, + name: String? = null, + className: String? = null, + init: (MdFilledButton.() -> Unit)? = null +) = MdFilledButton( + text = text, + disabled = disabled, + href = href, + target = target, + trailingIcon = trailingIcon, + type = type, + value = value, + name = name, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/button/MdFilledTonalButton.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/button/MdFilledTonalButton.kt new file mode 100644 index 0000000000..5ea35e4df0 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/button/MdFilledTonalButton.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.button + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.widget.LinkTarget +import io.kvision.core.Container + +/** + * A filled tonal button is an alternative middle ground between filled and outlined buttons. + * They're useful in contexts where a lower-priority button requires slightly more emphasis than an + * outline would give, such as "Next" in an onboarding flow. + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdFilledTonalButton( + text: String? = null, + disabled: Boolean = false, + href: String? = null, + target: LinkTarget? = null, + trailingIcon: Boolean = false, + type: ButtonType = ButtonType.Submit, + value: String? = null, + name: String? = null, + className: String? = null, + init: (MdFilledTonalButton.() -> Unit)? = null +) : MdButton( + tag = "md-filled-tonal-button", + text = text, + disabled = disabled, + href = href, + target = target, + trailingIcon = trailingIcon, + type = type, + value = value, + name = name, + className = className +) { + + init { + init?.let { this.it() } + } +} + +@ExperimentalMaterialApi +fun Container.filledTonalButton( + text: String? = null, + disabled: Boolean = false, + href: String? = null, + target: LinkTarget? = null, + trailingIcon: Boolean = false, + hasIcon: Boolean = trailingIcon, + type: ButtonType = ButtonType.Submit, + value: String? = null, + name: String? = null, + className: String? = null, + init: (MdFilledTonalButton.() -> Unit)? = null +) = MdFilledTonalButton( + text = text, + disabled = disabled, + href = href, + target = target, + trailingIcon = trailingIcon, + type = type, + value = value, + name = name, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/button/MdOutlinedButton.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/button/MdOutlinedButton.kt new file mode 100644 index 0000000000..9835805f38 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/button/MdOutlinedButton.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.button + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.widget.LinkTarget +import io.kvision.core.Container + +/** + * Outlined buttons are medium-emphasis buttons. + * They contain actions that are important, but aren’t the primary action in an app. + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdOutlinedButton( + text: String? = null, + disabled: Boolean = false, + href: String? = null, + target: LinkTarget? = null, + trailingIcon: Boolean = false, + type: ButtonType = ButtonType.Submit, + value: String? = null, + name: String? = null, + className: String? = null, + init: (MdOutlinedButton.() -> Unit)? = null +) : MdButton( + tag = "md-outlined-button", + text = text, + disabled = disabled, + href = href, + target = target, + trailingIcon = trailingIcon, + type = type, + value = value, + name = name, + className = className +) { + + init { + init?.let { this.it() } + } +} + +@ExperimentalMaterialApi +fun Container.outlinedButton( + text: String? = null, + disabled: Boolean = false, + href: String? = null, + target: LinkTarget? = null, + trailingIcon: Boolean = false, + type: ButtonType = ButtonType.Submit, + value: String? = null, + name: String? = null, + className: String? = null, + init: (MdOutlinedButton.() -> Unit)? = null +) = MdOutlinedButton( + text = text, + disabled = disabled, + href = href, + target = target, + trailingIcon = trailingIcon, + type = type, + value = value, + name = name, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/button/MdTextButton.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/button/MdTextButton.kt new file mode 100644 index 0000000000..fcd33f4bef --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/button/MdTextButton.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.button + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.widget.LinkTarget +import io.kvision.core.Container + +/** + * Text buttons are used for the lowest priority actions, especially when presenting multiple + * options. + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdTextButton( + text: String? = null, + disabled: Boolean = false, + href: String? = null, + target: LinkTarget? = null, + trailingIcon: Boolean = false, + type: ButtonType = ButtonType.Submit, + value: String? = null, + name: String? = null, + className: String? = null, + init: (MdTextButton.() -> Unit)? = null +) : MdButton( + tag = "md-text-button", + text = text, + disabled = disabled, + href = href, + target = target, + trailingIcon = trailingIcon, + type = type, + value = value, + name = name, + className = className +) { + + init { + init?.let { this.it() } + } +} + +@ExperimentalMaterialApi +fun Container.textButton( + text: String? = null, + disabled: Boolean = false, + href: String? = null, + target: LinkTarget? = null, + trailingIcon: Boolean = false, + type: ButtonType = ButtonType.Submit, + value: String? = null, + name: String? = null, + className: String? = null, + init: (MdTextButton.() -> Unit)? = null +) = MdTextButton( + text = text, + disabled = disabled, + href = href, + target = target, + trailingIcon = trailingIcon, + type = type, + value = value, + name = name, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/checkbox/MdCheckbox.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/checkbox/MdCheckbox.kt new file mode 100644 index 0000000000..ac55174dc5 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/checkbox/MdCheckbox.kt @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.checkbox + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.form.MdFormToggleInputWidget +import io.kvision.material.util.addBool +import io.kvision.material.widget.TouchTarget +import io.kvision.core.AttributeSetBuilder +import io.kvision.core.Container +import org.w3c.dom.events.Event + +/** + * Checkboxes allow users to select one or more items from a set. + * Checkboxes can turn an option on or off. + * + * There's one type of checkbox in Material. + * Use this selection control when the user needs to select one or more options from a list. + * + * See https://material-web.dev/components/checkbox/ + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdCheckbox( + checked: Boolean = false, + disabled: Boolean = false, + indeterminate: Boolean = false, + required: Boolean = false, + value: String = "on", + name: String? = null, + touchTarget: TouchTarget? = TouchTarget.Wrapper, + validationMessage: String? = null, + className: String? = null, + init: (MdCheckbox.() -> Unit)? = null +) : MdFormToggleInputWidget( + tag = "md-checkbox", + disabled = disabled, + required = required, + value = value, + name = name, + validationMessage = validationMessage, + className = className, +) { + + /** + * Whether or not the checkbox is selected. + */ + var checked by syncOnUpdate(checked) + + /** + * Whether or not the checkbox is indeterminate. + * + * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox#indeterminate_state_checkboxes + */ + var indeterminate by refreshOnUpdate(indeterminate) + + /** + * Checkbox touch target. + */ + var touchTarget by refreshOnUpdate(touchTarget) + + init { + init?.let { this.it() } + } + + /////////////////////////////////////////////////////////////////////////// + // Attributes + /////////////////////////////////////////////////////////////////////////// + + override fun buildAttributeSet(attributeSetBuilder: AttributeSetBuilder) { + super.buildAttributeSet(attributeSetBuilder) + + if (indeterminate) { + attributeSetBuilder.addBool("indeterminate") + } + + if (checked) { + attributeSetBuilder.addBool("checked") + } + + touchTarget?.let { + attributeSetBuilder.add("touch-target", it.value) + } + } + + /////////////////////////////////////////////////////////////////////////// + // State + /////////////////////////////////////////////////////////////////////////// + + override fun toggle() { + checked = !checked + } + + override fun onChange(event: Event) { + super.onChange(event) + checked = getElementD().checked == true + } +} + +@ExperimentalMaterialApi +fun Container.checkbox( + checked: Boolean = false, + disabled: Boolean = false, + indeterminate: Boolean = false, + required: Boolean = false, + value: String = "on", + name: String? = null, + touchTarget: TouchTarget? = TouchTarget.Wrapper, + validationMessage: String? = null, + className: String? = null, + init: (MdCheckbox.() -> Unit)? = null +) = MdCheckbox( + checked = checked, + disabled = disabled, + indeterminate = indeterminate, + required = required, + value = value, + name = name, + touchTarget = touchTarget, + validationMessage = validationMessage, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/chips/ChipEvents.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/chips/ChipEvents.kt new file mode 100644 index 0000000000..c42640840b --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/chips/ChipEvents.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.chips + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.utils.SnOn +import io.kvision.utils.event +import org.w3c.dom.events.Event + +/** + * Dispatched when `disabled` is toggled. + * [Event.bubbles] + */ +@ExperimentalMaterialApi +fun SnOn.updateFocus(action: (Event) -> Unit) { + event("update-focus", action) +} + +/** + * Dispatched when the remove button is clicked. + */ +@ExperimentalMaterialApi +fun SnOn.remove(action: (Event) -> Unit) where T: MdChip, T: RemovableChip { + event("remove", action) +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/chips/MdAssistChip.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/chips/MdAssistChip.kt new file mode 100644 index 0000000000..ace92c65f8 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/chips/MdAssistChip.kt @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.chips + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.util.addBool +import io.kvision.material.widget.LinkTarget +import io.kvision.core.AttributeSetBuilder + +/** + * Assist chips represent smart or automated actions that can span multiple apps, + * such as opening a calendar event from the home screen. + * + * Assist chips function as though the user asked an assistant to complete the action. + * They should appear dynamically and contextually in a UI. + * + * https://material-web.dev/components/chip/#assist-chip-example + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdAssistChip internal constructor( + tag: String, + label: String, + elevated: Boolean = false, + href: String? = null, + target: LinkTarget? = null, + disabled: Boolean = false, + alwaysFocusable: Boolean = false, + className: String? = null +) : MdChip( + tag = tag, + label = label, + disabled = disabled, + alwaysFocusable = alwaysFocusable, + className = className +) { + + /** + * Assist chip constructor. + */ + constructor( + label: String, + elevated: Boolean = false, + href: String? = null, + target: LinkTarget? = null, + disabled: Boolean = false, + alwaysFocusable: Boolean = false, + className: String? = null, + init: (MdAssistChip.() -> Unit)? = null + ) : this( + tag = "md-assist-chip", + label = label, + elevated = elevated, + href = href, + target = target, + disabled = disabled, + alwaysFocusable = alwaysFocusable, + className = className + ) { + init?.let { this.it() } + } + + /** + * Determines if the chip is elevated. + */ + var elevated by refreshOnUpdate(elevated) + + /** + * The URL that the chip points to. + */ + var href by refreshOnUpdate(href) + + /** + * Where to display the linked href URL for a link chip. + */ + var target by refreshOnUpdate(target) + + /////////////////////////////////////////////////////////////////////////// + // Attributes + /////////////////////////////////////////////////////////////////////////// + + override fun buildAttributeSet(attributeSetBuilder: AttributeSetBuilder) { + super.buildAttributeSet(attributeSetBuilder) + + if (elevated) { + attributeSetBuilder.addBool("elevated") + } + + href?.let { + attributeSetBuilder.add("href", it) + } + + target?.let { + attributeSetBuilder.add("target", it.value) + } + } +} + +@ExperimentalMaterialApi +fun MdChipSet.assistChip( + label: String, + elevated: Boolean = false, + href: String? = null, + target: LinkTarget? = null, + disabled: Boolean = false, + alwaysFocusable: Boolean = false, + className: String? = null, + init: (MdAssistChip.() -> Unit)? = null +) = MdAssistChip( + label = label, + elevated = elevated, + href = href, + target = target, + disabled = disabled, + alwaysFocusable = alwaysFocusable, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/chips/MdChip.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/chips/MdChip.kt new file mode 100644 index 0000000000..9688a74119 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/chips/MdChip.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.chips + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.slot.HasIconSlot +import io.kvision.material.util.addBool +import io.kvision.material.widget.MdItemWidget +import io.kvision.material.widget.Slot +import io.kvision.core.AttributeSetBuilder +import io.kvision.core.Component + +/** + * Chips help people enter information, make selections, filter content, or trigger actions. + * + * While buttons are expected to appear consistently and with familiar calls to action, + * chips should appear dynamically as a group of multiple interactive elements. + * + * See https://material-web.dev/components/chip/ + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +abstract class MdChip internal constructor( + tag: String, + label: String, + disabled: Boolean, + alwaysFocusable: Boolean, + className: String?, + internal val canBeRemoved: Boolean = false +) : MdItemWidget( + tag = tag, + className = className +), HasIconSlot { + + /** + * Whether or not the chip is disabled. + * Disabled chips are not focusable, unless always-focusable is set. + */ + var disabled by refreshOnUpdate(disabled) + + /** + * The label of the chip. + */ + var label by refreshOnUpdate(label) + + /** + * When true, allow disabled chips to be focused with arrow keys. + * Add this when a chip needs increased visibility when disabled. + * + * See https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_disabled_controls + * for more guidance on when this is needed. + */ + var alwaysFocusable by refreshOnUpdate(alwaysFocusable) + + /////////////////////////////////////////////////////////////////////////// + // Attributes + /////////////////////////////////////////////////////////////////////////// + + override fun buildAttributeSet(attributeSetBuilder: AttributeSetBuilder) { + super.buildAttributeSet(attributeSetBuilder) + attributeSetBuilder.add("label", translate(label)) + + if (disabled) { + attributeSetBuilder.addBool("disabled") + } + + if (alwaysFocusable) { + attributeSetBuilder.addBool("always-focusable") + } + } + + /////////////////////////////////////////////////////////////////////////// + // Slots + /////////////////////////////////////////////////////////////////////////// + + override fun icon(component: Component?) { + Slot.Icon(component) + } +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/chips/MdChipSet.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/chips/MdChipSet.kt new file mode 100644 index 0000000000..9d651f7e2d --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/chips/MdChipSet.kt @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.chips + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.widget.MdListWidget +import io.kvision.material.widget.toItemWidget +import io.kvision.material.widget.toItemWidgetArrayOrDefault +import io.kvision.core.Container +import io.kvision.core.onEvent +import io.kvision.utils.event +import org.w3c.dom.events.Event + +/** + * Chips should always appear in a set. + * Chip set are toolbars that can display any type of chip or other toolbar items. + * + * https://material-web.dev/components/chip/ + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdChipSet( + className: String? = null, + init: (MdChipSet.() -> Unit)? = null +) : MdListWidget( + tag = "md-chip-set", + className = className +) { + + private val chipRemoveEventListenerIds by lazy { mutableMapOf() } + + /** + * The chips of this chip set. + */ + val chips: Array + get() = toItemWidgetArrayOrDefault(getElementD()?.chips, listDelegate::items) + + init { + listDelegate.onAdded = ::onChipAdded + listDelegate.onRemoved = ::onChipRemoved + init?.let { this.it() } + } + + /////////////////////////////////////////////////////////////////////////// + // Events + /////////////////////////////////////////////////////////////////////////// + + /** + * Notifies of addition of a chip. + */ + private fun onChipAdded(chip: MdChip) { + if (chip.canBeRemoved) { + chipRemoveEventListenerIds[chip] = chip.onEvent { + event("remove", ::onChipRemovedFromUserInteraction) + } + } + } + + /** + * Notifies of removal of a chip. + */ + private fun onChipRemoved(chip: MdChip) { + if (chip.canBeRemoved) { + chipRemoveEventListenerIds[chip] + ?.let(chip::removeEventListener) + } + } + + /** + * Handles chip remove event. + * + * Prevents the default remove action of the underlying material HTML element and instead remove + * the [MdChip] instance from the [listDelegate] which in turns will trigger a rerendering. + */ + private fun onChipRemovedFromUserInteraction(event: Event) { + val chip = toItemWidget(event.target) ?: return + event.preventDefault() + listDelegate.remove(chip) + } +} + +@ExperimentalMaterialApi +fun Container.chipSet( + className: String? = null, + init: (MdChipSet.() -> Unit)? = null +) = MdChipSet( + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/chips/MdFilterChip.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/chips/MdFilterChip.kt new file mode 100644 index 0000000000..a394bd58b4 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/chips/MdFilterChip.kt @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.chips + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.util.addBool +import io.kvision.core.AttributeSetBuilder +import io.kvision.snabbdom.VNode + +/** + * Filter chips use tags or descriptive words to filter content. + * They can be a good alternative to toggle buttons or checkboxes. + * + * https://material-web.dev/components/chip/#filter-chip-example + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdFilterChip( + label: String, + elevated: Boolean = false, + removable: Boolean = false, + selected: Boolean = false, + disabled: Boolean = false, + alwaysFocusable: Boolean = false, + ariaLabelRemove: String? = null, + className: String? = null, + init: (MdFilterChip.() -> Unit)? = null +) : MdChip( + tag = "md-filter-chip", + label = label, + disabled = disabled, + alwaysFocusable = alwaysFocusable, + className = className, + canBeRemoved = true +), RemovableChip { + + /** + * Determines if the chip is elevated. + */ + var elevated by refreshOnUpdate(elevated) + + /** + * Indicates that the ship is removable. + */ + var removable by refreshOnUpdate(removable) + + /** + * Indicates that the chip is selected. + */ + var selected by refreshOnUpdate(selected) + + /** + * The aria label for remove action. + */ + var ariaLabelRemove by syncOnUpdate(ariaLabelRemove) + + init { + init?.let { this.it() } + } + + /////////////////////////////////////////////////////////////////////////// + // Lifecycle + /////////////////////////////////////////////////////////////////////////// + + override fun afterCreate(node: VNode) { + super.afterCreate(node) + ariaLabelRemove?.let { getElementD().ariaLabelRemove = translate(it) } + } + + /////////////////////////////////////////////////////////////////////////// + // Attributes + /////////////////////////////////////////////////////////////////////////// + + override fun buildAttributeSet(attributeSetBuilder: AttributeSetBuilder) { + super.buildAttributeSet(attributeSetBuilder) + + if (elevated) { + attributeSetBuilder.addBool("elevated") + } + + if (removable) { + attributeSetBuilder.addBool("removable") + } + + if (selected) { + attributeSetBuilder.addBool("selected") + } + } +} + +@ExperimentalMaterialApi +fun MdChipSet.filterChip( + label: String, + elevated: Boolean = false, + removable: Boolean = false, + selected: Boolean = false, + disabled: Boolean = false, + alwaysFocusable: Boolean = false, + ariaLabelRemove: String? = null, + className: String? = null, + init: (MdFilterChip.() -> Unit)? = null +) = MdFilterChip( + label = label, + elevated = elevated, + removable = removable, + selected = selected, + disabled = disabled, + alwaysFocusable = alwaysFocusable, + ariaLabelRemove = ariaLabelRemove, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/chips/MdInputChip.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/chips/MdInputChip.kt new file mode 100644 index 0000000000..c14f0781c2 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/chips/MdInputChip.kt @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.chips + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.util.addBool +import io.kvision.material.widget.LinkTarget +import io.kvision.core.AttributeSetBuilder +import io.kvision.snabbdom.VNode + +/** + * Input chips represent discrete pieces of information entered by a user, + * such as Gmail contacts or filter options within a search field. + * + * Input chips whose icons are user images may add the avatar attribute to display the image in a + * larger circle. + * + * https://material-web.dev/components/chip/#input-chip-example + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdInputChip( + label: String, + avatar: Boolean = false, + href: String? = null, + target: LinkTarget? = null, + removeOnly: Boolean = false, + selected: Boolean = false, + disabled: Boolean = false, + alwaysFocusable: Boolean = false, + ariaLabelRemove: String? = null, + className: String? = null, + init: (MdInputChip.() -> Unit)? = null +) : MdChip( + tag = "md-input-chip", + label = label, + disabled = disabled, + alwaysFocusable = alwaysFocusable, + className = className, + canBeRemoved = true +), RemovableChip { + + /** + * Indicates that the chip display an avatar. + */ + var avatar by refreshOnUpdate(avatar) + + /** + * The URL that the chip points to. + */ + var href by refreshOnUpdate(href) + + /** + * Where to display the linked href URL for a link chip. + */ + var target by refreshOnUpdate(target) + + /** + * Indicates that the ship is removeOnly. + */ + var removeOnly by refreshOnUpdate(removeOnly) + + /** + * Indicates that the chip is selected. + */ + var selected by refreshOnUpdate(selected) + + /** + * The aria label for remove action. + */ + var ariaLabelRemove by syncOnUpdate(ariaLabelRemove) + + init { + init?.let { this.it() } + } + + /////////////////////////////////////////////////////////////////////////// + // Lifecycle + /////////////////////////////////////////////////////////////////////////// + + override fun afterCreate(node: VNode) { + super.afterCreate(node) + ariaLabelRemove?.let { getElementD().ariaLabelRemove = translate(it) } + } + + /////////////////////////////////////////////////////////////////////////// + // Attributes + /////////////////////////////////////////////////////////////////////////// + + override fun buildAttributeSet(attributeSetBuilder: AttributeSetBuilder) { + super.buildAttributeSet(attributeSetBuilder) + + if (avatar) { + attributeSetBuilder.addBool("avatar") + } + + href?.let { + attributeSetBuilder.add("href", it) + } + + target?.let { + attributeSetBuilder.add("target", it.value) + } + + if (removeOnly) { + attributeSetBuilder.addBool("removeOnly") + } + + if (selected) { + attributeSetBuilder.addBool("selected") + } + } +} + +@ExperimentalMaterialApi +fun MdChipSet.inputChip( + label: String, + avatar: Boolean = false, + href: String? = null, + target: LinkTarget? = null, + removeOnly: Boolean = false, + selected: Boolean = false, + disabled: Boolean = false, + alwaysFocusable: Boolean = false, + ariaLabelRemove: String? = null, + className: String? = null, + init: (MdInputChip.() -> Unit)? = null +) = MdInputChip( + label = label, + avatar = avatar, + href = href, + target = target, + removeOnly = removeOnly, + selected = selected, + disabled = disabled, + alwaysFocusable = alwaysFocusable, + ariaLabelRemove = ariaLabelRemove, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/chips/MdSuggestionChip.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/chips/MdSuggestionChip.kt new file mode 100644 index 0000000000..5b8d6cd817 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/chips/MdSuggestionChip.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.chips + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.widget.LinkTarget + +/** + * Suggestion chips help narrow a user’s intent by presenting dynamically generated suggestions, + * such as possible responses or search filters. + * + * https://material-web.dev/components/chip/#suggestion-chip-example + * + * Note: assist and suggestion chips are functionally identical with different tokens. + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdSuggestionChip( + label: String, + elevated: Boolean = false, + href: String? = null, + target: LinkTarget? = null, + disabled: Boolean = false, + alwaysFocusable: Boolean = false, + className: String? = null, + init: (MdSuggestionChip.() -> Unit)? = null +) : MdAssistChip( + tag = "md-suggestion-chip", + label = label, + elevated = elevated, + href = href, + target = target, + disabled = disabled, + alwaysFocusable = alwaysFocusable, + className = className +) { + + init { + init?.let { this.it() } + } +} + +@ExperimentalMaterialApi +fun MdChipSet.suggestionChip( + label: String, + elevated: Boolean = false, + href: String? = null, + target: LinkTarget? = null, + disabled: Boolean = false, + alwaysFocusable: Boolean = false, + className: String? = null, + init: (MdSuggestionChip.() -> Unit)? = null +) = MdSuggestionChip( + label = label, + elevated = elevated, + href = href, + target = target, + disabled = disabled, + alwaysFocusable = alwaysFocusable, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/chips/RemovableChip.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/chips/RemovableChip.kt new file mode 100644 index 0000000000..8a41c570a8 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/chips/RemovableChip.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.chips + +/** + * Chip which displays a remove button. + */ +interface RemovableChip diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/container/MdContainer.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/container/MdContainer.kt new file mode 100644 index 0000000000..39de6fa279 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/container/MdContainer.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.container + +import io.kvision.panel.SimplePanel +import io.kvision.snabbdom.VNode + +/** + * Base class for material container. + * + * @author Maanrifa Bacar Ali + */ +abstract class MdContainer internal constructor( + protected val tag: String, + className: String? +) : SimplePanel(className) { + + /////////////////////////////////////////////////////////////////////////// + // Rendering + /////////////////////////////////////////////////////////////////////////// + + override fun render(): VNode { + return render(tag, childrenVNodes()) + } +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/container/MdListContainer.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/container/MdListContainer.kt new file mode 100644 index 0000000000..dfc5d8c892 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/container/MdListContainer.kt @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.container + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.util.requireElementD +import io.kvision.material.widget.MdItemWidget +import io.kvision.material.widget.toItemWidget +import io.kvision.material.widget.toItemWidgetArray + +/** + * Subclass of container which accepts any kind of child but provides access for those of type [T]. + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +abstract class MdListContainer internal constructor( + tag: String, + className: String? +) : MdContainer( + tag = tag, + className = className +) { + + /** + * Gets the direct items in this list. + */ + val items: Array + get() = toItemWidgetArray(getElementD()?.items) + + /////////////////////////////////////////////////////////////////////////// + // Items + /////////////////////////////////////////////////////////////////////////// + + /** + * Activates the next item in the list. + * If at the end of the list, the first item will be activated. + * + * If the list is empty, null will be returned. + */ + fun activateNextItem(): T? { + return toItemWidget(requireElementD()?.activateNextItem()) + } + + /** + * Activates the previous item in the list. + * If at the start of the list, the last item will be activated. + * + * If the list is empty, null will be returned. + */ + fun activatePreviousItem(): T? { + return toItemWidget(requireElementD()?.activatePreviousItem()) + } +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/dialog/DialogEvents.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/dialog/DialogEvents.kt new file mode 100644 index 0000000000..d05de94c67 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/dialog/DialogEvents.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.dialog + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.utils.SnOn +import io.kvision.utils.event +import org.w3c.dom.events.Event + +/** + * Dispatched when the dialog is opening before any animations. + */ +@ExperimentalMaterialApi +fun SnOn.open(action: (Event) -> Unit) { + event("open", action) +} + +/** + * Dispatched when the dialog has opened after any animations. + */ +@ExperimentalMaterialApi +fun SnOn.opened(action: (Event) -> Unit) { + event("opened", action) +} + +/** + * Dispatched when the dialog is closing before any animations. + */ +@ExperimentalMaterialApi +fun SnOn.close(action: (Event) -> Unit) { + event("close", action) +} + +/** + * Dispatched when the dialog has closed after any animations. + */ +@ExperimentalMaterialApi +fun SnOn.closed(action: (Event) -> Unit) { + event("closed", action) +} + +/** + * Dispatched when the dialog has been canceled by clicking on the scrim or pressing Escape. + */ +@ExperimentalMaterialApi +fun SnOn.cancel(action: (Event) -> Unit) { + event("cancel", action) +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/dialog/MdDialog.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/dialog/MdDialog.kt new file mode 100644 index 0000000000..ecad5749ea --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/dialog/MdDialog.kt @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.dialog + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.util.requireElementD +import io.kvision.material.slot.HasActionsSlot +import io.kvision.material.slot.HasContentSlot +import io.kvision.material.slot.HasHeadlineSlot +import io.kvision.material.slot.HasIconSlot +import io.kvision.material.widget.MdWidget +import io.kvision.material.widget.Slot +import io.kvision.core.AttributeSetBuilder +import io.kvision.core.Component +import io.kvision.core.Container +import io.kvision.snabbdom.VNode +import kotlin.js.Promise + +enum class DialogType(internal val value: String) { + Alert("alert") +} + +/** + * Dialogs provide important prompts in a user flow. + * + * See https://material-web.dev/components/dialog/ + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdDialog( + returnValue: String? = null, + type: DialogType? = null, + className: String? = null, + init: (MdDialog.() -> Unit)? = null +) : MdWidget( + tag = "md-dialog", + className = className +), HasActionsSlot, + HasContentSlot, + HasHeadlineSlot, + HasIconSlot { + + /** + * The type of dialog for accessibility. Set this to alert to announce a dialog as an alert + * dialog. + */ + var type by refreshOnUpdate(type) + + /** + * Gets or sets the dialog's return value, usually to indicate which button a user pressed to + * close it. + * + * https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/returnValue + */ + var returnValue by refreshOnUpdate(returnValue) + + /** + * Gets the opening animation for a dialog. Set to a new function to customize the animation. + */ + var openAnimation by syncOnUpdate<(() -> dynamic)?>(null) + + /** + * Gets the closing animation for a dialog. Set to a new function to customize the animation. + */ + var closeAnimation by syncOnUpdate<(() -> dynamic)?>(null) + + /** + * Indicates that the dialog is in an open state. + */ + val open: Boolean + get() = getElementD()?.open?.unsafeCast() ?: false + + init { + init?.let { this.it() } + } + + /////////////////////////////////////////////////////////////////////////// + // Lifecycle + /////////////////////////////////////////////////////////////////////////// + + override fun afterCreate(node: VNode) { + super.afterCreate(node) + getElementD().returnValue = returnValue + openAnimation?.let { getElementD().getOpenAnimation = it } + closeAnimation?.let { getElementD().getCloseAnimation = it } + } + + /////////////////////////////////////////////////////////////////////////// + // Attributes + /////////////////////////////////////////////////////////////////////////// + + override fun buildAttributeSet(attributeSetBuilder: AttributeSetBuilder) { + super.buildAttributeSet(attributeSetBuilder) + + type?.let { + attributeSetBuilder.add("type", it.value) + } + } + + /////////////////////////////////////////////////////////////////////////// + // Slots + /////////////////////////////////////////////////////////////////////////// + + /** + * Sets the headline of the dialog. + */ + override fun actions(component: Component?) { + Slot.Actions(component) + } + + /** + * Sets the content of the dialog. + */ + override fun content(component: Component?) { + Slot.Content(component) + } + + /** + * Sets the actions of the dialog. + */ + override fun headline(component: Component?) { + Slot.Headline(component) + } + + /** + * Sets the icon of the dialog. + */ + override fun icon(component: Component?) { + Slot.Icon(component) + } + + /////////////////////////////////////////////////////////////////////////// + // Display + /////////////////////////////////////////////////////////////////////////// + + /** + * Opens the dialog and fires a cancelable open event. + * After a dialog's animation, an opened event is fired. + * Add an autocomplete attribute to a child of the dialog that should receive focus after + * opening. + */ + @JsName("showDialog") + fun open(): Promise { + if (!visible) { + return Promise.reject(IllegalStateException("Dialog is not visible")) + } + + return requireElementD().show() as Promise + } + + /** + * Closes the dialog and fires a cancelable close event. + * After a dialog's animation, a closed event is fired. + */ + @JsName("closeDialog") + fun close(returnValue: String? = this.returnValue): Promise { + if (!visible) { + return Promise.reject(IllegalStateException("Dialog is not visible")) + } + + return requireElementD().close(returnValue) as Promise + } +} + +@ExperimentalMaterialApi +fun Container.dialog( + returnValue: String? = null, + type: DialogType? = null, + className: String? = null, + init: (MdDialog.() -> Unit)? = null +) = MdDialog( + returnValue = returnValue, + type = type, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/divider/MdDivider.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/divider/MdDivider.kt new file mode 100644 index 0000000000..57528e1454 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/divider/MdDivider.kt @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.divider + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.util.addBool +import io.kvision.material.widget.MdWidget +import io.kvision.core.AttributeSetBuilder +import io.kvision.core.Container + +/** + * Dividers are thin lines that group content in lists or other containers. + * + * // TODO add link to online doc when available + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdDivider( + inset: Boolean = false, + insetStart: Boolean = false, + insetEnd: Boolean = false, + className: String? = null, + init: (MdDivider.() -> Unit)? = null +) : MdWidget( + tag = "md-divider", + className = className +) { + + /** + * Indents the divider with equal padding on both sides. + */ + var inset by refreshOnUpdate(inset) + + /** + * Indents the divider with padding on the leading side. + */ + var insetStart by refreshOnUpdate(insetStart) + + /** + * Indents the divider with padding on the trailing side. + */ + var insetEnd by refreshOnUpdate(insetEnd) + + init { + init?.let { this.it() } + } + + /////////////////////////////////////////////////////////////////////////// + // Attributes + /////////////////////////////////////////////////////////////////////////// + + override fun buildAttributeSet(attributeSetBuilder: AttributeSetBuilder) { + super.buildAttributeSet(attributeSetBuilder) + + if (inset) { + attributeSetBuilder.addBool("inset") + } else { + if (insetStart) { + attributeSetBuilder.addBool("inset-start") + } + + if (insetEnd) { + attributeSetBuilder.addBool("inset-end") + } + } + } +} + +@ExperimentalMaterialApi +fun Container.divider( + inset: Boolean = false, + insetStart: Boolean = false, + insetEnd: Boolean = false, + className: String? = null, + init: (MdDivider.() -> Unit)? = null +) = MdDivider( + inset = inset, + insetStart = insetStart, + insetEnd = insetEnd, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/fab/MdBaseFab.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/fab/MdBaseFab.kt new file mode 100644 index 0000000000..7657284f4e --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/fab/MdBaseFab.kt @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.fab + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.widget.MdWidget +import io.kvision.material.widget.TouchTarget +import io.kvision.material.util.addBool +import io.kvision.material.slot.HasIconSlot +import io.kvision.material.widget.Slot +import io.kvision.core.AttributeSetBuilder +import io.kvision.core.Component + +enum class FabSize(internal val value: String) { + Small("small"), + Medium("medium"), + Large("large") +} + +/** + * FAB represents the most important action on a screen. It puts key actions within reach. + * + * Extended FABs help people take primary actions. + * They're wider than FABs to accommodate a text label and larger target area. + * + * See https://material-web.dev/components/fab + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +abstract class MdBaseFab internal constructor( + tag: String, + label: String?, + size: FabSize, + lowered: Boolean, + touchTarget: TouchTarget?, + className: String? +) : MdWidget( + tag = tag, + className = className +), HasIconSlot { + + /** + * The text to display on the FAB. + */ + var label by refreshOnUpdate(label) + + /** + * The size of the FAB. + * NOTE: Branded FABs cannot be sized to small, and Extended FABs do not have different sizes. + */ + var size by refreshOnUpdate(size) + + /** + * Lowers the FAB's elevation. + */ + var lowered by refreshOnUpdate(lowered) + + /** + * Touch target of (small size) FAB. + */ + var touchTarget by refreshOnUpdate(touchTarget) + + /////////////////////////////////////////////////////////////////////////// + // Attributes + /////////////////////////////////////////////////////////////////////////// + + override fun buildAttributeSet(attributeSetBuilder: AttributeSetBuilder) { + super.buildAttributeSet(attributeSetBuilder) + attributeSetBuilder.add("size", size.value) + + label?.let { + attributeSetBuilder.add("label", translate(it)) + } + + if (lowered) { + attributeSetBuilder.addBool("lowered") + } + + touchTarget?.let { + attributeSetBuilder.add("touch-target", it.value) + } + } + + /////////////////////////////////////////////////////////////////////////// + // Slots + /////////////////////////////////////////////////////////////////////////// + + override fun icon(component: Component?) { + Slot.Icon(component) + } +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/fab/MdBrandedFab.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/fab/MdBrandedFab.kt new file mode 100644 index 0000000000..31850c6d27 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/fab/MdBrandedFab.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.fab + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.widget.TouchTarget +import io.kvision.core.Container + +/** + * Branded FABs use a brightly colored logo for their icon. + * Unlike FAB, branded FABs do not have color variants. + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdBrandedFab( + label: String? = null, + size: FabSize = FabSize.Medium, + lowered: Boolean = false, + touchTarget: TouchTarget? = null, + className: String? = null, + init: (MdBrandedFab.() -> Unit)? = null +) : MdBaseFab( + tag = "md-branded-fab", + label = label, + size = size, + lowered = lowered, + touchTarget = touchTarget, + className = className +) { + + init { + init?.let { this.it() } + } +} + +@ExperimentalMaterialApi +fun Container.brandedFab( + label: String? = null, + size: FabSize = FabSize.Medium, + lowered: Boolean = false, + touchTarget: TouchTarget? = null, + className: String? = null, + init: (MdBrandedFab.() -> Unit)? = null +) = MdBrandedFab( + label = label, + size = size, + lowered = lowered, + touchTarget = touchTarget, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/fab/MdFab.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/fab/MdFab.kt new file mode 100644 index 0000000000..207665716e --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/fab/MdFab.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.fab + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.widget.TouchTarget +import io.kvision.core.AttributeSetBuilder +import io.kvision.core.Container + +enum class FabVariant(internal val value: String) { + Surface("surface"), + Primary("primary"), + Secondary("secondary"), + Tertiary("tertiary") +} + +/** + * FABs should display a clear and understandable icon. + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdFab( + label: String? = null, + variant: FabVariant = FabVariant.Surface, + size: FabSize = FabSize.Medium, + lowered: Boolean = false, + touchTarget: TouchTarget? = null, + className: String? = null, + init: (MdFab.() -> Unit)? = null +) : MdBaseFab( + tag = "md-fab", + label = label, + size = size, + lowered = lowered, + touchTarget = touchTarget, + className = className +) { + + /** + * The FAB color variant to render. + */ + var variant by refreshOnUpdate(variant) + + init { + init?.let { this.it() } + } + + /////////////////////////////////////////////////////////////////////////// + // Attributes + /////////////////////////////////////////////////////////////////////////// + + override fun buildAttributeSet(attributeSetBuilder: AttributeSetBuilder) { + super.buildAttributeSet(attributeSetBuilder) + attributeSetBuilder.add("variant", variant.value) + } +} + +@ExperimentalMaterialApi +fun Container.fab( + label: String? = null, + variant: FabVariant = FabVariant.Surface, + size: FabSize = FabSize.Medium, + lowered: Boolean = false, + touchTarget: TouchTarget? = null, + className: String? = null, + init: (MdFab.() -> Unit)? = null +) = MdFab( + label = label, + variant = variant, + size = size, + lowered = lowered, + touchTarget = touchTarget, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/form/MdFormInputWidget.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/form/MdFormInputWidget.kt new file mode 100644 index 0000000000..e234de76b1 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/form/MdFormInputWidget.kt @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.form + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.util.addBool +import io.kvision.material.util.requireElementD +import io.kvision.core.AttributeSetBuilder +import io.kvision.snabbdom.VNode +import org.w3c.dom.ValidityState + +/** + * Subclass of input widgets that are associated to a form. + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +abstract class MdFormInputWidget internal constructor( + tag: String, + disabled: Boolean, + required: Boolean, + value: V, + name: String?, + validationMessage: String?, + className: String?, +) : MdFormLabelWidget( + tag = tag, + disabled = disabled, + value = value, + name = name, + className = className +) { + + /** + * The initial validation message to set once the component is ready. + */ + private var initialValidationMessage = validationMessage + + /** + * Indicates that the component must provides a valid value when participating in + * form submission. + */ + var required by refreshOnUpdate(required) + + /** + * Message to display when the component value is not valid. + */ + val validationMessage: String? + get() = getElement()?.asDynamic()?.validationMessage as? String ?: initialValidationMessage + + /** + * Gets the element's current validity state. + */ + val validity: ValidityState? + get() = getElement()?.asDynamic()?.validity as? ValidityState + + /** + * Indicates that the element is a candidate for constraint validation. + */ + val willValidate: Boolean + get() = getElement()?.asDynamic()?.willValidate as? Boolean ?: false + + /////////////////////////////////////////////////////////////////////////// + // Lifecycle + /////////////////////////////////////////////////////////////////////////// + + override fun afterCreate(node: VNode) { + super.afterCreate(node) + initialValidationMessage?.let(this::setCustomValidity) + } + + /////////////////////////////////////////////////////////////////////////// + // Attributes + /////////////////////////////////////////////////////////////////////////// + + override fun buildAttributeSet(attributeSetBuilder: AttributeSetBuilder) { + super.buildAttributeSet(attributeSetBuilder) + + if (required) { + attributeSetBuilder.addBool("required") + } + } + + /////////////////////////////////////////////////////////////////////////// + // Validation + /////////////////////////////////////////////////////////////////////////// + + /** + * Indicates the validity of the value of the element. + * If the value is invalid, this method also fires the invalid event on the element. + * + * https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/checkValidity + */ + fun checkValidity(): Boolean { + return requireElementD().checkValidity() as Boolean + } + + /** + * Performs the same validity checking steps as the [checkValidity] method. + * If the value is invalid, this method also fires the invalid event on the element, and (if + * the event isn't canceled) reports the problem to the user. + * + * https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/reportValidity + */ + fun reportValidity(): Boolean { + return requireElementD().reportValidity() as Boolean + } + + /** + * Sets a custom validity message for the underlying element. + * + * https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setCustomValidity + */ + fun setCustomValidity(message: String) { + initialValidationMessage = message + requireElementD().setCustomValidity(translate(message)) + } +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/form/MdFormLabelWidget.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/form/MdFormLabelWidget.kt new file mode 100644 index 0000000000..3c5c58d32d --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/form/MdFormLabelWidget.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.form + +import io.kvision.material.ExperimentalMaterialApi +import org.w3c.dom.NodeList + +/** + * Subclass of widgets that are associated with form and holds labels. + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +abstract class MdFormLabelWidget internal constructor( + tag: String, + disabled: Boolean, + value: V, + name: String?, + className: String?, +) : MdFormWidget( + tag = tag, + disabled = disabled, + value = value, + name = name, + className = className +) { + + /** + * The labels this element is associated with. + */ + val labels: NodeList? + get() = getElement()?.asDynamic()?.labels as? NodeList +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/form/MdFormToggleInputWidget.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/form/MdFormToggleInputWidget.kt new file mode 100644 index 0000000000..420d20c70c --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/form/MdFormToggleInputWidget.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.form + +import io.kvision.material.ExperimentalMaterialApi + +/** + * Subclass of form associated input widgets that can be toggled. + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +abstract class MdFormToggleInputWidget internal constructor( + tag: String, + disabled: Boolean, + required: Boolean, + value: String, + name: String?, + validationMessage: String?, + className: String?, +) : MdFormInputWidget( + tag = tag, + disabled = disabled, + required = required, + value = value, + name = name, + validationMessage = validationMessage, + className = className +) { + + /////////////////////////////////////////////////////////////////////////// + // State + /////////////////////////////////////////////////////////////////////////// + + /** + * Toggle the checked/selected state of this widget. + */ + abstract fun toggle() +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/form/MdFormWidget.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/form/MdFormWidget.kt new file mode 100644 index 0000000000..aa7ebbc534 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/form/MdFormWidget.kt @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.form + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.util.add +import io.kvision.material.util.addBool +import io.kvision.material.widget.MdWidget +import io.kvision.core.AttributeSetBuilder +import org.w3c.dom.HTMLFormElement +import org.w3c.dom.events.Event + +/** + * Subclass of widgets that are associated with form. + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +abstract class MdFormWidget internal constructor( + tag: String, + disabled: Boolean, + value: V, + name: String?, + className: String?, +) : MdWidget( + tag = tag, + className = className +) { + + /** + * Whether or not the widget is disabled. + */ + var disabled by refreshOnUpdate(disabled) + + /** + * The value that is submitted to the form. + */ + var value by syncOnUpdate(value) + + /** + * The name to use in form submission. + */ + var name by refreshOnUpdate(name) + + /** + * The associated form element with which this element's value will submit. + */ + val form: HTMLFormElement? + get() = getElement()?.asDynamic()?.form as? HTMLFormElement + + init { + subscribeEvents() + } + + /////////////////////////////////////////////////////////////////////////// + // Attributes + /////////////////////////////////////////////////////////////////////////// + + override fun buildAttributeSet(attributeSetBuilder: AttributeSetBuilder) { + super.buildAttributeSet(attributeSetBuilder) + + if (disabled) { + attributeSetBuilder.addBool("disabled") + } + + name?.let { + attributeSetBuilder.add("name", it) + } + + value?.let { + attributeSetBuilder.add("value", it) + } + } + + /////////////////////////////////////////////////////////////////////////// + // Events + /////////////////////////////////////////////////////////////////////////// + + /** + * Indicates that the widget produce change events. + * Default to true. + */ + protected open fun hasChangeEvent(): Boolean = true + + /** + * Indicates that the widget produce input events. + * Default to false. + */ + protected open fun hasInputEvent(): Boolean = false + + /** + * Notifies about 'change' event. + * + * It is a good practice to compare DOM element value with object value before updating + * object value in order to avoid unnecessary refreshes. + */ + protected open fun onChange(event: Event) = Unit + + /** + * Notifies about 'input' event. + */ + protected open fun onInput(event: Event) = Unit + + /** + * Subscribe to events. + */ + private fun subscribeEvents() { + val changeEvent = hasChangeEvent() + val inputEvent = hasInputEvent() + + if (!changeEvent && !inputEvent) { + return + } + + setInternalEventListener> { + if (changeEvent) { + change = self::onChange + } + + if (inputEvent) { + input = self::onInput + } + } + } +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/icon/IconSlot.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/icon/IconSlot.kt new file mode 100644 index 0000000000..664ccd29d4 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/icon/IconSlot.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.icon + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.slot.HasIconSlot +import io.kvision.material.slot.HasLeadingSlot +import io.kvision.material.slot.HasSelectedSlot +import io.kvision.material.slot.HasTrailingSlot + +@ExperimentalMaterialApi +private inline fun iconSlot(name: String, block: (MdIcon) -> Unit): MdIcon { + return MdIcon(name).also(block) +} + +/** + * Sets the icon identified by [name] to the icon slot. + */ +@ExperimentalMaterialApi +fun HasIconSlot.icon(name: String): MdIcon = iconSlot(name, this::icon) + +/** + * Sets the icon identified by [name] to the selected slot. + */ +@ExperimentalMaterialApi +fun HasSelectedSlot.selectedIcon(name: String): MdIcon = iconSlot(name, this::selected) + +/** + * Sets the icon identified by [name] to the leading slot. + */ +@ExperimentalMaterialApi +fun HasLeadingSlot.leadingIcon(name: String): MdIcon = iconSlot(name, this::leading) + +/** + * Sets the icon identified by [name] to the trailing slot. + */ +@ExperimentalMaterialApi +fun HasTrailingSlot.trailingIcon(name: String): MdIcon = iconSlot(name, this::trailing) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/icon/MdIcon.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/icon/MdIcon.kt new file mode 100644 index 0000000000..4fd1625229 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/icon/MdIcon.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.icon + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.widget.MdWidget +import io.kvision.core.Container +import io.kvision.snabbdom.VNode + +/** + * Named material icon. + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdIcon( + name: String, + className: String? = null, + init: (MdIcon.() -> Unit)? = null +) : MdWidget( + tag = "md-icon", + className = className +) { + + /** + * Icon name. + */ + var name by refreshOnUpdate(name) + + init { + init?.let { this.it() } + } + + /////////////////////////////////////////////////////////////////////////// + // Rendering + /////////////////////////////////////////////////////////////////////////// + + override fun render(): VNode { + return renderWithText(name) + } +} + +@ExperimentalMaterialApi +fun Container.icon( + name: String, + className: String? = null, + init: (MdIcon.() -> Unit)? = null +): MdIcon = MdIcon( + name = name, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/iconbutton/MdFilledIconButton.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/iconbutton/MdFilledIconButton.kt new file mode 100644 index 0000000000..3d64cf40c9 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/iconbutton/MdFilledIconButton.kt @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.iconbutton + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.widget.LinkTarget +import io.kvision.core.Container + +/** + * Filled icon buttons have higher visual impact and are best for high emphasis actions. + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdFilledIconButton( + disabled: Boolean = false, + flipIconInRtl: Boolean = false, + href: String? = null, + target: LinkTarget? = null, + ariaLabelSelected: String? = null, + toggle: Boolean = false, + selected: Boolean = false, + type: IconButtonType = IconButtonType.Submit, + value: String? = null, + name: String? = null, + className: String? = null, + init: (MdFilledIconButton.() -> Unit)? = null +) : MdIconButton( + tag = "md-filled-icon-button", + disabled = disabled, + flipIconInRtl = flipIconInRtl, + href = href, + target = target, + ariaLabelSelected = ariaLabelSelected, + toggle = toggle, + selected = selected, + type = type, + value = value, + name = name, + className = className, +) { + init { + init?.let { this.it() } + } +} + +@ExperimentalMaterialApi +fun Container.filledIconButton( + disabled: Boolean = false, + flipIconInRtl: Boolean = false, + href: String? = null, + target: LinkTarget? = null, + ariaLabelSelected: String? = null, + toggle: Boolean = false, + selected: Boolean = false, + type: IconButtonType = IconButtonType.Submit, + value: String? = null, + name: String? = null, + className: String? = null, + init: (MdFilledIconButton.() -> Unit)? = null +) = MdFilledIconButton( + disabled = disabled, + flipIconInRtl = flipIconInRtl, + href = href, + target = target, + ariaLabelSelected = ariaLabelSelected, + toggle = toggle, + selected = selected, + type = type, + value = value, + name = name, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/iconbutton/MdFilledTonalIconButton.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/iconbutton/MdFilledTonalIconButton.kt new file mode 100644 index 0000000000..c3748a614c --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/iconbutton/MdFilledTonalIconButton.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.iconbutton + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.widget.LinkTarget +import io.kvision.core.Container + +/** + * Filled tonal icon buttons are a middle ground between filled and outlined icon buttons. + * They're useful in contexts where the button requires slightly more emphasis than an outline + * would give, such as a secondary action paired with a high emphasis action. + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdFilledTonalIconButton( + disabled: Boolean = false, + flipIconInRtl: Boolean = false, + href: String? = null, + target: LinkTarget? = null, + ariaLabelSelected: String? = null, + toggle: Boolean = false, + selected: Boolean = false, + type: IconButtonType = IconButtonType.Submit, + value: String? = null, + name: String? = null, + className: String? = null, + init: (MdFilledTonalIconButton.() -> Unit)? = null +) : MdIconButton( + tag = "md-filled-tonal-icon-button", + disabled = disabled, + flipIconInRtl = flipIconInRtl, + href = href, + target = target, + ariaLabelSelected = ariaLabelSelected, + toggle = toggle, + selected = selected, + type = type, + value = value, + name = name, + className = className, +) { + init { + init?.let { this.it() } + } +} + +@ExperimentalMaterialApi +fun Container.filledTonalIconButton( + disabled: Boolean = false, + flipIconInRtl: Boolean = false, + href: String? = null, + target: LinkTarget? = null, + ariaLabelSelected: String? = null, + toggle: Boolean = false, + selected: Boolean = false, + type: IconButtonType = IconButtonType.Submit, + value: String? = null, + name: String? = null, + className: String? = null, + init: (MdFilledTonalIconButton.() -> Unit)? = null +) = MdFilledTonalIconButton( + disabled = disabled, + flipIconInRtl = flipIconInRtl, + href = href, + target = target, + ariaLabelSelected = ariaLabelSelected, + toggle = toggle, + selected = selected, + type = type, + value = value, + name = name, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/iconbutton/MdIconButton.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/iconbutton/MdIconButton.kt new file mode 100644 index 0000000000..932c9f7c2b --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/iconbutton/MdIconButton.kt @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.iconbutton + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.form.MdFormLabelWidget +import io.kvision.material.util.addBool +import io.kvision.material.slot.HasIconSlot +import io.kvision.material.slot.HasSelectedSlot +import io.kvision.material.widget.LinkTarget +import io.kvision.material.widget.Slot +import io.kvision.core.AttributeSetBuilder +import io.kvision.core.Component +import io.kvision.core.Container +import org.w3c.dom.events.Event + +enum class IconButtonType(internal val value: String) { + Button("button"), + Submit("submit"), + Reset("reset") +} + +/** + * Icon buttons help people take supplementary actions with a single tap. + * + * Standard icon buttons do not have a background or outline, and have the lowest emphasis of the + * icon buttons. + * + * See https://material-web.dev/components/icon-button/ + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdIconButton internal constructor( + tag: String, + disabled: Boolean, + flipIconInRtl: Boolean, + href: String?, + target: LinkTarget?, + ariaLabelSelected: String?, + toggle: Boolean, + selected: Boolean, + type: IconButtonType, + value: String?, + name: String?, + className: String?, +) : MdFormLabelWidget( + tag = tag, + disabled = disabled, + value = value, + name = name, + className = className +), HasIconSlot, + HasSelectedSlot { + + /** + * Standard icon button constructor. + */ + constructor( + disabled: Boolean = false, + flipIconInRtl: Boolean = false, + href: String? = null, + target: LinkTarget? = null, + ariaLabelSelected: String? = null, + toggle: Boolean = false, + selected: Boolean = false, + type: IconButtonType = IconButtonType.Submit, + value: String? = null, + name: String? = null, + className: String? = null, + init: (MdIconButton.() -> Unit)? = null + ) : this( + tag = "md-icon-button", + disabled = disabled, + flipIconInRtl = flipIconInRtl, + href = href, + target = target, + ariaLabelSelected = ariaLabelSelected, + toggle = toggle, + selected = selected, + type = type, + value = value, + name = name, + className = className + ) { + init?.let { this.it() } + } + + /** + * Flips the icon if it is in an RTL context at startup. + */ + var flipIconInRtl: Boolean by refreshOnUpdate(flipIconInRtl) + + /** + * The URL that the link iconButton points to. + */ + var href by refreshOnUpdate(href) + + /** + * Where to display the linked href URL for a link iconButton. + * Common options include _blank to open in a new tab. + */ + var target by refreshOnUpdate(target) + + /** + * The aria-label of the button when the button is toggleable and selected. + */ + var ariaLabelSelected by refreshOnUpdate(ariaLabelSelected) + + /** + * When true, the button will toggle between selected and unselected states. + */ + var toggle by refreshOnUpdate(toggle) + + /** + * Sets the selected state. + * When false, displays the default icon. + * When true, displays the selected icon, or the default icon if no slot="selected" icon is + * provided. + */ + var selected by syncOnUpdate(selected) + + /** + * Button type. + */ + var type by refreshOnUpdate(type) + + /////////////////////////////////////////////////////////////////////////// + // Attributes + /////////////////////////////////////////////////////////////////////////// + + override fun buildAttributeSet(attributeSetBuilder: AttributeSetBuilder) { + super.buildAttributeSet(attributeSetBuilder) + attributeSetBuilder.add("type", type.value) + + if (flipIconInRtl) { + attributeSetBuilder.addBool("flip-icon-in-rtl") + } + + href?.let { + attributeSetBuilder.add("href", it) + } + + target?.let { + attributeSetBuilder.add("target", it.value) + } + + if (toggle) { + attributeSetBuilder.addBool("toggle") + } + + if (selected) { + attributeSetBuilder.addBool("selected") + } + + ariaLabelSelected?.let { + attributeSetBuilder.add("aria-label-selected", translate(it)) + } + } + + /////////////////////////////////////////////////////////////////////////// + // Slots + /////////////////////////////////////////////////////////////////////////// + + override fun icon(component: Component?) { + Slot.None(component) + } + + override fun selected(component: Component?) { + Slot.Selected(component) + } + + /////////////////////////////////////////////////////////////////////////// + // Events + /////////////////////////////////////////////////////////////////////////// + + override fun onChange(event: Event) { + super.onChange(event) + selected = getElementD()?.selected == true + } +} + +@ExperimentalMaterialApi +fun Container.iconButton( + disabled: Boolean = false, + flipIconInRtl: Boolean = false, + href: String? = null, + target: LinkTarget? = null, + ariaLabelSelected: String? = null, + toggle: Boolean = false, + selected: Boolean = false, + type: IconButtonType = IconButtonType.Submit, + value: String? = null, + name: String? = null, + className: String? = null, + init: (MdIconButton.() -> Unit)? = null +) = MdIconButton( + disabled = disabled, + flipIconInRtl = flipIconInRtl, + href = href, + target = target, + ariaLabelSelected = ariaLabelSelected, + toggle = toggle, + selected = selected, + type = type, + value = value, + name = name, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/iconbutton/MdOutlinedIconButton.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/iconbutton/MdOutlinedIconButton.kt new file mode 100644 index 0000000000..b76cb2f1f6 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/iconbutton/MdOutlinedIconButton.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.iconbutton + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.widget.LinkTarget +import io.kvision.core.Container + +/** + * Outlined icon buttons are medium-emphasis buttons. + * They're useful when an icon button needs more emphasis than a standard icon button but less + * than a filled or filled tonal icon button. + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdOutlinedIconButton( + disabled: Boolean = false, + flipIconInRtl: Boolean = false, + href: String? = null, + target: LinkTarget? = null, + ariaLabelSelected: String? = null, + toggle: Boolean = false, + selected: Boolean = false, + type: IconButtonType = IconButtonType.Submit, + value: String? = null, + name: String? = null, + className: String? = null, + init: (MdOutlinedIconButton.() -> Unit)? = null +) : MdIconButton( + tag = "md-outlined-icon-button", + disabled = disabled, + flipIconInRtl = flipIconInRtl, + href = href, + target = target, + ariaLabelSelected = ariaLabelSelected, + toggle = toggle, + selected = selected, + type = type, + value = value, + name = name, + className = className, +) { + init { + init?.let { this.it() } + } +} + +@ExperimentalMaterialApi +fun Container.outlinedIconButton( + disabled: Boolean = false, + flipIconInRtl: Boolean = false, + href: String? = null, + target: LinkTarget? = null, + ariaLabelSelected: String? = null, + toggle: Boolean = false, + selected: Boolean = false, + type: IconButtonType = IconButtonType.Submit, + value: String? = null, + name: String? = null, + className: String? = null, + init: (MdOutlinedIconButton.() -> Unit)? = null +) = MdOutlinedIconButton( + disabled = disabled, + flipIconInRtl = flipIconInRtl, + href = href, + target = target, + ariaLabelSelected = ariaLabelSelected, + toggle = toggle, + selected = selected, + type = type, + value = value, + name = name, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/list/MdList.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/list/MdList.kt new file mode 100644 index 0000000000..4e6aa2d780 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/list/MdList.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.list + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.container.MdListContainer +import io.kvision.core.Container + +/** + * Lists are continuous, vertical indexes of text and images. + * + * See https://material-web.dev/components/list/ + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdList( + className: String? = null, + init: (MdList.() -> Unit)? = null +) : MdListContainer( + tag = "md-list", + className = className +) { + + init { + init?.let { this.it() } + } +} + +@ExperimentalMaterialApi +fun Container.list( + className: String? = null, + init: (MdList.() -> Unit)? = null +) = MdList( + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/list/MdListItem.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/list/MdListItem.kt new file mode 100644 index 0000000000..8d3496d932 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/list/MdListItem.kt @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.list + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.slot.HasHeadlineSlot +import io.kvision.material.slot.HasLeadingSlot +import io.kvision.material.slot.HasSupportingTextSlot +import io.kvision.material.slot.HasTrailingSlot +import io.kvision.material.slot.HasTrailingSupportingTextSlot +import io.kvision.material.util.addBool +import io.kvision.material.widget.LinkTarget +import io.kvision.material.widget.MdItemWidget +import io.kvision.material.widget.Slot +import io.kvision.core.AttributeSetBuilder +import io.kvision.core.Component +import io.kvision.core.Container +import io.kvision.snabbdom.VNode + +enum class ListItemType(internal val value: String) { + Text("text"), + Button("button"), + Link("link") +} + +/** + * List items are element that belongs to a List. + * + * See https://material-web.dev/components/list/ + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdListItem( + text: String? = null, + type: ListItemType = ListItemType.Text, + disabled: Boolean = false, + href: String? = null, + target: LinkTarget? = null, + className: String? = null, + init: (MdListItem.() -> Unit)? = null +) : MdItemWidget( + tag = "md-list-item", + className = className +), HasHeadlineSlot, + HasLeadingSlot, + HasSupportingTextSlot, + HasTrailingSlot, + HasTrailingSupportingTextSlot { + + /** + * Text of this item. + */ + var text by refreshOnUpdate(text) + + /** + * Sets the behavior of the list item. + */ + var type: ListItemType by refreshOnUpdate(type) + + /** + * Disables the item and makes it non-selectable and non-interactive. + */ + var disabled by refreshOnUpdate(disabled) + + /** + * The URL that the item points to. + */ + var href by refreshOnUpdate(href) + + /** + * Where to display the linked href URL. + */ + var target by refreshOnUpdate(target) + + init { + init?.let { this.it() } + } + + /////////////////////////////////////////////////////////////////////////// + // Rendering + /////////////////////////////////////////////////////////////////////////// + + override fun render(): VNode { + return renderWithTranslatableText(text) + } + + /////////////////////////////////////////////////////////////////////////// + // Attributes + /////////////////////////////////////////////////////////////////////////// + + override fun buildAttributeSet(attributeSetBuilder: AttributeSetBuilder) { + super.buildAttributeSet(attributeSetBuilder) + attributeSetBuilder.add("type", type.value) + + if (disabled) { + attributeSetBuilder.addBool("disabled") + } + + href?.let { + attributeSetBuilder.add("href", it) + } + + target?.let { + attributeSetBuilder.add("target", it.value) + } + } + + /////////////////////////////////////////////////////////////////////////// + // Slots + /////////////////////////////////////////////////////////////////////////// + + override fun headline(component: Component?) { + Slot.Headline(component) + } + + override fun leading(component: Component?) { + Slot.Leading(component) + } + + override fun supportingText(component: Component?) { + Slot.SupportingText(component) + } + + override fun trailing(component: Component?) { + Slot.Trailing(component) + } + + override fun trailingSupportingText(component: Component?) { + Slot.TrailingSupportingText(component) + } +} + +@ExperimentalMaterialApi +fun Container.listItem( + text: String? = null, + type: ListItemType = ListItemType.Text, + disabled: Boolean = false, + href: String? = null, + target: LinkTarget? = null, + className: String? = null, + init: (MdListItem.() -> Unit)? = null +) = MdListItem( + text = text, + type = type, + disabled = disabled, + href = href, + target = target, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/menu/CloseMenuEvent.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/menu/CloseMenuEvent.kt new file mode 100644 index 0000000000..dd427210ab --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/menu/CloseMenuEvent.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.menu + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.widget.MdItemWidget +import io.kvision.material.widget.toItemWidget +import io.kvision.material.widget.toItemWidgetArray +import org.w3c.dom.CustomEvent + +external interface Reason { + val kind: String + val key: String? +} + +@ExperimentalMaterialApi +value class CloseMenuEvent internal constructor(val rawEvent: CustomEvent) { + + val initiator: T + get() = toItemWidget(rawEvent.asDynamic().detail.initiator)!! + + val reason: Reason + get() = rawEvent.asDynamic().detail.reason.unsafeCast() + + val itemPath: Array + get() = toItemWidgetArray(rawEvent.asDynamic().detail.itemPath) +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/menu/MaterialMenuDsl.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/menu/MaterialMenuDsl.kt new file mode 100644 index 0000000000..9a7e21138d --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/menu/MaterialMenuDsl.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.menu + +/** + * DSL Marker for menu building. + */ +@DslMarker +@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE) +annotation class MaterialMenuDsl diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/menu/MdMenu.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/menu/MdMenu.kt new file mode 100644 index 0000000000..50d3f09557 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/menu/MdMenu.kt @@ -0,0 +1,307 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.menu + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.container.MdListContainer +import io.kvision.material.util.add +import io.kvision.material.util.addBool +import io.kvision.material.util.requireElementD +import io.kvision.material.widget.Corner +import io.kvision.material.widget.FocusState +import io.kvision.core.AttributeSetBuilder +import io.kvision.core.Container + +internal const val DEFAULT_TYPEAHEAD_DELAY = 200 + +enum class MenuPositioning(internal val value: String) { + Absolute("absolute"), + Fixed("fixed"), + Document("document"), + Popover("popover") +} + +/** + * Menus display a list of choices on a temporary surface. + * + * See https://material-web.dev/components/menu/ + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdMenu( + anchor: String? = null, + positioning: MenuPositioning = MenuPositioning.Absolute, + quick: Boolean = false, + hasOverflow: Boolean = false, + xOffset: Int = 0, + yOffset: Int = 0, + typeaheadDelay: Int = DEFAULT_TYPEAHEAD_DELAY, + anchorCorner: Corner = Corner.EndStart, + corner: Corner = Corner.StartStart, + stayOpenOnOutsideClick: Boolean = false, + stayOpenOnFocusout: Boolean = false, + skipRestoreFocus: Boolean = false, + defaultFocus: FocusState = FocusState.FirstItem, + noNavigationWrap: Boolean = false, + className: String? = null, + init: (@MaterialMenuDsl MdMenu.() -> Unit)? = null +) : MdListContainer( + tag = "md-menu", + className = className +) { + + /** + * The ID of the element in the same root node in which the menu should align to. + * Overrides setting anchorElement = elementReference. + * + * NOTE: anchor or anchorElement must either be an HTMLElement or resolve to an HTMLElement in + * order for menu to open. + */ + var anchor by refreshOnUpdate(anchor) + + /** + * Whether the positioning algorithim should calculate relative to the parent of the anchor + * element (absolute) or relative to the window (fixed). + * + * NOTE: Fixed menus will not scroll with the page and will be fixed to the window instead. + */ + var positioning by refreshOnUpdate(positioning) + + /** + * Skips the opening and closing animations. + */ + var quick by refreshOnUpdate(quick) + + /** + * Displays overflow content like a submenu. + * Not required in most cases when [positioning] is [MenuPositioning.Popover]. + * + * NOTE: This may cause adverse effects if you set max-height and have items overflowing items + * in the "y" direction. + */ + var hasOverflow by refreshOnUpdate(hasOverflow) + + /** + * Offsets the menu's inline alignment from the anchor by the given number in pixels. This value + * is direction aware and will follow the LTR / RTL direction. + * + * e.g. LTR: positive -> right, negative -> left + * RTL: positive -> left, negative -> right + */ + var xOffset by refreshOnUpdate(xOffset) + + /** + * Offsets the menu's block alignment from the anchor by the given number in pixels. + * + * e.g. positive -> down, negative -> up + */ + var yOffset by refreshOnUpdate(yOffset) + + /** + * The max time between the keystrokes of the typeahead menu behavior before it clears the + * typeahead buffer. + */ + var typeaheadDelay by refreshOnUpdate(typeaheadDelay) + + /** + * The corner of the anchor which to align the menu in the standard logical property style of + * - e.g. `'end-start'`. + * + * NOTE: This value may not be respected by the menu positioning algorithm if the menu would + * render outisde the viewport. + */ + var anchorCorner by refreshOnUpdate(anchorCorner) + + /** + * The corner of the menu which to align the anchor in the standard logical property style of + * - e.g. `'start-start'`. + * + * NOTE: This value may not be respected by the menu positioning algorithm if the menu would + * render outisde the viewport. + */ + var menuCorner by refreshOnUpdate(corner) + + /** + * Keeps the user clicks outside the menu. + * + * NOTE: clicking outside may still cause focusout to close the menu so see + * [stayOpenOnFocusout]. + */ + var stayOpenOnOutsideClick by refreshOnUpdate(stayOpenOnOutsideClick) + + /** + * Keeps the menu open when focus leaves the menu's composed subtree. + * + * NOTE: Focusout behavior will stop propagation of the focusout event. Set this property to + * true to opt-out of menu's focusout handling altogether. + */ + var stayOpenOnFocusout by refreshOnUpdate(stayOpenOnFocusout) + + /** + * After closing, does not restore focus to the last focused element before the menu was opened. + */ + var skipRestoreFocus by refreshOnUpdate(skipRestoreFocus) + + /** + * The element that should be focused by default once opened. + * + * NOTE: When setting default focus to [FocusState.ListRoot], remember to change + * [tabindex] to `0` and change md-menu's display to something other than `display: contents` + * when necessary. + */ + var defaultFocus by refreshOnUpdate(defaultFocus) + + /** + * Turns off navigation wrapping. By default, navigating past the end of the + * menu items will wrap focus back to the beginning and vice versa. Use this + * for ARIA patterns that do not wrap focus, like combobox. + */ + var noNavigationWrap by refreshOnUpdate(noNavigationWrap) + + /** + * Indicates that the menu is in an open state. + */ + val open: Boolean + get() = getElementD()?.open == true + + init { + init?.let { this.it() } + } + + /////////////////////////////////////////////////////////////////////////// + // Attributes + /////////////////////////////////////////////////////////////////////////// + + override fun buildAttributeSet(attributeSetBuilder: AttributeSetBuilder) { + super.buildAttributeSet(attributeSetBuilder) + + anchor?.let { + attributeSetBuilder.add("anchor", it) + } + + attributeSetBuilder.add("positioning", positioning.value) + + if (quick) { + attributeSetBuilder.addBool("quick") + } + + if (hasOverflow) { + attributeSetBuilder.addBool("has-overflow") + } + + if (xOffset > 0) { + attributeSetBuilder.add("x-offset", xOffset) + } + + if (xOffset > 0) { + attributeSetBuilder.add("y-offset", yOffset) + } + + attributeSetBuilder.add("typeahead-delay", typeaheadDelay) + attributeSetBuilder.add("anchor-corner", anchorCorner.value) + attributeSetBuilder.add("menu-corner", menuCorner.value) + + if (stayOpenOnOutsideClick) { + attributeSetBuilder.addBool("stay-open-on-outside-click") + } + + if (stayOpenOnFocusout) { + attributeSetBuilder.addBool("stay-open-on-focusout") + } + + if (skipRestoreFocus) { + attributeSetBuilder.addBool("skip-restore-focus") + } + + attributeSetBuilder.add("default-focus", defaultFocus.value) + + if (noNavigationWrap) { + attributeSetBuilder.add("no-navigation-wrap") + } + } + + /////////////////////////////////////////////////////////////////////////// + // Display + /////////////////////////////////////////////////////////////////////////// + + /** + * Shows the menu. + */ + @JsName("showMenu") + fun open() { + requireElementD().show() + } + + /** + * Closes the menu. + */ + @JsName("closeMenu") + fun close() { + requireElementD().close() + } + + /** + * Toggles the menu open state. + */ + fun toggle() { + if (open) close() else open() + } +} + +@ExperimentalMaterialApi +fun Container.menu( + anchor: String? = null, + positioning: MenuPositioning = MenuPositioning.Absolute, + quick: Boolean = false, + hasOverflow: Boolean = false, + xOffset: Int = 0, + yOffset: Int = 0, + typeaheadDelay: Int = DEFAULT_TYPEAHEAD_DELAY, + anchorCorner: Corner = Corner.EndStart, + corner: Corner = Corner.StartStart, + stayOpenOnOutsideClick: Boolean = false, + stayOpenOnFocusout: Boolean = false, + skipRestoreFocus: Boolean = false, + defaultFocus: FocusState = FocusState.FirstItem, + noNavigationWrap: Boolean = false, + className: String? = null, + init: (@MaterialMenuDsl MdMenu.() -> Unit)? = null +) = MdMenu( + anchor = anchor, + positioning = positioning, + quick = quick, + hasOverflow = hasOverflow, + xOffset = xOffset, + yOffset = yOffset, + typeaheadDelay = typeaheadDelay, + anchorCorner = anchorCorner, + corner = corner, + stayOpenOnOutsideClick = stayOpenOnOutsideClick, + stayOpenOnFocusout = stayOpenOnFocusout, + skipRestoreFocus = skipRestoreFocus, + defaultFocus = defaultFocus, + noNavigationWrap = noNavigationWrap, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/menu/MdMenuItem.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/menu/MdMenuItem.kt new file mode 100644 index 0000000000..9cdc83d4c1 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/menu/MdMenuItem.kt @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.menu + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.slot.HasHeadlineSlot +import io.kvision.material.slot.HasLeadingSlot +import io.kvision.material.slot.HasSupportingTextSlot +import io.kvision.material.slot.HasTrailingSlot +import io.kvision.material.util.addBool +import io.kvision.material.widget.LinkTarget +import io.kvision.material.widget.MdItemWidget +import io.kvision.material.widget.Slot +import io.kvision.core.AttributeSetBuilder +import io.kvision.core.Component +import io.kvision.snabbdom.VNode + +enum class MenuItemType(internal val value: String) { + MenuItem("menuitem"), + Option("option"), + Button("button"), + Link("link") +} + +/** + * Menu items are element that belongs to a menu. + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdMenuItem( + disabled: Boolean = false, + type: MenuItemType = MenuItemType.MenuItem, + href: String? = null, + target: LinkTarget? = null, + keepOpen: Boolean = false, + selected: Boolean = false, + typeaheadText: String? = null, + className: String? = null, + init: (@MaterialMenuDsl MdMenuItem.() -> Unit)? = null +) : MdItemWidget( + tag = "md-menu-item", + className = className +), HasHeadlineSlot, + HasLeadingSlot, + HasSupportingTextSlot, + HasTrailingSlot { + + /** + * Disables the item and makes it non-selectable and non-interactive. + */ + var disabled by refreshOnUpdate(disabled) + + /** + * Sets the behavior and role of the menu item. + */ + var type by refreshOnUpdate(type) + + /** + * The URL that the item points to. + */ + var href by refreshOnUpdate(href) + + /** + * Where to display the linked href URL for a link button. + * Common options include _blank to open in a new tab. + */ + var target by refreshOnUpdate(target) + + /** + * Keeps the menu open if clicked or keyboard selected. + */ + var keepOpen by refreshOnUpdate(keepOpen) + + /** + * Sets the item in the selected visual state when a submenu is opened. + */ + var selected by syncOnUpdate(selected) + + /** + * The text that is selectable via typeahead. If not set, defaults to the innerText of the item + * slotted into the `headline` slot. + */ + var typeaheadText by syncOnUpdate(typeaheadText) + + init { + init?.let { this.it() } + } + + /////////////////////////////////////////////////////////////////////////// + // Lifecycle + /////////////////////////////////////////////////////////////////////////// + + override fun afterCreate(node: VNode) { + super.afterCreate(node) + typeaheadText?.let { getElementD().typeaheadText = translate(it) } + } + + /////////////////////////////////////////////////////////////////////////// + // Attributes + /////////////////////////////////////////////////////////////////////////// + + override fun buildAttributeSet(attributeSetBuilder: AttributeSetBuilder) { + super.buildAttributeSet(attributeSetBuilder) + attributeSetBuilder.add("type", type.value) + + if (disabled) { + attributeSetBuilder.addBool("disabled") + } + + href?.let { + attributeSetBuilder.add("href", it) + } + + target?.let { + attributeSetBuilder.add("target", it.value) + } + + if (keepOpen) { + attributeSetBuilder.addBool("keep-open") + } + + if (selected) { + attributeSetBuilder.addBool("selected") + } + } + + /////////////////////////////////////////////////////////////////////////// + // Slots + /////////////////////////////////////////////////////////////////////////// + + override fun headline(component: Component?) { + Slot.Headline(component) + } + + override fun leading(component: Component?) { + Slot.Leading(component) + } + + override fun supportingText(component: Component?) { + Slot.SupportingText(component) + } + + override fun trailing(component: Component?) { + Slot.Trailing(component) + } +} + +@ExperimentalMaterialApi +fun MdMenu.menuItem( + disabled: Boolean = false, + type: MenuItemType = MenuItemType.MenuItem, + href: String? = null, + target: LinkTarget? = null, + keepOpen: Boolean = false, + selected: Boolean = false, + typeaheadText: String? = null, + className: String? = null, + init: (@MaterialMenuDsl MdMenuItem.() -> Unit)? = null +) = MdMenuItem( + disabled = disabled, + type = type, + href = href, + target = target, + keepOpen = keepOpen, + selected = selected, + typeaheadText = typeaheadText, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/menu/MdSubMenu.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/menu/MdSubMenu.kt new file mode 100644 index 0000000000..8ea36103b4 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/menu/MdSubMenu.kt @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.menu + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.util.add +import io.kvision.material.util.requireElementD +import io.kvision.material.widget.Corner +import io.kvision.material.widget.MdWidget +import io.kvision.material.widget.Slot +import io.kvision.core.AttributeSetBuilder +import kotlin.js.Promise + +private const val DEFAULT_HOVER_OPEN_DELAY = 400 +private const val DEFAULT_HOVER_CLOSE_DELAY = 400 + +/** + * Submenus is nested inside a menu and display a list of choices on a temporary surface. + * + * See https://material-web.dev/components/menu/ + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdSubMenu( + anchorCorner: Corner = Corner.EndStart, + corner: Corner = Corner.StartStart, + hoverOpenDelay: Int = DEFAULT_HOVER_OPEN_DELAY, + hoverCloseDelay: Int = DEFAULT_HOVER_CLOSE_DELAY, + className: String? = null, + init: (@MaterialMenuDsl MdSubMenu.() -> Unit)? = null +) : MdWidget( + tag = "md-sub-menu", + className = className, +) { + + /** + * The [Corner] to set on the submenu. + */ + var anchorCorner by refreshOnUpdate(anchorCorner) + + /** + * The [Corner] to set on the submenu. + */ + var menuCorner by refreshOnUpdate(corner) + + /** + * The delay between mouseenter and submenu opening. + */ + var hoverOpenDelay by refreshOnUpdate(hoverOpenDelay) + + /** + * The delay between ponterleave and the submenu closing. + */ + var hoverCloseDelay by refreshOnUpdate(hoverCloseDelay) + + /** + * The submenu [MdMenuItem]. + */ + var item: MdMenuItem? = null + set(value) { + field = value + Slot.Item(value) + } + + /** + * The submenu [MdMenu]. + */ + var menu: MdMenu? = null + set(value) { + field = value + Slot.Menu(value) + } + + init { + init?.let { this.it() } + } + + /////////////////////////////////////////////////////////////////////////// + // Attributes + /////////////////////////////////////////////////////////////////////////// + + override fun buildAttributeSet(attributeSetBuilder: AttributeSetBuilder) { + super.buildAttributeSet(attributeSetBuilder) + attributeSetBuilder.add("anchor-corner", anchorCorner.value) + attributeSetBuilder.add("menu-corner", menuCorner.value) + attributeSetBuilder.add("hover-open-delay", hoverOpenDelay) + attributeSetBuilder.add("hover-close-delay", hoverCloseDelay) + } + + /////////////////////////////////////////////////////////////////////////// + // Display + /////////////////////////////////////////////////////////////////////////// + + /** + * Shows the submenu. + */ + @JsName("showMenu") + fun open(): Promise { + if (!visible) { + return Promise.reject(IllegalStateException("Submenu is not visible")) + } + + return requireElementD().show() as Promise + } + + /** + * Closes the submenu. + */ + @JsName("closeMenu") + fun close(returnValue: String?): Promise { + if (!visible) { + return Promise.reject(IllegalStateException("Submenu is not visible")) + } + + return requireElementD().close(returnValue) as Promise + } +} + +@ExperimentalMaterialApi +fun MdMenu.submenu( + anchorCorner: Corner = Corner.EndStart, + corner: Corner = Corner.StartStart, + hoverOpenDelay: Int = DEFAULT_HOVER_OPEN_DELAY, + hoverCloseDelay: Int = DEFAULT_HOVER_CLOSE_DELAY, + className: String? = null, + init: (@MaterialMenuDsl MdSubMenu.() -> Unit)? = null +) = MdSubMenu( + anchorCorner = anchorCorner, + corner = corner, + hoverOpenDelay = hoverOpenDelay, + hoverCloseDelay = hoverCloseDelay, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/menu/MenuAnchor.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/menu/MenuAnchor.kt new file mode 100644 index 0000000000..4884b45759 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/menu/MenuAnchor.kt @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.menu + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.core.Component +import io.kvision.core.Container +import io.kvision.core.Position +import io.kvision.core.Widget +import io.kvision.core.onClick +import io.kvision.html.Span +import io.kvision.panel.SimplePanel + +private const val MENU_ANCHOR_ID_PREFIX = "kv_md_menu_anchor_" +private var IdCounter = 0 + +/** + * Component which holds a menu and its anchor. + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +interface MenuAnchor : Component { + val menu: MdMenu + val anchor: W +} + +/** + * [MenuAnchor] implementation. + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +private class MenuAnchorImpl( + position: Position, + override val anchor: W, + initMenu: SimplePanel.() -> MdMenu +) : Span(), MenuAnchor { + + override val menu: MdMenu = initMenu() + + init { + this.position = position + add(anchor) + add(menu) + + menu.anchor = anchor.id ?: (MENU_ANCHOR_ID_PREFIX + IdCounter++).also { generatedId -> + anchor.id = generatedId + } + + anchor.onClick { + menu.toggle() + } + } +} + +/** + * Factory for obtaining an instance of [MenuAnchor]. + * + * The returned component is basically a [Component] that assign an id to [anchor] and affect + * that id to the [MdMenu] computed by [initMenu]. + * + * It also set a click listener on [anchor] that toggle the [MdMenu] visibility. + * + * This API is provided as a convenience for creating menu. + */ +@ExperimentalMaterialApi +fun MenuAnchor( + anchor: W, + position: Position = Position.RELATIVE, + initMenu: SimplePanel.() -> MdMenu +): MenuAnchor = MenuAnchorImpl( + position = position, + anchor = anchor, + initMenu = initMenu +) + +/** + * Factory for obtaining an instance of [MenuAnchor]. + * The returned component is added to this [Container]. + */ +@ExperimentalMaterialApi +fun Container.menuAnchor( + anchor: W, + position: Position = Position.RELATIVE, + initMenu: SimplePanel.() -> MdMenu +): MenuAnchor = MenuAnchor( + position = position, + anchor = anchor, + initMenu = initMenu +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/menu/MenuEvents.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/menu/MenuEvents.kt new file mode 100644 index 0000000000..1a0fad1ab1 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/menu/MenuEvents.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.menu + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.utils.SnOn +import io.kvision.utils.event +import org.w3c.dom.events.Event + +/** + * Fired before the opening animation begins. + */ +@ExperimentalMaterialApi +fun SnOn.opening(block: (Event) -> Unit) { + event("opening", block) +} + +/** + * Fired once the menu is open, after any animations. + */ +@ExperimentalMaterialApi +fun SnOn.opened(block: (Event) -> Unit) { + event("opened", block) +} + +/** + * Fired before the closing animation begins. + */ +@ExperimentalMaterialApi +fun SnOn.closing(block: (Event) -> Unit) { + event("closing", block) +} + +/** + * Fired once the menu is closed, after any animations. + */ +@ExperimentalMaterialApi +fun SnOn.closed(block: (Event) -> Unit) { + event("closed", block) +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/menu/MenuItemEvents.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/menu/MenuItemEvents.kt new file mode 100644 index 0000000000..ba8b32363e --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/menu/MenuItemEvents.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.menu + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.utils.SnOn +import io.kvision.utils.event +import org.w3c.dom.CustomEvent + +/** + * Closes the encapsulating menu on closable interaction. + * [CustomEvent.bubbles], [CustomEvent.composed] + */ +@ExperimentalMaterialApi +fun SnOn.closeMenu( + block: (CloseMenuEvent) -> Unit +) { + event("close-menu") { block(CloseMenuEvent(it as CustomEvent)) } +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/menu/SubMenuEvents.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/menu/SubMenuEvents.kt new file mode 100644 index 0000000000..757d14ed70 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/menu/SubMenuEvents.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.menu + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.utils.SnOn +import io.kvision.utils.event +import org.w3c.dom.events.Event + +/** + * Requests the parent menu to deselect other items when a submenu opens. + * [Event.bubbles], [Event.composed]. + */ +@ExperimentalMaterialApi +fun SnOn.deactivateItems(block: (Event) -> Unit) { + event("deactivate-items", block) +} + +/** + * Requests the parent to make the slotted item focusable and focus the item. + * [Event.bubbles], [Event.composed]. + */ +@ExperimentalMaterialApi +fun SnOn.requestActivation(block: (Event) -> Unit) { + event("request-activation", block) +} + +/** + * Requests the parent menu to deactivate the typeahead functionality when a submenu opens. + * [Event.bubbles], [Event.composed]. + */ +@ExperimentalMaterialApi +fun SnOn.deactivateTypeahead(block: (Event) -> Unit) { + event("deactivate-typeahead", block) +} + +/** + * Requests the parent menu to activate the typeahead functionality when a submenu closes. + * [Event.bubbles], [Event.composed]. + */ +@ExperimentalMaterialApi +fun SnOn.activateTypeahead(block: (Event) -> Unit) { + event("activate-typeahead", block) +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/progress/MdCircularProgress.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/progress/MdCircularProgress.kt new file mode 100644 index 0000000000..03c3211564 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/progress/MdCircularProgress.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.progress + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.core.Container + +/** + * Circular progress indicator type. + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdCircularProgress( + value: Number = 0, + max: Number = 1, + indeterminate: Boolean = false, + fourColor: Boolean = false, + className: String? = null, + init: (MdCircularProgress.() -> Unit)? = null +) : MdProgress( + tag = "md-circular-progress", + value = value, + max = max, + indeterminate = indeterminate, + fourColor = fourColor, + className = className +) { + + init { + init?.let { this.it() } + } +} + +@ExperimentalMaterialApi +fun Container.circularProgress( + value: Number = 0, + max: Number = 1, + indeterminate: Boolean = false, + fourColor: Boolean = false, + className: String? = null, + init: (MdCircularProgress.() -> Unit)? = null +) = MdCircularProgress( + value = value, + max = max, + indeterminate = indeterminate, + fourColor = fourColor, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/progress/MdLinearProgress.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/progress/MdLinearProgress.kt new file mode 100644 index 0000000000..5376380be4 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/progress/MdLinearProgress.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.progress + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.util.add +import io.kvision.core.AttributeSetBuilder +import io.kvision.core.Container + +/** + * Linear progress indicator type. + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdLinearProgress( + buffer: Number = 1, + value: Number = 0, + max: Number = 1, + indeterminate: Boolean = false, + fourColor: Boolean = false, + className: String? = null, + init: (MdLinearProgress.() -> Unit)? = null +) : MdProgress( + tag = "md-linear-progress", + value = value, + max = max, + indeterminate = indeterminate, + fourColor = fourColor, + className = className +) { + + /** + * Buffer amount to display, a fraction between 0 and [max]. + */ + var buffer by syncOnUpdate(buffer) + + init { + init?.let { this.it() } + } + + /////////////////////////////////////////////////////////////////////////// + // Attributes + /////////////////////////////////////////////////////////////////////////// + + override fun buildAttributeSet(attributeSetBuilder: AttributeSetBuilder) { + super.buildAttributeSet(attributeSetBuilder) + + if (!indeterminate) { + attributeSetBuilder.add("buffer", buffer) + } + } +} + +@ExperimentalMaterialApi +fun Container.linearProgress( + buffer: Number = 1, + value: Number = 0, + max: Number = 1, + indeterminate: Boolean = false, + fourColor: Boolean = false, + className: String? = null, + init: (MdLinearProgress.() -> Unit)? = null +) = MdLinearProgress( + buffer = buffer, + value = value, + max = max, + indeterminate = indeterminate, + fourColor = fourColor, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/progress/MdProgress.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/progress/MdProgress.kt new file mode 100644 index 0000000000..a93c5650e0 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/progress/MdProgress.kt @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.progress + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.widget.MdWidget +import io.kvision.material.util.add +import io.kvision.material.util.addBool +import io.kvision.core.AttributeSetBuilder + +/** + * Progress indicators inform users about the status of ongoing processes, such as loading an app + * or submitting a form. + * + * See https://material-web.dev/components/progress/ + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdProgress internal constructor( + tag: String, + value: Number, + max: Number, + indeterminate: Boolean, + fourColor: Boolean, + className: String? +) : MdWidget( + tag = tag, + className = className +) { + + /** + * Progress to display, a fraction between 0 and [max]. + */ + var value by syncOnUpdate(value) + + /** + * Maximum progress to display. + */ + var max by syncOnUpdate(max) + + /** + * Whether or not to display indeterminate progress, which gives no indication to how long an + * activity will take. + */ + var indeterminate by refreshOnUpdate(indeterminate) + + /** + * Whether or not to render indeterminate mode using 4 colors instead of one. + */ + var fourColor by refreshOnUpdate(fourColor) + + /////////////////////////////////////////////////////////////////////////// + // Attributes + /////////////////////////////////////////////////////////////////////////// + + override fun buildAttributeSet(attributeSetBuilder: AttributeSetBuilder) { + super.buildAttributeSet(attributeSetBuilder) + + if (indeterminate) { + attributeSetBuilder.addBool("indeterminate") + } else { + attributeSetBuilder.add("value", value) + attributeSetBuilder.add("max", max) + } + + if (fourColor) { + attributeSetBuilder.addBool("four-color") + } + } +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/radio/MdRadio.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/radio/MdRadio.kt new file mode 100644 index 0000000000..0a99a5f292 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/radio/MdRadio.kt @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.radio + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.form.MdFormInputWidget +import io.kvision.material.util.addBool +import io.kvision.material.widget.TouchTarget +import io.kvision.core.AttributeSetBuilder +import io.kvision.core.Container +import io.kvision.snabbdom.VNode +import org.w3c.dom.events.Event + +/** + * Radio buttons let people select one option from a set of options. + * + * See https://material-web.dev/components/radio/ + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdRadio( + id: String? = null, + checked: Boolean = false, + disabled: Boolean = false, + required: Boolean = false, + value: String = "on", + name: String? = null, + validationMessage: String? = null, + touchTarget: TouchTarget? = TouchTarget.Wrapper, + className: String? = null, + init: (MdRadio.() -> Unit)? = null +) : MdFormInputWidget( + tag = "md-radio", + disabled = disabled, + required = required, + value = value, + name = name, + validationMessage = validationMessage, + className = className +) { + + /** + * Whether or not the radio is selected. + */ + var checked by syncOnUpdate(checked) + + /** + * Radio touch target. + */ + var touchTarget by refreshOnUpdate(touchTarget) + + init { + this.id = id + init?.let { this.it() } + } + + /////////////////////////////////////////////////////////////////////////// + // Lifecycle + /////////////////////////////////////////////////////////////////////////// + + override fun afterCreate(node: VNode) { + super.afterCreate(node) + ActiveRadios.add(this) + } + + override fun afterDestroy() { + super.afterDestroy() + ActiveRadios.remove(this) + } + + /////////////////////////////////////////////////////////////////////////// + // Attributes + /////////////////////////////////////////////////////////////////////////// + + override fun buildAttributeSet(attributeSetBuilder: AttributeSetBuilder) { + super.buildAttributeSet(attributeSetBuilder) + + if (checked) { + attributeSetBuilder.addBool("checked") + } + + touchTarget?.let { + attributeSetBuilder.add("touch-target", it.value) + } + } + + /////////////////////////////////////////////////////////////////////////// + // Events + /////////////////////////////////////////////////////////////////////////// + + override fun onChange(event: Event) { + super.onChange(event) + + // Update all active radios with the same name as no change event is dispatched to radios + // that have been unchecked as a side effect. + ActiveRadios.forEach { radio -> + if (radio.name == name) { + radio.checked = radio.getElementD().checked == true + } + } + } + + internal companion object { + /** + * List of radios which are part of the DOM. + */ + val ActiveRadios by lazy { mutableListOf() } + } +} + +@ExperimentalMaterialApi +fun Container.radio( + id: String? = null, + checked: Boolean = false, + disabled: Boolean = false, + required: Boolean = false, + value: String = "on", + name: String? = null, + touchTarget: TouchTarget? = TouchTarget.Wrapper, + validationMessage: String? = null, + className: String? = null, + init: (MdRadio.() -> Unit)? = null +) = MdRadio( + id = id, + checked = checked, + disabled = disabled, + required = required, + value = value, + name = name, + touchTarget = touchTarget, + validationMessage = validationMessage, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/ripple/MdRipple.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/ripple/MdRipple.kt new file mode 100644 index 0000000000..d0477c61b2 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/ripple/MdRipple.kt @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.ripple + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.util.addBool +import io.kvision.material.util.requireElementD +import io.kvision.material.widget.MdWidget +import io.kvision.core.AttributeSetBuilder +import io.kvision.core.Container +import io.kvision.snabbdom.VNode +import org.w3c.dom.HTMLElement + +/** + * Ripples are state layers used to communicate the status of a component or interactive element. + * + * A state layer is a semi-transparent covering on an element that indicates its state. + * A layer can be applied to an entire element or in a circular shape. + * + * See https://material-web.dev/components/ripple/ + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdRipple( + disabled: Boolean = false, + htmlFor: String? = null, + control: HTMLElement? = null, + className: String? = null, + init: (MdRipple.() -> Unit)? = null +) : MdWidget( + tag = "md-ripple", + className = className +) { + + /** + * Disables the ripple. + */ + var disabled by refreshOnUpdate(disabled) + + /** + * The id of the element to control. + */ + var htmlFor by keepOnUpdate(htmlFor) + + /** + * The control element. + */ + var control by keepOnUpdate(control) + + init { + init?.let { this.it() } + } + + /////////////////////////////////////////////////////////////////////////// + // Lifecycle + /////////////////////////////////////////////////////////////////////////// + + override fun afterCreate(node: VNode) { + super.afterCreate(node) + htmlFor?.let { getElementD().htmlFor = it } + control?.let { getElementD().control = it } + } + + /////////////////////////////////////////////////////////////////////////// + // Attributes + /////////////////////////////////////////////////////////////////////////// + + override fun buildAttributeSet(attributeSetBuilder: AttributeSetBuilder) { + super.buildAttributeSet(attributeSetBuilder) + + if (disabled) { + attributeSetBuilder.addBool("disabled") + } + } + + fun attach(control: HTMLElement) { + requireElementD().attach(control) + } + + fun detach() { + requireElementD().detach() + } +} + +@ExperimentalMaterialApi +fun Container.ripple( + disabled: Boolean = false, + htmlFor: String? = null, + control: HTMLElement? = null, + className: String? = null, + init: (MdRipple.() -> Unit)? = null +) = MdRipple( + disabled = disabled, + htmlFor = htmlFor, + control = control, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/select/MdFilledSelect.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/select/MdFilledSelect.kt new file mode 100644 index 0000000000..17e9c0c83d --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/select/MdFilledSelect.kt @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.select + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.menu.DEFAULT_TYPEAHEAD_DELAY +import io.kvision.core.Container + +/** + * Filled select type. + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdFilledSelect( + label: String? = null, + quick: Boolean = false, + disabled: Boolean = false, + required: Boolean = false, + errorText: String? = null, + supportingText: String? = null, + error: Boolean = false, + menuPositioning: SelectMenuPositioning = SelectMenuPositioning.Popover, + typeaheadDelay: Int = DEFAULT_TYPEAHEAD_DELAY, + selectedIndex: Int = -1, + name: String? = null, + value: String? = null, + validationMessage: String? = null, + className: String? = null, + init: (MdFilledSelect.() -> Unit)? = null +) : MdSelect( + tag = "md-filled-select", + quick = quick, + disabled = disabled, + required = required, + errorText = errorText, + label = label, + supportingText = supportingText, + error = error, + menuPositioning = menuPositioning, + typeaheadDelay = typeaheadDelay, + selectedIndex = selectedIndex, + value = value, + name = name, + validationMessage = validationMessage, + className = className +) { + + init { + init?.let { this.it() } + } +} + +@ExperimentalMaterialApi +fun Container.filledSelect( + label: String? = null, + quick: Boolean = false, + disabled: Boolean = false, + required: Boolean = false, + errorText: String? = null, + supportingText: String? = null, + error: Boolean = false, + menuPositioning: SelectMenuPositioning = SelectMenuPositioning.Popover, + typeaheadDelay: Int = DEFAULT_TYPEAHEAD_DELAY, + selectedIndex: Int = -1, + name: String? = null, + value: String? = null, + validationMessage: String? = null, + className: String? = null, + init: (MdFilledSelect.() -> Unit)? = null +) = MdFilledSelect( + quick = quick, + value = value, + label = label, + disabled = disabled, + required = required, + errorText = errorText, + supportingText = supportingText, + error = error, + menuPositioning = menuPositioning, + typeaheadDelay = typeaheadDelay, + selectedIndex = selectedIndex, + name = name, + validationMessage = validationMessage, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/select/MdOutlinedSelect.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/select/MdOutlinedSelect.kt new file mode 100644 index 0000000000..33de60bc2f --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/select/MdOutlinedSelect.kt @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.select + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.menu.DEFAULT_TYPEAHEAD_DELAY +import io.kvision.core.Container + +/** + * Outlined select type. + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdOutlinedSelect( + label: String? = null, + quick: Boolean = false, + disabled: Boolean = false, + required: Boolean = false, + errorText: String? = null, + supportingText: String? = null, + error: Boolean = false, + menuPositioning: SelectMenuPositioning = SelectMenuPositioning.Popover, + typeaheadDelay: Int = DEFAULT_TYPEAHEAD_DELAY, + selectedIndex: Int = -1, + name: String? = null, + value: String? = null, + validationMessage: String? = null, + className: String? = null, + init: (MdOutlinedSelect.() -> Unit)? = null +) : MdSelect( + tag = "md-outlined-select", + quick = quick, + disabled = disabled, + required = required, + errorText = errorText, + label = label, + supportingText = supportingText, + error = error, + menuPositioning = menuPositioning, + typeaheadDelay = typeaheadDelay, + selectedIndex = selectedIndex, + value = value, + name = name, + validationMessage = validationMessage, + className = className +) { + + init { + init?.let { this.it() } + } +} + +@ExperimentalMaterialApi +fun Container.outlinedSelect( + label: String? = null, + quick: Boolean = false, + disabled: Boolean = false, + required: Boolean = false, + errorText: String? = null, + supportingText: String? = null, + error: Boolean = false, + menuPositioning: SelectMenuPositioning = SelectMenuPositioning.Popover, + typeaheadDelay: Int = DEFAULT_TYPEAHEAD_DELAY, + selectedIndex: Int = -1, + name: String? = null, + value: String? = null, + validationMessage: String? = null, + className: String? = null, + init: (MdOutlinedSelect.() -> Unit)? = null +) = MdOutlinedSelect( + quick = quick, + value = value, + label = label, + disabled = disabled, + required = required, + errorText = errorText, + supportingText = supportingText, + error = error, + menuPositioning = menuPositioning, + typeaheadDelay = typeaheadDelay, + selectedIndex = selectedIndex, + name = name, + validationMessage = validationMessage, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/select/MdSelect.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/select/MdSelect.kt new file mode 100644 index 0000000000..fc8e1a590b --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/select/MdSelect.kt @@ -0,0 +1,258 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.select + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.form.MdFormInputWidget +import io.kvision.material.util.add +import io.kvision.material.util.addBool +import io.kvision.material.util.requireElementD +import io.kvision.material.widget.MdListWidgetContainer +import io.kvision.material.widget.MdListWidgetContainerDelegate +import io.kvision.material.widget.toItemWidgetArray +import io.kvision.material.widget.toItemWidgetArrayOrDefault +import io.kvision.core.AttributeSetBuilder +import io.kvision.core.Component +import io.kvision.core.Container +import io.kvision.snabbdom.VNode +import org.w3c.dom.events.Event + +enum class SelectMenuPositioning(internal val value: String) { + Absolute("absolute"), + Fixed("fixed"), + Popover("popover") +} + +/** + * Select menus display a list of choices on temporary surfaces and display the currently selected + * menu item above the menu. + * + * See https://material-web.dev/components/select/ + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdSelect internal constructor( + tag: String, + quick: Boolean, + disabled: Boolean, + required: Boolean, + errorText: String?, + label: String?, + supportingText: String?, + error: Boolean, + menuPositioning: SelectMenuPositioning, + typeaheadDelay: Int, + selectedIndex: Int, + value: String?, + name: String?, + validationMessage: String?, + className: String? +) : MdFormInputWidget( + tag = tag, + disabled = disabled, + required = required, + value = value, + name = name, + validationMessage = validationMessage, + className = className +), MdListWidgetContainer { + + private val listDelegate = + MdListWidgetContainerDelegate(@Suppress("LeakingThis") this) + + /** + * Opens the menu synchronously with no animation. + */ + var quick by refreshOnUpdate(quick) + + /** + * The error message that replaces supporting text when error is true. + * If errorText is an empty string, then the [supportingText] will continue to show. + * This error message overrides the error message displayed by [reportValidity]. + */ + var errorText by refreshOnUpdate(errorText) + + /** + * The floating label for the field. + */ + var label by refreshOnUpdate(label) + + /** + * Conveys additional information below the select, such as how it should be used. + */ + var supportingText by refreshOnUpdate(supportingText) + + /** + * Gets or sets whether or not the select is in a visually invalid state. + * This error state overrides the error state controlled by [reportValidity]. + */ + var error by refreshOnUpdate(error) + + /** + * Whether or not the underlying md-menu should be position: fixed to display in a top-level manner, + * or position: absolute. + * + * position:fixed is useful for cases where select is inside of another element with stacking + * context and hidden overflows such as md-dialog. + */ + var menuPositioning by refreshOnUpdate(menuPositioning) + + /** + * The max time between the keystrokes of the typeahead select / menu behavior before it clears + * the typeahead buffer. + */ + var typeaheadDelay by refreshOnUpdate(typeaheadDelay) + + /** + * Index of the selected option. + */ + var selectedIndex: Int by syncOnUpdate(selectedIndex) + + /** + * List of available options. + */ + val options: Array + get() = toItemWidgetArrayOrDefault(getElementD()?.options, listDelegate::items) + + /** + * List of selected options. + */ + val selectedOptions: Array + get() = toItemWidgetArray(getElementD()?.selectedOptions) + + /////////////////////////////////////////////////////////////////////////// + // Lifecycle + /////////////////////////////////////////////////////////////////////////// + + override fun onParentChanged(parent: Container?) { + super.onParentChanged(parent) + listDelegate.updateParent(parent) + } + + override fun afterCreate(node: VNode) { + super.afterCreate(node) + getElementD().selectedIndex = selectedIndex + } + + /////////////////////////////////////////////////////////////////////////// + // Rendering + /////////////////////////////////////////////////////////////////////////// + + override fun childComponents(): Collection { + return listDelegate.items + } + + /////////////////////////////////////////////////////////////////////////// + // Attributes + /////////////////////////////////////////////////////////////////////////// + + override fun buildAttributeSet(attributeSetBuilder: AttributeSetBuilder) { + super.buildAttributeSet(attributeSetBuilder) + + if (quick) { + attributeSetBuilder.addBool("quick") + } + + label?.let { + attributeSetBuilder.add("label", translate(it)) + } + + errorText?.let { + attributeSetBuilder.add("error-text", translate(it)) + } + + supportingText?.let { + attributeSetBuilder.add("supporting-text", translate(it)) + } + + if (error) { + attributeSetBuilder.addBool("error") + } + + attributeSetBuilder.add("menu-positioning", menuPositioning.value) + attributeSetBuilder.add("typeahead-delay", typeaheadDelay) + } + + /////////////////////////////////////////////////////////////////////////// + // Selection + /////////////////////////////////////////////////////////////////////////// + + /** + * Selects an option given the value of the option. + */ + fun select(value: String) { + requireElementD().select(value) + } + + /** + * Selects an option given the index of the option. + */ + fun select(index: Int) { + requireElementD().selectIndex(index) + } + + /** + * Reset the select to its default value. + */ + fun reset() { + requireElementD().reset() + } + + override fun onChange(event: Event) { + super.onChange(event) + selectedIndex = getElementD().selectedIndex.unsafeCast() + } + + /////////////////////////////////////////////////////////////////////////// + // Options + /////////////////////////////////////////////////////////////////////////// + + override fun add(item: MdSelectOption) { + listDelegate.add(item) + } + + override fun add(position: Int, item: MdSelectOption) { + listDelegate.add(position, item) + } + + override fun addAll(items: List) { + listDelegate.addAll(items) + } + + override fun remove(item: MdSelectOption) { + listDelegate.add(item) + } + + override fun removeAt(position: Int) { + listDelegate.removeAt(position) + } + + override fun removeAll() { + listDelegate.removeAll() + } + + override fun disposeAll() { + listDelegate.disposeAll() + } +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/select/MdSelectOption.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/select/MdSelectOption.kt new file mode 100644 index 0000000000..c1be83f2b3 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/select/MdSelectOption.kt @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.select + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.slot.HasHeadlineSlot +import io.kvision.material.slot.HasLeadingSlot +import io.kvision.material.slot.HasSupportingTextSlot +import io.kvision.material.slot.HasTrailingSlot +import io.kvision.material.util.addBool +import io.kvision.material.widget.MdItemWidget +import io.kvision.material.widget.Slot +import io.kvision.core.AttributeSetBuilder +import io.kvision.core.Component +import io.kvision.snabbdom.VNode + +/** + * Select options are element that belongs to a select. + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdSelectOption( + value: String? = null, + disabled: Boolean = false, + selected: Boolean = false, + typeaheadText: String? = null, + className: String? = null, + init: (MdSelectOption.() -> Unit)? = null +) : MdItemWidget( + tag = "md-select-option", + className = className +), HasHeadlineSlot, + HasLeadingSlot, + HasSupportingTextSlot, + HasTrailingSlot { + + /** + * Disables the item and makes it non-selectable and non-interactive. + */ + var disabled by refreshOnUpdate(disabled) + + /** + * Sets the item in the selected visual state when a submenu is opened. + */ + var selected by refreshOnUpdate(selected) + + /** + * Form value of the option. + */ + var value by refreshOnUpdate(value) + + /** + * Option type. + */ + val type: String? + get() = getElementD()?.type?.unsafeCast() + + /** + * Typeahead text of the item. + */ + var typeaheadText by syncOnUpdate(typeaheadText) + + init { + init?.let { this.it() } + } + + /////////////////////////////////////////////////////////////////////////// + // Lifecycle + /////////////////////////////////////////////////////////////////////////// + + override fun afterCreate(node: VNode) { + super.afterCreate(node) + typeaheadText?.let { getElementD().typeaheadText = translate(it) } + } + + /////////////////////////////////////////////////////////////////////////// + // Attributes + /////////////////////////////////////////////////////////////////////////// + + override fun buildAttributeSet(attributeSetBuilder: AttributeSetBuilder) { + super.buildAttributeSet(attributeSetBuilder) + + if (disabled) { + attributeSetBuilder.addBool("disabled") + } + + if (selected) { + attributeSetBuilder.addBool("selected") + } + + value?.let { + attributeSetBuilder.add("value", it) + } + } + + /////////////////////////////////////////////////////////////////////////// + // Slots + /////////////////////////////////////////////////////////////////////////// + + override fun headline(component: Component?) { + Slot.Headline(component) + } + + override fun leading(component: Component?) { + Slot.Leading(component) + } + + override fun supportingText(component: Component?) { + Slot.SupportingText(component) + } + + override fun trailing(component: Component?) { + Slot.Trailing(component) + } +} + +@ExperimentalMaterialApi +fun MdSelect.selectOption( + value: String? = null, + disabled: Boolean = false, + selected: Boolean = false, + typeaheadText: String? = null, + className: String? = null, + init: (MdSelectOption.() -> Unit)? = null +) = MdSelectOption( + value = value, + disabled = disabled, + selected = selected, + typeaheadText = typeaheadText, + className = className, + init = init +).also(this::add) + diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/select/SelectEvents.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/select/SelectEvents.kt new file mode 100644 index 0000000000..ed02ab850a --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/select/SelectEvents.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.select + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.utils.SnOn +import io.kvision.utils.event +import org.w3c.dom.events.Event + +/** + * Fired when the select's menu is about to open. + */ +@ExperimentalMaterialApi +fun SnOn.opening(block: (Event) -> Unit) { + event("opening", block) +} + +/** + * Fired when the select's menu has finished animations and opened. + */ +@ExperimentalMaterialApi +fun SnOn.opened(block: (Event) -> Unit) { + event("opened", block) +} + +/** + * Fired when the select's menu is about to close. + */ +@ExperimentalMaterialApi +fun SnOn.closing(block: (Event) -> Unit) { + event("closing", block) +} + +/** + * Fired when the select's menu has finished animations and closed. + */ +@ExperimentalMaterialApi +fun SnOn.closed(block: (Event) -> Unit) { + event("closed", block) +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/select/SelectOptionEvents.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/select/SelectOptionEvents.kt new file mode 100644 index 0000000000..0814becc10 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/select/SelectOptionEvents.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.select + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.menu.CloseMenuEvent +import io.kvision.utils.SnOn +import io.kvision.utils.event +import org.w3c.dom.CustomEvent +import org.w3c.dom.events.Event + +/** + * Closes the encapsulating menu on closable interaction. + * [CustomEvent.bubbles], [CustomEvent.composed] + */ +@ExperimentalMaterialApi +fun SnOn.closeMenu( + block: (CloseMenuEvent) -> Unit +) { + event("close-menu") { block(CloseMenuEvent(it as CustomEvent)) } +} + +/** + * Requests the parent md-select to select this element (and deselect others if single-selection) + * when selected changed to true. + */ +@ExperimentalMaterialApi +fun SnOn.requestSelection(block: (Event) -> Unit) { + event("request-selection", block) +} + +/** + * Requests the parent md-select to deselect this element when selected changed to false. + */ +@ExperimentalMaterialApi +fun SnOn.requestDeselection(block: (Event) -> Unit) { + event("request-deselection", block) +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/slider/MdBaseSlider.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/slider/MdBaseSlider.kt new file mode 100644 index 0000000000..eb3dbc51ba --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/slider/MdBaseSlider.kt @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.slider + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.form.MdFormLabelWidget +import io.kvision.material.util.add +import io.kvision.material.util.addBool +import io.kvision.core.AttributeSetBuilder + +/** + * Base class for sliders. + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +abstract class MdBaseSlider internal constructor( + min: Number, + max: Number, + step: Number, + disabled: Boolean, + labeled: Boolean, + ticks: Boolean, + name: String?, + value: V, + className: String?, +) : MdFormLabelWidget( + tag = "md-slider", + disabled = disabled, + value = value, + name = name, + className = className, +) { + + /** + * Whether or not to show a value range. + */ + abstract val range: Boolean + + /** + * The slider minimum value + */ + var min by refreshOnUpdate(min) + + /** + * The slider maximum value + */ + var max by refreshOnUpdate(max) + + /** + * The step between values. + */ + var step by refreshOnUpdate(step) + + /** + * Whether or not to show tick marks. + */ + var ticks by refreshOnUpdate(ticks) + + /** + * Whether or not to show a value label when activated. + */ + var labeled by refreshOnUpdate(labeled) + + /////////////////////////////////////////////////////////////////////////// + // Attributes + /////////////////////////////////////////////////////////////////////////// + + override fun buildAttributeSet(attributeSetBuilder: AttributeSetBuilder) { + super.buildAttributeSet(attributeSetBuilder) + + attributeSetBuilder.add("min", min) + attributeSetBuilder.add("max", max) + + attributeSetBuilder.add("step", step) + + if (ticks) { + attributeSetBuilder.addBool("ticks") + } + + if (labeled) { + attributeSetBuilder.addBool("labeled") + } + + if (range) { + attributeSetBuilder.addBool("range") + } + } +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/slider/MdRangeSlider.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/slider/MdRangeSlider.kt new file mode 100644 index 0000000000..ed83891a2a --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/slider/MdRangeSlider.kt @@ -0,0 +1,247 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.slider + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.util.add +import io.kvision.core.AttributeSetBuilder +import io.kvision.core.Container +import io.kvision.snabbdom.VNode +import org.w3c.dom.events.Event + +/** + * Range Sliders expand upon [MdRangeSlider] using the same concepts but allow the user to select 2 + * values. + * + * The two values are still bounded by the value range but they also cannot cross each other. + * + * See https://material-web.dev/components/slider/ + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdRangeSlider( + disabled: Boolean = false, + min: Number = 0, + max: Number = 100, + valueStart: Number? = null, + valueEnd: Number? = null, + step: Number = 1, + ticks: Boolean = false, + labeled: Boolean = false, + valueLabelStart: String? = null, + valueLabelEnd: String? = null, + ariaLabelStart: String? = null, + ariaValueTextStart: String? = null, + ariaLabelEnd: String? = null, + ariaValueTextEnd: String? = null, + name: String? = null, + nameStart: String? = null, + nameEnd: String? = null, + className: String? = null, + init: (MdRangeSlider.() -> Unit)? = null +) : MdBaseSlider( + min = min, + max = max, + step = step, + disabled = disabled, + labeled = labeled, + ticks = ticks, + name = name, + value = null, + className = className +) { + + override val range: Boolean + get() = true + + /** + * The range slider start value. + */ + var valueStart by syncOnUpdate(valueStart ?: rangeSliderInitialStartValue(min, max)) + + /** + * The rangeSlider end value. + */ + var valueEnd by syncOnUpdate(valueEnd ?: rangeSliderInitialEndValue(min, max)) + + /** + * An optional label for the range slider's start value. + * If not set, the label is the [valueStart] itself. + */ + var valueLabelStart by refreshOnUpdate(valueLabelStart) + + /** + * An optional label for the range slider's end value. + * If not set, the label is the [valueEnd] itself. + */ + var valueLabelEnd by refreshOnUpdate(valueLabelEnd) + + /** + * Aria label for the range slider's start handle. + */ + var ariaLabelStart by refreshOnUpdate(ariaLabelStart) + + /** + * Aria value text for the range slider's start value. + */ + var ariaValueTextStart by refreshOnUpdate(ariaValueTextStart) + + /** + * Aria label for the range slider's end handle. + */ + var ariaLabelEnd by refreshOnUpdate(ariaLabelEnd) + + /** + * Aria value text for the range slider's end value. + */ + var ariaValueTextEnd by refreshOnUpdate(ariaValueTextEnd) + + /** + * The HTML name to use in form submission for a range slider's starting value. + * Use [name] instead if both the start and end values should use the same name. + */ + var nameStart by syncOnUpdate(nameStart) + + /** + * The HTML name to use in form submission for a range slider's ending value. + * Use [name] instead if both the start and end values should use the same name. + */ + var nameEnd by syncOnUpdate(nameEnd) + + init { + init?.let { this.it() } + } + + /////////////////////////////////////////////////////////////////////////// + // Lifecycle + /////////////////////////////////////////////////////////////////////////// + + override fun afterCreate(node: VNode) { + super.afterCreate(node) + nameStart?.let { getElementD().nameStart = it } + nameEnd?.let { getElementD().nameEnd = it } + } + + /////////////////////////////////////////////////////////////////////////// + // Attributes + /////////////////////////////////////////////////////////////////////////// + + override fun buildAttributeSet(attributeSetBuilder: AttributeSetBuilder) { + super.buildAttributeSet(attributeSetBuilder) + attributeSetBuilder.add("value-start", valueStart) + attributeSetBuilder.add("value-end", valueEnd) + + valueLabelStart?.let { + attributeSetBuilder.add("value-label-start", it) + } + + valueLabelEnd?.let { + attributeSetBuilder.add("value-label-end", it) + } + + ariaLabelStart?.let { + attributeSetBuilder.add("aria-label-start", it) + } + + ariaValueTextStart?.let { + attributeSetBuilder.add("aria-valuetext-start", it) + } + + ariaLabelEnd?.let { + attributeSetBuilder.add("aria-label-end", it) + } + + ariaValueTextEnd?.let { + attributeSetBuilder.add("aria-valuetext-end", it) + } + } + + /////////////////////////////////////////////////////////////////////////// + // Events + /////////////////////////////////////////////////////////////////////////// + + override fun onChange(event: Event) { + valueStart = getElementD().valueStart.unsafeCast() + valueEnd = getElementD().valueEnd.unsafeCast() + } +} + +@ExperimentalMaterialApi +fun Container.rangeSlider( + disabled: Boolean = false, + min: Number = 0, + max: Number = 100, + valueStart: Number? = null, + valueEnd: Number? = null, + step: Number = 1, + ticks: Boolean = false, + labeled: Boolean = false, + valueLabelStart: String? = null, + valueLabelEnd: String? = null, + ariaLabelStart: String? = null, + ariaValueTextStart: String? = null, + ariaLabelEnd: String? = null, + ariaValueTextEnd: String? = null, + name: String? = null, + nameStart: String? = null, + nameEnd: String? = null, + className: String? = null, + init: (MdRangeSlider.() -> Unit)? = null +) = MdRangeSlider( + disabled = disabled, + min = min, + max = max, + valueStart = valueStart, + valueEnd = valueEnd, + valueLabelStart = valueLabelStart, + valueLabelEnd = valueLabelEnd, + ariaLabelStart = ariaLabelStart, + ariaValueTextStart = ariaValueTextStart, + ariaLabelEnd = ariaLabelEnd, + ariaValueTextEnd = ariaValueTextEnd, + step = step, + ticks = ticks, + labeled = labeled, + name = name, + nameStart = nameStart, + nameEnd = nameEnd, + className = className, + init = init +).also(this::add) + +/** + * Computes the initial start value of a range slider. + */ +@Suppress("UNUSED_PARAMETER") +private fun rangeSliderInitialStartValue(min: Number, max: Number): Number { + return js("(min + max) / 3").unsafeCast() +} + +/** + * Computes the initial end value of a range slider. + */ +@Suppress("UNUSED_PARAMETER") +private fun rangeSliderInitialEndValue(min: Number, max: Number): Number { + return js("((min + max) / 3) * 2").unsafeCast() +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/slider/MdSlider.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/slider/MdSlider.kt new file mode 100644 index 0000000000..534acd8138 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/slider/MdSlider.kt @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.slider + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.core.AttributeSetBuilder +import io.kvision.core.Container +import org.w3c.dom.events.Event + +/** + * Sliders allow users to make selections from a range of values.. + * They're ideal for adjusting settings such as volume and brightness, or for applying image filters. + * + * Sliders can use icons or labels to represent a numeric or relative scale. + * + * See https://material-web.dev/components/slider/ + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdSlider( + disabled: Boolean = false, + min: Number = 0, + max: Number = 100, + value: Number? = null, + step: Number = 1, + valueLabel: String? = null, + ticks: Boolean = false, + labeled: Boolean = false, + name: String? = null, + className: String? = null, + init: (MdSlider.() -> Unit)? = null +) : MdBaseSlider( + min = min, + max = max, + step = step, + disabled = disabled, + labeled = labeled, + ticks = ticks, + name = name, + value = value ?: sliderInitialValue(min, max), + className = className +) { + + override val range: Boolean + get() = false + + /** + * An optional label for the slider's value. + * If not set, the label is the [value] itself. + */ + var valueLabel by refreshOnUpdate(valueLabel) + + + init { + init?.let { this.it() } + } + + /////////////////////////////////////////////////////////////////////////// + // Attributes + /////////////////////////////////////////////////////////////////////////// + + override fun buildAttributeSet(attributeSetBuilder: AttributeSetBuilder) { + super.buildAttributeSet(attributeSetBuilder) + + valueLabel?.let { + attributeSetBuilder.add("value-label", it) + } + } + + /////////////////////////////////////////////////////////////////////////// + // Events + /////////////////////////////////////////////////////////////////////////// + + override fun onChange(event: Event) { + value = getElementD().value.unsafeCast() + } +} + +@ExperimentalMaterialApi +fun Container.slider( + disabled: Boolean = false, + min: Number = 0, + max: Number = 100, + value: Number? = null, + step: Number = 1, + valueLabel: String? = null, + ticks: Boolean = false, + labeled: Boolean = false, + name: String? = null, + className: String? = null, + init: (MdSlider.() -> Unit)? = null +) = MdSlider( + disabled = disabled, + min = min, + max = max, + value = value, + step = step, + valueLabel = valueLabel, + ticks = ticks, + labeled = labeled, + name = name, + className = className, + init = init +).also(this::add) + +/** + * Computes the initial value of a slider. + */ +@Suppress("UNUSED_PARAMETER") +private fun sliderInitialValue(min: Number, max: Number): Number { + return js("(min + max) / 2").unsafeCast() +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/slot/Slots.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/slot/Slots.kt new file mode 100644 index 0000000000..4b4f695f94 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/slot/Slots.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.slot + +import io.kvision.core.Component + +/** + * Has an actions slot. + */ +interface HasActionsSlot { + fun actions(component: Component?) +} + +/** + * Has a content slot. + */ +interface HasContentSlot { + fun content(component: Component?) +} + +/** + * Has an headline slot. + */ +interface HasHeadlineSlot { + fun headline(component: Component?) +} + +/** + * Has an icon slot. + */ +interface HasIconSlot { + fun icon(component: Component?) +} + +/** + * Has a leading slot. + */ +interface HasLeadingSlot { + fun leading(component: Component?) +} + +/** + * Has a selected slot. + */ +interface HasSelectedSlot { + fun selected(component: Component?) +} + +/** + * Has a supporting text slot. + */ +interface HasSupportingTextSlot { + fun supportingText(component: Component?) +} + +/** + * Has a trailing slot. + */ +interface HasTrailingSlot { + fun trailing(component: Component?) +} + +/** + * Has a trailing supporting text slot. + */ +interface HasTrailingSupportingTextSlot { + fun trailingSupportingText(component: Component?) +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/slot/TextSlots.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/slot/TextSlots.kt new file mode 100644 index 0000000000..0fe1488915 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/slot/TextSlots.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.slot + +import io.kvision.html.Div + +private inline fun textSlot(text: String, block: (Div) -> Unit): Div { + return Div(content = text).also(block) +} + +/** + * Sets the [text] to the content slot. + */ +fun HasContentSlot.content(text: String) = textSlot(text, ::content) + +/** + * Sets the [text] to the headline slot. + */ +fun HasHeadlineSlot.headline(text: String) = textSlot(text, ::headline) + +/** + * Sets the [text] to the supporting text slot. + */ +fun HasSupportingTextSlot.supportingText(text: String) = textSlot(text, ::supportingText) + +/** + * Sets the [text] to the trailing supporting text slot. + */ +fun HasTrailingSupportingTextSlot.trailingSupportingText(text: String) = + textSlot(text, ::trailingSupportingText) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/switch/MdSwitch.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/switch/MdSwitch.kt new file mode 100644 index 0000000000..1f051ed4da --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/switch/MdSwitch.kt @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.switch + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.form.MdFormToggleInputWidget +import io.kvision.material.util.addBool +import io.kvision.core.AttributeSetBuilder +import io.kvision.core.Container +import org.w3c.dom.events.Event + +/** + * Switches toggle the state of an item on or off. + * + * See https://material-web.dev/components/switch/ + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdSwitch( + selected: Boolean = false, + disabled: Boolean = false, + icons: Boolean = false, + showOnlySelectedIcon: Boolean = false, + required: Boolean = false, + value: String = "on", + name: String? = null, + validationMessage: String? = null, + className: String? = null, + init: (MdSwitch.() -> Unit)? = null +) : MdFormToggleInputWidget( + tag = "md-switch", + disabled = disabled, + required = required, + value = value, + name = name, + validationMessage = validationMessage, + className = className +) { + + /** + * Puts the switch in the selected state and sets the form submission value to the value + * property. + */ + var selected by syncOnUpdate(selected) + + /** + * Shows both the selected and deselected icons. + */ + var icons by refreshOnUpdate(icons) + + /** + * Shows only the selected icon, and not the deselected icon. + * If true, overrides the behavior of the icons property. + */ + var showOnlySelectedIcon by refreshOnUpdate(showOnlySelectedIcon) + + init { + init?.let { this.it() } + } + + /////////////////////////////////////////////////////////////////////////// + // Attributes + /////////////////////////////////////////////////////////////////////////// + + override fun buildAttributeSet(attributeSetBuilder: AttributeSetBuilder) { + super.buildAttributeSet(attributeSetBuilder) + + if (icons) { + attributeSetBuilder.addBool("icons") + } + + if (selected) { + attributeSetBuilder.addBool("selected") + } + + if (showOnlySelectedIcon) { + attributeSetBuilder.addBool("show-only-selected-icon") + } + } + + /////////////////////////////////////////////////////////////////////////// + // State + /////////////////////////////////////////////////////////////////////////// + + override fun toggle() { + selected = !selected + } + + override fun onChange(event: Event) { + super.onChange(event) + selected = getElementD().selected == true + } +} + +@ExperimentalMaterialApi +fun Container.switch( + selected: Boolean = false, + disabled: Boolean = false, + icons: Boolean = false, + showOnlySelectedIcon: Boolean = false, + required: Boolean = false, + value: String = "on", + name: String? = null, + validationMessage: String? = null, + className: String? = null, + init: (MdSwitch.() -> Unit)? = null +) = MdSwitch( + selected = selected, + disabled = disabled, + icons = icons, + showOnlySelectedIcon = showOnlySelectedIcon, + required = required, + value = value, + name = name, + validationMessage = validationMessage, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/tabs/MdPrimaryTab.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/tabs/MdPrimaryTab.kt new file mode 100644 index 0000000000..08cfd7c516 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/tabs/MdPrimaryTab.kt @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.tabs + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.util.addBool +import io.kvision.core.AttributeSetBuilder + +/** + * Primary tabs are placed at the top of the content pane under a top app bar. + * They display the main content destinations. + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdPrimaryTab( + text: String, + inlineIcon: Boolean = false, + active: Boolean = false, + className: String? = null, + init: (MdPrimaryTab.() -> Unit)? = null +) : MdTab( + tag = "md-primary-tab", + text = text, + active = active, + className = className +) { + + /** + * Whether or not the icon renders inline with label or stacked vertically. + */ + var inlineIcon by refreshOnUpdate(inlineIcon) + + init { + init?.let { this.it() } + } + + /////////////////////////////////////////////////////////////////////////// + // Attributes + /////////////////////////////////////////////////////////////////////////// + + override fun buildAttributeSet(attributeSetBuilder: AttributeSetBuilder) { + super.buildAttributeSet(attributeSetBuilder) + + if (inlineIcon) { + attributeSetBuilder.addBool("inline-icon") + } + } +} + +@ExperimentalMaterialApi +fun MdTabs.primaryTab( + text: String, + inlineIcon: Boolean = false, + active: Boolean = false, + className: String? = null, + init: (MdPrimaryTab.() -> Unit)? = null +) = MdPrimaryTab( + text = text, + inlineIcon = inlineIcon, + active = active, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/tabs/MdSecondaryTab.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/tabs/MdSecondaryTab.kt new file mode 100644 index 0000000000..9fdbf2d09a --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/tabs/MdSecondaryTab.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.tabs + +import io.kvision.material.ExperimentalMaterialApi + +/** + * Secondary tabs are used within a content area to further separate related content and establish + * hierarchy. + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdSecondaryTab( + text: String, + active: Boolean = false, + className: String? = null, + init: (MdSecondaryTab.() -> Unit)? = null +) : MdTab( + tag = "md-secondary-tab", + text = text, + active = active, + className = className +) { + + init { + init?.let { this.it() } + } +} + +@ExperimentalMaterialApi +fun MdTabs.secondaryTab( + text: String, + active: Boolean = false, + className: String? = null, + init: (MdSecondaryTab.() -> Unit)? = null +) = MdSecondaryTab( + text = text, + active = active, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/tabs/MdTab.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/tabs/MdTab.kt new file mode 100644 index 0000000000..67ce5c9170 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/tabs/MdTab.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.tabs + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.slot.HasIconSlot +import io.kvision.material.util.addBool +import io.kvision.material.widget.MdItemWidget +import io.kvision.material.widget.Slot +import io.kvision.core.AttributeSetBuilder +import io.kvision.core.Component +import io.kvision.snabbdom.VNode + +/** + * Tabs are element that belongs to a tab bar. + * + * See https://material-web.dev/components/tabs/ + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdTab internal constructor( + tag: String, + text: String, + active: Boolean, + className: String? +) : MdItemWidget( + tag = tag, + className = className +), HasIconSlot { + + /** + * The label of the tab. + */ + var text by refreshOnUpdate(text) + + /** + * Whether or not the tab is selected. + */ + var active by syncOnUpdate(active) + + /////////////////////////////////////////////////////////////////////////// + // Rendering + /////////////////////////////////////////////////////////////////////////// + + override fun render(): VNode { + return renderWithTranslatableText(text) + } + + /////////////////////////////////////////////////////////////////////////// + // Attributes + /////////////////////////////////////////////////////////////////////////// + + override fun buildAttributeSet(attributeSetBuilder: AttributeSetBuilder) { + super.buildAttributeSet(attributeSetBuilder) + + if (active) { + attributeSetBuilder.addBool("active") + } + } + + /////////////////////////////////////////////////////////////////////////// + // Slots + /////////////////////////////////////////////////////////////////////////// + + override fun icon(component: Component?) { + Slot.Icon(component) + } +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/tabs/MdTabs.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/tabs/MdTabs.kt new file mode 100644 index 0000000000..ea62ef471e --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/tabs/MdTabs.kt @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.tabs + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.util.addBool +import io.kvision.material.util.requireElementD +import io.kvision.material.widget.MdListWidget +import io.kvision.material.widget.toItemWidget +import io.kvision.material.widget.toItemWidgetArrayOrDefault +import io.kvision.core.AttributeSetBuilder +import io.kvision.core.Container +import io.kvision.snabbdom.VNode +import kotlin.js.Promise + +/** + * Tabs organize groups of related content that are at the same level of hierarchy. + * + * See https://material-web.dev/components/tabs/ + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdTabs( + autoActivate: Boolean = false, + activeTabIndex: Int = -1, + className: String? = null, + init: (MdTabs.() -> Unit)? = null +) : MdListWidget( + tag = "md-tabs", + className = className +) { + + /** + * Whether or not to automatically select a tab when it is focused. + */ + var autoActivate by refreshOnUpdate(autoActivate) + + /** + * Index of the current active tab. + */ + var activeTabIndex by syncOnUpdate(activeTabIndex) + + /** + * The tabs of this tab bar. + */ + val tabs: Array + get() = toItemWidgetArrayOrDefault(getElementD()?.tabs, listDelegate::items) + + /** + * Current active tab. + * + * Note: setting active tab from an instance of [MdTab] would be quite complicated due to DOM + * element lifecycle. Favor the use of [activeTabIndex] to set the active tab. + */ + val activeTab: MdTab? + get() = toItemWidget(getElementD()?.activeTab) + + init { + init?.let { this.it() } + + setInternalEventListener { + change = { + self.activeTabIndex = getElementD().activeTabIndex.unsafeCast() + } + } + } + + /////////////////////////////////////////////////////////////////////////// + // Lifecycle + /////////////////////////////////////////////////////////////////////////// + + override fun afterCreate(node: VNode) { + super.afterCreate(node) + getElementD().activeTabIndex = activeTabIndex + } + + /////////////////////////////////////////////////////////////////////////// + // Attributes + /////////////////////////////////////////////////////////////////////////// + + override fun buildAttributeSet(attributeSetBuilder: AttributeSetBuilder) { + super.buildAttributeSet(attributeSetBuilder) + + if (autoActivate) { + attributeSetBuilder.addBool("auto-activate") + } + } + + /////////////////////////////////////////////////////////////////////////// + // Scrolling + /////////////////////////////////////////////////////////////////////////// + + /** + * Scrolls the toolbar, if overflowing, to the active tab. + */ + fun scrollToTab(): Promise = doScrollToTab(null) + + /** + * Scrolls the toolbar, if overflowing, to the tab at [index]. + */ + fun scrollToTab(index: Int): Promise = doScrollToTab { + listDelegate.items[index] + } + + /** + * Scrolls the toolbar, if overflowing, to the [tab]. + */ + fun scrollToTab(tab: MdTab): Promise = doScrollToTab { tab } + + /** + * Scrolls to the tab provided by [tab]. If the tab's DOM element is not available, the scrolls + * target will be the active tab. + */ + private fun doScrollToTab(tab: (() -> MdTab?)?): Promise { + if (!visible) { + return Promise.reject(IllegalStateException("Tabs is not visible")) + } + + return requireElementD() + .scrollToTab(tab?.invoke()?.getElement() ?: undefined) + .unsafeCast>() + } +} + +@ExperimentalMaterialApi +fun Container.tabs( + autoActivate: Boolean = false, + activeTabIndex: Int = -1, + className: String? = null, + init: (MdTabs.() -> Unit)? = null +) = MdTabs( + autoActivate = autoActivate, + activeTabIndex = activeTabIndex, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/textfield/MaterielUnsupportedTextFieldType.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/textfield/MaterielUnsupportedTextFieldType.kt new file mode 100644 index 0000000000..bc93561dd8 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/textfield/MaterielUnsupportedTextFieldType.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.textfield + +@RequiresOptIn( + message = "This text field type is not fully supported and may not behave as expected.", + level = RequiresOptIn.Level.WARNING +) +@Retention(AnnotationRetention.BINARY) +@Target(AnnotationTarget.FIELD) +annotation class MaterielUnsupportedTextFieldType diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/textfield/MdFilledTextField.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/textfield/MdFilledTextField.kt new file mode 100644 index 0000000000..3e9cb0c529 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/textfield/MdFilledTextField.kt @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.textfield + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.core.Container +import io.kvision.html.Autocomplete + +/** + * Filled text field type. + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdFilledTextField( + disabled: Boolean = false, + error: Boolean = false, + errorText: String? = null, + label: String? = null, + required: Boolean = false, + value: String? = null, + prefixText: String? = null, + suffixText: String? = null, + supportingText: String? = null, + rows: Int = 2, + cols: Int = 20, + inputMode: TextFieldInputMode? = null, + max: TextFieldRangeConstraint? = null, + maxLength: Int = -1, + min: TextFieldRangeConstraint? = null, + minLength: Int = -1, + pattern: String? = null, + placeholder: String? = null, + readOnly: Boolean = false, + multiple: Boolean = false, + step: Number? = null, + type: TextFieldInputType = TextFieldInputType.Text, + autoComplete: Autocomplete? = null, + name: String? = null, + validationMessage: String? = null, + className: String? = null, + init: (MdFilledTextField.() -> Unit)? = null +) : MdTextField( + tag = "md-filled-text-field", + disabled = disabled, + error = error, + errorText = errorText, + label = label, + required = required, + value = value, + prefixText = prefixText, + suffixText = suffixText, + supportingText = supportingText, + rows = rows, + cols = cols, + inputMode = inputMode, + max = max, + maxLength = maxLength, + min = min, + minLength = minLength, + pattern = pattern, + placeholder = placeholder, + readOnly = readOnly, + multiple = multiple, + step = step, + type = type, + autoComplete = autoComplete, + name = name, + validationMessage = validationMessage, + className = className +) { + + init { + init?.let { this.it() } + } +} + +@ExperimentalMaterialApi +fun Container.filledTextField( + disabled: Boolean = false, + error: Boolean = false, + errorText: String? = null, + label: String? = null, + required: Boolean = false, + value: String? = null, + prefixText: String? = null, + suffixText: String? = null, + supportingText: String? = null, + rows: Int = 2, + cols: Int = 20, + inputMode: TextFieldInputMode? = null, + max: TextFieldRangeConstraint? = null, + maxLength: Int = -1, + min: TextFieldRangeConstraint? = null, + minLength: Int = -1, + pattern: String? = null, + placeholder: String? = null, + readOnly: Boolean = false, + multiple: Boolean = false, + step: Number? = null, + type: TextFieldInputType = TextFieldInputType.Text, + autoComplete: Autocomplete? = null, + name: String? = null, + validationMessage: String? = null, + className: String? = null, + init: (MdFilledTextField.() -> Unit)? = null +) = MdFilledTextField( + disabled = disabled, + error = error, + errorText = errorText, + label = label, + required = required, + value = value, + prefixText = prefixText, + suffixText = suffixText, + supportingText = supportingText, + rows = rows, + cols = cols, + inputMode = inputMode, + max = max, + maxLength = maxLength, + min = min, + minLength = minLength, + pattern = pattern, + placeholder = placeholder, + readOnly = readOnly, + multiple = multiple, + step = step, + type = type, + autoComplete = autoComplete, + name = name, + validationMessage = validationMessage, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/textfield/MdOutlinedTextField.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/textfield/MdOutlinedTextField.kt new file mode 100644 index 0000000000..2943931bad --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/textfield/MdOutlinedTextField.kt @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.textfield + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.core.Container +import io.kvision.html.Autocomplete + +/** + * Outlined text field type. + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdOutlinedTextField( + disabled: Boolean = false, + error: Boolean = false, + errorText: String? = null, + label: String? = null, + required: Boolean = false, + value: String? = null, + prefixText: String? = null, + suffixText: String? = null, + supportingText: String? = null, + rows: Int = 2, + cols: Int = 20, + inputMode: TextFieldInputMode? = null, + max: TextFieldRangeConstraint? = null, + maxLength: Int = -1, + min: TextFieldRangeConstraint? = null, + minLength: Int = -1, + pattern: String? = null, + placeholder: String? = null, + readOnly: Boolean = false, + multiple: Boolean = false, + step: Number? = null, + type: TextFieldInputType = TextFieldInputType.Text, + autoComplete: Autocomplete? = null, + name: String? = null, + validationMessage: String? = null, + className: String? = null, + init: (MdOutlinedTextField.() -> Unit)? = null +) : MdTextField( + tag = "md-outlined-text-field", + disabled = disabled, + error = error, + errorText = errorText, + label = label, + required = required, + value = value, + prefixText = prefixText, + suffixText = suffixText, + supportingText = supportingText, + rows = rows, + cols = cols, + inputMode = inputMode, + max = max, + maxLength = maxLength, + min = min, + minLength = minLength, + pattern = pattern, + placeholder = placeholder, + readOnly = readOnly, + multiple = multiple, + step = step, + type = type, + autoComplete = autoComplete, + name = name, + validationMessage = validationMessage, + className = className +) { + + init { + init?.let { this.it() } + } +} + +@ExperimentalMaterialApi +fun Container.outlinedTextField( + disabled: Boolean = false, + error: Boolean = false, + errorText: String? = null, + label: String? = null, + required: Boolean = false, + value: String? = null, + prefixText: String? = null, + suffixText: String? = null, + supportingText: String? = null, + rows: Int = 2, + cols: Int = 20, + inputMode: TextFieldInputMode? = null, + max: TextFieldRangeConstraint? = null, + maxLength: Int = -1, + min: TextFieldRangeConstraint? = null, + minLength: Int = -1, + pattern: String? = null, + placeholder: String? = null, + readOnly: Boolean = false, + multiple: Boolean = false, + step: Number? = null, + type: TextFieldInputType = TextFieldInputType.Text, + autoComplete: Autocomplete? = null, + name: String? = null, + validationMessage: String? = null, + className: String? = null, + init: (MdOutlinedTextField.() -> Unit)? = null +) = MdOutlinedTextField( + disabled = disabled, + error = error, + errorText = errorText, + label = label, + required = required, + value = value, + prefixText = prefixText, + suffixText = suffixText, + supportingText = supportingText, + rows = rows, + cols = cols, + inputMode = inputMode, + max = max, + maxLength = maxLength, + min = min, + minLength = minLength, + pattern = pattern, + placeholder = placeholder, + readOnly = readOnly, + multiple = multiple, + step = step, + type = type, + autoComplete = autoComplete, + name = name, + validationMessage = validationMessage, + className = className, + init = init +).also(this::add) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/textfield/MdTextField.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/textfield/MdTextField.kt new file mode 100644 index 0000000000..6ffb7f306e --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/textfield/MdTextField.kt @@ -0,0 +1,479 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.textfield + +import io.kvision.material.ExperimentalMaterialApi +import io.kvision.material.form.MdFormInputWidget +import io.kvision.material.slot.HasLeadingSlot +import io.kvision.material.slot.HasTrailingSlot +import io.kvision.material.util.add +import io.kvision.material.util.addBool +import io.kvision.material.util.isNaN +import io.kvision.material.util.requireElementD +import io.kvision.material.widget.Slot +import io.kvision.core.AttributeSetBuilder +import io.kvision.core.Component +import io.kvision.html.Autocomplete +import io.kvision.snabbdom.VNode +import org.w3c.dom.SelectionMode +import org.w3c.dom.events.Event +import kotlin.js.Date + +/** + * Text fields let users enter text into a UI. + * + * See https://material-web.dev/components/text-field/ + * + * @author Maanrifa Bacar Ali + */ +@ExperimentalMaterialApi +open class MdTextField internal constructor( + tag: String, + disabled: Boolean, + error: Boolean, + errorText: String?, + label: String?, + required: Boolean, + value: String?, + prefixText: String?, + suffixText: String?, + supportingText: String?, + rows: Int, + cols: Int, + inputMode: TextFieldInputMode?, + max: TextFieldRangeConstraint?, + maxLength: Int, + min: TextFieldRangeConstraint?, + minLength: Int, + pattern: String?, + placeholder: String?, + readOnly: Boolean, + multiple: Boolean, + step: Number?, + type: TextFieldInputType, + autoComplete: Autocomplete?, + name: String?, + validationMessage: String?, + className: String? +) : MdFormInputWidget( + tag = tag, + disabled = disabled, + required = required, + value = value.orEmpty(), + name = name, + validationMessage = validationMessage, + className = className +), HasLeadingSlot, + HasTrailingSlot { + + /** + * Gets or sets whether or not the text field is in a visually invalid state. + * This error state overrides the error state controlled by [reportValidity]. + */ + var error by refreshOnUpdate(error) + + /** + * The error message that replaces supporting text when [error] is true. + * If [errorText] is an empty string, then the supporting text will continue to show. + * This error message overrides the error message displayed by [reportValidity]. + */ + var errorText by refreshOnUpdate(errorText) + + /** + * Label of the text field. + */ + var label by refreshOnUpdate(label) + + /** + * An optional prefix to display before the input value. + */ + var prefixText by refreshOnUpdate(prefixText) + + /** + * An optional suffix to display after the input value. + */ + var suffixText by refreshOnUpdate(suffixText) + + /** + * Conveys additional information below the text field, such as how it should be used. + */ + var supportingText by refreshOnUpdate(supportingText) + + /** + * The number of rows to display for a [TextFieldInputType.TextArea] text field. + */ + var rows by refreshOnUpdate(rows) + + /** + * The number of cols to display for a [TextFieldInputType.TextArea] text field. + */ + var cols by refreshOnUpdate(cols) + + /** + * Hints at the type of data that might be entered by the user while editing the element or + * its contents. This allows a browser to display an appropriate virtual keyboard. + * https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/inputmode + */ + var inputMode by refreshOnUpdate(inputMode) + + /** + * Defines the greatest value in the range of permitted values. + * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#max + */ + var max by refreshOnUpdate(max) + + /** + * The maximum number of characters a user can enter into the text field. + * Set to -1 for none. + * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#maxlength + */ + var maxLength by refreshOnUpdate(maxLength) + + /** + * Defines the most negative value in the range of permitted values. + * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#min + */ + var min by refreshOnUpdate(min) + + /** + * The minimum number of characters a user can enter into the text field. + * Set to -1 for none. + * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#minlength + */ + var minLength by refreshOnUpdate(minLength) + + /** + * A regular expression that the text field's value must match to pass constraint validation. + * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#pattern + */ + var pattern by refreshOnUpdate(pattern) + + /** + * Defines the text displayed in a form control when the text field has no value. + * https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/placeholder + */ + var placeholder by refreshOnUpdate(placeholder) + + /** + * Indicates whether or not a user should be able to edit the text field's value. + * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#readonly + */ + var readOnly by refreshOnUpdate(readOnly) + + /** + * Indicates that input accepts multiple email addresses. + * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email#multiple + */ + var multiple by refreshOnUpdate(multiple) + + /** + * Returns or sets the element's step attribute, which works with min and max to limit the + * increments at which a numeric or date-time value can be set. + * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#step + */ + var step by refreshOnUpdate(step) + + /** + * The type to use, defaults to "text". The type greatly changes how the text field + * behaves. + * + * See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#input_types for more + * details on each input type. + */ + var type by refreshOnUpdate(type) + + /** + * Describes what, if any, type of autocomplete functionality the input should provide. + * https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete + */ + var autoComplete by refreshOnUpdate(autoComplete) + + /** + * The direction in which selection occurred. + */ + var selectionDirection by syncOnUpdate( + initialValue = null, + transform = { it?.value } + ) + + /** + * The starting position or offset of a text selection. + */ + var selectionStart by syncOnUpdate(null) + + /** + * The end position or offset of a text selection. + */ + var selectionEnd by syncOnUpdate(null) + + /** + * The text field's value as a number. + */ + var valueAsNumber by keepOnUpdate(null) + + /** + * The text field's value as a Date. + */ + var valueAsDate by keepOnUpdate(null) + + init { + setInternalEventListener { + select = { self.onSelect() } + } + } + + /////////////////////////////////////////////////////////////////////////// + // Lifecycle + /////////////////////////////////////////////////////////////////////////// + + override fun afterCreate(node: VNode) { + super.afterCreate(node) + selectionStart?.let { getElementD().selectionStart = it } + selectionEnd?.let { getElementD().selectionEnd = it } + selectionDirection?.let { getElementD().selectionDirection = it.value } + valueAsDate?.let { getElementD().valueAsDate = it } + + if (!valueAsNumber.isNaN()) { + getElementD().valueAsNumber = valueAsNumber + } + } + + /////////////////////////////////////////////////////////////////////////// + // Attribute + /////////////////////////////////////////////////////////////////////////// + + override fun buildAttributeSet(attributeSetBuilder: AttributeSetBuilder) { + super.buildAttributeSet(attributeSetBuilder) + + if (error) { + attributeSetBuilder.addBool("error") + } + + errorText?.let { + attributeSetBuilder.add("error-text", translate(it)) + } + + label?.let { + attributeSetBuilder.add("label", translate(it)) + } + + prefixText?.let { + attributeSetBuilder.add("prefix-text", translate(it)) + } + + suffixText?.let { + attributeSetBuilder.add("suffix-text", translate(it)) + } + + supportingText?.let { + attributeSetBuilder.add("supporting-text", translate(it)) + } + + if (type == TextFieldInputType.TextArea) { + attributeSetBuilder.add("rows", rows) + attributeSetBuilder.add("cols", cols) + } + + inputMode?.let { + attributeSetBuilder.add("inputmode", it.value) + } + + max?.let { + attributeSetBuilder.add("max", it.stringValue(type)) + } + + if (maxLength != -1) { + attributeSetBuilder.add("maxlength", maxLength) + } + + min?.let { + attributeSetBuilder.add("min", it.stringValue(type)) + } + + if (minLength != -1) { + attributeSetBuilder.add("minlength", minLength) + } + + pattern?.let { + attributeSetBuilder.add("pattern", it) + } + + placeholder?.let { + attributeSetBuilder.add("placeholder", translate(it)) + } + + if (readOnly) { + attributeSetBuilder.addBool("readonly") + } + + if (multiple) { + attributeSetBuilder.addBool("multiple") + } + + step?.let { + attributeSetBuilder.add("step", it) + } + + attributeSetBuilder.add("type", type.value) + + autoComplete?.let { + attributeSetBuilder.add("autocomplete", it.type) + } + } + + /////////////////////////////////////////////////////////////////////////// + // Slots + /////////////////////////////////////////////////////////////////////////// + + override fun leading(component: Component?) { + Slot.LeadingIcon(component) + } + + override fun trailing(component: Component?) { + Slot.TrailingIcon(component) + } + + /////////////////////////////////////////////////////////////////////////// + // Value + /////////////////////////////////////////////////////////////////////////// + + private fun refreshValue() { + value = getElementD().value.unsafeCast() + } + + /** + * Replaces a range of text with a new string. + * + * https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setRangeText + */ + fun setRangeText(replacement: String) { + requireElementD().setRangeText(replacement) + refreshValue() + } + + /** + * Replaces a range of text with a new string. + * + * https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setRangeText + */ + fun setRangeText( + replacement: String, + start: Int, + end: Int, + selectionMode: SelectionMode + ) { + requireElementD().setRangeText(replacement, start, end, selectionMode) + refreshValue() + } + + /** + * Reset the text field to its default value. + */ + fun reset() { + requireElementD().reset() + refreshValue() + } + + /////////////////////////////////////////////////////////////////////////// + // Selection + /////////////////////////////////////////////////////////////////////////// + + /** + * Selects all the text in the text field. + * + * https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/select + */ + fun select() { + requireElementD().select() + } + + /** + * Sets the start and end positions of a selection in the text field. + * + * https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setSelectionRange + */ + fun setSelectionRange( + start: Int?, + end: Int?, + direction: TextFieldSelectionDirection? = null + ) { + requireElementD().setSelectionRange(start, end, direction?.value) + } + + /////////////////////////////////////////////////////////////////////////// + // Step + /////////////////////////////////////////////////////////////////////////// + + /** + * Decrements the value of a numeric type text field by [MdTextField.step] or [stepDecrement] + * step number of times. + * + * https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/stepDown + */ + fun stepDown(stepDecrement: Number? = null) { + if (stepDecrement.isNaN()) { + requireElementD().stepDown() + } else { + requireElementD().stepDown(stepDecrement) + } + + refreshValue() + } + + /** + * Increments the value of a numeric type text field by [MdTextField.step] or [stepIncrement] + * step number of times. + * + * https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/stepUp + */ + fun stepUp(stepIncrement: Number? = null) { + if (stepIncrement.isNaN()) { + requireElementD().stepUp() + } else { + requireElementD().stepUp(stepIncrement) + } + + refreshValue() + } + + /////////////////////////////////////////////////////////////////////////// + // Events + /////////////////////////////////////////////////////////////////////////// + + override fun hasInputEvent(): Boolean = true + + override fun onInput(event: Event) { + super.onInput(event) + refreshValue() + } + + private fun onSelect() { + selectionStart = getElementD().selectionStart.unsafeCast() + selectionEnd = getElementD().selectionEnd.unsafeCast() + + val newSelectionDirection = getElementD().selectionDirection.unsafeCast() + + selectionDirection = TextFieldSelectionDirection.entries + .first { it.value == newSelectionDirection } + } +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/textfield/TextFieldTypes.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/textfield/TextFieldTypes.kt new file mode 100644 index 0000000000..d50123ce0a --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/textfield/TextFieldTypes.kt @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.textfield + +import io.kvision.material.textfield.TextFieldRangeConstraint.Numeric +import io.kvision.material.textfield.TextFieldRangeConstraint.Raw +import io.kvision.material.textfield.TextFieldRangeConstraint.Temporal +import kotlin.js.Date + +/** + * Input type. + */ +enum class TextFieldInputType(internal val value: String) { + // Fully supported + Text("text"), + TextArea("textarea"), + Email("email"), + Number("number"), + Password("password"), + Search("search"), + Tel("tel"), + Url("url"), + + // Partially supported + @MaterielUnsupportedTextFieldType + Color("color"), + + @MaterielUnsupportedTextFieldType + Date("date"), + + @MaterielUnsupportedTextFieldType + DatetimeLocal("datetime-local"), + + @MaterielUnsupportedTextFieldType + File("file"), + + @MaterielUnsupportedTextFieldType + Month("month"), + + @MaterielUnsupportedTextFieldType + Time("time"), + + @MaterielUnsupportedTextFieldType + Week("week") +} + +/** + * Input mode. + */ +enum class TextFieldInputMode(internal val value: String) { + None("none"), + Text("text"), + Decimal("decimal"), + Numeric("numeric"), + Tel("tel"), + Search("search"), + Email("email"), + Url("url") +} + +/** + * Selection direction. + */ +enum class TextFieldSelectionDirection(internal val value: String) { + None("none"), + Forward("forward"), + Backward("backward") +} + +/////////////////////////////////////////////////////////////////////////// +// Constraint +/////////////////////////////////////////////////////////////////////////// + +private fun Int.toPaddedString(length: Int) = toString().padStart(length, '0') + +/** + * Range constraint for min and max properties. + * + * https://developer.mozilla.org/en-US/docs/Web/HTML/Constraint_validation + */ +sealed interface TextFieldRangeConstraint { + + fun stringValue(type: TextFieldInputType): String + + /** + * For range and number input types. + */ + value class Numeric(private val value: Number) : TextFieldRangeConstraint { + + override fun stringValue(type: TextFieldInputType): String { + require(type == TextFieldInputType.Number) { + "`${type.value}` input type does not support numeric constraint" + } + + return value.toString() + } + } + + /** + * For date, time and datetime-local (UTC) input types. + * + * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date + * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/time + * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/datetime-local + */ + value class Temporal(private val date: Date) : TextFieldRangeConstraint { + + @OptIn(MaterielUnsupportedTextFieldType::class) + override fun stringValue(type: TextFieldInputType): String { + return when (type) { + TextFieldInputType.DatetimeLocal -> date.toISOString() + + TextFieldInputType.Date -> buildString { + append(date.getFullYear().toPaddedString(4)) + append('-') + append(date.getMonth().toPaddedString(2)) + append('-') + append(date.getDate().toPaddedString(2)) + } + + TextFieldInputType.Time -> buildString { + append(date.getHours().toPaddedString(2)) + append(':') + append(date.getMinutes().toPaddedString(2)) + append(':') + append(date.getSeconds().toPaddedString(2)) + } + + else -> throw IllegalArgumentException("`${type.value}` input type does not support temporal constraint") + } + } + } + + /** + * For month input type. + * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/month + */ + data class Month(val year: Int, val month: Int) : TextFieldRangeConstraint { + + @OptIn(MaterielUnsupportedTextFieldType::class) + override fun stringValue(type: TextFieldInputType): String { + require(type == TextFieldInputType.Month) { + "`${type.value}` input type does not support month constraint" + } + + return buildString { + append(year.toPaddedString(4)) + append('-') + append(month.toPaddedString(2)) + } + } + } + + /** + * For week input type. + * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/week + */ + data class Week(val year: Int, val week: Int) : TextFieldRangeConstraint { + + @OptIn(MaterielUnsupportedTextFieldType::class) + override fun stringValue(type: TextFieldInputType): String { + require(type == TextFieldInputType.Week) { + "`${type.value}` input type does not support week constraint" + } + + return buildString { + append(year.toPaddedString(4)) + append('-') + append('W') + append(week.toPaddedString(2)) + } + } + } + + /** + * Free constraint value. + */ + value class Raw(private val value: String) : TextFieldRangeConstraint { + + override fun stringValue(type: TextFieldInputType): String { + return value + } + } + + companion object { + + /** + * Returns a [Numeric] constraint. + */ + operator fun invoke(value: Number) = Numeric(value) + + /** + * Returns a [Temporal] constraint. + */ + operator fun invoke(value : Date) = Temporal(value) + + /** + * Returns a [Raw] constraint. + */ + operator fun invoke(value: String) = Raw(value) + } +} + +/** + * Returns a [Numeric] constraint. + */ +inline val Number.constraint: TextFieldRangeConstraint + get() = TextFieldRangeConstraint(this) + +/** + * Returns a [Temporal] constraint. + */ +inline val Date.constraint: TextFieldRangeConstraint + get() = TextFieldRangeConstraint(this) + +/** + * Returns a [Raw] constraint. + */ +inline val String.constraint: TextFieldRangeConstraint + get() = TextFieldRangeConstraint(this) diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/util/Assert.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/util/Assert.kt new file mode 100644 index 0000000000..1f7bae21f2 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/util/Assert.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.util + +import io.kvision.core.Widget + +internal fun Widget.requireElementD(): dynamic { + return checkNotNull(getElementD()) { "DOM element has not been created." } +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/util/Attribute.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/util/Attribute.kt new file mode 100644 index 0000000000..6aca754c0b --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/util/Attribute.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.util + +import io.kvision.core.AttributeSetBuilder + +internal fun AttributeSetBuilder.addBool(name: String, value: Boolean = true) { + add(name, value.toString()) +} + +internal fun AttributeSetBuilder.add(name: String, value: Any) { + add(name, value.toString()) +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/util/Keep.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/util/Keep.kt new file mode 100644 index 0000000000..a2ab7d7ae0 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/util/Keep.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.util + +import io.kvision.core.Widget +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +private class WidgetPropertyKeepDelegate(initialValue: T) : ReadWriteProperty { + + private var lastValue = initialValue + + override fun getValue(thisRef: Widget, property: KProperty<*>): T { + val element = thisRef.getElementD() ?: return lastValue + return element[property.name].unsafeCast() + } + + override fun setValue(thisRef: Widget, property: KProperty<*>, value: T) { + lastValue = value + val element = thisRef.getElementD() ?: return + element[property.name] = value + } +} + +/** + * Delegate provider that provides a delegate that reads and writes new values directly to the DOM + * element by their property name. + * + * If the DOM element is not available at the time of read, the last set value will be returned + * instead. + * + * The write operation to the DOM element will only be performed if the DOM element is available. + */ +internal value class WidgetPropertyKeepDelegateProvider(private val initialValue: T) { + + operator fun provideDelegate( + thisRef: Widget?, + prop: KProperty<*> + ): ReadWriteProperty { + return WidgetPropertyKeepDelegate(initialValue) + } +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/util/Number.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/util/Number.kt new file mode 100644 index 0000000000..bae0a50668 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/util/Number.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.util + +/** + * Whether a [Number] is NaN. + */ +internal fun Number?.isNaN(): Boolean { + if (this === null || this === undefined) { + return true + } + + return this !== this +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/util/Sync.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/util/Sync.kt new file mode 100644 index 0000000000..71335fabd8 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/util/Sync.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.util + +import io.kvision.core.Widget +import io.kvision.core.WidgetRefreshDelegate +import kotlin.reflect.KProperty + +/** + * Delegate provider that synchronise the delegated property value with the DOM element property + * value by their property name. + * + * Properties names between kotlin object and JS one must match for magic to operate. + * No type checking will be done regarding the JS object. + */ +internal value class WidgetPropertySyncDelegateProvider( + private val refreshDelegateProvider: ( + thisRef: Widget?, + prop: KProperty<*>, + refreshFunction: (T) -> Unit + ) -> WidgetRefreshDelegate +) { + + operator fun provideDelegate(thisRef: Widget?, prop: KProperty<*>): WidgetRefreshDelegate { + return refreshDelegateProvider(thisRef, prop) change@{ newValue -> + val element = thisRef?.getElementD() ?: return@change + val elementValue = element[prop.name]?.unsafeCast() + + if (elementValue != newValue) { + element[prop.name] = newValue + } + } + } +} + +/** + * Delegate provider that synchronise the delegated property value with the DOM element property + * value by their property name. + * + * The value is first transformed using [transform] before it is compared and assigned to the JS + * object. + * + * Properties names between kotlin object and JS one must match for magic to operate. + * No type checking will be done regarding the JS object. + */ +internal class WidgetPropertySyncTransformDelegateProvider( + private val transform: (T) -> U, + private val refreshDelegateProvider: ( + thisRef: Widget?, + prop: KProperty<*>, + refreshFunction: (T) -> Unit + ) -> WidgetRefreshDelegate +) { + + operator fun provideDelegate(thisRef: Widget?, prop: KProperty<*>): WidgetRefreshDelegate { + return refreshDelegateProvider(thisRef, prop) change@{ newValue -> + val element = thisRef?.getElementD() ?: return@change + val elementValue = element[prop.name]?.unsafeCast() + val transformedValue = transform(newValue) + + if (elementValue != transformedValue) { + element[prop.name] = transformedValue + } + } + } +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/widget/Corner.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/widget/Corner.kt new file mode 100644 index 0000000000..56bb0e626e --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/widget/Corner.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.widget + +enum class Corner(internal val value: String) { + EndStart("end-start"), + EndEnd("end-end"), + StartEnd("start-end"), + StartStart("start-start") +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/widget/FocusState.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/widget/FocusState.kt new file mode 100644 index 0000000000..ae4a3994a4 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/widget/FocusState.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.widget + +enum class FocusState(internal val value: String) { + FirstItem("first-item"), + LastItem("last-item"), + ListRoot("list-root"), + None("none") +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/widget/LinkTarget.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/widget/LinkTarget.kt new file mode 100644 index 0000000000..2cef271889 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/widget/LinkTarget.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.widget + +sealed class LinkTarget(internal val value: String) { + data object Blank: LinkTarget("_blank") + data object Self: LinkTarget("_self") + data object Parent: LinkTarget("_parent") + data object Top: LinkTarget("_top") + class Frame(name: String): LinkTarget(name) +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/widget/MdItemWidget.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/widget/MdItemWidget.kt new file mode 100644 index 0000000000..e9fa88c231 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/widget/MdItemWidget.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.widget + +import io.kvision.snabbdom.VNode +import io.kvision.utils.delete +import org.w3c.dom.HTMLElement + +/** + * Subclass of widget that are meant to be added to a list container. + * + * @author Maanrifa Bacar Ali + */ +abstract class MdItemWidget internal constructor( + tag: String, + className: String? +) : MdWidget( + tag = tag, + className = className +) { + + /////////////////////////////////////////////////////////////////////////// + // Lifecycle + /////////////////////////////////////////////////////////////////////////// + + override fun afterCreate(node: VNode) { + super.afterCreate(node) + getElement()!!.mdItemWidget = this + } +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/widget/MdListWidget.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/widget/MdListWidget.kt new file mode 100644 index 0000000000..a3958210b6 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/widget/MdListWidget.kt @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.widget + +import io.kvision.core.Component +import io.kvision.core.Container + +/** + * Subclass of widgets which only accept child of type [T]. + * + * @author Maanrifa Bacar Ali + */ +abstract class MdListWidget internal constructor( + tag: String, + className: String? +) : MdWidget( + tag = tag, + className = className, +), MdListWidgetContainer { + + internal val listDelegate = MdListWidgetContainerDelegate(@Suppress("LeakingThis") this) + + /////////////////////////////////////////////////////////////////////////// + // Lifecycle + /////////////////////////////////////////////////////////////////////////// + + override fun onParentChanged(parent: Container?) { + super.onParentChanged(parent) + listDelegate.updateParent(parent) + } + + /////////////////////////////////////////////////////////////////////////// + // Rendering + /////////////////////////////////////////////////////////////////////////// + + override fun childComponents(): Collection { + return listDelegate.items + } + + /////////////////////////////////////////////////////////////////////////// + // Container + /////////////////////////////////////////////////////////////////////////// + + override fun add(item: T) { + listDelegate.add(item) + } + + override fun add(position: Int, item: T) { + listDelegate.add(position, item) + } + + override fun addAll(items: List) { + listDelegate.addAll(items) + } + + override fun remove(item: T) { + listDelegate.add(item) + } + + override fun removeAt(position: Int) { + listDelegate.removeAt(position) + } + + override fun removeAll() { + listDelegate.removeAll() + } + + override fun disposeAll() { + listDelegate.disposeAll() + } +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/widget/MdListWidgetContainer.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/widget/MdListWidgetContainer.kt new file mode 100644 index 0000000000..f216293b2d --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/widget/MdListWidgetContainer.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.widget + +import io.kvision.core.Container + +/** + * Widget that only accepts child of type [T]. + * It is a simpler replacement for [Container] which accept any kind of child. + * + * @author Maanrifa Bacar Ali + */ +interface MdListWidgetContainer { + + /** + * Adds given [item] to the current widget. + */ + fun add(item: T) + + /** + * Adds given [item] to the current widget at the given [position]. + */ + fun add(position: Int, item: T) + + /** + * Adds a list of [items] to the current widget. + */ + fun addAll(items: List) + + /** + * Removes given [item] from the current widget. + */ + fun remove(item: T) + + /** + * Removes the item from the current widget at the given [position]. + */ + fun removeAt(position: Int) + + /** + * Removes all items from the current widget. + */ + fun removeAll() + + /** + * Removes all items from the current widget and disposes them. + */ + fun disposeAll() +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/widget/MdListWidgetContainerDelegate.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/widget/MdListWidgetContainerDelegate.kt new file mode 100644 index 0000000000..d328b9dda6 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/widget/MdListWidgetContainerDelegate.kt @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.widget + +import io.kvision.core.Container +import io.kvision.core.Widget + +/** + * Implementation of [MdListWidgetContainer]. + * + * @author Maanrifa Bacar Ali + */ +internal class MdListWidgetContainerDelegate( + private val widget: Widget +) : MdListWidgetContainer { + + val items = mutableListOf() + var onAdded: ((item: T) -> Unit)? = null + var onRemoved: ((item: T) -> Unit)? = null + + /** + * Sets the new parent to all the items. + */ + fun updateParent(parent: Container?) { + items.forEach { it.parent = parent } + } + + /////////////////////////////////////////////////////////////////////////// + // Items + /////////////////////////////////////////////////////////////////////////// + + override fun add(item: T) { + items.add(item) + item.parent?.remove(item) + item.parent = widget.parent + onAdded?.invoke(item) + widget.refresh() + } + + override fun add(position: Int, item: T) { + items.add(position, item) + item.parent?.remove(item) + item.parent = widget.parent + onAdded?.invoke(item) + widget.refresh() + } + + override fun addAll(items: List) { + this.items.addAll(items) + + this.items.forEach { item -> + item.parent?.remove(item) + item.parent = widget.parent + onAdded?.invoke(item) + } + + widget.refresh() + } + + override fun remove(item: T) { + if (items.remove(item)) { + item.clearParent() + onRemoved?.invoke(item) + widget.refresh() + } + } + + override fun removeAt(position: Int) { + items + .getOrNull(position) + ?.also { item -> + items.removeAt(position) + onRemoved?.invoke(item) + } + ?.clearParent() + ?.also { widget.refresh() } + } + + override fun removeAll() { + items + .onEach { item -> + item.clearParent() + onRemoved?.invoke(item) + } + .clear() + + widget.refresh() + } + + override fun disposeAll() { + items.onEach { it.dispose() } + removeAll() + } +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/widget/MdWidget.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/widget/MdWidget.kt new file mode 100644 index 0000000000..eaa1a92947 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/widget/MdWidget.kt @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.widget + +import io.kvision.material.util.WidgetPropertyKeepDelegateProvider +import io.kvision.material.util.WidgetPropertySyncDelegateProvider +import io.kvision.material.util.WidgetPropertySyncTransformDelegateProvider +import io.kvision.core.Component +import io.kvision.core.Container +import io.kvision.core.Widget +import io.kvision.snabbdom.VNode + +/** + * Base class for material widget. + * + * @author Maanrifa Bacar Ali + */ +abstract class MdWidget internal constructor( + protected val tag: String, + className: String? +) : Widget(className) { + + private val slotComponents = mutableMapOf() + + override var parent: Container? + get() = super.parent + set(value) { + super.parent = value + onParentChanged(value) + attachSlots() + } + + init { + @Suppress("LeakingThis") + useSnabbdomDistinctKey() + } + + /////////////////////////////////////////////////////////////////////////// + // Lifecycle + /////////////////////////////////////////////////////////////////////////// + + protected open fun onParentChanged(parent: Container?) = Unit + + override fun dispose() { + super.dispose() + + slotComponents + .onEach { component -> + component.value.dispose() + component.value.clearParent() + } + .clear() + } + + /////////////////////////////////////////////////////////////////////////// + // Rendering + /////////////////////////////////////////////////////////////////////////// + + override fun render(): VNode { + return renderWithContentAndChildren() + } + + protected fun renderWithText(text: String?): VNode { + return renderWithContentAndChildren(text) + } + + protected fun renderWithTranslatableText(text: String?): VNode { + return renderWithContentAndChildren(translate(text)) + } + + private fun renderWithContentAndChildren(text: String? = null): VNode { + return if (text == null && slotComponents.isEmpty()) { + render(tag, childrenVNodes()) + } else if (text == null && slotComponents.isNotEmpty()) { + render(tag, slotVNodes() + childrenVNodes()) + } else if (text != null && slotComponents.isEmpty()) { + render(tag, arrayOf(text) + childrenVNodes()) + } else { + render(tag, arrayOf(text) + slotVNodes() + childrenVNodes()) + } + } + + private fun slotVNodes(): Array { + return slotComponents.values.toVNodeArray() + + } + + private fun childrenVNodes(): Array { + return childComponents() + .takeIf { it.isNotEmpty() } + ?.toVNodeArray() + ?: emptyArray() + } + + @Suppress("UnsafeCastFromDynamic") + private fun Collection.toVNodeArray(): Array { + return toTypedArray() + .asDynamic() + .filter { c: Component -> c.visible } + .map { c: Component -> c.renderVNode() } + } + + protected open fun childComponents(): Collection { + return emptyList() + } + + /////////////////////////////////////////////////////////////////////////// + // Slots + /////////////////////////////////////////////////////////////////////////// + + private fun attachSlots() { + if (slotComponents.isNotEmpty()) { + slotComponents.values.forEach(this::attachParent) + } + } + + private fun attachParent(child: Component) { + child.clearParent() + child.parent = parent + } + + internal operator fun Slot.invoke(child: Component?) { + slotComponents + .remove(this) + ?.clearParent() + + if (child != null) { + slotComponents[this] = child + + if (this != Slot.None) { + child.setAttribute("slot", value) + } + + // The parent maybe null if called from constructor DSL + attachParent(child) + } + + refresh() + } + + /////////////////////////////////////////////////////////////////////////// + // Delegate + /////////////////////////////////////////////////////////////////////////// + + /** + * Keep the last set value but return it only if the DOM element is not available. + * DOM element value will be updated if available. + * + * @see [WidgetPropertyKeepDelegateProvider] + */ + internal fun keepOnUpdate(initialValue: T): WidgetPropertyKeepDelegateProvider { + return WidgetPropertyKeepDelegateProvider(initialValue) + } + + /** + * Synchronise changes between JS object and Kotlin object by their property name. + * The main purpose is to avoid full and unnecessary widget refreshing. + * + * @see [WidgetPropertySyncDelegateProvider] + */ + internal fun syncOnUpdate(initialValue: T): WidgetPropertySyncDelegateProvider { + return WidgetPropertySyncDelegateProvider { thisRef, prop, refreshFunction -> + refreshOnUpdate(initialValue, refreshFunction).provideDelegate(thisRef, prop) + } + } + + /** + * Synchronise changes between JS object and Kotlin object by their property name. + * The value is first transformed using [transform] before it is assigned to element. + * The main purpose is to avoid full and unnecessary widget refreshing. + * + * @see [WidgetPropertySyncTransformDelegateProvider] + */ + internal fun syncOnUpdate( + initialValue: T, + transform: (T) -> U + ): WidgetPropertySyncTransformDelegateProvider { + return WidgetPropertySyncTransformDelegateProvider(transform) { thisRef, prop, refreshFunction -> + refreshOnUpdate(initialValue, refreshFunction).provideDelegate(thisRef, prop) + } + } +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/widget/Slot.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/widget/Slot.kt new file mode 100644 index 0000000000..916936d871 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/widget/Slot.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.widget + +internal enum class Slot(val value: String) { + None(""), + Actions("actions"), + Content("content"), + Headline("headline"), + Icon("icon"), + Item("item"), + Leading("start"), + LeadingIcon("leading-icon"), + Menu("menu"), + Selected("selected"), + SupportingText("supporting-text"), + Trailing("end"), + TrailingIcon("trailing-icon"), + TrailingSupportingText("trailing-supporting-text"); +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/widget/TouchTarget.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/widget/TouchTarget.kt new file mode 100644 index 0000000000..61d668c69f --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/widget/TouchTarget.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.widget + +enum class TouchTarget(internal val value: String) { + None("none"), + Wrapper("wrapper") +} diff --git a/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/widget/UnsafeItemWidget.kt b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/widget/UnsafeItemWidget.kt new file mode 100644 index 0000000000..99c3f588d5 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsMain/kotlin/io/kvision/material/widget/UnsafeItemWidget.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * Copyright (c) 2024 Maanrifa Bacar Ali + * + * 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 io.kvision.material.widget + +import io.kvision.utils.delete +import org.w3c.dom.HTMLElement + +private const val JS_WIDGET_ITEM_PROPERTY_NAME = "kvMdItemWidget" + +/** + * Stores the [MdItemWidget] into the [HTMLElement] in order to be retrieved later. + */ +internal var HTMLElement.mdItemWidget: MdItemWidget? + get() = asDynamic()[JS_WIDGET_ITEM_PROPERTY_NAME] as? MdItemWidget + set(value) { + if (value == null) { + delete(asDynamic(), JS_WIDGET_ITEM_PROPERTY_NAME) + } else { + asDynamic()[JS_WIDGET_ITEM_PROPERTY_NAME] = value + } + } + +/** + * Retrieves and casts the [MdItemWidget] of type [T] from [target]. + * Returns null if [target] is null or does not contain a valid [MdItemWidget]. + */ +internal fun toItemWidget(target: dynamic): T? { + if (target == null) { + return null + } + + return (target as? HTMLElement)?.mdItemWidget?.unsafeCast() +} + +/** + * Maps and casts array of [MdItemWidget] of type [T] from [target]. [target] is expected to be an + * array of [HTMLElement]. + * + * Returns an empty array if mapping could not succeed. + * + * No type checking is done regarding the items themselves. + */ +internal fun toItemWidgetArray(target: dynamic): Array { + if (target == null || target !is Array<*>) { + return emptyArray() + } + + return target + .map { item: HTMLElement -> item.mdItemWidget.unsafeCast() } + .unsafeCast>() +} + + +/** + * Maps and casts array of [MdItemWidget] of type [T] from [target]. [target] is expected to be an + * array of [HTMLElement]. + * + * Returns an array computed by if mapping could not succeed. + * + * No type checking is done regarding the items themselves. + */ +internal inline fun toItemWidgetArrayOrDefault( + target: dynamic, + default: () -> Collection +): Array = toItemWidgetArray(target) + .takeIf(Array<*>::isNotEmpty) + ?: default().toTypedArray() diff --git a/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/button/ButtonSpec.kt b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/button/ButtonSpec.kt new file mode 100644 index 0000000000..55549a406e --- /dev/null +++ b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/button/ButtonSpec.kt @@ -0,0 +1,10 @@ +package test.io.kvision.material.button + +import io.kvision.test.DomSpec + +/** + * TODO + */ +class ButtonSpec: DomSpec { + +} diff --git a/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/checkbox/CheckboxSpec.kt b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/checkbox/CheckboxSpec.kt new file mode 100644 index 0000000000..191abbe509 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/checkbox/CheckboxSpec.kt @@ -0,0 +1,10 @@ +package test.io.kvision.material.checkbox + +import io.kvision.test.DomSpec + +/** + * TODO + */ +class CheckboxSpec: DomSpec { + +} diff --git a/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/chips/ChipSetSpec.kt b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/chips/ChipSetSpec.kt new file mode 100644 index 0000000000..5e82394ad8 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/chips/ChipSetSpec.kt @@ -0,0 +1,10 @@ +package test.io.kvision.material.chips + +import io.kvision.test.DomSpec + +/** + * TODO + */ +class ChipSetSpec: DomSpec { + +} diff --git a/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/chips/ChipSpec.kt b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/chips/ChipSpec.kt new file mode 100644 index 0000000000..b441935990 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/chips/ChipSpec.kt @@ -0,0 +1,10 @@ +package test.io.kvision.material.chips + +import io.kvision.test.DomSpec + +/** + * TODO + */ +class ChipSpec: DomSpec { + +} diff --git a/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/dialog/DialogSpec.kt b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/dialog/DialogSpec.kt new file mode 100644 index 0000000000..b008cca9b5 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/dialog/DialogSpec.kt @@ -0,0 +1,10 @@ +package test.io.kvision.material.dialog + +import io.kvision.test.DomSpec + +/** + * TODO + */ +class DialogSpec: DomSpec { + +} diff --git a/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/divider/DividerSpec.kt b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/divider/DividerSpec.kt new file mode 100644 index 0000000000..f3bd34ff5d --- /dev/null +++ b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/divider/DividerSpec.kt @@ -0,0 +1,10 @@ +package test.io.kvision.material.divider + +import io.kvision.test.DomSpec + +/** + * TODO + */ +class DividerSpec: DomSpec { + +} diff --git a/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/fab/FabSpec.kt b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/fab/FabSpec.kt new file mode 100644 index 0000000000..c73b44503b --- /dev/null +++ b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/fab/FabSpec.kt @@ -0,0 +1,10 @@ +package test.io.kvision.material.fab + +import io.kvision.test.DomSpec + +/** + * TODO + */ +class FabSpec: DomSpec { + +} diff --git a/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/icon/IconSpec.kt b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/icon/IconSpec.kt new file mode 100644 index 0000000000..7eb9efec36 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/icon/IconSpec.kt @@ -0,0 +1,10 @@ +package test.io.kvision.material.icon + +import io.kvision.test.DomSpec + +/** + * TODO + */ +class IconSpec: DomSpec { + +} diff --git a/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/iconbutton/IconButtonSpec.kt b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/iconbutton/IconButtonSpec.kt new file mode 100644 index 0000000000..baab38be7c --- /dev/null +++ b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/iconbutton/IconButtonSpec.kt @@ -0,0 +1,10 @@ +package test.io.kvision.material.iconbutton + +import io.kvision.test.DomSpec + +/** + * TODO + */ +class IconButtonSpec: DomSpec { + +} diff --git a/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/list/ListItemSpec.kt b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/list/ListItemSpec.kt new file mode 100644 index 0000000000..2f362a419a --- /dev/null +++ b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/list/ListItemSpec.kt @@ -0,0 +1,10 @@ +package test.io.kvision.material.list + +import io.kvision.test.DomSpec + +/** + * TODO + */ +class ListItemSpec: DomSpec { + +} diff --git a/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/list/ListSpec.kt b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/list/ListSpec.kt new file mode 100644 index 0000000000..5587d409e4 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/list/ListSpec.kt @@ -0,0 +1,10 @@ +package test.io.kvision.material.list + +import io.kvision.test.DomSpec + +/** + * TODO + */ +class ListSpec: DomSpec { + +} diff --git a/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/menu/MenuItemSpec.kt b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/menu/MenuItemSpec.kt new file mode 100644 index 0000000000..cb4cdec2cd --- /dev/null +++ b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/menu/MenuItemSpec.kt @@ -0,0 +1,10 @@ +package test.io.kvision.material.menu + +import io.kvision.test.DomSpec + +/** + * TODO + */ +class MenuItemSpec: DomSpec { + +} diff --git a/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/menu/MenuSpec.kt b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/menu/MenuSpec.kt new file mode 100644 index 0000000000..c743c25b29 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/menu/MenuSpec.kt @@ -0,0 +1,10 @@ +package test.io.kvision.material.menu + +import io.kvision.test.DomSpec + +/** + * TODO + */ +class MenuSpec: DomSpec { + +} diff --git a/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/menu/SubMenuSpec.kt b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/menu/SubMenuSpec.kt new file mode 100644 index 0000000000..a1f888dc4a --- /dev/null +++ b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/menu/SubMenuSpec.kt @@ -0,0 +1,10 @@ +package test.io.kvision.material.menu + +import io.kvision.test.DomSpec + +/** + * TODO + */ +class SubMenuSpec: DomSpec { + +} diff --git a/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/progress/CircularProgressSpec.kt b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/progress/CircularProgressSpec.kt new file mode 100644 index 0000000000..82a7454f8f --- /dev/null +++ b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/progress/CircularProgressSpec.kt @@ -0,0 +1,10 @@ +package test.io.kvision.material.progress + +import io.kvision.test.DomSpec + +/** + * TODO + */ +class CircularProgressSpec: DomSpec { + +} diff --git a/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/progress/LinearProgressSpec.kt b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/progress/LinearProgressSpec.kt new file mode 100644 index 0000000000..ae2ecc0312 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/progress/LinearProgressSpec.kt @@ -0,0 +1,10 @@ +package test.io.kvision.material.progress + +import io.kvision.test.DomSpec + +/** + * TODO + */ +class LinearProgressSpec: DomSpec { + +} diff --git a/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/radio/RadioSpec.kt b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/radio/RadioSpec.kt new file mode 100644 index 0000000000..9330da876c --- /dev/null +++ b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/radio/RadioSpec.kt @@ -0,0 +1,10 @@ +package test.io.kvision.material.radio + +import io.kvision.test.DomSpec + +/** + * TODO + */ +class RadioSpec: DomSpec { + +} diff --git a/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/ripple/RippleSpec.kt b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/ripple/RippleSpec.kt new file mode 100644 index 0000000000..a894bab106 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/ripple/RippleSpec.kt @@ -0,0 +1,10 @@ +package test.io.kvision.material.ripple + +import io.kvision.test.DomSpec + +/** + * TODO + */ +class RippleSpec: DomSpec { + +} diff --git a/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/select/SelectOptionSpec.kt b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/select/SelectOptionSpec.kt new file mode 100644 index 0000000000..8727de45bb --- /dev/null +++ b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/select/SelectOptionSpec.kt @@ -0,0 +1,10 @@ +package test.io.kvision.material.select + +import io.kvision.test.DomSpec + +/** + * TODO + */ +class SelectOptionSpec: DomSpec { + +} diff --git a/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/select/SelectSpec.kt b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/select/SelectSpec.kt new file mode 100644 index 0000000000..fff7d1909f --- /dev/null +++ b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/select/SelectSpec.kt @@ -0,0 +1,10 @@ +package test.io.kvision.material.select + +import io.kvision.test.DomSpec + +/** + * TODO + */ +class SelectSpec: DomSpec { + +} diff --git a/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/slider/RangeSliderSpec.kt b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/slider/RangeSliderSpec.kt new file mode 100644 index 0000000000..32630f7960 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/slider/RangeSliderSpec.kt @@ -0,0 +1,10 @@ +package test.io.kvision.material.slider + +import io.kvision.test.DomSpec + +/** + * TODO + */ +class RangeSliderSpec: DomSpec { + +} diff --git a/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/slider/SliderSpec.kt b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/slider/SliderSpec.kt new file mode 100644 index 0000000000..693e4571fe --- /dev/null +++ b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/slider/SliderSpec.kt @@ -0,0 +1,10 @@ +package test.io.kvision.material.slider + +import io.kvision.test.DomSpec + +/** + * TODO + */ +class SliderSpec: DomSpec { + +} diff --git a/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/switch/SwitchSpec.kt b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/switch/SwitchSpec.kt new file mode 100644 index 0000000000..ceb58db97f --- /dev/null +++ b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/switch/SwitchSpec.kt @@ -0,0 +1,10 @@ +package test.io.kvision.material.switch + +import io.kvision.test.DomSpec + +/** + * TODO + */ +class SwitchSpec: DomSpec { + +} diff --git a/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/tabs/TabSpec.kt b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/tabs/TabSpec.kt new file mode 100644 index 0000000000..6bc0f6a0a6 --- /dev/null +++ b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/tabs/TabSpec.kt @@ -0,0 +1,10 @@ +package test.io.kvision.material.tabs + +import io.kvision.test.DomSpec + +/** + * TODO + */ +class TabSpec: DomSpec { + +} diff --git a/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/tabs/TabsSpec.kt b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/tabs/TabsSpec.kt new file mode 100644 index 0000000000..4df39156db --- /dev/null +++ b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/tabs/TabsSpec.kt @@ -0,0 +1,10 @@ +package test.io.kvision.material.tabs + +import io.kvision.test.DomSpec + +/** + * TODO + */ +class TabsSpec: DomSpec { + +} diff --git a/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/textfield/TextFieldSpec.kt b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/textfield/TextFieldSpec.kt new file mode 100644 index 0000000000..18dad3360d --- /dev/null +++ b/kvision-modules/kvision-material/src/jsTest/kotlin/test/io/kvision/material/textfield/TextFieldSpec.kt @@ -0,0 +1,10 @@ +package test.io.kvision.material.textfield + +import io.kvision.test.DomSpec + +/** + * TODO + */ +class TextFieldSpec: DomSpec { + +} diff --git a/kvision-modules/kvision-material/webpack.config.d/css.js b/kvision-modules/kvision-material/webpack.config.d/css.js new file mode 100644 index 0000000000..3ed3ebc15d --- /dev/null +++ b/kvision-modules/kvision-material/webpack.config.d/css.js @@ -0,0 +1,2 @@ +config.module.rules.push({ test: /\.css$/, use: ["style-loader", { loader: "css-loader", options: {sourceMap: false} } ] }); + diff --git a/kvision-modules/kvision-material/webpack.config.d/file.js b/kvision-modules/kvision-material/webpack.config.d/file.js new file mode 100644 index 0000000000..aa64925a0a --- /dev/null +++ b/kvision-modules/kvision-material/webpack.config.d/file.js @@ -0,0 +1,6 @@ +config.module.rules.push( + { + test: /\.(jpe?g|png|gif|svg)$/i, + type: 'asset/resource' + } +); diff --git a/settings.gradle.kts b/settings.gradle.kts index 901058aae2..5e9f2378e8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -26,6 +26,7 @@ include( "kvision-modules:kvision-imask", "kvision-modules:kvision-jquery", "kvision-modules:kvision-maps", + "kvision-modules:kvision-material", "kvision-modules:kvision-pace", "kvision-modules:kvision-print", "kvision-modules:kvision-react",